@docsector/docsector-reader 2.0.7 → 2.2.0

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.
Files changed (32) hide show
  1. package/README.md +19 -7
  2. package/bin/docsector.js +28 -7
  3. package/docsector.config.js +13 -1
  4. package/package.json +1 -1
  5. package/src/components/DMenu.vue +241 -12
  6. package/src/components/DMenuItem.vue +25 -2
  7. package/src/components/DPageBar.vue +35 -5
  8. package/src/components/DPageMeta.vue +40 -26
  9. package/src/i18n/helpers.js +84 -18
  10. package/src/i18n/index.js +5 -3
  11. package/src/i18n/languages/en-US.hjson +14 -0
  12. package/src/i18n/languages/pt-BR.hjson +14 -0
  13. package/src/i18n/path.js +15 -2
  14. package/src/index.js +2 -2
  15. package/src/layouts/DefaultLayout.vue +16 -2
  16. package/src/pages/.old/v0.x/guide/getting-started.overview.en-US.md +7 -0
  17. package/src/pages/.old/v0.x/guide/getting-started.overview.pt-BR.md +7 -0
  18. package/src/pages/.old/v0.x/guide.book.js +12 -0
  19. package/src/pages/.old/v0.x/guide.index.js +28 -0
  20. package/src/pages/guide/configuration.overview.en-US.md +13 -2
  21. package/src/pages/guide/configuration.overview.pt-BR.md +13 -2
  22. package/src/pages/guide/pages-and-routing.overview.en-US.md +6 -2
  23. package/src/pages/guide/pages-and-routing.overview.pt-BR.md +6 -2
  24. package/src/pages/guide.index.js +3 -1
  25. package/src/pages/manual/components/d-menu.overview.en-US.md +6 -2
  26. package/src/pages/manual/components/d-menu.overview.pt-BR.md +6 -2
  27. package/src/pages/manual/components/d-page-meta.overview.en-US.md +1 -0
  28. package/src/pages/manual/components/d-page-meta.overview.pt-BR.md +1 -0
  29. package/src/pages/manual.index.js +2 -1
  30. package/src/quasar.factory.js +648 -91
  31. package/src/router/routes.js +129 -95
  32. package/src/store/App.js +15 -5
@@ -67,19 +67,63 @@ function normalizePathForMatch (path) {
67
67
  return String(path || '').replace(/\\/g, '/')
68
68
  }
69
69
 
70
- /**
71
- * List top-level page registry definition files.
72
- *
73
- * Includes:
74
- * - src/pages/index.js (legacy)
75
- * - src/pages/*.book.js
76
- * - src/pages/*.index.js
77
- */
78
- function getPagesRegistryFiles (projectRoot) {
70
+ const CURRENT_VERSION_KEY = '__current__'
71
+
72
+ function trimSlashes (value) {
73
+ return String(value || '').replace(/^\/+|\/+$/g, '')
74
+ }
75
+
76
+ function normalizeVersionId (value) {
77
+ return String(value || '').trim()
78
+ }
79
+
80
+ function normalizeRoutePrefix (value) {
81
+ const normalized = trimSlashes(value)
82
+ return normalized ? `/${normalized}` : ''
83
+ }
84
+
85
+ function getVersionRoots (projectRoot) {
79
86
  const pagesDir = resolve(projectRoot, 'src', 'pages')
80
87
  if (!existsSync(pagesDir)) return []
81
88
 
82
- const names = readdirSync(pagesDir, { withFileTypes: true })
89
+ const roots = [
90
+ {
91
+ versionId: CURRENT_VERSION_KEY,
92
+ current: true,
93
+ rootDir: pagesDir,
94
+ importPrefix: '',
95
+ sourceRoot: '',
96
+ routePrefix: ''
97
+ }
98
+ ]
99
+
100
+ const oldDir = resolve(pagesDir, '.old')
101
+ if (!existsSync(oldDir)) return roots
102
+
103
+ const oldVersionRoots = readdirSync(oldDir, { withFileTypes: true })
104
+ .filter(entry => entry.isDirectory())
105
+ .map(entry => entry.name)
106
+ .filter(name => normalizeVersionId(name).length > 0)
107
+ .sort()
108
+
109
+ for (const versionId of oldVersionRoots) {
110
+ roots.push({
111
+ versionId,
112
+ current: false,
113
+ rootDir: resolve(oldDir, versionId),
114
+ importPrefix: `.old/${versionId}/`,
115
+ sourceRoot: `.old/${versionId}`,
116
+ routePrefix: normalizeRoutePrefix(versionId)
117
+ })
118
+ }
119
+
120
+ return roots
121
+ }
122
+
123
+ function getPagesRegistryFilesForRoot (root) {
124
+ if (!root || !existsSync(root.rootDir)) return []
125
+
126
+ const names = readdirSync(root.rootDir, { withFileTypes: true })
83
127
  .filter(entry => entry.isFile())
84
128
  .map(entry => entry.name)
85
129
 
@@ -89,37 +133,80 @@ function getPagesRegistryFiles (projectRoot) {
89
133
  return /^[^/]+\.book\.js$/.test(name) || /^[^/]+\.index\.js$/.test(name)
90
134
  })
91
135
  .sort()
92
- .map(name => resolve(pagesDir, name))
136
+ .map(name => resolve(root.rootDir, name))
137
+ }
138
+
139
+ function normalizeBookConfig (rawConfig = {}, fallbackId = 'manual', index = 0) {
140
+ const resolvedId = rawConfig.id || fallbackId || `book-${index + 1}`
141
+ const label = rawConfig.label || (resolvedId.charAt(0).toUpperCase() + resolvedId.slice(1))
142
+
143
+ return {
144
+ ...rawConfig,
145
+ id: resolvedId,
146
+ label,
147
+ icon: rawConfig.icon || 'menu_book',
148
+ order: rawConfig.order ?? (index + 1),
149
+ color: normalizeBookColorConfig(rawConfig.color)
150
+ }
151
+ }
152
+
153
+ function buildPageRoutePath (entry, subpage, { leadingSlash = false } = {}) {
154
+ const versionPrefix = trimSlashes(entry?.versionPrefix || '')
155
+ const base = trimSlashes(`${entry?.book || 'manual'}${entry?.pagePath || ''}`)
156
+ const routePath = [versionPrefix, base, trimSlashes(subpage)].filter(Boolean).join('/')
157
+
158
+ return leadingSlash ? `/${routePath}` : routePath
159
+ }
160
+
161
+ function resolveMarkdownSourceFile (pagesDir, entry, subpage, lang) {
162
+ const sourceRoot = entry?.sourceRoot || ''
163
+ return resolve(pagesDir, sourceRoot, `${entry.book}${entry.pagePath}.${subpage}.${lang}.md`)
164
+ }
165
+
166
+ /**
167
+ * List top-level page registry definition files.
168
+ *
169
+ * Includes:
170
+ * - src/pages/index.js (legacy)
171
+ * - src/pages/*.book.js
172
+ * - src/pages/*.index.js
173
+ */
174
+ function getPagesRegistryFiles (projectRoot) {
175
+ return getVersionRoots(projectRoot)
176
+ .flatMap(root => getPagesRegistryFilesForRoot(root))
93
177
  }
94
178
 
95
179
  /**
96
180
  * Discover configured books from src/pages/*.book.js paired with *.index.js.
97
181
  */
98
182
  function getBookRegistryEntries (projectRoot) {
99
- const pagesDir = resolve(projectRoot, 'src', 'pages')
100
- if (!existsSync(pagesDir)) return []
101
-
102
- const names = readdirSync(pagesDir, { withFileTypes: true })
103
- .filter(entry => entry.isFile())
104
- .map(entry => entry.name)
105
-
106
- const books = names
107
- .filter(name => /^[^/]+\.book\.js$/.test(name))
108
- .sort()
109
-
110
183
  const entries = []
111
- for (const bookFile of books) {
112
- const baseName = bookFile.slice(0, -'.book.js'.length)
113
- const indexFile = `${baseName}.index.js`
114
- if (!names.includes(indexFile)) continue
115
-
116
- entries.push({
117
- id: baseName,
118
- bookFile,
119
- indexFile,
120
- bookPath: resolve(pagesDir, bookFile),
121
- indexPath: resolve(pagesDir, indexFile)
122
- })
184
+ for (const root of getVersionRoots(projectRoot)) {
185
+ const names = readdirSync(root.rootDir, { withFileTypes: true })
186
+ .filter(entry => entry.isFile())
187
+ .map(entry => entry.name)
188
+
189
+ const books = names
190
+ .filter(name => /^[^/]+\.book\.js$/.test(name))
191
+ .sort()
192
+
193
+ for (const bookFile of books) {
194
+ const baseName = bookFile.slice(0, -'.book.js'.length)
195
+ const indexFile = `${baseName}.index.js`
196
+ if (!names.includes(indexFile)) continue
197
+
198
+ entries.push({
199
+ versionId: root.versionId,
200
+ currentVersion: root.current,
201
+ routePrefix: root.routePrefix,
202
+ sourceRoot: root.sourceRoot,
203
+ id: baseName,
204
+ bookFile: `${root.importPrefix}${bookFile}`,
205
+ indexFile: `${root.importPrefix}${indexFile}`,
206
+ bookPath: resolve(root.rootDir, bookFile),
207
+ indexPath: resolve(root.rootDir, indexFile)
208
+ })
209
+ }
123
210
  }
124
211
 
125
212
  return entries
@@ -170,7 +257,93 @@ function buildVirtualBooksModule (projectRoot) {
170
257
 
171
258
  // Legacy fallback: support projects that still define src/pages/index.js only.
172
259
  if (bookEntries.length === 0) {
173
- return `import legacyPages from 'pages'
260
+ return `import docsectorConfig from 'docsector.config.js'
261
+ import legacyPages from 'pages'
262
+
263
+ const CURRENT_VERSION_KEY = ${JSON.stringify(CURRENT_VERSION_KEY)}
264
+
265
+ const normalizeVersionBadge = (rawBadge, { released, releaseStatus }) => {
266
+ const normalizedStatus = String(releaseStatus || '').toLowerCase()
267
+ const deprecated = normalizedStatus === 'deprecated'
268
+ const defaultColor = deprecated ? 'negative' : (released ? 'positive' : 'warning')
269
+ const defaultTextColor = (deprecated || released) ? 'white' : 'dark'
270
+
271
+ if (rawBadge === false || rawBadge === null) {
272
+ return { label: releaseStatus, color: defaultColor, textColor: defaultTextColor }
273
+ }
274
+
275
+ if (typeof rawBadge === 'string') {
276
+ return { label: rawBadge, color: defaultColor, textColor: defaultTextColor }
277
+ }
278
+
279
+ if (typeof rawBadge === 'object' && rawBadge !== null) {
280
+ const label = rawBadge.label || rawBadge.text || releaseStatus
281
+ if (!label) {
282
+ return null
283
+ }
284
+
285
+ return {
286
+ ...rawBadge,
287
+ label,
288
+ color: rawBadge.color || defaultColor,
289
+ textColor: rawBadge.textColor || defaultTextColor
290
+ }
291
+ }
292
+
293
+ return { label: releaseStatus || (released ? 'released' : 'draft'), color: defaultColor, textColor: defaultTextColor }
294
+ }
295
+
296
+ const normalizeVersionDescriptor = (raw, fallback = {}) => {
297
+ const value = typeof raw === 'string' ? { id: raw, label: raw } : (raw || {})
298
+ const current = value.current === true || fallback.current === true
299
+ const id = current
300
+ ? (value.id || docsectorConfig.branding?.version || fallback.id || 'current')
301
+ : (value.id || fallback.id || value.label || '')
302
+ const label = value.label || id
303
+ const normalizedPrefix = String(value.routePrefix || fallback.routePrefix || id || '').replace(/^\\/+|\\/+$/g, '')
304
+ const configuredStatus = value.deprecated === true || fallback.deprecated === true
305
+ ? 'deprecated'
306
+ : (value.releaseStatus || value.status || fallback.releaseStatus || fallback.status)
307
+ const explicitlyReleased = value.released !== undefined
308
+ ? value.released !== false
309
+ : (fallback.released !== undefined ? fallback.released !== false : null)
310
+ const released = configuredStatus === 'deprecated'
311
+ ? true
312
+ : (explicitlyReleased ?? !['draft', 'unreleased', 'preview', 'next'].includes(String(configuredStatus || '').toLowerCase()))
313
+ const releaseStatus = configuredStatus || (released ? 'released' : 'draft')
314
+ const badge = normalizeVersionBadge(value.badge ?? value.releaseBadge ?? fallback.badge ?? fallback.releaseBadge, { released, releaseStatus })
315
+
316
+ return {
317
+ ...fallback,
318
+ ...value,
319
+ id,
320
+ label,
321
+ released,
322
+ releaseStatus,
323
+ deprecated: releaseStatus === 'deprecated',
324
+ badge,
325
+ current,
326
+ routePrefix: current ? '' : (normalizedPrefix ? '/' + normalizedPrefix : ''),
327
+ sourceRoot: current ? '' : (value.sourceRoot || fallback.sourceRoot || (id ? '.old/' + id : ''))
328
+ }
329
+ }
330
+
331
+ const discoveredVersions = [
332
+ normalizeVersionDescriptor(null, { id: CURRENT_VERSION_KEY, current: true })
333
+ ]
334
+
335
+ const configuredVersions = Array.isArray(docsectorConfig.branding?.versions) ? docsectorConfig.branding.versions : []
336
+ const currentVersion = normalizeVersionDescriptor(
337
+ configuredVersions.find(version => typeof version === 'object' && version?.current === true),
338
+ { id: docsectorConfig.branding?.version || CURRENT_VERSION_KEY, current: true }
339
+ )
340
+
341
+ export const versions = [currentVersion]
342
+
343
+ export const versionById = versions.reduce((accumulator, version) => {
344
+ accumulator[version.id] = version
345
+ return accumulator
346
+ }, {})
174
347
 
175
348
  const defaultBook = {
176
349
  id: 'manual',
@@ -185,13 +358,33 @@ const defaultBook = {
185
358
 
186
359
  const normalizedPages = legacyPages || {}
187
360
 
188
- export const books = {
189
- manual: {
190
- config: defaultBook,
191
- routes: normalizedPages
361
+ export const booksByVersion = {
362
+ [currentVersion.id]: {
363
+ version: currentVersion,
364
+ books: {
365
+ manual: {
366
+ config: defaultBook,
367
+ routes: normalizedPages
368
+ }
369
+ },
370
+ allBooks: [defaultBook]
192
371
  }
193
372
  }
194
373
 
374
+ export const books = booksByVersion[currentVersion.id].books
375
+
376
+ export const pageEntries = Object.entries(normalizedPages).map(([pagePath, page]) => ({
377
+ version: currentVersion.id,
378
+ versionLabel: currentVersion.label,
379
+ versionCurrent: true,
380
+ versionPrefix: '',
381
+ sourceRoot: '',
382
+ book: page?.config?.book ?? page?.config?.type ?? 'manual',
383
+ bookConfig: defaultBook,
384
+ pagePath,
385
+ page
386
+ }))
387
+
195
388
  export const allBooks = [defaultBook]
196
389
  export const allPages = normalizedPages
197
390
 
@@ -199,17 +392,30 @@ export default books
199
392
  `
200
393
  }
201
394
 
202
- const imports = []
395
+ const imports = ['import docsectorConfig from \'docsector.config.js\'']
203
396
  const rows = []
397
+ const discoveredVersionIds = new Map()
204
398
 
205
399
  for (const [index, entry] of bookEntries.entries()) {
206
400
  imports.push(`import __book_${index} from 'pages/${entry.bookFile}'`)
207
401
  imports.push(`import __routes_${index} from 'pages/${entry.indexFile}'`)
208
- rows.push(` { fallbackId: ${JSON.stringify(entry.id)}, config: __book_${index}, routes: __routes_${index} }`)
402
+ rows.push(` { versionId: ${JSON.stringify(entry.versionId)}, currentVersion: ${JSON.stringify(entry.currentVersion)}, routePrefix: ${JSON.stringify(entry.routePrefix)}, sourceRoot: ${JSON.stringify(entry.sourceRoot)}, fallbackId: ${JSON.stringify(entry.id)}, config: __book_${index}, routes: __routes_${index} }`)
403
+ discoveredVersionIds.set(entry.versionId, {
404
+ id: entry.versionId,
405
+ current: entry.currentVersion,
406
+ routePrefix: entry.routePrefix,
407
+ sourceRoot: entry.sourceRoot
408
+ })
209
409
  }
210
410
 
411
+ const discoveredVersions = Array.from(discoveredVersionIds.values())
412
+
211
413
  return `${imports.join('\n')}
212
414
 
415
+ const CURRENT_VERSION_KEY = ${JSON.stringify(CURRENT_VERSION_KEY)}
416
+
417
+ const discoveredVersions = ${JSON.stringify(discoveredVersions, null, 2)}
418
+
213
419
  const entries = [
214
420
  ${rows.join(',\n')}
215
421
  ]
@@ -219,6 +425,39 @@ const DEFAULT_BOOK_COLORS = Object.freeze({
219
425
  inactive: 'rgba(255, 255, 255, 0.72)'
220
426
  })
221
427
 
428
+ const normalizeVersionBadge = (rawBadge, { released, releaseStatus }) => {
429
+ const normalizedStatus = String(releaseStatus || '').toLowerCase()
430
+ const deprecated = normalizedStatus === 'deprecated'
431
+ const defaultColor = deprecated ? 'negative' : (released ? 'positive' : 'warning')
432
+ const defaultTextColor = (deprecated || released) ? 'white' : 'dark'
433
+
434
+ if (rawBadge === false || rawBadge === null) {
435
+ return { label: releaseStatus, color: defaultColor, textColor: defaultTextColor }
436
+ }
437
+
438
+ if (typeof rawBadge === 'string') {
439
+ return { label: rawBadge, color: defaultColor, textColor: defaultTextColor }
440
+ }
441
+
442
+ if (typeof rawBadge === 'object' && rawBadge !== null) {
443
+ const label = rawBadge.label || rawBadge.text || releaseStatus
444
+ if (!label) {
445
+ return null
446
+ }
447
+
448
+ return {
449
+ ...rawBadge,
450
+ label,
451
+ color: rawBadge.color || defaultColor,
452
+ textColor: rawBadge.textColor || defaultTextColor
453
+ }
454
+ }
455
+
456
+ return { label: releaseStatus || (released ? 'released' : 'draft'), color: defaultColor, textColor: defaultTextColor }
457
+ }
458
+
459
+ const trimSlashes = (value) => String(value || '').replace(/^\\/+|\\/+$/g, '')
460
+
222
461
  const normalizeBookColor = (rawColor) => {
223
462
  if (typeof rawColor === 'object' && rawColor !== null && !Array.isArray(rawColor)) {
224
463
  const active = typeof rawColor.active === 'string' && rawColor.active.trim().length > 0
@@ -243,7 +482,93 @@ const normalizeBookColor = (rawColor) => {
243
482
  return { ...DEFAULT_BOOK_COLORS }
244
483
  }
245
484
 
246
- export const books = entries.reduce((accumulator, entry, index) => {
485
+ const normalizeVersionDescriptor = (raw, fallback = {}) => {
486
+ const value = typeof raw === 'string' ? { id: raw, label: raw } : (raw || {})
487
+ const current = value.current === true || fallback.current === true
488
+ const id = current
489
+ ? (value.id || docsectorConfig.branding?.version || fallback.id || 'current')
490
+ : (value.id || fallback.id || value.label || '')
491
+ const label = value.label || id
492
+ const prefixSource = value.routePrefix ?? fallback.routePrefix ?? id
493
+ const normalizedPrefix = trimSlashes(prefixSource)
494
+ const configuredStatus = value.deprecated === true || fallback.deprecated === true
495
+ ? 'deprecated'
496
+ : (value.releaseStatus || value.status || fallback.releaseStatus || fallback.status)
497
+ const explicitlyReleased = value.released !== undefined
498
+ ? value.released !== false
499
+ : (fallback.released !== undefined ? fallback.released !== false : null)
500
+ const released = configuredStatus === 'deprecated'
501
+ ? true
502
+ : (explicitlyReleased ?? !['draft', 'unreleased', 'preview', 'next'].includes(String(configuredStatus || '').toLowerCase()))
503
+ const releaseStatus = configuredStatus || (released ? 'released' : 'draft')
504
+ const badge = normalizeVersionBadge(value.badge ?? value.releaseBadge ?? fallback.badge ?? fallback.releaseBadge, { released, releaseStatus })
505
+
506
+ return {
507
+ ...fallback,
508
+ ...value,
509
+ id,
510
+ label,
511
+ released,
512
+ releaseStatus,
513
+ deprecated: releaseStatus === 'deprecated',
514
+ badge,
515
+ current,
516
+ routePrefix: current ? '' : (normalizedPrefix ? '/' + normalizedPrefix : ''),
517
+ sourceRoot: current ? '' : (value.sourceRoot || fallback.sourceRoot || (id ? '.old/' + id : ''))
518
+ }
519
+ }
520
+
521
+ const configuredVersions = Array.isArray(docsectorConfig.branding?.versions) ? docsectorConfig.branding.versions : []
522
+ const currentVersion = normalizeVersionDescriptor(
523
+ configuredVersions.find(version => typeof version === 'object' && version?.current === true),
524
+ { id: docsectorConfig.branding?.version || CURRENT_VERSION_KEY, current: true }
525
+ )
526
+
527
+ const configuredVersionDescriptors = configuredVersions
528
+ .filter(version => !(typeof version === 'object' && version?.current === true))
529
+ .map(version => {
530
+ const value = typeof version === 'string' ? { id: version, label: version } : version
531
+ const discovered = discoveredVersions.find(item => item.id === value?.id || item.id === value?.label) || {}
532
+ const isCurrent = value?.id === currentVersion.id || value?.label === currentVersion.id
533
+
534
+ return normalizeVersionDescriptor(value, isCurrent ? { ...currentVersion, current: true } : discovered)
535
+ })
536
+
537
+ export const versions = [currentVersion]
538
+
539
+ for (const version of configuredVersionDescriptors) {
540
+ if (!versions.some(item => item.id === version.id)) {
541
+ versions.push(version)
542
+ }
543
+ }
544
+
545
+ for (const discovered of discoveredVersions) {
546
+ if (discovered.current) continue
547
+ if (!versions.some(item => item.id === discovered.id)) {
548
+ versions.push(normalizeVersionDescriptor(null, discovered))
549
+ }
550
+ }
551
+
552
+ export const versionById = versions.reduce((accumulator, version) => {
553
+ accumulator[version.id] = version
554
+ return accumulator
555
+ }, {})
556
+
557
+ const resolveEntryVersion = (entry) => {
558
+ if (entry.currentVersion === true || entry.versionId === CURRENT_VERSION_KEY) {
559
+ return currentVersion
560
+ }
561
+
562
+ return versionById[entry.versionId] || normalizeVersionDescriptor(null, {
563
+ id: entry.versionId,
564
+ current: false,
565
+ routePrefix: entry.routePrefix,
566
+ sourceRoot: entry.sourceRoot
567
+ })
568
+ }
569
+
570
+ export const booksByVersion = entries.reduce((accumulator, entry, index) => {
571
+ const version = resolveEntryVersion(entry)
247
572
  const config = entry.config || {}
248
573
  const resolvedId = config.id || entry.fallbackId || ('book-' + (index + 1))
249
574
  const label = config.label || (resolvedId.charAt(0).toUpperCase() + resolvedId.slice(1))
@@ -253,17 +578,30 @@ export const books = entries.reduce((accumulator, entry, index) => {
253
578
  label,
254
579
  icon: config.icon || 'menu_book',
255
580
  order: config.order ?? (index + 1),
256
- color: normalizeBookColor(config.color)
581
+ color: normalizeBookColor(config.color),
582
+ version: version.id,
583
+ versionPrefix: version.routePrefix
584
+ }
585
+
586
+ if (!accumulator[version.id]) {
587
+ accumulator[version.id] = {
588
+ version,
589
+ books: {},
590
+ allBooks: []
591
+ }
257
592
  }
258
593
 
259
- accumulator[resolvedId] = {
594
+ accumulator[version.id].books[resolvedId] = {
260
595
  config: normalizedConfig,
261
596
  routes: entry.routes || {}
262
597
  }
598
+ accumulator[version.id].allBooks = Object.values(accumulator[version.id].books).map(book => book.config)
263
599
  return accumulator
264
600
  }, {})
265
601
 
266
- export const allBooks = Object.values(books).map(book => book.config)
602
+ export const books = booksByVersion[currentVersion.id]?.books || {}
603
+
604
+ export const allBooks = booksByVersion[currentVersion.id]?.allBooks || []
267
605
  export const allPages = Object.values(books).reduce((accumulator, book) => {
268
606
  return {
269
607
  ...accumulator,
@@ -271,6 +609,34 @@ export const allPages = Object.values(books).reduce((accumulator, book) => {
271
609
  }
272
610
  }, {})
273
611
 
612
+ export const pageEntries = Object.entries(booksByVersion).flatMap(([versionId, versionBooks]) => {
613
+ const version = versionBooks.version
614
+
615
+ return Object.entries(versionBooks.books || {}).flatMap(([bookId, book]) => {
616
+ const fallbackBook = book?.config?.id || bookId || 'manual'
617
+
618
+ return Object.entries(book?.routes || {}).map(([pagePath, page]) => {
619
+ const bookName = page?.config?.book ?? page?.config?.type ?? fallbackBook
620
+ const pathSegments = String(pagePath || '').replace(/^\\//, '').split('/').filter(Boolean)
621
+ const i18nSegments = version.current ? [bookName, ...pathSegments] : [version.id, bookName, ...pathSegments]
622
+
623
+ return {
624
+ version: versionId,
625
+ versionLabel: version.label,
626
+ versionCurrent: version.current,
627
+ versionPrefix: version.routePrefix,
628
+ sourceRoot: version.sourceRoot || '',
629
+ book: bookName,
630
+ bookConfig: book.config,
631
+ pagePath,
632
+ page,
633
+ i18nSegments,
634
+ unversionedPath: '/' + bookName + pagePath
635
+ }
636
+ })
637
+ })
638
+ })
639
+
274
640
  export default books
275
641
  `
276
642
  }
@@ -283,6 +649,17 @@ async function loadBooksRegistry (projectRoot) {
283
649
 
284
650
  // Legacy fallback
285
651
  if (entries.length === 0) {
652
+ const configPath = resolve(projectRoot, 'docsector.config.js')
653
+ const { default: config = {} } = existsSync(configPath)
654
+ ? await import(pathToFileURL(configPath).href)
655
+ : { default: {} }
656
+ const currentVersion = {
657
+ id: config.branding?.version || 'current',
658
+ label: config.branding?.version || 'current',
659
+ current: true,
660
+ routePrefix: '',
661
+ sourceRoot: ''
662
+ }
286
663
  const legacyPath = resolve(projectRoot, 'src', 'pages', 'index.js')
287
664
  const pages = existsSync(legacyPath)
288
665
  ? ((await import(pathToFileURL(legacyPath).href)).default || {})
@@ -296,52 +673,203 @@ async function loadBooksRegistry (projectRoot) {
296
673
  color: {
297
674
  active: 'white',
298
675
  inactive: 'rgba(255, 255, 255, 0.72)'
676
+ },
677
+ version: currentVersion.id,
678
+ versionPrefix: ''
679
+ }
680
+
681
+ const books = {
682
+ manual: {
683
+ config: defaultBook,
684
+ routes: pages
299
685
  }
300
686
  }
301
687
 
688
+ const pageEntries = getBookPageEntries({
689
+ [currentVersion.id]: {
690
+ version: currentVersion,
691
+ books
692
+ }
693
+ })
694
+
302
695
  return {
303
- books: {
304
- manual: {
305
- config: defaultBook,
306
- routes: pages
696
+ versions: [currentVersion],
697
+ booksByVersion: {
698
+ [currentVersion.id]: {
699
+ version: currentVersion,
700
+ books,
701
+ allBooks: [defaultBook]
307
702
  }
308
703
  },
704
+ pageEntries,
705
+ books,
309
706
  allBooks: [defaultBook],
310
707
  allPages: pages
311
708
  }
312
709
  }
313
710
 
314
- const books = {}
711
+ const configPath = resolve(projectRoot, 'docsector.config.js')
712
+ const { default: config = {} } = existsSync(configPath)
713
+ ? await import(pathToFileURL(configPath).href)
714
+ : { default: {} }
715
+
716
+ const discoveredVersions = Array.from(new Map(entries.map(entry => [entry.versionId, {
717
+ id: entry.versionId,
718
+ current: entry.currentVersion,
719
+ routePrefix: entry.routePrefix,
720
+ sourceRoot: entry.sourceRoot
721
+ }])).values())
722
+
723
+ const normalizeVersionBadge = (rawBadge, { released, releaseStatus }) => {
724
+ const normalizedStatus = String(releaseStatus || '').toLowerCase()
725
+ const deprecated = normalizedStatus === 'deprecated'
726
+ const defaultColor = deprecated ? 'negative' : (released ? 'positive' : 'warning')
727
+ const defaultTextColor = (deprecated || released) ? 'white' : 'dark'
728
+
729
+ if (rawBadge === false || rawBadge === null) {
730
+ return { label: releaseStatus, color: defaultColor, textColor: defaultTextColor }
731
+ }
732
+
733
+ if (typeof rawBadge === 'string') {
734
+ return { label: rawBadge, color: defaultColor, textColor: defaultTextColor }
735
+ }
736
+
737
+ if (typeof rawBadge === 'object' && rawBadge !== null) {
738
+ const label = rawBadge.label || rawBadge.text || releaseStatus
739
+ if (!label) {
740
+ return null
741
+ }
742
+
743
+ return {
744
+ ...rawBadge,
745
+ label,
746
+ color: rawBadge.color || defaultColor,
747
+ textColor: rawBadge.textColor || defaultTextColor
748
+ }
749
+ }
750
+
751
+ return { label: releaseStatus || (released ? 'released' : 'draft'), color: defaultColor, textColor: defaultTextColor }
752
+ }
753
+
754
+ const normalizeVersionDescriptor = (raw, fallback = {}) => {
755
+ const value = typeof raw === 'string' ? { id: raw, label: raw } : (raw || {})
756
+ const current = value.current === true || fallback.current === true
757
+ const id = current
758
+ ? (value.id || config.branding?.version || fallback.id || 'current')
759
+ : (value.id || fallback.id || value.label || '')
760
+ const label = value.label || id
761
+ const prefixSource = value.routePrefix ?? fallback.routePrefix ?? id
762
+ const normalizedPrefix = trimSlashes(prefixSource)
763
+ const configuredStatus = value.deprecated === true || fallback.deprecated === true
764
+ ? 'deprecated'
765
+ : (value.releaseStatus || value.status || fallback.releaseStatus || fallback.status)
766
+ const explicitlyReleased = value.released !== undefined
767
+ ? value.released !== false
768
+ : (fallback.released !== undefined ? fallback.released !== false : null)
769
+ const released = configuredStatus === 'deprecated'
770
+ ? true
771
+ : (explicitlyReleased ?? !['draft', 'unreleased', 'preview', 'next'].includes(String(configuredStatus || '').toLowerCase()))
772
+ const releaseStatus = configuredStatus || (released ? 'released' : 'draft')
773
+ const badge = normalizeVersionBadge(value.badge ?? value.releaseBadge ?? fallback.badge ?? fallback.releaseBadge, { released, releaseStatus })
774
+
775
+ return {
776
+ ...fallback,
777
+ ...value,
778
+ id,
779
+ label,
780
+ released,
781
+ releaseStatus,
782
+ deprecated: releaseStatus === 'deprecated',
783
+ badge,
784
+ current,
785
+ routePrefix: current ? '' : (normalizedPrefix ? `/${normalizedPrefix}` : ''),
786
+ sourceRoot: current ? '' : (value.sourceRoot || fallback.sourceRoot || (id ? `.old/${id}` : ''))
787
+ }
788
+ }
789
+
790
+ const configuredVersions = Array.isArray(config.branding?.versions) ? config.branding.versions : []
791
+ const currentVersion = normalizeVersionDescriptor(
792
+ configuredVersions.find(version => typeof version === 'object' && version?.current === true),
793
+ { id: config.branding?.version || CURRENT_VERSION_KEY, current: true }
794
+ )
795
+ const versions = [currentVersion]
796
+
797
+ for (const configured of configuredVersions) {
798
+ if (typeof configured === 'object' && configured?.current === true) continue
799
+
800
+ const value = typeof configured === 'string' ? { id: configured, label: configured } : configured
801
+ const discovered = discoveredVersions.find(item => item.id === value?.id || item.id === value?.label) || {}
802
+ const isCurrent = value?.id === currentVersion.id || value?.label === currentVersion.id
803
+ const version = normalizeVersionDescriptor(value, isCurrent ? { ...currentVersion, current: true } : discovered)
804
+
805
+ if (!versions.some(item => item.id === version.id)) {
806
+ versions.push(version)
807
+ }
808
+ }
809
+
810
+ for (const discovered of discoveredVersions) {
811
+ if (discovered.current) continue
812
+ if (!versions.some(item => item.id === discovered.id)) {
813
+ versions.push(normalizeVersionDescriptor(null, discovered))
814
+ }
815
+ }
816
+
817
+ const versionById = versions.reduce((accumulator, version) => {
818
+ accumulator[version.id] = version
819
+ return accumulator
820
+ }, {})
821
+
822
+ const booksByVersion = {}
823
+ const currentBooks = {}
315
824
  const allPages = {}
316
825
 
317
826
  for (const [index, entry] of entries.entries()) {
827
+ const rawVersion = entry.currentVersion || entry.versionId === CURRENT_VERSION_KEY
828
+ ? currentVersion
829
+ : (versionById[entry.versionId] || normalizeVersionDescriptor(null, entry))
318
830
  const { default: rawConfig = {} } = await import(pathToFileURL(entry.bookPath).href)
319
831
  const { default: routes = {} } = await import(pathToFileURL(entry.indexPath).href)
320
832
 
321
- const resolvedId = rawConfig.id || entry.id || `book-${index + 1}`
322
- const label = rawConfig.label || (resolvedId.charAt(0).toUpperCase() + resolvedId.slice(1))
323
-
324
833
  const config = {
325
- ...rawConfig,
326
- id: resolvedId,
327
- label,
328
- icon: rawConfig.icon || 'menu_book',
329
- order: rawConfig.order ?? (index + 1),
330
- color: normalizeBookColorConfig(rawConfig.color)
834
+ ...normalizeBookConfig(rawConfig, entry.id, index),
835
+ version: rawVersion.id,
836
+ versionPrefix: rawVersion.routePrefix
837
+ }
838
+
839
+ if (!booksByVersion[rawVersion.id]) {
840
+ booksByVersion[rawVersion.id] = {
841
+ version: rawVersion,
842
+ books: {},
843
+ allBooks: []
844
+ }
331
845
  }
332
846
 
333
- books[resolvedId] = {
847
+ booksByVersion[rawVersion.id].books[config.id] = {
334
848
  config,
335
849
  routes
336
850
  }
337
851
 
338
- Object.assign(allPages, routes || {})
852
+ if (rawVersion.current) {
853
+ currentBooks[config.id] = {
854
+ config,
855
+ routes
856
+ }
857
+ Object.assign(allPages, routes || {})
858
+ }
859
+ }
860
+
861
+ for (const versionBooks of Object.values(booksByVersion)) {
862
+ versionBooks.allBooks = Object.values(versionBooks.books).map(book => book.config)
339
863
  }
340
864
 
341
- const allBooks = Object.values(books).map(book => book.config)
865
+ const allBooks = Object.values(currentBooks).map(book => book.config)
866
+ const pageEntries = getBookPageEntries(booksByVersion)
342
867
 
343
868
  return {
344
- books,
869
+ versions,
870
+ booksByVersion,
871
+ pageEntries,
872
+ books: currentBooks,
345
873
  allBooks,
346
874
  allPages
347
875
  }
@@ -355,18 +883,39 @@ async function loadBooksRegistry (projectRoot) {
355
883
  * `guide.index.js` and `manual.index.js`. Build artifacts that need concrete
356
884
  * URLs must iterate per book instead.
357
885
  */
358
- function getBookPageEntries (books = {}) {
886
+ function getBookPageEntries (booksByVersion = {}) {
359
887
  const pageEntries = []
360
888
 
361
- for (const [bookId, book] of Object.entries(books || {})) {
362
- const fallbackBook = book?.config?.id || bookId || 'manual'
889
+ for (const [versionId, versionBooks] of Object.entries(booksByVersion || {})) {
890
+ const version = versionBooks?.version || {
891
+ id: versionId,
892
+ label: versionId,
893
+ current: true,
894
+ routePrefix: '',
895
+ sourceRoot: ''
896
+ }
363
897
 
364
- for (const [pagePath, page] of Object.entries(book?.routes || {})) {
365
- pageEntries.push({
366
- book: resolvePageBook(page?.config, fallbackBook),
367
- pagePath,
368
- page
369
- })
898
+ for (const [bookId, book] of Object.entries(versionBooks?.books || {})) {
899
+ const fallbackBook = book?.config?.id || bookId || 'manual'
900
+
901
+ for (const [pagePath, page] of Object.entries(book?.routes || {})) {
902
+ const bookName = resolvePageBook(page?.config, fallbackBook)
903
+ const pathSegments = String(pagePath || '').replace(/^\//, '').split('/').filter(Boolean)
904
+
905
+ pageEntries.push({
906
+ version: version.id,
907
+ versionLabel: version.label || version.id,
908
+ versionCurrent: version.current === true,
909
+ versionPrefix: version.routePrefix || '',
910
+ sourceRoot: version.sourceRoot || '',
911
+ book: bookName,
912
+ bookConfig: book.config,
913
+ pagePath,
914
+ page,
915
+ i18nSegments: version.current === true ? [bookName, ...pathSegments] : [version.id, bookName, ...pathSegments],
916
+ unversionedPath: `/${bookName}${pagePath}`
917
+ })
918
+ }
370
919
  }
371
920
  }
372
921
 
@@ -385,7 +934,11 @@ function isPagesRegistryFile (projectRoot, changedPath) {
385
934
  const relativePath = normalizedPath.slice(prefix.length)
386
935
 
387
936
  if (relativePath === 'index.js') return true
388
- return /^[^/]+\.book\.js$/.test(relativePath) || /^[^/]+\.index\.js$/.test(relativePath)
937
+ if (/^[^/]+\.book\.js$/.test(relativePath) || /^[^/]+\.index\.js$/.test(relativePath)) {
938
+ return true
939
+ }
940
+
941
+ return /^\.old\/[^/]+\/(?:index\.js|[^/]+\.book\.js|[^/]+\.index\.js)$/.test(relativePath)
389
942
  }
390
943
 
391
944
  /**
@@ -489,8 +1042,7 @@ function createPrerenderMetaPlugin (projectRoot) {
489
1042
  // Dynamic import books registry and docsector config
490
1043
  const configUrl = pathToFileURL(resolve(projectRoot, 'docsector.config.js')).href
491
1044
 
492
- const { books } = await loadBooksRegistry(projectRoot)
493
- const pageEntries = getBookPageEntries(books)
1045
+ const { pageEntries } = await loadBooksRegistry(projectRoot)
494
1046
  const { default: config } = await import(configUrl)
495
1047
 
496
1048
  const brandingName = config.branding?.name || ''
@@ -508,7 +1060,8 @@ function createPrerenderMetaPlugin (projectRoot) {
508
1060
 
509
1061
  let count = 0
510
1062
 
511
- for (const { book, pagePath, page } of pageEntries) {
1063
+ for (const entry of pageEntries) {
1064
+ const { page } = entry
512
1065
  if (page.config === null) continue
513
1066
 
514
1067
  const titleData = page.data?.[defaultLang] || page.data?.['*'] || page.data?.['en-US'] || Object.values(page.data || {})[0]
@@ -526,7 +1079,7 @@ function createPrerenderMetaPlugin (projectRoot) {
526
1079
  if (page.config.subpages?.vs) subpages.push('vs')
527
1080
 
528
1081
  for (const subpage of subpages) {
529
- const routePath = `${book}${pagePath}/${subpage}`
1082
+ const routePath = buildPageRoutePath(entry, subpage)
530
1083
 
531
1084
  const html = baseHtml
532
1085
  .replace(/<title>[^<]*<\/title>/, () => `<title>${fullTitle}</title>`)
@@ -1101,13 +1654,13 @@ function createMarkdownBuildPlugin (projectRoot) {
1101
1654
  const configUrl = pathToFileURL(resolve(projectRoot, 'docsector.config.js')).href
1102
1655
 
1103
1656
  const { default: config } = await import(configUrl)
1104
- const { books } = await loadBooksRegistry(projectRoot)
1105
- const pageEntries = getBookPageEntries(books)
1657
+ const { pageEntries } = await loadBooksRegistry(projectRoot)
1106
1658
 
1107
1659
  const defaultLang = config.defaultLanguage || config.languages?.[0]?.value || 'en-US'
1108
1660
  let count = 0
1109
1661
 
1110
- for (const { book, pagePath, page } of pageEntries) {
1662
+ for (const entry of pageEntries) {
1663
+ const { page } = entry
1111
1664
  if (page.config === null) continue
1112
1665
  if (page.config.status === 'empty') continue
1113
1666
 
@@ -1116,10 +1669,10 @@ function createMarkdownBuildPlugin (projectRoot) {
1116
1669
  if (page.config.subpages?.vs) subpages.push('vs')
1117
1670
 
1118
1671
  for (const subpage of subpages) {
1119
- const srcFile = resolve(pagesDir, `${book}${pagePath}.${subpage}.${defaultLang}.md`)
1672
+ const srcFile = resolveMarkdownSourceFile(pagesDir, entry, subpage, defaultLang)
1120
1673
  if (!existsSync(srcFile)) continue
1121
1674
 
1122
- const routePath = `${book}${pagePath}/${subpage}`
1675
+ const routePath = buildPageRoutePath(entry, subpage)
1123
1676
  const destFile = resolve(distDir, `${routePath}.md`)
1124
1677
  const destDir = resolve(destFile, '..')
1125
1678
 
@@ -1154,7 +1707,8 @@ function createMarkdownBuildPlugin (projectRoot) {
1154
1707
  const today = new Date().toISOString().split('T')[0]
1155
1708
  let urls = ''
1156
1709
 
1157
- for (const { book, pagePath, page } of pageEntries) {
1710
+ for (const entry of pageEntries) {
1711
+ const { page } = entry
1158
1712
  if (page.config === null) continue
1159
1713
  if (page.config.status === 'empty') continue
1160
1714
 
@@ -1163,10 +1717,10 @@ function createMarkdownBuildPlugin (projectRoot) {
1163
1717
  if (page.config.subpages?.vs) subpages.push('vs')
1164
1718
 
1165
1719
  for (const subpage of subpages) {
1166
- const srcFile = resolve(pagesDir, `${book}${pagePath}.${subpage}.${defaultLang}.md`)
1720
+ const srcFile = resolveMarkdownSourceFile(pagesDir, entry, subpage, defaultLang)
1167
1721
  if (!existsSync(srcFile)) continue
1168
1722
 
1169
- const routePath = `/${book}${pagePath}/${subpage}`
1723
+ const routePath = buildPageRoutePath(entry, subpage, { leadingSlash: true })
1170
1724
  urls += ` <url>\n <loc>${siteUrl}${routePath}</loc>\n <lastmod>${today}</lastmod>\n </url>\n`
1171
1725
  }
1172
1726
  }
@@ -1185,7 +1739,8 @@ function createMarkdownBuildPlugin (projectRoot) {
1185
1739
 
1186
1740
  const llmsSections = {}
1187
1741
 
1188
- for (const { book, pagePath, page } of pageEntries) {
1742
+ for (const entry of pageEntries) {
1743
+ const { book, pagePath, page } = entry
1189
1744
  if (page.config === null) continue
1190
1745
  if (page.config.status === 'empty') continue
1191
1746
 
@@ -1200,10 +1755,10 @@ function createMarkdownBuildPlugin (projectRoot) {
1200
1755
  if (page.config.subpages?.vs) subpages.push('vs')
1201
1756
 
1202
1757
  for (const subpage of subpages) {
1203
- const srcFile = resolve(pagesDir, `${book}${pagePath}.${subpage}.${defaultLang}.md`)
1758
+ const srcFile = resolveMarkdownSourceFile(pagesDir, entry, subpage, defaultLang)
1204
1759
  if (!existsSync(srcFile)) continue
1205
1760
 
1206
- const routePath = `${book}${pagePath}/${subpage}`
1761
+ const routePath = buildPageRoutePath(entry, subpage)
1207
1762
  const mdUrl = `${siteUrl}/${routePath}.md`
1208
1763
  const pageUrl = `${siteUrl}/${routePath}`
1209
1764
 
@@ -1805,7 +2360,8 @@ export async function onRequest (context) {
1805
2360
 
1806
2361
  // Collect page index for MCP
1807
2362
  const mcpPages = []
1808
- for (const { book, pagePath, page } of pageEntries) {
2363
+ for (const entry of pageEntries) {
2364
+ const { book, pagePath, page } = entry
1809
2365
  if (page.config === null) continue
1810
2366
  if (page.config.status === 'empty') continue
1811
2367
 
@@ -1820,13 +2376,14 @@ export async function onRequest (context) {
1820
2376
  if (page.config.subpages?.vs) subpageList.push('vs')
1821
2377
 
1822
2378
  for (const subpage of subpageList) {
1823
- const srcFile = resolve(pagesDir, `${book}${pagePath}.${subpage}.${defaultLang}.md`)
2379
+ const srcFile = resolveMarkdownSourceFile(pagesDir, entry, subpage, defaultLang)
1824
2380
  if (!existsSync(srcFile)) continue
1825
2381
 
1826
2382
  mcpPages.push({
1827
- path: `${book}${pagePath}/${subpage}`,
2383
+ path: buildPageRoutePath(entry, subpage),
1828
2384
  title: defaultTitle,
1829
2385
  book,
2386
+ version: entry.version,
1830
2387
  type: book,
1831
2388
  subpage
1832
2389
  })