@@ -19,94 +19,88 @@ function segmentToTitle(segment: string, prevSegment: string | null) {
1919 return result === 'Index' ? 'Introduction' : result ;
2020}
2121
22+ interface DirMeta {
23+ title ?: string ;
24+ collapsed ?: boolean ;
25+ draft ?: boolean ;
26+ /** Ordered list of child slugs (filenames without .md, or directory names). Unlisted items sort alphabetically after listed ones. */
27+ items ?: string [ ] ;
28+ }
29+
2230interface WarpDriveFrontMatter {
23- categoryTitle ?: string ;
2431 title ?: string ;
25- categoryOrder ?: number ;
26- order ?: number ;
2732 draft ?: boolean ;
28- collapsed ?: boolean ;
2933}
34+
3035interface GuideGroup {
31- /**
32- * The Text To Display
33- */
3436 text : string ;
35- /**
36- * The Path For This group
37- * "On Disc".
38- */
3937 path : string ;
40- /**
41- * The URL Slug For This group
42- * if different from the path.
43- *
44- * This is currently unused but is set
45- * by the frontmatter of an `index.md` file
46- * in the directory.
47- */
4838 slug : string ;
49- /**
50- * This will be the categoryIndex specified by
51- * the frontmatter of an `index.md` file in the directory.
52- *
53- * Else it will be set to the next open index available
54- * once "known" indeces have been assigned.
55- */
56- index : number | null ;
57- /**
58- * Whether the directory should default to open or closed.
59- *
60- * This is set by the frontmatter of an `index.md` file in the directory.
61- * else by config above in this file, and defaults to `true`.
62- */
39+ /** Items list from this directory's _meta.json, used to sort this group's children. */
40+ orderedItems ?: string [ ] ;
6341 collapsed : boolean | null ;
64- /**
65- * The child items/groups of this group, if any.
66- */
6742 items : Record < string , GuideGroup > ;
68- /**
69- *
70- */
7143 link ?: string ;
7244}
7345
46+ function normPath ( p : string ) : string {
47+ return p . split ( path . sep ) . join ( '/' ) ;
48+ }
49+
7450export async function getGuidesStructure ( ) {
7551 const GuidesDirectoryPath = path . join ( __dirname , '../docs.warp-drive.io/guides' ) ;
52+
53+ // Load all _meta.json files up front; keys are forward-slash dir paths relative to GuidesDirectoryPath
54+ const metaFiles = globSync ( '**/_meta.json' , { cwd : GuidesDirectoryPath } ) ;
55+ const dirMeta = new Map < string , DirMeta > ( ) ;
56+ for ( const metaFile of metaFiles ) {
57+ const dirPath = path . dirname ( metaFile ) ;
58+ const key = dirPath === '.' ? '' : normPath ( dirPath ) ;
59+ dirMeta . set ( key , JSON . parse ( readFileSync ( path . join ( GuidesDirectoryPath , metaFile ) , 'utf-8' ) ) as DirMeta ) ;
60+ }
61+
7662 const glob = globSync ( '**/*.md' , { cwd : GuidesDirectoryPath } ) ;
7763 const groups : Record < string , GuideGroup > = { } ;
7864
7965 for ( const filepath of glob ) {
80- const slugPath = [ ] ;
66+ const slugPath : string [ ] = [ ] ;
8167 const text = readFileSync ( path . join ( GuidesDirectoryPath , filepath ) , 'utf-8' ) ;
8268 const frontMatter = fm < WarpDriveFrontMatter > ( text ) ;
8369
8470 if ( frontMatter . attributes . draft ) {
85- // skip hidden files
71+ continue ;
72+ }
73+
74+ // Skip files whose immediate parent directory is marked draft in _meta.json
75+ const fileDir = normPath ( path . dirname ( filepath ) ) ;
76+ if ( fileDir !== '.' && dirMeta . get ( fileDir ) ?. draft ) {
8677 continue ;
8778 }
8879
8980 if ( filepath === 'index.md' ) {
90- groups [ 'the-manual' ] = groups [ 'the-manual' ] || {
91- text : frontMatter . attributes . categoryTitle ! ,
92- path : filepath ,
93- slug : filepath ,
94- index : frontMatter . attributes . categoryOrder || 0 ,
95- collapsed : frontMatter . attributes . collapsed || true ,
81+ const rootMeta = dirMeta . get ( '' ) ?? { } ;
82+ const theManualMeta = dirMeta . get ( 'the-manual' ) ?? { } ;
83+ groups [ 'the-manual' ] = groups [ 'the-manual' ] ?? {
84+ text : rootMeta . title ?? 'The Manual' ,
85+ path : 'the-manual' ,
86+ slug : 'the-manual' ,
87+ orderedItems : theManualMeta . items ,
88+ collapsed : rootMeta . collapsed ?? true ,
9689 link : '/guides/index.md' ,
9790 items : { } ,
9891 } ;
9992 Object . assign ( groups [ 'the-manual' ] , {
100- text : frontMatter . attributes . categoryTitle ! ,
101- index : frontMatter . attributes . categoryOrder || 0 ,
102- collapsed : frontMatter . attributes . collapsed || true ,
93+ text : rootMeta . title ?? 'The Manual' ,
94+ path : 'the-manual' ,
95+ slug : 'the-manual' ,
96+ orderedItems : theManualMeta . items ,
97+ collapsed : rootMeta . collapsed ?? true ,
10398 link : '/guides/index.md' ,
10499 } ) ;
105- groups [ 'the-manual' ] . items [ filepath ] = {
106- text : frontMatter . attributes . title ! ,
107- path : filepath ,
108- slug : filepath ,
109- index : frontMatter . attributes . order ?? 0 ,
100+ groups [ 'the-manual' ] . items [ 'index.md' ] = {
101+ text : frontMatter . attributes . title ?? 'Introduction' ,
102+ path : 'index.md' ,
103+ slug : 'index.md' ,
110104 collapsed : false ,
111105 items : { } ,
112106 link : '/guides/index.md' ,
@@ -119,7 +113,6 @@ export async function getGuidesStructure() {
119113 let isIndex = false ;
120114
121115 if ( lastSegment === 'index.md' ) {
122- // we treat index files as the main entry to any guides directory
123116 lastSegment = segments . pop ( ) ! ;
124117
125118 if ( ! lastSegment ) {
@@ -130,22 +123,27 @@ export async function getGuidesStructure() {
130123 }
131124
132125 let group = groups ;
133- let parent = null ;
126+ let parent : GuideGroup | null = null ;
134127
135- // build out nodes for each segment
136- // if there is not one yet.
137128 for ( let i = 0 ; i < segments . length ; i ++ ) {
138129 const prevSegment = i > 0 ? segments [ i - 1 ] : null ;
139130 const segment = segments [ i ] ;
140131 slugPath . push ( segment ) ;
141132 const key = slugPath . join ( '.' ) ;
142- const collapsed = AlwaysOpenGroups . includes ( key ) ? null : DefaultOpenGroups . includes ( key ) ? false : true ;
133+ const segmentMeta = dirMeta . get ( normPath ( slugPath . join ( path . sep ) ) ) ?? { } ;
134+ const collapsed =
135+ segmentMeta . collapsed !== undefined
136+ ? segmentMeta . collapsed
137+ : AlwaysOpenGroups . includes ( key )
138+ ? null
139+ : DefaultOpenGroups . includes ( key )
140+ ? false
141+ : true ;
143142
144- // setup a nested segment if we don't already have one
145143 if ( ! group [ segment ] ) {
146144 group [ segment ] = {
147- text : segmentToTitle ( segment , prevSegment ) ,
148- index : null ,
145+ text : segmentMeta . title ?? segmentToTitle ( segment , prevSegment ) ,
146+ orderedItems : segmentMeta . items ,
149147 path : segment ,
150148 slug : segment ,
151149 collapsed,
@@ -161,71 +159,58 @@ export async function getGuidesStructure() {
161159 const key = slugPath . join ( '.' ) ;
162160 const realUrl = `/guides/${ filepath } ` ;
163161
164- // setup our leaf-most segment for this file
165- // if needed, it may exist from a child directory already
166162 if ( ! group [ lastSegment ] ) {
163+ const leafMeta = dirMeta . get ( normPath ( slugPath . join ( path . sep ) ) ) ?? { } ;
167164 group [ lastSegment ] = {
168- text : segmentToTitle ( lastSegment , parent ? parent . path : null ) ,
169- index : null ,
165+ text : leafMeta . title ?? segmentToTitle ( lastSegment , parent ? parent . path : null ) ,
166+ orderedItems : leafMeta . items ,
170167 path : lastSegment ,
171168 slug : lastSegment ,
172- collapsed : AlwaysOpenGroups . includes ( key ) ? null : DefaultOpenGroups . includes ( key ) ? false : true ,
169+ collapsed :
170+ leafMeta . collapsed !== undefined
171+ ? leafMeta . collapsed
172+ : AlwaysOpenGroups . includes ( key )
173+ ? null
174+ : DefaultOpenGroups . includes ( key )
175+ ? false
176+ : true ,
173177 items : { } ,
174- // if we are an index file, this has the effect of setting the link on the parent node
175- // this seems to work even though there's an issue
176- // that says it doesn't: https://github.com/vuejs/vitepress/issues/2989
177- // however:
178- // when doing this, the "next page" feature breaks for
179- // these pages, so for now we just do non-clickable headers.
180178 link : realUrl ,
181179 } ;
182180 } else {
183- // the segment was previously generated from a file in a child directory on the same path.
184- // we need to add in the link.
185181 group [ lastSegment ] . link = realUrl ;
186182 }
187183
188- // update the leaf-most segment with any frontmatter info
189184 const leaf = group [ lastSegment ] ! ;
190185
191- // if the leaf is the index, we need to update the category entry
192- // and then generate an item entry for it.
193186 if ( isIndex ) {
194- if ( 'collapsed' in frontMatter . attributes ) {
195- leaf . collapsed = frontMatter . attributes . collapsed ! ;
196- }
197- if ( 'categoryOrder' in frontMatter . attributes ) {
198- leaf . index = frontMatter . attributes . categoryOrder ! ;
199- }
200- if ( 'categoryTitle' in frontMatter . attributes ) {
201- leaf . text = frontMatter . attributes . categoryTitle ! ;
202- }
187+ const leafDirPath = normPath ( slugPath . join ( path . sep ) ) ;
188+ const leafMeta = dirMeta . get ( leafDirPath ) ?? { } ;
189+
190+ if ( leafMeta . draft ) continue ;
191+
192+ // _meta.json is authoritative for category metadata; always apply it
193+ if ( leafMeta . title !== undefined ) leaf . text = leafMeta . title ;
194+ if ( leafMeta . collapsed !== undefined ) leaf . collapsed = leafMeta . collapsed ;
195+ leaf . orderedItems = leafMeta . items ;
203196
204- // generate the entry for the file itself unless we are a top-level index file
205197 leaf . items [ 'index.md' ] = {
206198 path : 'index.md' ,
207199 slug : 'index.md' ,
208200 collapsed : false ,
209201 text : frontMatter . attributes . title ?? 'Overview' ,
210- index : frontMatter . attributes . order ?? 0 ,
211- link : group [ lastSegment ] ! . link ,
202+ link : group [ lastSegment ] ! . link ! ,
212203 items : { } ,
213204 } ;
214205 } else {
215- // update the leaf's title and order
216206 if ( frontMatter . attributes . title ) {
217207 leaf . text = frontMatter . attributes . title ;
218208 }
219- if ( 'order' in frontMatter . attributes ) {
220- leaf . index = frontMatter . attributes . order ! ;
221- }
222209 }
223210 }
224211
225- // deep iterate converting items objects to arrays
226- const result = deepConvert ( groups ) ;
227- // console.log(JSON.stringify(result, null, 2));
228- // console.log(JSON.stringify(rewritten, null, 2));
212+ const rootMeta = dirMeta . get ( '' ) ?? { } ;
213+ const result = deepConvert ( groups , rootMeta . items ) ;
229214 const structure = { paths : result } ;
230215
231216 writeFileSync (
@@ -240,50 +225,51 @@ export async function getGuidesStructure() {
240225 return { paths : result } ;
241226}
242227
243- function deepConvert ( obj : Record < string , any > ) {
228+ function deepConvert ( obj : Record < string , any > , orderedItems ?: string [ ] ) {
244229 const groups = Array . from ( Object . values ( obj ) ) ;
245- const sortedGroups = new Array ( groups . length ) . fill ( null ) ;
246230
247231 for ( const group of groups ) {
248- if ( group . index !== null ) {
249- if ( group . index < 0 || group . index >= groups . length ) {
250- throw new Error ( `Invalid index ${ group . index } for ${ group . path } , must be between 0 and ${ groups . length - 1 } ` ) ;
251- }
252- if ( sortedGroups [ group . index ] !== null ) {
253- throw new Error ( `Duplicate index ${ group . index } for ${ group . path } , matches ${ sortedGroups [ group . index ] } ` ) ;
254- }
255- sortedGroups [ group . index ] = group ;
256- }
257-
258- delete group . path ;
259- delete group . slug ;
260-
261232 if ( group . items ) {
262233 if ( Object . keys ( group . items ) . length === 0 ) {
263234 delete group . items ;
264235 delete group . collapsed ;
265236 } else {
266- group . items = deepConvert ( group . items ) ;
237+ // Each group carries orderedItems from its own _meta.json for sorting its children
238+ group . items = deepConvert ( group . items , group . orderedItems ) ;
267239
268- if ( ! group . link && ! group . items [ 0 ] . items ) {
269- group . link = group . items [ 0 ] . link ;
240+ if ( ! group . link && ! group . items [ 0 ] ? .items ) {
241+ group . link = group . items [ 0 ] ? .link ;
270242 }
271243 }
272244 }
245+ delete group . orderedItems ;
273246 }
274247
275- for ( const group of groups ) {
276- if ( group . index === null ) {
277- // find the first null index and insert
278- const firstNullIndex = sortedGroups . findIndex ( ( g ) => g === null ) ;
279- if ( firstNullIndex !== - 1 ) {
280- sortedGroups [ firstNullIndex ] = group ;
281- group . index = firstNullIndex ;
282- }
248+ // index.md synthetic entries always first; otherwise sort by orderedItems list, then alphabetically
249+ groups . sort ( ( a , b ) => {
250+ if ( a . slug === 'index.md' ) return - 1 ;
251+ if ( b . slug === 'index.md' ) return 1 ;
252+
253+ if ( orderedItems ?. length ) {
254+ const aKey = ( a . slug ?? '' ) . replace ( / \. m d $ / , '' ) ;
255+ const bKey = ( b . slug ?? '' ) . replace ( / \. m d $ / , '' ) ;
256+ const aIdx = orderedItems . indexOf ( aKey ) ;
257+ const bIdx = orderedItems . indexOf ( bKey ) ;
258+ if ( aIdx === - 1 && bIdx === - 1 ) return ( a . text ?? '' ) . localeCompare ( b . text ?? '' ) ;
259+ if ( aIdx === - 1 ) return 1 ;
260+ if ( bIdx === - 1 ) return - 1 ;
261+ return aIdx - bIdx ;
283262 }
263+
264+ return ( a . text ?? '' ) . localeCompare ( b . text ?? '' ) ;
265+ } ) ;
266+
267+ for ( const group of groups ) {
268+ delete group . path ;
269+ delete group . slug ;
284270 }
285271
286- return sortedGroups ;
272+ return groups ;
287273}
288274
289275type SidebarItem = { text : string ; items ?: SidebarItem [ ] ; link ?: string ; collapsed ?: boolean } ;
0 commit comments