@@ -7,14 +7,29 @@ import TurndownService, { Options, Node as TurndownNode } from "turndown";
77
88const { gfm } = require ( "@joplin/turndown-plugin-gfm" ) ;
99
10+ interface PluginOptionGroupMatcher {
11+ matcher : string ;
12+ position ?: number ;
13+ title ?: string ;
14+ }
15+
16+ interface PluginOptionGroup {
17+ matchers : PluginOptionGroupMatcher [ ] ;
18+ parentPath : string ;
19+ title : string ;
20+ }
21+
1022interface PluginOptions {
1123 docsDir : string ;
1224 // An optional description appended after the title
1325 description ?: string ;
14- // Additional path to ignore in addition to the categories
26+ // Additional paths to ignore, in addition to the categories.
1527 ignorePaths ?: string [ ] ;
16- // A title for the links that are identified at the root. By default: General
17- generalCategoryTitle ?: string ;
28+ // A title for the links that are identified at the root. By default: Miscellaneous
29+ miscCategoryTitle ?: string ;
30+ // A custom list of groups. Matching routes will be listed under these.
31+ // Particularly useful to reorganize routes available at the root.
32+ groups ?: PluginOptionGroup [ ] ;
1833}
1934
2035/**
@@ -36,7 +51,8 @@ export default function docusaurusPluginLLMs(
3651 docsDir,
3752 description,
3853 ignorePaths = [ ] ,
39- generalCategoryTitle = "General"
54+ miscCategoryTitle = "Miscellaneous" ,
55+ groups = [ ]
4056 } = {
4157 docsDir : "docs" ,
4258 ...userOptions
@@ -51,7 +67,7 @@ export default function docusaurusPluginLLMs(
5167 ) === undefined
5268 ) ;
5369
54- // Group
70+ // Group routes
5571 const groupedRoutes = allRoutes . reduce < GroupedRoutes > ( ( acc , path ) => {
5672 if ( path . endsWith ( "/" ) ) {
5773 const category = path . slice ( 0 , - 1 ) ;
@@ -64,17 +80,95 @@ export default function docusaurusPluginLLMs(
6480 } ;
6581 }
6682
67- const category = dirname ( path ) ;
83+ const resolveGroup = ( ) :
84+ | {
85+ group : PluginOptionGroup ;
86+ matcher : PluginOptionGroupMatcher ;
87+ }
88+ | undefined => {
89+ for ( const { matchers, ...rest } of groups ) {
90+ const matcher = matchers . find ( ( { matcher } ) =>
91+ path . includes ( matcher )
92+ ) ;
93+
94+ if ( matcher !== undefined ) {
95+ return { group : { matchers, ...rest } , matcher } ;
96+ }
97+ }
98+
99+ return undefined ;
100+ } ;
101+
102+ const matchingGroup = resolveGroup ( ) ;
103+
104+ if ( matchingGroup === undefined ) {
105+ const groupPath = dirname ( path ) ;
106+
107+ return {
108+ ...acc ,
109+ [ groupPath ] : {
110+ ...( acc [ groupPath ] ?? { } ) ,
111+ children : [ ...( acc [ groupPath ] ?. children ?? [ ] ) , { path } ]
112+ }
113+ } ;
114+ }
115+
116+ const {
117+ group : { parentPath, title, matchers } ,
118+ matcher : { title : matcherTitle }
119+ } = matchingGroup ;
120+
121+ const children = [
122+ ...( acc [ parentPath ] ?. children ?? [ ] ) ,
123+ { path, ...( matcherTitle !== undefined && { title : matcherTitle } ) }
124+ ] . sort ( ( { path : pathA } , { path : pathB } ) => {
125+ const findPosition = ( childPath : string ) : number | undefined =>
126+ matchers . find ( ( { matcher } ) => childPath . includes ( matcher ) )
127+ ?. position ;
128+
129+ const positionPathA = findPosition ( pathA ) ;
130+ const positionPathB = findPosition ( pathB ) ;
131+
132+ return ( positionPathA ?? 0 ) - ( positionPathB ?? 0 ) ;
133+ } ) ;
68134
69135 return {
70136 ...acc ,
71- [ category ] : {
72- ...( acc [ category ] ?? { } ) ,
73- children : [ ...( acc [ category ] ?. children ?? [ ] ) , path ]
137+ [ parentPath ] : {
138+ ...( acc [ parentPath ] ?? { } ) ,
139+ ...( title !== undefined && { title } ) ,
140+ children
74141 }
75142 } ;
76143 } , { } ) ;
77144
145+ const sortedGroupedRoutes = Object . entries ( groupedRoutes ) . sort (
146+ ( [ keyA , _ ] , [ keyB , __ ] ) => {
147+ // We want groups first.
148+ const isGroup = ( key : string ) : boolean =>
149+ groups . find ( ( { parentPath } ) => parentPath === key ) !== undefined ;
150+
151+ if ( isGroup ( keyA ) ) {
152+ return - 1 ;
153+ }
154+
155+ if ( isGroup ( keyB ) ) {
156+ return 1 ;
157+ }
158+
159+ // We want "Misc" at the end.
160+ if ( keyA === `/${ docsDir } ` ) {
161+ return 1 ;
162+ }
163+
164+ if ( keyB === `/${ docsDir } ` ) {
165+ return - 1 ;
166+ }
167+
168+ return keyA . localeCompare ( keyB ) ;
169+ }
170+ ) ;
171+
78172 // Prepare markdown content and path
79173
80174 const { outDir, siteConfig } = context ;
@@ -100,17 +194,17 @@ export default function docusaurusPluginLLMs(
100194
101195 // Create /llms.txt
102196 await generateLlmsTxt ( {
103- groupedRoutes,
197+ groupedRoutes : sortedGroupedRoutes ,
104198 dataRoutes,
105199 description,
106200 docsDir,
107- generalCategoryTitle ,
201+ miscCategoryTitle ,
108202 ...context
109203 } ) ;
110204
111205 // Create /llms-full.txt
112206 await generateLlmsTxtFull ( {
113- groupedRoutes,
207+ groupedRoutes : sortedGroupedRoutes ,
114208 dataRoutes,
115209 description,
116210 docsDir,
@@ -120,7 +214,19 @@ export default function docusaurusPluginLLMs(
120214 } ;
121215}
122216
123- type GroupedRoutes = Record < string , { children : string [ ] } > ;
217+ interface GroupedRouteChild {
218+ path : string ;
219+ title ?: string ;
220+ }
221+
222+ interface GroupedRoute {
223+ title ?: string ;
224+ children : GroupedRouteChild [ ] ;
225+ }
226+
227+ type GroupedRoutes = Record < string , GroupedRoute > ;
228+
229+ type SortedGroupedRoutes = [ string , GroupedRoute ] [ ] ;
124230
125231interface RouteMarkdownData {
126232 relativePath : string ;
@@ -309,15 +415,18 @@ const generateLlmsTxt = async ({
309415 outDir,
310416 description,
311417 docsDir,
312- generalCategoryTitle
418+ miscCategoryTitle
313419} : {
314- groupedRoutes : GroupedRoutes ;
420+ groupedRoutes : SortedGroupedRoutes ;
315421 dataRoutes : RoutesData ;
316422} & Pick < LoadContext , "siteConfig" | "outDir" > &
317423 Pick < PluginOptions , "description" | "docsDir" > &
318- Required < Pick < PluginOptions , "generalCategoryTitle" > > ) => {
319- const buildLink = ( route : string ) : string | undefined => {
320- const data = dataRoutes . get ( route ) ;
424+ Required < Pick < PluginOptions , "miscCategoryTitle" > > ) => {
425+ const buildLink = ( {
426+ path,
427+ title : customTitle
428+ } : GroupedRouteChild ) : string | undefined => {
429+ const data = dataRoutes . get ( path ) ;
321430
322431 if ( data === undefined ) {
323432 return undefined ;
@@ -328,27 +437,24 @@ const generateLlmsTxt = async ({
328437 metadata : { title, description }
329438 } = data ;
330439
331- return `- [${ title ?? "" } ](${ url } ${ relativePath } )${ description !== undefined && description !== "" ? `: ${ description } ` : "" } ` ;
440+ return `- [${ customTitle ?? title ?? "" } ](${ url } ${ relativePath } )${ description !== undefined && description !== "" ? `: ${ description } ` : "" } ` ;
332441 } ;
333442
334- const buildTitle = ( key : string ) => {
443+ const buildTitle = ( { key, title } : { key : string ; title ?: string } ) => {
335444 if ( key === `/${ docsDir } ` ) {
336- return `## ${ generalCategoryTitle } ` ;
445+ return `## ${ miscCategoryTitle } ` ;
337446 }
338447
339- const titles = key
340- . replaceAll ( `/${ docsDir } /` , "" )
341- . split ( "/" )
342- . map ( capitalize )
343- . join ( " - " ) ;
448+ const titles =
449+ title ??
450+ key . replaceAll ( `/${ docsDir } /` , "" ) . split ( "/" ) . map ( capitalize ) . join ( " - " ) ;
344451
345452 return `## ${ titles } ` ;
346453 } ;
347454
348- const content = Object . entries ( groupedRoutes )
349- . sort ( ( [ keyA , _ ] , [ keyB , __ ] ) => keyA . localeCompare ( keyB ) )
455+ const content = groupedRoutes
350456 . map (
351- ( [ key , { children } ] ) => `${ buildTitle ( key ) }
457+ ( [ key , { children, title } ] ) => `${ buildTitle ( { key, title } ) }
352458
353459${ children . map ( buildLink ) . join ( "\n" ) } `
354460 )
@@ -370,12 +476,12 @@ const generateLlmsTxtFull = async ({
370476 outDir,
371477 description
372478} : {
373- groupedRoutes : GroupedRoutes ;
479+ groupedRoutes : SortedGroupedRoutes ;
374480 dataRoutes : RoutesData ;
375481} & Pick < LoadContext , "siteConfig" | "outDir" > &
376482 Pick < PluginOptions , "description" | "docsDir" > ) => {
377- const buildMarkdown = ( route : string ) : string | undefined => {
378- const data = dataRoutes . get ( route ) ;
483+ const buildMarkdown = ( { path } : GroupedRouteChild ) : string | undefined => {
484+ const data = dataRoutes . get ( path ) ;
379485
380486 if ( data === undefined ) {
381487 return undefined ;
@@ -388,8 +494,7 @@ const generateLlmsTxtFull = async ({
388494 return markdown ;
389495 } ;
390496
391- const content = Object . entries ( groupedRoutes )
392- . sort ( ( [ keyA , _ ] , [ keyB , __ ] ) => keyA . localeCompare ( keyB ) )
497+ const content = groupedRoutes
393498 . map ( ( [ _ , { children } ] ) => children . map ( buildMarkdown ) . join ( "\n\n" ) )
394499 . join ( "\n\n" ) ;
395500
0 commit comments