@docsector/docsector-reader 2.0.6 → 2.1.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.
@@ -116,12 +116,13 @@ export function filter (source) {
116
116
  * @param {Object} options.mdModules - Result of recursively globbing markdown files under ../pages with eager raw imports
117
117
  * @param {Object} [options.pages] - Legacy merged page registry from virtual:docsector-books (allPages)
118
118
  * @param {Object} [options.books] - Book registry from virtual:docsector-books (preferred, avoids path collisions)
119
+ * @param {Array} [options.pageEntries] - Version-aware page entries from virtual:docsector-books
119
120
  * @param {Object} options.boot - Boot meta from pages/boot.js
120
121
  * @param {string[]} [options.langs] - Language codes to process (auto-detected from langModules if omitted)
121
122
  * @param {Object<string,string>} [options.homePageOverride] - Optional per-language Home markdown override
122
123
  * @returns {Object} Complete i18n messages object keyed by locale
123
124
  */
124
- export function buildMessages ({ langModules, mdModules, pages, books, boot, langs, homePageOverride = {} }) {
125
+ export function buildMessages ({ langModules, mdModules, pages, books, pageEntries, boot, langs, homePageOverride = {} }) {
125
126
  // Auto-detect languages from HJSON files if not provided
126
127
  if (!langs) {
127
128
  langs = Object.keys(langModules).map(key => {
@@ -133,8 +134,9 @@ export function buildMessages ({ langModules, mdModules, pages, books, boot, lan
133
134
 
134
135
  const i18n = {}
135
136
 
136
- function load (topPage, path, subpage, lang) {
137
- const key = `../pages/${topPage}/${path}.${subpage}.${lang}.md`
137
+ function load (topPage, path, subpage, lang, sourceRoot = '') {
138
+ const normalizedSourceRoot = String(sourceRoot || '').replace(/^\/+|\/+$/g, '')
139
+ const key = `../pages/${normalizedSourceRoot ? normalizedSourceRoot + '/' : ''}${topPage}/${path}.${subpage}.${lang}.md`
138
140
  const content = mdModules[key]
139
141
 
140
142
  if (!content) {
@@ -200,20 +202,30 @@ export function buildMessages ({ langModules, mdModules, pages, books, boot, lan
200
202
  return match[1].trim()
201
203
  }
202
204
 
203
- const pageEntries = []
205
+ const resolvedPageEntries = []
204
206
 
205
- if (books && typeof books === 'object' && Object.keys(books).length > 0) {
207
+ if (Array.isArray(pageEntries) && pageEntries.length > 0) {
208
+ for (const entry of pageEntries) {
209
+ resolvedPageEntries.push({
210
+ key: entry.pagePath,
211
+ page: entry.page,
212
+ fallbackBook: entry.book,
213
+ sourceRoot: entry.sourceRoot || '',
214
+ i18nSegments: entry.i18nSegments
215
+ })
216
+ }
217
+ } else if (books && typeof books === 'object' && Object.keys(books).length > 0) {
206
218
  for (const [bookId, book] of Object.entries(books)) {
207
219
  const routes = book?.routes || {}
208
220
  const fallbackBook = book?.config?.id || bookId || 'manual'
209
221
 
210
222
  for (const [key, page] of Object.entries(routes)) {
211
- pageEntries.push({ key, page, fallbackBook })
223
+ resolvedPageEntries.push({ key, page, fallbackBook })
212
224
  }
213
225
  }
214
226
  } else {
215
227
  for (const [key, page] of Object.entries(pages || {})) {
216
- pageEntries.push({ key, page, fallbackBook: null })
228
+ resolvedPageEntries.push({ key, page, fallbackBook: null })
217
229
  }
218
230
  }
219
231
 
@@ -249,8 +261,8 @@ export function buildMessages ({ langModules, mdModules, pages, books, boot, lan
249
261
  i18n[lang]._.home.overview.source = loadHomepage(lang)
250
262
 
251
263
  // @ Iterate pages
252
- for (const entry of pageEntries) {
253
- const { key, page, fallbackBook } = entry
264
+ for (const entry of resolvedPageEntries) {
265
+ const { key, page, fallbackBook, sourceRoot = '', i18nSegments: entryI18nSegments } = entry
254
266
  const path = key.startsWith('/') ? key.slice(1) : key
255
267
 
256
268
  const config = page.config
@@ -258,13 +270,13 @@ export function buildMessages ({ langModules, mdModules, pages, books, boot, lan
258
270
  const meta = page.meta || boot.meta
259
271
 
260
272
  const topPage = config?.book ?? config?.type ?? fallbackBook ?? 'manual'
261
- if (i18n[lang]._[topPage] === undefined) {
262
- i18n[lang]._[topPage] = {}
263
- }
273
+ const i18nSegments = Array.isArray(entryI18nSegments) && entryI18nSegments.length > 0
274
+ ? entryI18nSegments
275
+ : [topPage, ...path.split('/').filter(Boolean)]
264
276
 
265
277
  // ---
266
278
 
267
- const _ = path.split('/').reduce((accumulator, current) => {
279
+ const _ = i18nSegments.reduce((accumulator, current, index) => {
268
280
  let node = accumulator[current]
269
281
 
270
282
  // Set object if not exists
@@ -275,7 +287,7 @@ export function buildMessages ({ langModules, mdModules, pages, books, boot, lan
275
287
 
276
288
  // @ Set metadata
277
289
  // title
278
- if (node._ === undefined) {
290
+ if (index === i18nSegments.length - 1 && node._ === undefined) {
279
291
  node._ = data?.[lang]?.title || data?.['*']?.title || data?.['en-US']?.title || ''
280
292
  }
281
293
 
@@ -283,6 +295,10 @@ export function buildMessages ({ langModules, mdModules, pages, books, boot, lan
283
295
  return node
284
296
  }
285
297
 
298
+ if (index < i18nSegments.length - 1) {
299
+ return node
300
+ }
301
+
286
302
  // Set subpages sources if not exists
287
303
  if (node.overview === undefined) {
288
304
  node.overview = {
@@ -307,7 +323,7 @@ export function buildMessages ({ langModules, mdModules, pages, books, boot, lan
307
323
  }
308
324
 
309
325
  return node
310
- }, i18n[lang]._[topPage])
326
+ }, i18n[lang]._)
311
327
 
312
328
  // ---
313
329
 
@@ -322,14 +338,14 @@ export function buildMessages ({ langModules, mdModules, pages, books, boot, lan
322
338
 
323
339
  // @ Subpages
324
340
  // Overview
325
- _.overview.source = load(topPage, path, 'overview', lang)
341
+ _.overview.source = load(topPage, path, 'overview', lang, sourceRoot)
326
342
  // showcase
327
343
  if (config.subpages?.showcase === true) {
328
- _.showcase.source = load(topPage, path, 'showcase', lang)
344
+ _.showcase.source = load(topPage, path, 'showcase', lang, sourceRoot)
329
345
  }
330
346
  // Vs
331
347
  if (config.subpages?.vs === true) {
332
- _.vs.source = load(topPage, path, 'vs', lang)
348
+ _.vs.source = load(topPage, path, 'vs', lang, sourceRoot)
333
349
  }
334
350
  }
335
351
  }
package/src/i18n/index.js CHANGED
@@ -5,10 +5,12 @@ import homePageOverride from 'virtual:docsector-homepage-override'
5
5
  // @ Import language HJSON files (Vite-compatible eager import)
6
6
  const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
7
7
  // @ Import markdown files (Vite-compatible eager import as raw strings)
8
- const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
8
+ const currentMdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
9
+ const oldMdModules = import.meta.glob('../pages/.old/**/*.md', { eager: true, query: '?raw', import: 'default' })
10
+ const mdModules = { ...currentMdModules, ...oldMdModules }
9
11
 
10
12
  // @ Import pages
11
13
  import boot from 'pages/boot'
12
- import { books } from 'virtual:docsector-books'
14
+ import { books, pageEntries } from 'virtual:docsector-books'
13
15
 
14
- export default buildMessages({ langModules, mdModules, books, boot, homePageOverride })
16
+ export default buildMessages({ langModules, mdModules, books, pageEntries, boot, homePageOverride })
package/src/i18n/path.js CHANGED
@@ -73,6 +73,19 @@ export function splitDotPathSegments (dotPath) {
73
73
  return normalized.split('.').filter(Boolean)
74
74
  }
75
75
 
76
+ function splitI18nPathSegments (path) {
77
+ if (Array.isArray(path)) {
78
+ return normalizeSegments(path)
79
+ }
80
+
81
+ const normalized = String(path || '').trim()
82
+ if (normalized.includes('/')) {
83
+ return splitRoutePathSegments(normalized)
84
+ }
85
+
86
+ return splitDotPathSegments(normalized)
87
+ }
88
+
76
89
  export function routeTitleI18nPath (routePath) {
77
90
  return buildI18nPath('_', ...splitRoutePathSegments(routePath), '_')
78
91
  }
@@ -82,11 +95,11 @@ export function routeSubpageSourceI18nPath (routePath, subpage = 'overview') {
82
95
  }
83
96
 
84
97
  export function pageTitleI18nPath (basePath) {
85
- return buildI18nPath('_', ...splitDotPathSegments(basePath), '_')
98
+ return buildI18nPath('_', ...splitI18nPathSegments(basePath), '_')
86
99
  }
87
100
 
88
101
  export function pageValueI18nPath (absolutePath, key = 'source') {
89
- return buildI18nPath('_', ...splitDotPathSegments(absolutePath), key)
102
+ return buildI18nPath('_', ...splitI18nPathSegments(absolutePath), key)
90
103
  }
91
104
 
92
105
  export function namespacedLabelI18nPath (book, nodePath) {
package/src/index.js CHANGED
@@ -27,7 +27,7 @@
27
27
  * @param {string} config.branding.name - Project name displayed in sidebar
28
28
  * @param {string} config.branding.version - Version label
29
29
  * @param {string} [config.branding.description] - Project description (used in llms.txt)
30
- * @param {string[]} [config.branding.versions] - Available versions for dropdown
30
+ * @param {Array<string|Object>} [config.branding.versions] - Available versions for dropdown. Current docs keep unprefixed routes; archived versions can live in src/pages/.old/<version>/ and use /<version>/ route prefixes. Version objects may set released:false/status:'draft', status:'deprecated'/deprecated:true, or badge:{label,color,textColor} for selector badges.
31
31
  * @param {Object} config.links - External links
32
32
  * @param {string} [config.links.github] - GitHub repository URL
33
33
  * @param {string} [config.links.discussions] - GitHub discussions URL
@@ -111,7 +111,7 @@ export function createDocsector (config = {}) {
111
111
  logo: '/images/logo.png',
112
112
  name: 'My Project',
113
113
  version: 'v0.0.1',
114
- versions: ['v0.0.1'],
114
+ versions: [{ id: 'v0.0.1', current: true, released: false }],
115
115
  ...config.branding
116
116
  },
117
117
 
@@ -59,7 +59,7 @@ import { useMeta, colors } from 'quasar'
59
59
 
60
60
  import DMenu from '../components/DMenu.vue'
61
61
  import docsectorConfig from 'docsector.config.js'
62
- import { allBooks } from 'virtual:docsector-books'
62
+ import { allBooks, booksByVersion } from 'virtual:docsector-books'
63
63
  import { pageTitleI18nPath } from '../i18n/path'
64
64
 
65
65
  defineOptions({ name: 'LayoutDefault' })
@@ -178,8 +178,20 @@ const getBookTabStyle = (book) => {
178
178
  }
179
179
  }
180
180
 
181
+ const activeVersionId = computed(() => {
182
+ return route.matched?.[0]?.meta?.version ?? route.meta?.version ?? null
183
+ })
184
+
185
+ const activeVersionBooks = computed(() => {
186
+ if (activeVersionId.value && booksByVersion?.[activeVersionId.value]?.allBooks) {
187
+ return booksByVersion[activeVersionId.value].allBooks
188
+ }
189
+
190
+ return allBooks || []
191
+ })
192
+
181
193
  const sortedBooks = computed(() => {
182
- return [...(allBooks || [])]
194
+ return [...activeVersionBooks.value]
183
195
  .filter(book => book && typeof book.id === 'string' && book.id.length > 0)
184
196
  .sort((a, b) => {
185
197
  const orderA = Number.isFinite(a.order) ? a.order : Number.MAX_SAFE_INTEGER
@@ -261,10 +273,12 @@ function openSettingsDialog () {
261
273
 
262
274
  function getFirstRoutePathByBook (bookId) {
263
275
  const routes = router.options?.routes || []
276
+ const versionId = activeVersionId.value
264
277
  let fallbackPath = null
265
278
 
266
279
  for (const topRoute of routes) {
267
280
  if (!topRoute || typeof topRoute.path !== 'string') continue
281
+ if (versionId && topRoute.meta?.version !== versionId) continue
268
282
  if ((topRoute.meta?.book ?? topRoute.meta?.type) !== bookId) continue
269
283
 
270
284
  const children = Array.isArray(topRoute.children) ? topRoute.children : []
@@ -0,0 +1,7 @@
1
+ ## Archived Version
2
+
3
+ This page is a minimal archived `v0.x` fixture used to validate Docsector Reader version switching.
4
+
5
+ ## Current Behavior
6
+
7
+ Selecting `v0.x` in the sidebar keeps the active book and page when an archived equivalent exists, then serves this Markdown from `src/pages/.old/v0.x`.
@@ -0,0 +1,7 @@
1
+ ## Versao Arquivada
2
+
3
+ Esta pagina e um fixture minimo arquivado `v0.x` usado para validar a troca de versoes do Docsector Reader.
4
+
5
+ ## Comportamento Atual
6
+
7
+ Ao selecionar `v0.x` no menu lateral, o leitor preserva o book e a pagina ativos quando existe um equivalente arquivado, entao serve este Markdown a partir de `src/pages/.old/v0.x`.
@@ -0,0 +1,12 @@
1
+ import { defineBook } from '../../../index.js'
2
+
3
+ export default defineBook({
4
+ id: 'guide',
5
+ label: 'Guide',
6
+ icon: 'history_edu',
7
+ order: 1,
8
+ color: {
9
+ active: 'white',
10
+ inactive: 'white'
11
+ }
12
+ })
@@ -0,0 +1,28 @@
1
+ export default {
2
+ '/getting-started': {
3
+ config: {
4
+ icon: 'history',
5
+ status: 'done',
6
+ meta: {
7
+ description: {
8
+ 'en-US': 'Getting Started - Archived v0.x documentation of Docsector Reader',
9
+ 'pt-BR': 'Comecando - Documentacao arquivada v0.x do Docsector Reader'
10
+ }
11
+ },
12
+ book: 'guide',
13
+ menu: {
14
+ header: {
15
+ icon: 'history_edu',
16
+ label: 'Archived v0.x'
17
+ }
18
+ },
19
+ subpages: {
20
+ showcase: false
21
+ }
22
+ },
23
+ data: {
24
+ 'en-US': { title: 'Getting Started v0.x' },
25
+ 'pt-BR': { title: 'Comecando v0.x' }
26
+ }
27
+ }
28
+ }
@@ -9,10 +9,17 @@ branding: &#123;
9
9
  logo: '/images/logo.png', // Path to logo in public/
10
10
  name: 'My Project', // Project name shown in sidebar
11
11
  version: 'v1.0.0', // Current version badge
12
- versions: ['v1.0.0'] // Version dropdown options
12
+ versions: [
13
+ &#123; id: 'v1.0.0', current: true, released: false &#125;,
14
+ &#123; id: 'v0.x', released: true, status: 'deprecated' &#125;
15
+ ] // Version dropdown options
13
16
  &#125;
14
17
  ```
15
18
 
19
+ The current version keeps unprefixed routes such as `/guide/getting-started/overview/`. Archived major versions can live under `src/pages/.old/&#123;version&#125;/` using the same book registry and Markdown layout, and are exposed at `/<version>/...`, for example `/v0.x/guide/getting-started/overview/`.
20
+
21
+ Every version shows a release badge next to the version in the selector. Released versions default to `released`; versions with `released: false` or `status: 'draft'` default to `draft`; versions with `status: 'deprecated'` or `deprecated: true` default to `deprecated` in red. Use `badge: &#123; label, color, textColor &#125;` to customize the badge.
22
+
16
23
  The `logo` path is relative to the `public/` folder. Recommended size: **85×85px**.
17
24
 
18
25
  ## Links
@@ -84,7 +91,11 @@ export default &#123;
84
91
  logo: '/images/logo.png',
85
92
  name: 'Acme Docs',
86
93
  version: 'v2.1.0',
87
- versions: ['v2.1.0', 'v2.0.0', 'v1.0.0']
94
+ versions: [
95
+ &#123; id: 'v2.1.0', current: true, released: false &#125;,
96
+ &#123; id: 'v2.0.0', released: true &#125;,
97
+ &#123; id: 'v1.0.0', released: true, status: 'deprecated' &#125;
98
+ ]
88
99
  &#125;,
89
100
  links: &#123;
90
101
  github: 'https://github.com/acme/acme',
@@ -9,10 +9,17 @@ branding: &#123;
9
9
  logo: '/images/logo.png', // Caminho do logo em public/
10
10
  name: 'Meu Projeto', // Nome exibido na sidebar
11
11
  version: 'v1.0.0', // Badge de versão atual
12
- versions: ['v1.0.0'] // Opções do dropdown de versão
12
+ versions: [
13
+ &#123; id: 'v1.0.0', current: true, released: false &#125;,
14
+ &#123; id: 'v0.x', released: true, status: 'deprecated' &#125;
15
+ ] // Opções do dropdown de versão
13
16
  &#125;
14
17
  ```
15
18
 
19
+ A versão atual mantém rotas sem prefixo, como `/guide/getting-started/overview/`. Versões major arquivadas podem ficar em `src/pages/.old/&#123;version&#125;/` usando o mesmo layout de book registry e Markdown, e são expostas em `/<version>/...`, por exemplo `/v0.x/guide/getting-started/overview/`.
20
+
21
+ Toda versão mostra um badge de release ao lado da versão no seletor. Versões lançadas usam `released` por padrão; versões com `released: false` ou `status: 'draft'` usam `draft`; versões com `status: 'deprecated'` ou `deprecated: true` usam `deprecated` em vermelho. Use `badge: &#123; label, color, textColor &#125;` para customizar o badge.
22
+
16
23
  O caminho do `logo` é relativo à pasta `public/`. Tamanho recomendado: **85×85px**.
17
24
 
18
25
  ## Links
@@ -84,7 +91,11 @@ export default &#123;
84
91
  logo: '/images/logo.png',
85
92
  name: 'Acme Docs',
86
93
  version: 'v2.1.0',
87
- versions: ['v2.1.0', 'v2.0.0', 'v1.0.0']
94
+ versions: [
95
+ &#123; id: 'v2.1.0', current: true, released: false &#125;,
96
+ &#123; id: 'v2.0.0', released: true &#125;,
97
+ &#123; id: 'v1.0.0', released: true, status: 'deprecated' &#125;
98
+ ]
88
99
  &#125;,
89
100
  links: &#123;
90
101
  github: 'https://github.com/acme/acme',
@@ -83,3 +83,5 @@ Routes are automatically generated from the page registry. A page with path `/my
83
83
  - `/guide/my-page/overview` — Main content tab
84
84
  - `/guide/my-page/showcase` — Showcase tab (if enabled)
85
85
  - `/guide/my-page/vs` — Comparison tab (if enabled)
86
+
87
+ Archived major versions use the same structure under `src/pages/.old/&#123;version&#125;/`. A page registered in `src/pages/.old/v0.x/guide.index.js` produces `/v0.x/guide/my-page/overview` while the current version remains `/guide/my-page/overview`.
@@ -83,3 +83,5 @@ Rotas são geradas automaticamente a partir do registro de páginas. Uma página
83
83
  - `/guide/my-page/overview` — Aba de conteúdo principal
84
84
  - `/guide/my-page/showcase` — Aba de demonstração (se habilitada)
85
85
  - `/guide/my-page/vs` — Aba de comparação (se habilitada)
86
+
87
+ Versões major arquivadas usam a mesma estrutura em `src/pages/.old/&#123;version&#125;/`. Uma página registrada em `src/pages/.old/v0.x/guide.index.js` produz `/v0.x/guide/my-page/overview`, enquanto a versão atual continua em `/guide/my-page/overview`.
@@ -26,7 +26,11 @@ Reads from `docsector.config.js`:
26
26
  - `branding.logo` — Project logo image
27
27
  - `branding.name` — Project name text
28
28
  - `branding.version` — Current version
29
- - `branding.versions` — Version dropdown options
29
+ - `branding.versions` — Version dropdown options, including optional release badges
30
+
31
+ The selector keeps current docs on unprefixed routes and switches archived versions to prefixed routes. For example, current `/guide/getting-started/overview/` can switch to archived `/v0.x/guide/getting-started/overview/` when a matching page exists under `src/pages/.old/v0.x/`.
32
+
33
+ Every version object shows a badge after the version label. Released versions default to `released`; versions with `released: false` or `status: 'draft'` default to `draft`; versions with `status: 'deprecated'` or `deprecated: true` default to `deprecated` in red. The badge can be customized with `badge: { label, color, textColor }`.
30
34
 
31
35
  ## External Links
32
36
 
@@ -43,7 +47,7 @@ Each link is conditionally rendered — set to `null` in the config to hide:
43
47
 
44
48
  ## Page Tree
45
49
 
46
- The page tree is built from the router's routes at component creation time. Routes are grouped by their basepath (second URL segment). Groups with a `menu.header` configuration get an expansion panel with a sticky header.
50
+ The page tree is built from the router's routes at component creation time. Routes are filtered by active version and book, then grouped by their page basepath. Groups with a `menu.header` configuration get an expansion panel with a sticky header.
47
51
 
48
52
  ## Menu Item Grouping
49
53
 
@@ -26,7 +26,11 @@ Lê do `docsector.config.js`:
26
26
  - `branding.logo` — Imagem do logo do projeto
27
27
  - `branding.name` — Texto do nome do projeto
28
28
  - `branding.version` — Versão atual
29
- - `branding.versions` — Opções do dropdown de versão
29
+ - `branding.versions` — Opções do dropdown de versão, incluindo badges opcionais de release
30
+
31
+ O seletor mantém a documentação atual em rotas sem prefixo e troca versões arquivadas para rotas prefixadas. Por exemplo, `/guide/getting-started/overview/` pode trocar para `/v0.x/guide/getting-started/overview/` quando existe uma página equivalente em `src/pages/.old/v0.x/`.
32
+
33
+ Todo objeto de versão mostra um badge depois do label da versão. Versões lançadas usam `released` por padrão; versões com `released: false` ou `status: 'draft'` usam `draft`; versões com `status: 'deprecated'` ou `deprecated: true` usam `deprecated` em vermelho. O badge pode ser customizado com `badge: { label, color, textColor }`.
30
34
 
31
35
  ## Links Externos
32
36
 
@@ -43,7 +47,7 @@ Cada link é renderizado condicionalmente — defina como `null` no config para
43
47
 
44
48
  ## Árvore de Páginas
45
49
 
46
- A árvore de páginas é construída a partir das rotas do roteador no momento de criação do componente. Rotas são agrupadas pelo seu basepath (segundo segmento da URL). Grupos com configuração `menu.header` recebem um painel de expansão com header sticky.
50
+ A árvore de páginas é construída a partir das rotas do roteador no momento de criação do componente. Rotas são filtradas pela versão e pelo book ativos, depois agrupadas pelo basepath da página. Grupos com configuração `menu.header` recebem um painel de expansão com header sticky.
47
51
 
48
52
  ## Agrupamento de Itens
49
53