@docsector/docsector-reader 1.7.1 → 2.0.1
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/.eslintrc.cjs +2 -0
- package/README.md +91 -9
- package/bin/docsector.js +27 -13
- package/package.json +1 -1
- package/src/components/DH1.vue +2 -1
- package/src/components/DMenu.vue +70 -30
- package/src/components/DMenuItem.vue +12 -6
- package/src/components/DPage.vue +124 -31
- package/src/components/DPageAnchor.vue +13 -1
- package/src/components/DPageBar.vue +2 -1
- package/src/components/DPageMeta.vue +9 -4
- package/src/components/DPageSection.vue +2 -1
- package/src/composables/useWebMcp.js +2 -1
- package/src/i18n/helpers.js +36 -9
- package/src/i18n/index.js +2 -2
- package/src/i18n/path.js +101 -0
- package/src/index.js +25 -2
- package/src/layouts/DefaultLayout.vue +181 -2
- package/src/pages/guide/getting-started.overview.en-US.md +3 -2
- package/src/pages/guide/getting-started.overview.pt-BR.md +3 -2
- package/src/pages/guide/pages-and-routing.overview.en-US.md +6 -6
- package/src/pages/guide/pages-and-routing.overview.pt-BR.md +6 -6
- package/src/pages/guide.book.js +12 -0
- package/src/pages/guide.index.js +184 -0
- package/src/pages/manual.book.js +12 -0
- package/src/pages/{index.js → manual.index.js} +13 -216
- package/src/quasar.factory.js +398 -56
- package/src/router/routes.js +132 -46
package/src/components/DPage.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
|
2
|
+
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
|
|
3
3
|
import { useStore } from 'vuex'
|
|
4
4
|
import { useRoute, useRouter } from 'vue-router'
|
|
5
5
|
import { useQuasar } from 'quasar'
|
|
@@ -24,6 +24,38 @@ const props = defineProps({
|
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
const pageScrollArea = ref(null)
|
|
27
|
+
const pageContainer = ref(null)
|
|
28
|
+
const submenu = ref(null)
|
|
29
|
+
const pageMinHeight = ref('calc(100vh - 86px)')
|
|
30
|
+
const submenuHeight = ref('36px')
|
|
31
|
+
const pageBottomInset = ref('0px')
|
|
32
|
+
|
|
33
|
+
const updatePageMinHeight = () => {
|
|
34
|
+
const pageContainerEl = pageContainer.value?.$el || pageContainer.value
|
|
35
|
+
const submenuEl = submenu.value?.$el || submenu.value
|
|
36
|
+
|
|
37
|
+
if (!pageContainerEl || !submenuEl) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const pageContainerStyles = window.getComputedStyle(pageContainerEl)
|
|
42
|
+
const headerHeight = Number.parseFloat(pageContainerStyles.paddingTop) || 0
|
|
43
|
+
const measuredSubmenuHeight = submenuEl.offsetHeight || 0
|
|
44
|
+
const isMobile = $q.screen.lt.md
|
|
45
|
+
const totalOffset = Math.max(0, Math.round(headerHeight + (isMobile ? 0 : measuredSubmenuHeight)))
|
|
46
|
+
|
|
47
|
+
pageMinHeight.value = `calc(100vh - ${totalOffset}px)`
|
|
48
|
+
submenuHeight.value = `${Math.max(36, Math.round(measuredSubmenuHeight))}px`
|
|
49
|
+
pageBottomInset.value = isMobile ? submenuHeight.value : '0px'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const schedulePageMinHeightUpdate = () => {
|
|
53
|
+
window.requestAnimationFrame(() => {
|
|
54
|
+
window.requestAnimationFrame(() => {
|
|
55
|
+
updatePageMinHeight()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
}
|
|
27
59
|
|
|
28
60
|
const overview = computed(() => route.matched[0].path)
|
|
29
61
|
const showcase = computed(() => {
|
|
@@ -164,6 +196,10 @@ const handleMainScrollKeys = (event) => {
|
|
|
164
196
|
|
|
165
197
|
onMounted(() => {
|
|
166
198
|
window.addEventListener('keydown', handleMainScrollKeys)
|
|
199
|
+
window.addEventListener('resize', schedulePageMinHeightUpdate)
|
|
200
|
+
nextTick(() => {
|
|
201
|
+
schedulePageMinHeightUpdate()
|
|
202
|
+
})
|
|
167
203
|
|
|
168
204
|
router.beforeEach((to, from, next) => {
|
|
169
205
|
resetPageScroll()
|
|
@@ -180,38 +216,60 @@ onMounted(() => {
|
|
|
180
216
|
|
|
181
217
|
onBeforeUnmount(() => {
|
|
182
218
|
window.removeEventListener('keydown', handleMainScrollKeys)
|
|
219
|
+
window.removeEventListener('resize', schedulePageMinHeightUpdate)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
watch(() => route.fullPath, () => {
|
|
223
|
+
nextTick(() => {
|
|
224
|
+
schedulePageMinHeightUpdate()
|
|
225
|
+
})
|
|
183
226
|
})
|
|
184
227
|
</script>
|
|
185
228
|
|
|
186
229
|
<template>
|
|
187
|
-
<q-page-container
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
230
|
+
<q-page-container
|
|
231
|
+
id="page-container"
|
|
232
|
+
ref="pageContainer"
|
|
233
|
+
:style="{
|
|
234
|
+
'--d-page-min-height': pageMinHeight,
|
|
235
|
+
'--d-submenu-height': submenuHeight,
|
|
236
|
+
'--d-page-bottom-inset': pageBottomInset
|
|
237
|
+
}"
|
|
238
|
+
>
|
|
239
|
+
<q-toolbar
|
|
240
|
+
id="submenu"
|
|
241
|
+
ref="submenu"
|
|
242
|
+
class="bg-grey-8 text-white"
|
|
243
|
+
:class="$q.screen.lt.md ? 'd-submenu--mobile' : 'd-submenu--desktop'"
|
|
244
|
+
>
|
|
245
|
+
<div class="d-submenu__content">
|
|
246
|
+
<q-toolbar-title class="toolbar-container">
|
|
247
|
+
<q-btn-group :class="$q.screen.lt.md ? 'mobile' : null">
|
|
248
|
+
<q-btn
|
|
249
|
+
v-if="overview && (showcase || vs)"
|
|
250
|
+
no-caps flat
|
|
251
|
+
:class="pActive('/overview')"
|
|
252
|
+
:label="$t('submenu.overview')" icon="pageview"
|
|
253
|
+
@click="subroute('/overview')"
|
|
254
|
+
/>
|
|
255
|
+
<q-btn
|
|
256
|
+
v-if="showcase"
|
|
257
|
+
no-caps flat
|
|
258
|
+
:class="pActive('/showcase')"
|
|
259
|
+
:label="$t('submenu.showcase')" icon="play_circle_filled"
|
|
260
|
+
@click="subroute('/showcase')"
|
|
261
|
+
/>
|
|
262
|
+
<q-btn
|
|
263
|
+
v-if="vs"
|
|
264
|
+
no-caps flat
|
|
265
|
+
:class="pActive('/vs')"
|
|
266
|
+
:label="$t('submenu.versus')" icon="compare"
|
|
267
|
+
@click="subroute('/vs')"
|
|
268
|
+
/>
|
|
269
|
+
</q-btn-group>
|
|
270
|
+
</q-toolbar-title>
|
|
271
|
+
<q-btn class="d-submenu__toggle" @click="toggleSectionsTree" icon="account_tree" />
|
|
272
|
+
</div>
|
|
215
273
|
</q-toolbar>
|
|
216
274
|
|
|
217
275
|
<q-page id="page">
|
|
@@ -236,7 +294,7 @@ onBeforeUnmount(() => {
|
|
|
236
294
|
|
|
237
295
|
.content,
|
|
238
296
|
.content > div.scroll
|
|
239
|
-
min-height: calc(100vh - 86px)
|
|
297
|
+
min-height: var(--d-page-min-height, calc(100vh - 86px))
|
|
240
298
|
|
|
241
299
|
.content > div.scroll > div.q-scrollarea__content
|
|
242
300
|
max-width: 100%
|
|
@@ -244,9 +302,10 @@ onBeforeUnmount(() => {
|
|
|
244
302
|
|
|
245
303
|
.content:not(.no-padding) > div.scroll > div.q-scrollarea__content
|
|
246
304
|
padding: 15px
|
|
305
|
+
padding-bottom: calc(15px + var(--d-page-bottom-inset, 0px) + env(safe-area-inset-bottom, 0px))
|
|
247
306
|
|
|
248
307
|
#page
|
|
249
|
-
min-height: calc(100vh - 86px) !important
|
|
308
|
+
min-height: var(--d-page-min-height, calc(100vh - 86px)) !important
|
|
250
309
|
|
|
251
310
|
#scroll-container
|
|
252
311
|
width: 100%
|
|
@@ -259,10 +318,23 @@ onBeforeUnmount(() => {
|
|
|
259
318
|
box-shadow: 0 2px 4px -1px rgba(0,0,0,0.2), 0 4px 5px rgba(0,0,0,0.14), 0 1px 6px rgba(0,0,0,0.12)
|
|
260
319
|
overflow: visible
|
|
261
320
|
|
|
321
|
+
.d-submenu__content
|
|
322
|
+
width: calc(100% - 30px)
|
|
323
|
+
max-width: 1200px
|
|
324
|
+
min-height: inherit
|
|
325
|
+
margin: 0 auto
|
|
326
|
+
display: flex
|
|
327
|
+
align-items: center
|
|
328
|
+
align-self: stretch
|
|
329
|
+
|
|
330
|
+
.d-submenu__toggle
|
|
331
|
+
flex: 0 0 auto
|
|
332
|
+
|
|
262
333
|
.on-left
|
|
263
334
|
margin-right: 5px
|
|
264
335
|
.toolbar-container
|
|
265
336
|
overflow: visible
|
|
337
|
+
padding: 0
|
|
266
338
|
.q-btn-group
|
|
267
339
|
box-shadow: none
|
|
268
340
|
&.mobile
|
|
@@ -276,6 +348,19 @@ onBeforeUnmount(() => {
|
|
|
276
348
|
&:not(.focus-helper)
|
|
277
349
|
margin-left: 6px
|
|
278
350
|
|
|
351
|
+
#submenu.d-submenu--mobile
|
|
352
|
+
position: fixed
|
|
353
|
+
left: 0
|
|
354
|
+
right: 0
|
|
355
|
+
bottom: 0
|
|
356
|
+
z-index: 1500
|
|
357
|
+
min-height: 40px
|
|
358
|
+
padding-bottom: env(safe-area-inset-bottom, 0px)
|
|
359
|
+
box-shadow: 0 -1px 2px rgba(0,0,0,0.12), 0 -2px 6px rgba(0,0,0,0.08)
|
|
360
|
+
|
|
361
|
+
.d-submenu__content
|
|
362
|
+
align-items: flex-end
|
|
363
|
+
|
|
279
364
|
#submenu a,
|
|
280
365
|
#submenu button
|
|
281
366
|
border-radius: 0
|
|
@@ -289,6 +374,10 @@ body.body--light
|
|
|
289
374
|
background-color: #fff !important
|
|
290
375
|
color: #000
|
|
291
376
|
box-shadow: 0 10px 0 0 #fff
|
|
377
|
+
|
|
378
|
+
#submenu.d-submenu--mobile a.active,
|
|
379
|
+
#submenu.d-submenu--mobile button.active
|
|
380
|
+
box-shadow: 0 -10px 0 0 #fff
|
|
292
381
|
// Dark
|
|
293
382
|
body.body--dark
|
|
294
383
|
#submenu a.active,
|
|
@@ -297,6 +386,10 @@ body.body--dark
|
|
|
297
386
|
color: #fff
|
|
298
387
|
box-shadow: 0 10px 0 0 var(--q-dark-page)
|
|
299
388
|
|
|
389
|
+
#submenu.d-submenu--mobile a.active,
|
|
390
|
+
#submenu.d-submenu--mobile button.active
|
|
391
|
+
box-shadow: 0 -10px 0 0 var(--q-dark-page)
|
|
392
|
+
|
|
300
393
|
body.mobile.body--dark
|
|
301
394
|
.q-drawer--right
|
|
302
395
|
background: rgba(18, 0, 0, 0.7)
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
3
3
|
import { useStore } from 'vuex'
|
|
4
4
|
import { useQuasar } from 'quasar'
|
|
5
|
+
import { useI18n } from 'vue-i18n'
|
|
5
6
|
import { useRoute } from "vue-router";
|
|
6
7
|
|
|
7
8
|
import useNavigator from '../composables/useNavigator'
|
|
9
|
+
import { pageTitleI18nPath } from '../i18n/path'
|
|
8
10
|
|
|
9
11
|
const store = useStore()
|
|
10
12
|
const $q = useQuasar()
|
|
11
13
|
const route = useRoute()
|
|
14
|
+
const { t } = useI18n()
|
|
12
15
|
const { navigate, anchor, selected: navigatorSelected } = useNavigator()
|
|
13
16
|
|
|
14
17
|
const scrolling = ref(null)
|
|
@@ -47,6 +50,15 @@ const stylize = computed(() => {
|
|
|
47
50
|
}
|
|
48
51
|
})
|
|
49
52
|
|
|
53
|
+
const fallbackNodeLabel = computed(() => {
|
|
54
|
+
const base = store.state.i18n.base
|
|
55
|
+
if (!base) {
|
|
56
|
+
return ''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return t(pageTitleI18nPath(base))
|
|
60
|
+
})
|
|
61
|
+
|
|
50
62
|
const scrollToActiveAnchor = () => {
|
|
51
63
|
if (scrolling.value) {
|
|
52
64
|
clearTimeout(scrolling.value)
|
|
@@ -109,7 +121,7 @@ onBeforeUnmount(() => {
|
|
|
109
121
|
>
|
|
110
122
|
<template v-slot:default-header="props">
|
|
111
123
|
<b v-if="props.node.label" v-html="props.node.label"></b>
|
|
112
|
-
<b v-else>{{
|
|
124
|
+
<b v-else>{{ fallbackNodeLabel }}</b>
|
|
113
125
|
</template>
|
|
114
126
|
</q-tree>
|
|
115
127
|
</template>
|
|
@@ -7,6 +7,7 @@ import { copyToClipboard, useQuasar } from 'quasar'
|
|
|
7
7
|
|
|
8
8
|
import docsectorConfig from 'docsector.config.js'
|
|
9
9
|
import gitDates from 'virtual:docsector-git-dates'
|
|
10
|
+
import { pageValueI18nPath } from '../i18n/path'
|
|
10
11
|
|
|
11
12
|
const $q = useQuasar()
|
|
12
13
|
const store = useStore()
|
|
@@ -59,7 +60,7 @@ const rawMarkdown = computed(() => {
|
|
|
59
60
|
const absolute = store.state.i18n.absolute
|
|
60
61
|
if (!absolute) return ''
|
|
61
62
|
|
|
62
|
-
const source = t(
|
|
63
|
+
const source = t(pageValueI18nPath(absolute, 'source'))
|
|
63
64
|
if (!source) return ''
|
|
64
65
|
|
|
65
66
|
return String(source)
|
|
@@ -6,6 +6,7 @@ import { openURL } from 'quasar'
|
|
|
6
6
|
import { useI18n } from 'vue-i18n'
|
|
7
7
|
|
|
8
8
|
import docsectorConfig from 'docsector.config.js'
|
|
9
|
+
import { pageValueI18nPath, routeTitleI18nPath } from '../i18n/path'
|
|
9
10
|
|
|
10
11
|
const store = useStore()
|
|
11
12
|
const route = useRoute()
|
|
@@ -72,7 +73,7 @@ const progress = computed(() => {
|
|
|
72
73
|
const currentLang = locale.value
|
|
73
74
|
|
|
74
75
|
// Count headers (## and ###) in the default language source
|
|
75
|
-
const defaultSourcePath =
|
|
76
|
+
const defaultSourcePath = pageValueI18nPath(i18nPathAbsolute, 'source')
|
|
76
77
|
const defaultSource = te(defaultSourcePath, defaultLang) ? tm(defaultSourcePath, defaultLang) : ''
|
|
77
78
|
|
|
78
79
|
if (!defaultSource || typeof defaultSource !== 'string') {
|
|
@@ -106,7 +107,7 @@ const progress = computed(() => {
|
|
|
106
107
|
|
|
107
108
|
const languages = computed(() => {
|
|
108
109
|
const i18nPathAbsolute = store.state.i18n.absolute
|
|
109
|
-
const translations =
|
|
110
|
+
const translations = pageValueI18nPath(i18nPathAbsolute, '_translations')
|
|
110
111
|
const i18nLocales = availableLocales
|
|
111
112
|
let fallbackLastUpdated = null
|
|
112
113
|
|
|
@@ -165,6 +166,10 @@ const hideRemoteHomeFooterMeta = computed(() => {
|
|
|
165
166
|
|
|
166
167
|
return isRemoteHome && isHomePage
|
|
167
168
|
})
|
|
169
|
+
|
|
170
|
+
const getRouteTitle = (path) => {
|
|
171
|
+
return t(routeTitleI18nPath(path))
|
|
172
|
+
}
|
|
168
173
|
</script>
|
|
169
174
|
|
|
170
175
|
<template>
|
|
@@ -197,11 +202,11 @@ const hideRemoteHomeFooterMeta = computed(() => {
|
|
|
197
202
|
<router-link class="link col" v-if="prev" :to="`${prev}/overview/`">
|
|
198
203
|
<div class="text-caption">{{ $t('page.nav.prev') }}</div>
|
|
199
204
|
<q-icon name="navigate_before" />
|
|
200
|
-
<span>{{
|
|
205
|
+
<span>{{ getRouteTitle(prev) }}</span>
|
|
201
206
|
</router-link>
|
|
202
207
|
<router-link class="link col" v-if="next" :to="`${next}/overview/`">
|
|
203
208
|
<div class="text-caption">{{ $t('page.nav.next') }}</div>
|
|
204
|
-
<span>{{
|
|
209
|
+
<span>{{ getRouteTitle(next) }}</span>
|
|
205
210
|
<q-icon name="navigate_next" />
|
|
206
211
|
</router-link>
|
|
207
212
|
</nav>
|
|
@@ -14,6 +14,7 @@ import DPageSourceCode from './DPageSourceCode.vue'
|
|
|
14
14
|
import DMermaidDiagram from './DMermaidDiagram.vue'
|
|
15
15
|
import DPageBlockquote from './DPageBlockquote.vue'
|
|
16
16
|
import DQuickLinks from './DQuickLinks.vue'
|
|
17
|
+
import { pageValueI18nPath } from '../i18n/path'
|
|
17
18
|
|
|
18
19
|
const props = defineProps({
|
|
19
20
|
id: {
|
|
@@ -122,7 +123,7 @@ const tokenized = computed(() => {
|
|
|
122
123
|
return []
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
const source = t(
|
|
126
|
+
const source = t(pageValueI18nPath(absolute, 'source'))
|
|
126
127
|
const normalizedSource = String(source)
|
|
127
128
|
.replace(/{/g, '{')
|
|
128
129
|
.replace(/}/g, '}')
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import docsectorConfig from 'docsector.config.js'
|
|
2
|
+
import { pageValueI18nPath } from '../i18n/path'
|
|
2
3
|
|
|
3
4
|
let activeCleanup = null
|
|
4
5
|
|
|
@@ -253,7 +254,7 @@ function createToolDefinitions ({
|
|
|
253
254
|
|
|
254
255
|
let content = ''
|
|
255
256
|
if (includeContent && absolute) {
|
|
256
|
-
const source = translate(
|
|
257
|
+
const source = translate(pageValueI18nPath(absolute, 'source'))
|
|
257
258
|
if (source) {
|
|
258
259
|
content = decodeMarkdownSource(source)
|
|
259
260
|
}
|
package/src/i18n/helpers.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* import { buildMessages } from '@docsector/docsector-reader/i18n'
|
|
10
10
|
* import boot from 'pages/boot'
|
|
11
|
-
* import pages from '
|
|
11
|
+
* import { allPages as pages } from 'virtual:docsector-books'
|
|
12
12
|
*
|
|
13
13
|
* const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
|
|
14
|
-
* const mdModules = import.meta.glob('../pages
|
|
14
|
+
* const mdModules = import.meta.glob('all markdown files under ../pages recursively', { eager: true, query: '?raw', import: 'default' })
|
|
15
15
|
*
|
|
16
16
|
* export default buildMessages({ langModules, mdModules, pages, boot })
|
|
17
17
|
*/
|
|
@@ -113,14 +113,15 @@ export function filter (source) {
|
|
|
113
113
|
*
|
|
114
114
|
* @param {Object} options
|
|
115
115
|
* @param {Object} options.langModules - Result of import.meta.glob('./languages/*.hjson', { eager: true })
|
|
116
|
-
* @param {Object} options.mdModules - Result of
|
|
117
|
-
* @param {Object} options.pages -
|
|
116
|
+
* @param {Object} options.mdModules - Result of recursively globbing markdown files under ../pages with eager raw imports
|
|
117
|
+
* @param {Object} [options.pages] - Legacy merged page registry from virtual:docsector-books (allPages)
|
|
118
|
+
* @param {Object} [options.books] - Book registry from virtual:docsector-books (preferred, avoids path collisions)
|
|
118
119
|
* @param {Object} options.boot - Boot meta from pages/boot.js
|
|
119
120
|
* @param {string[]} [options.langs] - Language codes to process (auto-detected from langModules if omitted)
|
|
120
121
|
* @param {Object<string,string>} [options.homePageOverride] - Optional per-language Home markdown override
|
|
121
122
|
* @returns {Object} Complete i18n messages object keyed by locale
|
|
122
123
|
*/
|
|
123
|
-
export function buildMessages ({ langModules, mdModules, pages, boot, langs, homePageOverride = {} }) {
|
|
124
|
+
export function buildMessages ({ langModules, mdModules, pages, books, boot, langs, homePageOverride = {} }) {
|
|
124
125
|
// Auto-detect languages from HJSON files if not provided
|
|
125
126
|
if (!langs) {
|
|
126
127
|
langs = Object.keys(langModules).map(key => {
|
|
@@ -199,6 +200,23 @@ export function buildMessages ({ langModules, mdModules, pages, boot, langs, hom
|
|
|
199
200
|
return match[1].trim()
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
const pageEntries = []
|
|
204
|
+
|
|
205
|
+
if (books && typeof books === 'object' && Object.keys(books).length > 0) {
|
|
206
|
+
for (const [bookId, book] of Object.entries(books)) {
|
|
207
|
+
const routes = book?.routes || {}
|
|
208
|
+
const fallbackBook = book?.config?.id || bookId || 'manual'
|
|
209
|
+
|
|
210
|
+
for (const [key, page] of Object.entries(routes)) {
|
|
211
|
+
pageEntries.push({ key, page, fallbackBook })
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
for (const [key, page] of Object.entries(pages || {})) {
|
|
216
|
+
pageEntries.push({ key, page, fallbackBook: null })
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
202
220
|
// @ Iterate langs
|
|
203
221
|
for (const lang of langs) {
|
|
204
222
|
// Load HJSON language file
|
|
@@ -231,14 +249,18 @@ export function buildMessages ({ langModules, mdModules, pages, boot, langs, hom
|
|
|
231
249
|
i18n[lang]._.home.overview.source = loadHomepage(lang)
|
|
232
250
|
|
|
233
251
|
// @ Iterate pages
|
|
234
|
-
for (const
|
|
235
|
-
const
|
|
252
|
+
for (const entry of pageEntries) {
|
|
253
|
+
const { key, page, fallbackBook } = entry
|
|
254
|
+
const path = key.startsWith('/') ? key.slice(1) : key
|
|
236
255
|
|
|
237
256
|
const config = page.config
|
|
238
257
|
const data = page.data
|
|
239
258
|
const meta = page.meta || boot.meta
|
|
240
259
|
|
|
241
|
-
const topPage = config?.type ?? 'manual'
|
|
260
|
+
const topPage = config?.book ?? config?.type ?? fallbackBook ?? 'manual'
|
|
261
|
+
if (i18n[lang]._[topPage] === undefined) {
|
|
262
|
+
i18n[lang]._[topPage] = {}
|
|
263
|
+
}
|
|
242
264
|
|
|
243
265
|
// ---
|
|
244
266
|
|
|
@@ -254,7 +276,7 @@ export function buildMessages ({ langModules, mdModules, pages, boot, langs, hom
|
|
|
254
276
|
// @ Set metadata
|
|
255
277
|
// title
|
|
256
278
|
if (node._ === undefined) {
|
|
257
|
-
node._ = data[lang]?.title || data['*']?.title
|
|
279
|
+
node._ = data?.[lang]?.title || data?.['*']?.title || data?.['en-US']?.title || ''
|
|
258
280
|
}
|
|
259
281
|
|
|
260
282
|
if (config === null) {
|
|
@@ -293,6 +315,11 @@ export function buildMessages ({ langModules, mdModules, pages, boot, langs, hom
|
|
|
293
315
|
continue
|
|
294
316
|
}
|
|
295
317
|
|
|
318
|
+
const hasInternalLink = typeof config?.link?.to === 'string' && config.link.to.trim().length > 0
|
|
319
|
+
if (hasInternalLink) {
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
|
|
296
323
|
// @ Subpages
|
|
297
324
|
// Overview
|
|
298
325
|
_.overview.source = load(topPage, path, 'overview', lang)
|
package/src/i18n/index.js
CHANGED
|
@@ -9,6 +9,6 @@ const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?r
|
|
|
9
9
|
|
|
10
10
|
// @ Import pages
|
|
11
11
|
import boot from 'pages/boot'
|
|
12
|
-
import
|
|
12
|
+
import { books } from 'virtual:docsector-books'
|
|
13
13
|
|
|
14
|
-
export default buildMessages({ langModules, mdModules,
|
|
14
|
+
export default buildMessages({ langModules, mdModules, books, boot, homePageOverride })
|
package/src/i18n/path.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
2
|
+
|
|
3
|
+
function escapeSegment (segment) {
|
|
4
|
+
return String(segment)
|
|
5
|
+
.replace(/\\/g, '\\\\')
|
|
6
|
+
.replace(/'/g, "\\'")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeSegments (segmentsInput, accumulator = []) {
|
|
10
|
+
for (const segment of segmentsInput) {
|
|
11
|
+
if (Array.isArray(segment)) {
|
|
12
|
+
normalizeSegments(segment, accumulator)
|
|
13
|
+
continue
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (segment === undefined || segment === null) {
|
|
17
|
+
continue
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const normalized = String(segment).trim()
|
|
21
|
+
if (normalized.length === 0) {
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
accumulator.push(normalized)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return accumulator
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildI18nPath (...segmentsInput) {
|
|
32
|
+
const segments = normalizeSegments(segmentsInput)
|
|
33
|
+
if (segments.length === 0) {
|
|
34
|
+
return ''
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let path = ''
|
|
38
|
+
|
|
39
|
+
for (const [index, segment] of segments.entries()) {
|
|
40
|
+
if (index === 0) {
|
|
41
|
+
path += IDENTIFIER_PATTERN.test(segment)
|
|
42
|
+
? segment
|
|
43
|
+
: `['${escapeSegment(segment)}']`
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
path += IDENTIFIER_PATTERN.test(segment)
|
|
48
|
+
? `.${segment}`
|
|
49
|
+
: `['${escapeSegment(segment)}']`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return path
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function splitRoutePathSegments (routePath) {
|
|
56
|
+
const normalized = String(routePath || '')
|
|
57
|
+
.replace(/\/+$/, '')
|
|
58
|
+
.replace(/^\//, '')
|
|
59
|
+
|
|
60
|
+
if (normalized.length === 0) {
|
|
61
|
+
return []
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return normalized.split('/').filter(Boolean)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function splitDotPathSegments (dotPath) {
|
|
68
|
+
const normalized = String(dotPath || '').trim()
|
|
69
|
+
if (normalized.length === 0) {
|
|
70
|
+
return []
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return normalized.split('.').filter(Boolean)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function routeTitleI18nPath (routePath) {
|
|
77
|
+
return buildI18nPath('_', ...splitRoutePathSegments(routePath), '_')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function routeSubpageSourceI18nPath (routePath, subpage = 'overview') {
|
|
81
|
+
return buildI18nPath('_', ...splitRoutePathSegments(routePath), subpage, 'source')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function pageTitleI18nPath (basePath) {
|
|
85
|
+
return buildI18nPath('_', ...splitDotPathSegments(basePath), '_')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function pageValueI18nPath (absolutePath, key = 'source') {
|
|
89
|
+
return buildI18nPath('_', ...splitDotPathSegments(absolutePath), key)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function namespacedLabelI18nPath (book, nodePath) {
|
|
93
|
+
const normalizedNodePath = String(nodePath || '').replace(/^\./, '')
|
|
94
|
+
|
|
95
|
+
return buildI18nPath(
|
|
96
|
+
'_',
|
|
97
|
+
String(book || 'manual'),
|
|
98
|
+
...splitDotPathSegments(normalizedNodePath),
|
|
99
|
+
'_'
|
|
100
|
+
)
|
|
101
|
+
}
|
package/src/index.js
CHANGED
|
@@ -234,11 +234,34 @@ export function createDocsector (config = {}) {
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Define a Docsector book entry for the pages registry.
|
|
239
|
+
*
|
|
240
|
+
* @param {Object} config - Book configuration (id, label, icon, order, color)
|
|
241
|
+
* @param {Object|string} [config.color] - Tab text color settings
|
|
242
|
+
* @param {string} [config.color.active] - Active tab text color token (Quasar color key, CSS var, or CSS color)
|
|
243
|
+
* @param {string} [config.color.inactive] - Inactive tab text color token (Quasar color key, CSS var, or CSS color)
|
|
244
|
+
* @returns {Object} Normalized book definition
|
|
245
|
+
*/
|
|
246
|
+
export function defineBook (config = {}) {
|
|
247
|
+
const resolvedId = typeof config.id === 'string' ? config.id : ''
|
|
248
|
+
const resolvedLabel = config.label
|
|
249
|
+
|| (resolvedId ? `${resolvedId.charAt(0).toUpperCase()}${resolvedId.slice(1)}` : '')
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
...config,
|
|
253
|
+
...(resolvedId ? { id: resolvedId } : {}),
|
|
254
|
+
...(resolvedLabel ? { label: resolvedLabel } : {})
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
237
258
|
/**
|
|
238
259
|
* Define a Docsector page entry for the pages registry.
|
|
239
260
|
*
|
|
240
261
|
* @param {Object} options - Page options
|
|
241
|
-
* @param {Object} options.config - Page configuration (icon, status,
|
|
262
|
+
* @param {Object} options.config - Page configuration (icon, status, book, menu, subpages, link)
|
|
263
|
+
* @param {Object} [options.config.link] - Optional internal navigation link (menu shortcut)
|
|
264
|
+
* @param {string} options.config.link.to - Internal destination path (e.g. '/guide/getting-started/overview/')
|
|
242
265
|
* @param {Object} options.data - Per-language titles { 'en-US': { title: '...' } }
|
|
243
266
|
* @returns {Object} Page definition
|
|
244
267
|
*/
|
|
@@ -249,4 +272,4 @@ export function definePage (options) {
|
|
|
249
272
|
}
|
|
250
273
|
}
|
|
251
274
|
|
|
252
|
-
export default { createDocsector, definePage }
|
|
275
|
+
export default { createDocsector, defineBook, definePage }
|