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+ type DocumentReflection ,
9+ JSX ,
10+ KindRouter ,
11+ type Models ,
12+ type NavigationElement ,
13+ ParameterType ,
14+ type ProjectReflection ,
15+ RendererEvent ,
16+ } from 'typedoc' ;
717import { formatTypeDocToolbar } from './formatTypeDocToolbar.js' ;
18+ import { hoistOtherCategoryInArray , hoistOtherCategoryInNav } from './hoist.js' ;
819import { insertAtomicSearchBox } from './insertAtomicSearchBox.js' ;
920import { insertBetaNote } from './insertBetaNote.js' ;
1021import { insertCustomComments } from './insertCustomComments.js' ;
1122import { insertMetaTags } from './insertMetaTags.js' ;
1223import { insertSiteHeaderBar } from './insertSiteHeaderBar.js' ;
24+ import { applyTopLevelRenameArray } from './renaming.js' ;
25+ import {
26+ applyNestedOrderingArray ,
27+ applyNestedOrderingNode ,
28+ applyTopLevelOrderingArray ,
29+ applyTopLevelOrderingNode ,
30+ } from './sortNodes.js' ;
31+ import type { TFrontMatter , TNavNode } from './types.js' ;
32+
33+ class KebabRouter extends KindRouter {
34+ // Optional: keep .html (default) or change if you want
35+ extension = '.html' ;
36+
37+ protected getIdealBaseName ( refl : Models . Reflection ) : string {
38+ const name = refl . getFullName ?.( ) ?? refl . name ?? '' ;
39+ if ( ! ( refl as DocumentReflection ) ?. frontmatter ?. slug )
40+ return this . getUrlSafeName ( name ) ;
41+ const { slug} = ( refl as DocumentReflection ) . frontmatter as TFrontMatter ;
42+
43+ return `documents/${ slug } ` ;
44+ }
45+ }
1346
1447const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
1548
1649/**
1750 * Called by TypeDoc when loaded as a plugin.
1851 */
19- export function load ( app : Application ) {
52+ export const load = ( app : Application ) => {
53+ app . options . addDeclaration ( {
54+ name : 'hoistOther.fallbackCategory' ,
55+ help : "Name of the fallback category to hoist (defaults to defaultCategory or 'Other')." ,
56+ type : ParameterType . String ,
57+ } ) ;
58+
59+ app . options . addDeclaration ( {
60+ name : 'hoistOther.topLevelGroup' ,
61+ help : "Name of the top-level group whose children should be promoted to root (default 'Documents')." ,
62+ type : ParameterType . String ,
63+ } ) ;
64+
65+ app . options . addDeclaration ( {
66+ name : 'hoistOther.topLevelOrder' ,
67+ help : 'An array to sort the top level nav by.' ,
68+ type : ParameterType . Array ,
69+ } ) ;
70+
71+ app . options . addDeclaration ( {
72+ name : 'hoistOther.nestedOrder' ,
73+ help : "Object mapping parent title -> ordering array for its children. Use '*' for a default. If omitted, children are sorted alphabetically." ,
74+ type : ParameterType . Mixed ,
75+ } ) ;
76+
77+ app . options . addDeclaration ( {
78+ name : 'hoistOther.renameModulesTo' ,
79+ help : "If set, rename any top-level group titled 'Modules' to this string." ,
80+ type : ParameterType . String ,
81+ } ) ;
82+
83+ const originalMethodName = 'getNavigation' ;
84+ let originalMethod : (
85+ project : ProjectReflection
86+ ) => NavigationElement [ ] | null = null ;
87+ app . renderer . on ( 'beginRender' , ( ) => {
88+ const theme = app . renderer . theme as DefaultTheme | undefined ;
89+ if ( ! theme ) return ;
90+
91+ originalMethod = theme . getNavigation ;
92+
93+ if ( ! originalMethod ) return ;
94+
95+ const opts = app . options ;
96+ const fallback =
97+ ( opts . getValue ( 'hoistOther.fallbackCategory' ) as string ) ||
98+ ( opts . getValue ( 'defaultCategory' ) as string ) ||
99+ 'Other' ;
100+
101+ const topLevelGroup =
102+ ( opts . getValue ( 'hoistOther.topLevelGroup' ) as string ) || 'Documents' ;
103+
104+ const topLevelOrder =
105+ ( opts . getValue ( 'hoistOther.topLevelOrder' ) as string [ ] | undefined ) ||
106+ undefined ;
107+
108+ let nestedOrder = opts . getValue ( 'hoistOther.nestedOrder' ) as
109+ | Record < string , string [ ] >
110+ | string
111+ | undefined ;
112+ if ( typeof nestedOrder === 'string' ) {
113+ try {
114+ nestedOrder = JSON . parse ( nestedOrder ) ;
115+ } catch { }
116+ }
117+
118+ const renameModulesTo =
119+ ( opts . getValue ( 'hoistOther.renameModulesTo' ) as string | undefined ) ||
120+ undefined ;
121+
122+ const typedNestedOrder = nestedOrder as Record < string , string [ ] > ;
123+
124+ theme . getNavigation = function wrappedNavigation (
125+ this : unknown ,
126+ ...args : unknown [ ]
127+ ) {
128+ const nav = originalMethod ! . apply ( this , args ) ;
129+
130+ // The nav shape can be an array of nodes or a single root with children
131+ if ( Array . isArray ( nav ) ) {
132+ if ( renameModulesTo ?. trim ( ) ) {
133+ applyTopLevelRenameArray ( nav , 'Modules' , renameModulesTo . trim ( ) ) ;
134+ }
135+
136+ hoistOtherCategoryInArray ( nav as TNavNode [ ] , fallback , topLevelGroup ) ;
137+
138+ if ( topLevelOrder ?. length ) {
139+ applyTopLevelOrderingArray ( nav as TNavNode [ ] , topLevelOrder ) ;
140+ }
141+
142+ applyNestedOrderingArray ( nav as TNavNode [ ] , typedNestedOrder ) ;
143+ } else if ( nav && typeof nav === 'object' ) {
144+ if ( renameModulesTo ?. trim ( ) && Array . isArray ( nav . children ) ) {
145+ applyTopLevelRenameArray (
146+ nav . children ,
147+ 'Modules' ,
148+ renameModulesTo . trim ( )
149+ ) ;
150+ }
151+
152+ hoistOtherCategoryInNav ( nav as TNavNode , fallback ) ;
153+ if (
154+ ( nav as TNavNode ) . children &&
155+ topLevelOrder &&
156+ topLevelOrder . length
157+ ) {
158+ applyTopLevelOrderingNode ( nav as TNavNode , topLevelOrder ) ;
159+ }
160+ applyNestedOrderingNode ( nav as TNavNode , typedNestedOrder ) ;
161+ }
162+ return nav ;
163+ } ;
164+ } ) ;
165+
20166 // Need the Meta Tags to be inserted first, or it causes issues with the navigation sidebar
21167 app . renderer . hooks . on ( 'head.begin' , ( ) => (
22168 < >
@@ -119,14 +265,8 @@ export function load(app: Application) {
119265 </ >
120266 ) ) ;
121267
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 = ( ) => {
268+ app . renderer . on ( RendererEvent . END , ( ) => {
269+ const baseAssetsPath = '../../documentation/assets' ;
130270 const filesToCopy = [
131271 'css/docs-style.css' ,
132272 'css/main-new.css' ,
@@ -145,19 +285,28 @@ export function load(app: Application) {
145285 ] ;
146286
147287 filesToCopy . forEach ( ( filePath ) => {
148- const file = createFileCopyEntry ( filePath ) ;
288+ const file = {
289+ from : resolve ( __dirname , `${ baseAssetsPath } /${ filePath } ` ) ,
290+ to : resolve ( app . options . getValue ( 'out' ) , `assets/${ filePath } ` ) ,
291+ } ;
149292 cpSync ( file . from , file . to ) ;
150293 } ) ;
151294
152295 const darkModeJs = {
153296 from : resolve ( __dirname , '../../documentation/dist/dark-mode.js' ) ,
154297 to : resolve ( app . options . getValue ( 'out' ) , 'assets/vars/dark-mode.js' ) ,
155298 } ;
299+ // Restore original to avoid side effects
300+ const theme = app . renderer . theme as DefaultTheme | undefined ;
301+ if ( theme && originalMethodName && originalMethod ) {
302+ theme [ originalMethodName ] = originalMethod ;
303+ }
304+ originalMethod = null ;
156305
157306 cpSync ( darkModeJs . from , darkModeJs . to ) ;
158- } ;
307+ } ) ;
159308
160- app . renderer . on ( RendererEvent . END , onRenderEnd ) ;
309+ app . renderer . defineRouter ( 'kebab' , KebabRouter ) ;
161310
162311 app . converter . on ( Converter . EVENT_CREATE_DECLARATION , insertCustomComments ) ;
163- }
312+ } ;
0 commit comments