11import { cpSync } from 'node:fs' ;
22import { dirname , resolve } from 'node:path' ;
33import { fileURLToPath } from 'node:url' ;
4- // following docs https://typedoc.org/guides/development/#plugins
5- // eslint-disable-next-line n/no-unpublished-import
6- import { type Application , Converter , JSX , RendererEvent } from 'typedoc' ;
4+ import {
5+ type Application ,
6+ Converter ,
7+ type DefaultTheme ,
8+ JSX ,
9+ type NavigationElement ,
10+ ParameterType ,
11+ type ProjectReflection ,
12+ RendererEvent ,
13+ } from 'typedoc' ;
714import { formatTypeDocToolbar } from './formatTypeDocToolbar.js' ;
15+ import { hoistOtherCategoryInArray , hoistOtherCategoryInNav } from './hoist.js' ;
816import { insertAtomicSearchBox } from './insertAtomicSearchBox.js' ;
917import { insertBetaNote } from './insertBetaNote.js' ;
1018import { insertCustomComments } from './insertCustomComments.js' ;
1119import { insertMetaTags } from './insertMetaTags.js' ;
1220import { insertSiteHeaderBar } from './insertSiteHeaderBar.js' ;
21+ import { applyTopLevelRenameArray } from './renaming.js' ;
22+ import {
23+ applyNestedOrderingArray ,
24+ applyNestedOrderingNode ,
25+ applyTopLevelOrderingArray ,
26+ applyTopLevelOrderingNode ,
27+ } from './sortNodes.js' ;
28+ import type { NavNode } from './types.js' ;
1329
1430const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
1531
1632/**
1733 * Called by TypeDoc when loaded as a plugin.
1834 */
19- export function load ( app : Application ) {
35+ export const load = ( app : Application ) => {
36+ app . options . addDeclaration ( {
37+ name : 'hoistOther.fallbackCategory' ,
38+ help : "Name of the fallback category to hoist (defaults to defaultCategory or 'Other')." ,
39+ type : ParameterType . String ,
40+ } ) ;
41+
42+ app . options . addDeclaration ( {
43+ name : 'hoistOther.topLevelGroup' ,
44+ help : "Name of the top-level group whose children should be promoted to root (default 'Documents')." ,
45+ type : ParameterType . String ,
46+ } ) ;
47+
48+ app . options . addDeclaration ( {
49+ name : 'hoistOther.topLevelOrder' ,
50+ help : 'An array to sort the top level nav by.' ,
51+ type : ParameterType . Array ,
52+ } ) ;
53+
54+ app . options . addDeclaration ( {
55+ name : 'hoistOther.nestedOrder' ,
56+ help : "Object mapping parent title -> ordering array for its children. Use '*' for a default. If omitted, children are sorted alphabetically." ,
57+ type : ParameterType . Mixed ,
58+ } ) ;
59+
60+ app . options . addDeclaration ( {
61+ name : 'hoistOther.renameModulesTo' ,
62+ help : "If set, rename any top-level group titled 'Modules' to this string." ,
63+ type : ParameterType . String ,
64+ } ) ;
65+
66+ const originalMethodName = 'getNavigation' ;
67+ let originalMethod : (
68+ project : ProjectReflection
69+ ) => NavigationElement [ ] | null = null ;
70+ app . renderer . on ( 'beginRender' , ( ) => {
71+ const theme = app . renderer . theme as DefaultTheme | undefined ;
72+ if ( ! theme ) return ;
73+
74+ originalMethod = theme . getNavigation ;
75+
76+ if ( ! originalMethod ) return ;
77+
78+ const opts = app . options ;
79+ const fallback =
80+ ( opts . getValue ( 'hoistOther.fallbackCategory' ) as string ) ||
81+ ( opts . getValue ( 'defaultCategory' ) as string ) ||
82+ 'Other' ;
83+
84+ const topLevelGroup =
85+ ( opts . getValue ( 'hoistOther.topLevelGroup' ) as string ) || 'Documents' ;
86+
87+ const topLevelOrder =
88+ ( opts . getValue ( 'hoistOther.topLevelOrder' ) as string [ ] | undefined ) ||
89+ undefined ;
90+
91+ let nestedOrder = opts . getValue ( 'hoistOther.nestedOrder' ) as
92+ | Record < string , string [ ] >
93+ | string
94+ | undefined ;
95+ if ( typeof nestedOrder === 'string' ) {
96+ try {
97+ nestedOrder = JSON . parse ( nestedOrder ) ;
98+ } catch { }
99+ }
100+
101+ const renameModulesTo =
102+ ( opts . getValue ( 'hoistOther.renameModulesTo' ) as string | undefined ) ||
103+ undefined ;
104+
105+ const typedNestedOrder = nestedOrder as Record < string , string [ ] > ;
106+
107+ theme . getNavigation = function wrappedNavigation (
108+ this : unknown ,
109+ ...args : unknown [ ]
110+ ) {
111+ const nav = originalMethod ! . apply ( this , args ) ;
112+
113+ // The nav shape can be an array of nodes or a single root with children
114+ if ( Array . isArray ( nav ) ) {
115+ if ( renameModulesTo ?. trim ( ) ) {
116+ applyTopLevelRenameArray ( nav , 'Modules' , renameModulesTo . trim ( ) ) ;
117+ }
118+
119+ hoistOtherCategoryInArray ( nav as NavNode [ ] , fallback , topLevelGroup ) ;
120+
121+ if ( topLevelOrder ?. length ) {
122+ applyTopLevelOrderingArray ( nav as NavNode [ ] , topLevelOrder ) ;
123+ }
124+
125+ applyNestedOrderingArray ( nav as NavNode [ ] , typedNestedOrder ) ;
126+ } else if ( nav && typeof nav === 'object' ) {
127+ if ( renameModulesTo ?. trim ( ) && Array . isArray ( nav . children ) ) {
128+ applyTopLevelRenameArray (
129+ nav . children ,
130+ 'Modules' ,
131+ renameModulesTo . trim ( )
132+ ) ;
133+ }
134+
135+ hoistOtherCategoryInNav ( nav as NavNode , fallback ) ;
136+ if (
137+ ( nav as NavNode ) . children &&
138+ topLevelOrder &&
139+ topLevelOrder . length
140+ ) {
141+ applyTopLevelOrderingNode ( nav as NavNode , topLevelOrder ) ;
142+ }
143+ applyNestedOrderingNode ( nav as NavNode , typedNestedOrder ) ;
144+ }
145+ return nav ;
146+ } ;
147+ } ) ;
148+
20149 // Need the Meta Tags to be inserted first, or it causes issues with the navigation sidebar
21150 app . renderer . hooks . on ( 'head.begin' , ( ) => (
22151 < >
@@ -119,14 +248,8 @@ export function load(app: Application) {
119248 </ >
120249 ) ) ;
121250
122- const baseAssetsPath = '../../documentation/assets' ;
123-
124- const createFileCopyEntry = ( sourcePath : string ) => ( {
125- from : resolve ( __dirname , `${ baseAssetsPath } /${ sourcePath } ` ) ,
126- to : resolve ( app . options . getValue ( 'out' ) , `assets/${ sourcePath } ` ) ,
127- } ) ;
128-
129- const onRenderEnd = ( ) => {
251+ app . renderer . on ( RendererEvent . END , ( ) => {
252+ const baseAssetsPath = '../../documentation/assets' ;
130253 const filesToCopy = [
131254 'css/docs-style.css' ,
132255 'css/main-new.css' ,
@@ -145,19 +268,26 @@ export function load(app: Application) {
145268 ] ;
146269
147270 filesToCopy . forEach ( ( filePath ) => {
148- const file = createFileCopyEntry ( filePath ) ;
271+ const file = {
272+ from : resolve ( __dirname , `${ baseAssetsPath } /${ filePath } ` ) ,
273+ to : resolve ( app . options . getValue ( 'out' ) , `assets/${ filePath } ` ) ,
274+ } ;
149275 cpSync ( file . from , file . to ) ;
150276 } ) ;
151277
152278 const darkModeJs = {
153279 from : resolve ( __dirname , '../../documentation/dist/dark-mode.js' ) ,
154280 to : resolve ( app . options . getValue ( 'out' ) , 'assets/vars/dark-mode.js' ) ,
155281 } ;
282+ // Restore original to avoid side effects
283+ const theme = app . renderer . theme as DefaultTheme | undefined ;
284+ if ( theme && originalMethodName && originalMethod ) {
285+ theme [ originalMethodName ] = originalMethod ;
286+ }
287+ originalMethod = null ;
156288
157289 cpSync ( darkModeJs . from , darkModeJs . to ) ;
158- } ;
159-
160- app . renderer . on ( RendererEvent . END , onRenderEnd ) ;
290+ } ) ;
161291
162292 app . converter . on ( Converter . EVENT_CREATE_DECLARATION , insertCustomComments ) ;
163- }
293+ } ;
0 commit comments