@uniweb/build 0.6.16 → 0.6.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/i18n/index.js +13 -12
- package/src/i18n/sync.js +37 -1
- package/src/site/content-collector.js +292 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.18",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"sharp": "^0.33.2"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"@uniweb/runtime": "0.5.21",
|
|
54
53
|
"@uniweb/schemas": "0.2.1",
|
|
54
|
+
"@uniweb/runtime": "0.5.21",
|
|
55
55
|
"@uniweb/content-reader": "1.1.2"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
package/src/i18n/index.js
CHANGED
|
@@ -169,20 +169,17 @@ async function resolveLocales(configLocales, localesPath) {
|
|
|
169
169
|
/**
|
|
170
170
|
* Extract manifest from site content and write to file
|
|
171
171
|
* @param {string} siteRoot - Site root directory
|
|
172
|
+
* @param {Object} siteContent - Collected site content (from collectSiteContent)
|
|
172
173
|
* @param {Object} options - Options
|
|
173
174
|
* @returns {Object} { manifest, report }
|
|
174
175
|
*/
|
|
175
|
-
export async function extractManifest(siteRoot, options = {}) {
|
|
176
|
+
export async function extractManifest(siteRoot, siteContent, options = {}) {
|
|
176
177
|
const {
|
|
177
178
|
localesDir = DEFAULTS.localesDir,
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
verbose = false,
|
|
180
|
+
dryRun = false
|
|
180
181
|
} = options
|
|
181
182
|
|
|
182
|
-
// Load site content
|
|
183
|
-
const siteContentRaw = await readFile(siteContentPath, 'utf-8')
|
|
184
|
-
const siteContent = JSON.parse(siteContentRaw)
|
|
185
|
-
|
|
186
183
|
// Extract translatable content
|
|
187
184
|
const manifest = extractTranslatableContent(siteContent)
|
|
188
185
|
|
|
@@ -203,8 +200,10 @@ export async function extractManifest(siteRoot, options = {}) {
|
|
|
203
200
|
// Generate sync report
|
|
204
201
|
const report = syncManifests(previousManifest, manifest)
|
|
205
202
|
|
|
206
|
-
// Write new manifest
|
|
207
|
-
|
|
203
|
+
// Write new manifest (skip in dry-run mode)
|
|
204
|
+
if (!dryRun) {
|
|
205
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2))
|
|
206
|
+
}
|
|
208
207
|
|
|
209
208
|
if (verbose) {
|
|
210
209
|
console.log(formatSyncReport(report))
|
|
@@ -222,7 +221,7 @@ export async function extractManifest(siteRoot, options = {}) {
|
|
|
222
221
|
* @returns {Promise<Object>} { manifest, report }
|
|
223
222
|
*/
|
|
224
223
|
export async function extractCollectionManifest(siteRoot, options = {}) {
|
|
225
|
-
const { localesDir = DEFAULTS.localesDir } = options
|
|
224
|
+
const { localesDir = DEFAULTS.localesDir, dryRun = false } = options
|
|
226
225
|
|
|
227
226
|
// Extract translatable content from collections
|
|
228
227
|
const manifest = await extractCollectionContent(siteRoot)
|
|
@@ -249,8 +248,10 @@ export async function extractCollectionManifest(siteRoot, options = {}) {
|
|
|
249
248
|
// Generate sync report
|
|
250
249
|
const report = syncManifests(previousManifest, manifest)
|
|
251
250
|
|
|
252
|
-
// Write new manifest
|
|
253
|
-
|
|
251
|
+
// Write new manifest (skip in dry-run mode)
|
|
252
|
+
if (!dryRun) {
|
|
253
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2))
|
|
254
|
+
}
|
|
254
255
|
|
|
255
256
|
return { manifest, report }
|
|
256
257
|
}
|
package/src/i18n/sync.js
CHANGED
|
@@ -143,6 +143,15 @@ export function formatSyncReport(report) {
|
|
|
143
143
|
|
|
144
144
|
if (report.moved.length > 0) {
|
|
145
145
|
lines.push(` ↻ ${report.moved.length} strings moved (contexts updated)`)
|
|
146
|
+
for (const item of report.moved.slice(0, 5)) {
|
|
147
|
+
const preview = truncate(item.source, 40)
|
|
148
|
+
const oldCtx = formatContext(item.previousContexts?.[0])
|
|
149
|
+
const newCtx = formatContext(item.currentContexts?.[0])
|
|
150
|
+
lines.push(` - "${preview}" ${oldCtx} → ${newCtx}`)
|
|
151
|
+
}
|
|
152
|
+
if (report.moved.length > 5) {
|
|
153
|
+
lines.push(` ... and ${report.moved.length - 5} more`)
|
|
154
|
+
}
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
if (report.changed.length > 0) {
|
|
@@ -150,7 +159,7 @@ export function formatSyncReport(report) {
|
|
|
150
159
|
for (const item of report.changed.slice(0, 5)) {
|
|
151
160
|
const prevPreview = truncate(item.previousSource, 30)
|
|
152
161
|
const currPreview = truncate(item.source, 30)
|
|
153
|
-
lines.push(` -
|
|
162
|
+
lines.push(` - "${prevPreview}" → "${currPreview}"`)
|
|
154
163
|
}
|
|
155
164
|
if (report.changed.length > 5) {
|
|
156
165
|
lines.push(` ... and ${report.changed.length - 5} more`)
|
|
@@ -159,15 +168,42 @@ export function formatSyncReport(report) {
|
|
|
159
168
|
|
|
160
169
|
if (report.added.length > 0) {
|
|
161
170
|
lines.push(` + ${report.added.length} new strings`)
|
|
171
|
+
for (const item of report.added.slice(0, 5)) {
|
|
172
|
+
const preview = truncate(item.source, 40)
|
|
173
|
+
const ctx = formatContext(item.contexts?.[0])
|
|
174
|
+
lines.push(` - "${preview}" ${ctx}`)
|
|
175
|
+
}
|
|
176
|
+
if (report.added.length > 5) {
|
|
177
|
+
lines.push(` ... and ${report.added.length - 5} more`)
|
|
178
|
+
}
|
|
162
179
|
}
|
|
163
180
|
|
|
164
181
|
if (report.removed.length > 0) {
|
|
165
182
|
lines.push(` - ${report.removed.length} strings removed`)
|
|
183
|
+
for (const item of report.removed.slice(0, 5)) {
|
|
184
|
+
const preview = truncate(item.source, 40)
|
|
185
|
+
const ctx = formatContext(item.contexts?.[0])
|
|
186
|
+
lines.push(` - "${preview}" ${ctx}`)
|
|
187
|
+
}
|
|
188
|
+
if (report.removed.length > 5) {
|
|
189
|
+
lines.push(` ... and ${report.removed.length - 5} more`)
|
|
190
|
+
}
|
|
166
191
|
}
|
|
167
192
|
|
|
168
193
|
return lines.join('\n')
|
|
169
194
|
}
|
|
170
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Format a context object for display
|
|
198
|
+
*/
|
|
199
|
+
function formatContext(context) {
|
|
200
|
+
if (!context) return ''
|
|
201
|
+
const location = context.page || context.collection || ''
|
|
202
|
+
const section = context.section || context.item || ''
|
|
203
|
+
if (!location && !section) return ''
|
|
204
|
+
return `(${location}:${section})`
|
|
205
|
+
}
|
|
206
|
+
|
|
171
207
|
/**
|
|
172
208
|
* Truncate string for display
|
|
173
209
|
*/
|
|
@@ -203,10 +203,12 @@ function isIgnoredFolder(name) {
|
|
|
203
203
|
/**
|
|
204
204
|
* Read folder configuration, determining content mode from config file presence.
|
|
205
205
|
*
|
|
206
|
-
* - folder.yml present →
|
|
207
|
-
* - page.yml present →
|
|
206
|
+
* - folder.yml present → folder mode (md files are child pages)
|
|
207
|
+
* - page.yml present → page mode (md files are sections of this page)
|
|
208
208
|
* - Neither → inherit mode from parent
|
|
209
209
|
*
|
|
210
|
+
* Internal mode values: 'pages' (folder mode), 'sections' (page mode)
|
|
211
|
+
*
|
|
210
212
|
* @param {string} dirPath - Directory path
|
|
211
213
|
* @param {string} inheritedMode - Mode inherited from parent ('sections' or 'pages')
|
|
212
214
|
* @returns {Promise<{config: Object, mode: string, source: string}>}
|
|
@@ -220,7 +222,7 @@ async function readFolderConfig(dirPath, inheritedMode) {
|
|
|
220
222
|
if (Object.keys(pageYml).length > 0) {
|
|
221
223
|
return { config: pageYml, mode: 'sections', source: 'page.yml' }
|
|
222
224
|
}
|
|
223
|
-
// Check for empty folder.yml (presence signals
|
|
225
|
+
// Check for empty folder.yml (presence signals folder mode even if empty)
|
|
224
226
|
if (existsSync(join(dirPath, 'folder.yml'))) {
|
|
225
227
|
return { config: {}, mode: 'pages', source: 'folder.yml' }
|
|
226
228
|
}
|
|
@@ -269,27 +271,134 @@ function compareFilenames(a, b) {
|
|
|
269
271
|
}
|
|
270
272
|
|
|
271
273
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
+
* Extract the name from a config array item.
|
|
275
|
+
* Handles both string entries ("hero") and object entries ({ features: [...] }).
|
|
276
|
+
* @param {*} item - Array item from sections: or pages: config
|
|
277
|
+
* @returns {string|null} The name, or null if not a valid entry
|
|
278
|
+
*/
|
|
279
|
+
function extractItemName(item) {
|
|
280
|
+
if (typeof item === 'string') return item
|
|
281
|
+
if (typeof item === 'object' && item !== null) {
|
|
282
|
+
const keys = Object.keys(item)
|
|
283
|
+
if (keys.length === 1) return keys[0]
|
|
284
|
+
}
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Parse a config array that may contain '...' rest markers.
|
|
290
|
+
*
|
|
291
|
+
* Returns structured info:
|
|
292
|
+
* - mode 'strict': no '...' — only listed items visible in navigation
|
|
293
|
+
* - mode 'inclusive': '...' present — pinned items + auto-discovered rest
|
|
294
|
+
* - mode 'all': array is just ['...'] — equivalent to omitting config
|
|
295
|
+
*
|
|
296
|
+
* @param {Array} arr - Config array (may contain '...' strings and/or objects)
|
|
297
|
+
* @returns {{ mode: 'strict'|'inclusive'|'all', before: Array, after: Array }|null}
|
|
298
|
+
*/
|
|
299
|
+
function parseWildcardArray(arr) {
|
|
300
|
+
if (!Array.isArray(arr) || arr.length === 0) return null
|
|
301
|
+
|
|
302
|
+
const firstRestIndex = arr.indexOf('...')
|
|
303
|
+
if (firstRestIndex === -1) {
|
|
304
|
+
return { mode: 'strict', before: [...arr], after: [] }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Find last '...' index
|
|
308
|
+
let lastRestIndex = firstRestIndex
|
|
309
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
310
|
+
if (arr[i] === '...') { lastRestIndex = i; break }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const before = arr.slice(0, firstRestIndex).filter(x => x !== '...')
|
|
314
|
+
const after = arr.slice(lastRestIndex + 1).filter(x => x !== '...')
|
|
315
|
+
|
|
316
|
+
if (before.length === 0 && after.length === 0) {
|
|
317
|
+
return { mode: 'all', before: [], after: [] }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { mode: 'inclusive', before, after }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Apply wildcard-aware ordering to a list of named items.
|
|
274
325
|
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
326
|
+
* - strict: listed items first in listed order, then unlisted (all items returned)
|
|
327
|
+
* - inclusive: before items, then rest (in existing order), then after items
|
|
328
|
+
* - all/null: return items unchanged
|
|
277
329
|
*
|
|
278
330
|
* @param {Array} items - Items with a .name property
|
|
279
|
-
* @param {Array
|
|
280
|
-
* @returns {Array} Reordered items
|
|
331
|
+
* @param {{ mode: string, before: Array, after: Array }|null} parsed - From parseWildcardArray
|
|
332
|
+
* @returns {Array} Reordered items
|
|
281
333
|
*/
|
|
282
|
-
function
|
|
283
|
-
if (!
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
334
|
+
function applyWildcardOrder(items, parsed) {
|
|
335
|
+
if (!parsed || parsed.mode === 'all') return items
|
|
336
|
+
|
|
337
|
+
const itemMap = new Map(items.map(i => [i.name, i]))
|
|
338
|
+
const beforeNames = parsed.before.map(extractItemName).filter(Boolean)
|
|
339
|
+
const afterNames = parsed.after.map(extractItemName).filter(Boolean)
|
|
340
|
+
const allPinnedNames = new Set([...beforeNames, ...afterNames])
|
|
341
|
+
|
|
342
|
+
const beforeItems = beforeNames.filter(n => itemMap.has(n)).map(n => itemMap.get(n))
|
|
343
|
+
const afterItems = afterNames.filter(n => itemMap.has(n)).map(n => itemMap.get(n))
|
|
344
|
+
const rest = items.filter(i => !allPinnedNames.has(i.name))
|
|
345
|
+
|
|
346
|
+
if (parsed.mode === 'strict') {
|
|
347
|
+
// Listed items first, then unlisted (hiding is applied separately)
|
|
348
|
+
return [...beforeItems, ...rest]
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Inclusive: before + rest + after
|
|
352
|
+
return [...beforeItems, ...rest, ...afterItems]
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Find the markdown file for a section name, handling numeric prefixes.
|
|
357
|
+
* Tries exact match first ("hero.md"), then prefix-based ("1-hero.md").
|
|
358
|
+
*
|
|
359
|
+
* @param {string} pagePath - Directory containing section files
|
|
360
|
+
* @param {string} sectionName - Logical section name (e.g., 'hero')
|
|
361
|
+
* @param {string[]} [cachedFiles] - Pre-read directory listing (optimization)
|
|
362
|
+
* @returns {{ filePath: string, stableName: string, prefix: string|null }|null}
|
|
363
|
+
*/
|
|
364
|
+
function findSectionFile(pagePath, sectionName, cachedFiles) {
|
|
365
|
+
const exactPath = join(pagePath, `${sectionName}.md`)
|
|
366
|
+
if (existsSync(exactPath)) {
|
|
367
|
+
return { filePath: exactPath, stableName: sectionName, prefix: null }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const files = cachedFiles || []
|
|
371
|
+
for (const file of files) {
|
|
372
|
+
if (!isMarkdownFile(file)) continue
|
|
373
|
+
const { name } = parse(file)
|
|
374
|
+
const { prefix, name: parsedName } = parseNumericPrefix(name)
|
|
375
|
+
if (parsedName === sectionName) {
|
|
376
|
+
return { filePath: join(pagePath, file), stableName: sectionName, prefix }
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return null
|
|
289
381
|
}
|
|
290
382
|
|
|
291
383
|
/**
|
|
292
|
-
*
|
|
384
|
+
* Extract a direct child's folder name from its route, relative to parentRoute.
|
|
385
|
+
* Returns null for the index page (route === parentRoute) or non-direct-children.
|
|
386
|
+
*
|
|
387
|
+
* @param {string} route - Page route (e.g., '/about')
|
|
388
|
+
* @param {string} parentRoute - Parent route (e.g., '/')
|
|
389
|
+
* @returns {string|null}
|
|
390
|
+
*/
|
|
391
|
+
function getDirectChildName(route, parentRoute) {
|
|
392
|
+
if (!route || route === parentRoute) return null
|
|
393
|
+
const prefix = parentRoute === '/' ? '/' : parentRoute + '/'
|
|
394
|
+
if (!route.startsWith(prefix)) return null
|
|
395
|
+
const rest = route.slice(prefix.length)
|
|
396
|
+
if (rest.includes('/')) return null
|
|
397
|
+
return rest
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Process a markdown file as a standalone page (folder mode).
|
|
293
402
|
* Creates a page with a single section from the markdown content.
|
|
294
403
|
*
|
|
295
404
|
* @param {string} filePath - Path to markdown file
|
|
@@ -480,6 +589,9 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
|
|
|
480
589
|
}
|
|
481
590
|
let lastModified = null
|
|
482
591
|
|
|
592
|
+
// Cache directory listing for prefix-based file resolution
|
|
593
|
+
const cachedFiles = await readdir(pagePath)
|
|
594
|
+
|
|
483
595
|
let index = 1
|
|
484
596
|
for (const item of sectionsConfig) {
|
|
485
597
|
let sectionName
|
|
@@ -505,13 +617,14 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
|
|
|
505
617
|
// Build section ID
|
|
506
618
|
const id = parentId ? `${parentId}.${index}` : String(index)
|
|
507
619
|
|
|
508
|
-
// Look for the markdown file
|
|
509
|
-
const
|
|
510
|
-
if (!
|
|
620
|
+
// Look for the markdown file (exact match or prefix-based, e.g., "hero" → "1-hero.md")
|
|
621
|
+
const found = findSectionFile(pagePath, sectionName, cachedFiles)
|
|
622
|
+
if (!found) {
|
|
511
623
|
console.warn(`[content-collector] Section file not found: ${sectionName}.md`)
|
|
512
624
|
index++
|
|
513
625
|
continue
|
|
514
626
|
}
|
|
627
|
+
const filePath = found.filePath
|
|
515
628
|
|
|
516
629
|
// Process the section
|
|
517
630
|
// Use sectionName as stable ID for scroll targeting (e.g., "hero", "features")
|
|
@@ -577,8 +690,9 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
577
690
|
|
|
578
691
|
// Check for explicit sections configuration
|
|
579
692
|
const { sections: sectionsConfig } = pageConfig
|
|
693
|
+
const sectionsParsed = Array.isArray(sectionsConfig) ? parseWildcardArray(sectionsConfig) : null
|
|
580
694
|
|
|
581
|
-
if (sectionsConfig === undefined || sectionsConfig === '*') {
|
|
695
|
+
if (sectionsConfig === undefined || sectionsConfig === '*' || sectionsParsed?.mode === 'all') {
|
|
582
696
|
// Default behavior: discover all .md files, sort by numeric prefix
|
|
583
697
|
const files = await readdir(pagePath)
|
|
584
698
|
const mdFiles = files.filter(isMarkdownFile).sort(compareFilenames)
|
|
@@ -607,8 +721,79 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
607
721
|
// Build hierarchy from dot notation
|
|
608
722
|
hierarchicalSections = buildSectionHierarchy(sections)
|
|
609
723
|
|
|
724
|
+
} else if (sectionsParsed?.mode === 'inclusive') {
|
|
725
|
+
// Inclusive: pinned sections + auto-discovered rest via '...' wildcard
|
|
726
|
+
const files = await readdir(pagePath)
|
|
727
|
+
const mdFiles = files.filter(isMarkdownFile).sort(compareFilenames)
|
|
728
|
+
|
|
729
|
+
// Build name → file info map from discovered files
|
|
730
|
+
const discoveredMap = new Map()
|
|
731
|
+
for (const file of mdFiles) {
|
|
732
|
+
const { name } = parse(file)
|
|
733
|
+
const { prefix, name: stableName } = parseNumericPrefix(name)
|
|
734
|
+
const key = stableName || name
|
|
735
|
+
if (!discoveredMap.has(key)) {
|
|
736
|
+
discoveredMap.set(key, { file, prefix, stableName: key })
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Create items with .name property for applyWildcardOrder
|
|
741
|
+
const allItems = [...discoveredMap.keys()].map(name => ({ name }))
|
|
742
|
+
const ordered = applyWildcardOrder(allItems, sectionsParsed)
|
|
743
|
+
|
|
744
|
+
// Collect subsection configs from the original array (e.g., { features: [a, b] })
|
|
745
|
+
const subsectionConfigs = new Map()
|
|
746
|
+
for (const item of [...sectionsParsed.before, ...sectionsParsed.after]) {
|
|
747
|
+
if (typeof item === 'object' && item !== null) {
|
|
748
|
+
const keys = Object.keys(item)
|
|
749
|
+
if (keys.length === 1) {
|
|
750
|
+
subsectionConfigs.set(keys[0], item[keys[0]])
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Process sections in wildcard-expanded order
|
|
756
|
+
const sections = []
|
|
757
|
+
let sectionIndex = 1
|
|
758
|
+
for (const { name } of ordered) {
|
|
759
|
+
const entry = discoveredMap.get(name)
|
|
760
|
+
if (!entry) {
|
|
761
|
+
console.warn(`[content-collector] Section '${name}' not found in ${pagePath}`)
|
|
762
|
+
continue
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const id = String(sectionIndex)
|
|
766
|
+
const { section, assetCollection: sectionAssets, iconCollection: sectionIcons } =
|
|
767
|
+
await processMarkdownFile(join(pagePath, entry.file), id, siteRoot, entry.stableName)
|
|
768
|
+
sections.push(section)
|
|
769
|
+
pageAssetCollection = mergeAssetCollections(pageAssetCollection, sectionAssets)
|
|
770
|
+
pageIconCollection = mergeIconCollections(pageIconCollection, sectionIcons)
|
|
771
|
+
|
|
772
|
+
// Track last modified
|
|
773
|
+
const fileStat = await stat(join(pagePath, entry.file))
|
|
774
|
+
if (!lastModified || fileStat.mtime > lastModified) {
|
|
775
|
+
lastModified = fileStat.mtime
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Process subsections if configured (e.g., { features: [logocloud, stats] })
|
|
779
|
+
const subsections = subsectionConfigs.get(name)
|
|
780
|
+
if (Array.isArray(subsections) && subsections.length > 0) {
|
|
781
|
+
const subResult = await processExplicitSections(subsections, pagePath, siteRoot, id)
|
|
782
|
+
section.subsections = subResult.sections
|
|
783
|
+
pageAssetCollection = mergeAssetCollections(pageAssetCollection, subResult.assetCollection)
|
|
784
|
+
pageIconCollection = mergeIconCollections(pageIconCollection, subResult.iconCollection)
|
|
785
|
+
if (subResult.lastModified && (!lastModified || subResult.lastModified > lastModified)) {
|
|
786
|
+
lastModified = subResult.lastModified
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
sectionIndex++
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
hierarchicalSections = buildSectionHierarchy(sections)
|
|
794
|
+
|
|
610
795
|
} else if (Array.isArray(sectionsConfig) && sectionsConfig.length > 0) {
|
|
611
|
-
//
|
|
796
|
+
// Strict: explicit sections array (only listed sections processed)
|
|
612
797
|
const result = await processExplicitSections(sectionsConfig, pagePath, siteRoot)
|
|
613
798
|
hierarchicalSections = result.sections
|
|
614
799
|
pageAssetCollection = result.assetCollection
|
|
@@ -720,9 +905,13 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
720
905
|
function determineIndexPage(orderConfig, availableFolders) {
|
|
721
906
|
const { pages: pagesArray, index: indexName } = orderConfig || {}
|
|
722
907
|
|
|
723
|
-
// 1. Explicit pages array - first item is index
|
|
908
|
+
// 1. Explicit pages array - first non-'...' item is index
|
|
724
909
|
if (Array.isArray(pagesArray) && pagesArray.length > 0) {
|
|
725
|
-
|
|
910
|
+
const parsed = parseWildcardArray(pagesArray)
|
|
911
|
+
if (parsed && parsed.before.length > 0) {
|
|
912
|
+
return extractItemName(parsed.before[0])
|
|
913
|
+
}
|
|
914
|
+
// Array starts with '...' or is ['...'] — no index from pages, fall through
|
|
726
915
|
}
|
|
727
916
|
|
|
728
917
|
// 2. Explicit index property
|
|
@@ -786,7 +975,6 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
786
975
|
// Read folder.yml or page.yml to determine mode and get config
|
|
787
976
|
const { config: dirConfig, mode: dirMode } = await readFolderConfig(entryPath, contentMode)
|
|
788
977
|
const numericOrder = typeof dirConfig.order === 'number' ? dirConfig.order : undefined
|
|
789
|
-
const childOrderArray = Array.isArray(dirConfig.order) ? dirConfig.order : undefined
|
|
790
978
|
|
|
791
979
|
pageFolders.push({
|
|
792
980
|
name: entry,
|
|
@@ -796,8 +984,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
796
984
|
dirMode,
|
|
797
985
|
childOrderConfig: {
|
|
798
986
|
pages: dirConfig.pages,
|
|
799
|
-
index: dirConfig.index
|
|
800
|
-
order: childOrderArray
|
|
987
|
+
index: dirConfig.index
|
|
801
988
|
}
|
|
802
989
|
})
|
|
803
990
|
}
|
|
@@ -811,14 +998,26 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
811
998
|
return a.name.localeCompare(b.name)
|
|
812
999
|
})
|
|
813
1000
|
|
|
814
|
-
// Apply
|
|
815
|
-
|
|
1001
|
+
// Apply ordering: pages: (wildcard-aware) > order: [array] (backward compat) > default
|
|
1002
|
+
let orderedFolders
|
|
1003
|
+
let strictPageNames = null
|
|
1004
|
+
|
|
1005
|
+
const pagesParsed = Array.isArray(orderConfig?.pages) ? parseWildcardArray(orderConfig.pages) : null
|
|
1006
|
+
|
|
1007
|
+
if (pagesParsed && pagesParsed.mode !== 'all') {
|
|
1008
|
+
orderedFolders = applyWildcardOrder(pageFolders, pagesParsed)
|
|
1009
|
+
if (pagesParsed.mode === 'strict') {
|
|
1010
|
+
strictPageNames = new Set(pagesParsed.before.map(extractItemName).filter(Boolean))
|
|
1011
|
+
}
|
|
1012
|
+
} else {
|
|
1013
|
+
orderedFolders = pageFolders
|
|
1014
|
+
}
|
|
816
1015
|
|
|
817
1016
|
// Check if this directory contains version folders (versioned section)
|
|
818
1017
|
const folderNames = orderedFolders.map(f => f.name)
|
|
819
1018
|
const detectedVersions = detectVersions(folderNames)
|
|
820
1019
|
|
|
821
|
-
// If versioned section, handle version folders specially (always
|
|
1020
|
+
// If versioned section, handle version folders specially (always page mode)
|
|
822
1021
|
if (detectedVersions && !versionContext) {
|
|
823
1022
|
const parentConfig = await readYamlFile(join(dirPath, 'page.yml'))
|
|
824
1023
|
const versionMeta = buildVersionMetadata(detectedVersions, parentConfig)
|
|
@@ -877,12 +1076,28 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
877
1076
|
}
|
|
878
1077
|
}
|
|
879
1078
|
|
|
880
|
-
// Apply
|
|
881
|
-
|
|
1079
|
+
// Apply ordering: pages: (wildcard-aware) > order: [array] (backward compat) > default
|
|
1080
|
+
let orderedMdPages
|
|
1081
|
+
let strictPageNamesFM = null
|
|
882
1082
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1083
|
+
const pagesParsedFM = Array.isArray(orderConfig?.pages) ? parseWildcardArray(orderConfig.pages) : null
|
|
1084
|
+
|
|
1085
|
+
if (pagesParsedFM && pagesParsedFM.mode !== 'all') {
|
|
1086
|
+
orderedMdPages = applyWildcardOrder(mdPageItems, pagesParsedFM)
|
|
1087
|
+
if (pagesParsedFM.mode === 'strict') {
|
|
1088
|
+
strictPageNamesFM = new Set(pagesParsedFM.before.map(extractItemName).filter(Boolean))
|
|
1089
|
+
}
|
|
1090
|
+
} else {
|
|
1091
|
+
orderedMdPages = mdPageItems
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// In folder mode, determine index: pages: first item, or explicit index:
|
|
1095
|
+
let indexName = null
|
|
1096
|
+
if (pagesParsedFM && pagesParsedFM.before.length > 0) {
|
|
1097
|
+
indexName = extractItemName(pagesParsedFM.before[0])
|
|
1098
|
+
} else {
|
|
1099
|
+
indexName = orderConfig?.index || null
|
|
1100
|
+
}
|
|
886
1101
|
|
|
887
1102
|
// Add md-file-pages
|
|
888
1103
|
for (const { name, result } of orderedMdPages) {
|
|
@@ -906,7 +1121,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
906
1121
|
const isIndex = entry === indexName
|
|
907
1122
|
|
|
908
1123
|
if (dirMode === 'sections') {
|
|
909
|
-
// Subdirectory overrides to
|
|
1124
|
+
// Subdirectory overrides to page mode — process normally
|
|
910
1125
|
const result = await processPage(entryPath, entry, siteRoot, {
|
|
911
1126
|
isIndex, parentRoute, parentFetch, versionContext
|
|
912
1127
|
})
|
|
@@ -917,7 +1132,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
917
1132
|
iconCollection = mergeIconCollections(iconCollection, pageIcons)
|
|
918
1133
|
pages.push(page)
|
|
919
1134
|
|
|
920
|
-
// Recurse into subdirectories (
|
|
1135
|
+
// Recurse into subdirectories (page mode)
|
|
921
1136
|
const childParentRoute = isIndex ? parentRoute : page.route
|
|
922
1137
|
const childFetch = page.fetch || parentFetch
|
|
923
1138
|
const subResult = await collectPagesRecursive(entryPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext, 'sections')
|
|
@@ -929,7 +1144,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
929
1144
|
}
|
|
930
1145
|
}
|
|
931
1146
|
} else {
|
|
932
|
-
// Container directory in
|
|
1147
|
+
// Container directory in folder mode — create minimal page, recurse
|
|
933
1148
|
const containerRoute = isIndex
|
|
934
1149
|
? parentRoute
|
|
935
1150
|
: parentRoute === '/' ? `/${entry}` : `${parentRoute}/${entry}`
|
|
@@ -971,7 +1186,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
971
1186
|
|
|
972
1187
|
pages.push(containerPage)
|
|
973
1188
|
|
|
974
|
-
// Recurse in
|
|
1189
|
+
// Recurse in folder mode
|
|
975
1190
|
const subResult = await collectPagesRecursive(entryPath, containerRoute, siteRoot, childOrderConfig, parentFetch, versionContext, 'pages')
|
|
976
1191
|
pages.push(...subResult.pages)
|
|
977
1192
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
@@ -982,6 +1197,17 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
982
1197
|
}
|
|
983
1198
|
}
|
|
984
1199
|
|
|
1200
|
+
// When pages: is strict (no '...'), hide unlisted direct children from navigation
|
|
1201
|
+
if (strictPageNamesFM) {
|
|
1202
|
+
for (const page of pages) {
|
|
1203
|
+
const childName = getDirectChildName(page.route, parentRoute)
|
|
1204
|
+
|| (page.sourcePath ? getDirectChildName(page.sourcePath, parentRoute) : null)
|
|
1205
|
+
if (childName && !strictPageNamesFM.has(childName) && !page.hidden) {
|
|
1206
|
+
page.hidden = true
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
985
1211
|
return { pages, assetCollection, iconCollection, notFound, versionedScopes }
|
|
986
1212
|
}
|
|
987
1213
|
|
|
@@ -1000,8 +1226,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1000
1226
|
const isIndex = entry === indexPageName
|
|
1001
1227
|
|
|
1002
1228
|
if (dirMode === 'pages') {
|
|
1003
|
-
// Child directory switches to
|
|
1004
|
-
// create container page with empty sections, recurse in
|
|
1229
|
+
// Child directory switches to folder mode (has folder.yml) —
|
|
1230
|
+
// create container page with empty sections, recurse in folder mode
|
|
1005
1231
|
const containerRoute = isIndex
|
|
1006
1232
|
? parentRoute
|
|
1007
1233
|
: parentRoute === '/' ? `/${entry}` : `${parentRoute}/${entry}`
|
|
@@ -1090,6 +1316,17 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1090
1316
|
}
|
|
1091
1317
|
}
|
|
1092
1318
|
|
|
1319
|
+
// When pages: is strict (no '...'), hide unlisted direct children from navigation
|
|
1320
|
+
if (strictPageNames) {
|
|
1321
|
+
for (const page of pages) {
|
|
1322
|
+
const childName = getDirectChildName(page.route, parentRoute)
|
|
1323
|
+
|| (page.sourcePath ? getDirectChildName(page.sourcePath, parentRoute) : null)
|
|
1324
|
+
if (childName && !strictPageNames.has(childName) && !page.hidden) {
|
|
1325
|
+
page.hidden = true
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1093
1330
|
return { pages, assetCollection, iconCollection, notFound, versionedScopes }
|
|
1094
1331
|
}
|
|
1095
1332
|
|
|
@@ -1230,8 +1467,7 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1230
1467
|
// Extract page ordering config from site.yml
|
|
1231
1468
|
const siteOrderConfig = {
|
|
1232
1469
|
pages: siteConfig.pages,
|
|
1233
|
-
index: siteConfig.index
|
|
1234
|
-
order: Array.isArray(siteConfig.order) ? siteConfig.order : undefined
|
|
1470
|
+
index: siteConfig.index
|
|
1235
1471
|
}
|
|
1236
1472
|
|
|
1237
1473
|
// Determine root content mode from folder.yml/page.yml presence in pages directory
|
|
@@ -1289,8 +1525,11 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1289
1525
|
page.parent = parentPage ? parentPage.route : null
|
|
1290
1526
|
}
|
|
1291
1527
|
|
|
1292
|
-
//
|
|
1293
|
-
|
|
1528
|
+
// Page order is determined by per-level sorting during collection:
|
|
1529
|
+
// 1. Numeric 'order' property in page.yml (lower first, within each level)
|
|
1530
|
+
// 2. pages: array in parent config (wildcard-aware, overrides numeric order)
|
|
1531
|
+
// 3. order: [array] in parent config (non-strict, backward compat)
|
|
1532
|
+
// No global re-sort — collection order is authoritative.
|
|
1294
1533
|
|
|
1295
1534
|
// Log asset summary
|
|
1296
1535
|
const assetCount = Object.keys(assetCollection.assets).length
|
|
@@ -1333,4 +1572,12 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1333
1572
|
}
|
|
1334
1573
|
}
|
|
1335
1574
|
|
|
1575
|
+
// Exported for testing
|
|
1576
|
+
export {
|
|
1577
|
+
extractItemName,
|
|
1578
|
+
parseWildcardArray,
|
|
1579
|
+
applyWildcardOrder,
|
|
1580
|
+
getDirectChildName
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1336
1583
|
export default collectSiteContent
|