@docsector/docsector-reader 4.0.0 → 4.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/README.md +5 -4
- package/bin/docsector.js +1 -1
- package/package.json +1 -1
- package/src/components/DBlockExpandable.vue +1 -0
- package/src/components/DPageSection.vue +5 -0
- package/src/components/DPageTokens.vue +10 -1
- package/src/components/DSubpage.vue +14 -2
- package/src/composables/useActiveAnchor.js +42 -0
- package/src/composables/useNavigator.js +24 -17
- package/src/home-page-mode.js +5 -0
- package/src/pages/manual/basic/d-page-anchor.overview.en-US.md +1 -1
- package/src/pages/manual/basic/d-page-anchor.overview.pt-BR.md +1 -1
- package/src/quasar.factory.js +13 -11
- package/src/store/Page.js +4 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
<h1 align="center">Docsector Reader 📖</h1>
|
|
5
5
|
<p align="center">
|
|
6
|
-
<i>A documentation rendering engine built with Vue 3, Quasar v2 and Vite.</i>
|
|
6
|
+
<i>A documentation rendering engine built with Vue 3, Quasar v2 and Vite with AI features.</i>
|
|
7
7
|
</p>
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="https://www.npmjs.com/package/@docsector/docsector-reader">
|
|
@@ -50,7 +50,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
50
50
|
- 🚨 **GitHub-Style Alerts** — Native support for `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, and `[!CAUTION]`
|
|
51
51
|
- 🌍 **Internationalization (i18n)** — Multi-language support with HJSON locale files and per-page translations
|
|
52
52
|
- 🌗 **Dark/Light Mode** — Automatic theme switching with Quasar Dark Plugin
|
|
53
|
-
- 🔗 **Anchor Navigation** — Right-side Table of Contents tree with scroll tracking
|
|
53
|
+
- 🔗 **Anchor Navigation** — Right-side Table of Contents tree with stable scroll tracking, auto-scroll to the active section, and active-heading resolution based on the last heading that crossed the content threshold
|
|
54
54
|
- 🖱️ **Active Menu Item UX** — Active menu entries keep pointer cursor, clear URL hash without redundant navigation, and prevent accidental label text selection
|
|
55
55
|
- 🔎 **Search** — Menu search across all documentation content and tags
|
|
56
56
|
- 📱 **Responsive** — Mobile-friendly with collapsible sidebar and drawers
|
|
@@ -65,7 +65,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
65
65
|
- 📊 **Translation Progress** — Automatic translation percentage based on header coverage
|
|
66
66
|
- 🌐 **Accurate Available Translations** — Locale availability counter now uses actual localized page source presence, avoiding false negatives when metadata is equal
|
|
67
67
|
- 🏠 **Markdown Home at Root** — Homepage is rendered from `src/pages/Homepage.{lang}.md` directly at `/`
|
|
68
|
-
- 🌍 **Remote README as Home** — Optional build-time remote README source for homepage with automatic local fallback
|
|
68
|
+
- 🌍 **Remote README as Home** — Optional build-time remote README source for homepage with automatic local fallback and automatic primary-title handoff when the remote README already provides the project heading
|
|
69
69
|
- 🔗 **GitHub-Compatible Heading Anchors** — Markdown headings use GitHub-style slugs so standard README Table of Contents links work inside Docsector
|
|
70
70
|
- 🧬 **Scaffolded Homepage Override Wiring** — New consumer projects automatically wire `virtual:docsector-homepage-override` into i18n message building
|
|
71
71
|
- 📖 **Expandable Markdown Sections** — Use `<d-block-expandable title="...">...</d-block-expandable>` to collapse secondary content while keeping rich Markdown support inside the body
|
|
@@ -360,7 +360,8 @@ You can configure Docsector Reader to use a remote README as homepage content.
|
|
|
360
360
|
|
|
361
361
|
- Fetch happens at build-time.
|
|
362
362
|
- The same README content is used for all configured languages.
|
|
363
|
-
-
|
|
363
|
+
- When the remote README resolves successfully, Docsector hides the autogenerated homepage title and uses the README's own primary heading in the rendered content.
|
|
364
|
+
- If fetch fails, it falls back to local `src/pages/Homepage.{lang}.md` by default and keeps the usual autogenerated homepage title.
|
|
364
365
|
- Standard GitHub-style heading links and README Table of Contents fragments keep working in the rendered homepage.
|
|
365
366
|
|
|
366
367
|
### Configure
|
package/bin/docsector.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docsector/docsector-reader",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "A documentation rendering engine built with Vue 3, Quasar v2 and Vite. Transform Markdown into beautiful, navigable documentation sites.",
|
|
5
5
|
"productName": "Docsector Reader",
|
|
6
6
|
"author": "Rodrigo de Araujo Vieira",
|
|
@@ -11,6 +11,10 @@ defineProps({
|
|
|
11
11
|
id: {
|
|
12
12
|
type: Number,
|
|
13
13
|
required: true
|
|
14
|
+
},
|
|
15
|
+
renderPrimaryHeading: {
|
|
16
|
+
type: Boolean,
|
|
17
|
+
default: false
|
|
14
18
|
}
|
|
15
19
|
})
|
|
16
20
|
|
|
@@ -32,6 +36,7 @@ const tokenized = computed(() => {
|
|
|
32
36
|
<section>
|
|
33
37
|
<d-page-tokens
|
|
34
38
|
:id="id"
|
|
39
|
+
:render-primary-heading="renderPrimaryHeading"
|
|
35
40
|
:tokens="tokenized"
|
|
36
41
|
/>
|
|
37
42
|
</section>
|
|
@@ -8,6 +8,10 @@ defineProps({
|
|
|
8
8
|
type: Number,
|
|
9
9
|
default: 0
|
|
10
10
|
},
|
|
11
|
+
renderPrimaryHeading: {
|
|
12
|
+
type: Boolean,
|
|
13
|
+
default: false
|
|
14
|
+
},
|
|
11
15
|
tokens: {
|
|
12
16
|
type: Array,
|
|
13
17
|
default: () => []
|
|
@@ -34,8 +38,13 @@ import DBlockStepper from './DBlockStepper.vue'
|
|
|
34
38
|
|
|
35
39
|
<template>
|
|
36
40
|
<template v-for="(token, index) in tokens" :key="`${token.tag}-${index}`">
|
|
41
|
+
<h1
|
|
42
|
+
v-if="token.tag === 'h1' && renderPrimaryHeading"
|
|
43
|
+
:id="token.anchorId"
|
|
44
|
+
v-html="token.content"
|
|
45
|
+
></h1>
|
|
37
46
|
<d-h2
|
|
38
|
-
v-if="token.tag === 'h2'"
|
|
47
|
+
v-else-if="token.tag === 'h2'"
|
|
39
48
|
:id="token.anchorId"
|
|
40
49
|
:value="token.content"
|
|
41
50
|
/>
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import { useRoute } from 'vue-router'
|
|
4
|
+
import { useStore } from 'vuex'
|
|
5
|
+
import { homePageSourceMode } from 'virtual:docsector-homepage-override'
|
|
4
6
|
// components
|
|
5
7
|
import DPage from "./DPage.vue";
|
|
6
8
|
import DPageBar from "./DPageBar.vue";
|
|
7
9
|
import DH1 from "./DH1.vue";
|
|
8
10
|
import DPageSection from "./DPageSection.vue";
|
|
11
|
+
import { usesRemoteReadmeHomeContent } from '../home-page-mode'
|
|
9
12
|
|
|
10
13
|
const route = useRoute()
|
|
14
|
+
const store = useStore()
|
|
11
15
|
|
|
12
16
|
const id = computed(() => {
|
|
13
17
|
const path = route.path
|
|
@@ -19,6 +23,13 @@ const id = computed(() => {
|
|
|
19
23
|
|
|
20
24
|
return hash >>> 0
|
|
21
25
|
})
|
|
26
|
+
|
|
27
|
+
const usesRemoteReadmeHome = computed(() => {
|
|
28
|
+
return usesRemoteReadmeHomeContent({
|
|
29
|
+
pageBase: store.state.page.base,
|
|
30
|
+
homePageSourceMode
|
|
31
|
+
})
|
|
32
|
+
})
|
|
22
33
|
</script>
|
|
23
34
|
|
|
24
35
|
<template>
|
|
@@ -26,11 +37,12 @@ const id = computed(() => {
|
|
|
26
37
|
<header>
|
|
27
38
|
<d-page-bar />
|
|
28
39
|
<hr />
|
|
29
|
-
<d-h1 :id="0" />
|
|
40
|
+
<d-h1 v-if="!usesRemoteReadmeHome" :id="0" />
|
|
41
|
+
<span v-else id="0" aria-hidden="true"></span>
|
|
30
42
|
</header>
|
|
31
43
|
|
|
32
44
|
<main>
|
|
33
|
-
<d-page-section :id="id" />
|
|
45
|
+
<d-page-section :id="id" :render-primary-heading="usesRemoteReadmeHome" />
|
|
34
46
|
</main>
|
|
35
47
|
</d-page>
|
|
36
48
|
</template>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const DEFAULT_ACTIVE_ANCHOR_OFFSET = 50
|
|
2
|
+
|
|
3
|
+
const toFiniteNumber = (value) => {
|
|
4
|
+
const normalized = Number(value)
|
|
5
|
+
return Number.isFinite(normalized) ? normalized : 0
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getActiveAnchorId({
|
|
9
|
+
anchors = [],
|
|
10
|
+
scrollTop = 0,
|
|
11
|
+
scrollOffset = DEFAULT_ACTIVE_ANCHOR_OFFSET,
|
|
12
|
+
rootAnchorId = 0,
|
|
13
|
+
getAnchorOffsetTop = () => undefined
|
|
14
|
+
} = {}) {
|
|
15
|
+
const thresholdTop = Math.max(0, toFiniteNumber(scrollTop)) + Math.max(0, toFiniteNumber(scrollOffset))
|
|
16
|
+
let activeAnchorId = rootAnchorId
|
|
17
|
+
const seenAnchors = new Set()
|
|
18
|
+
|
|
19
|
+
for (const anchorId of Array.isArray(anchors) ? anchors : []) {
|
|
20
|
+
if (anchorId === null || anchorId === undefined || anchorId === false || anchorId === 0 || anchorId === '0') {
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (seenAnchors.has(anchorId)) {
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
seenAnchors.add(anchorId)
|
|
29
|
+
|
|
30
|
+
const resolvedOffsetTop = Number(getAnchorOffsetTop(anchorId))
|
|
31
|
+
|
|
32
|
+
if (!Number.isFinite(resolvedOffsetTop)) {
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (thresholdTop >= resolvedOffsetTop) {
|
|
37
|
+
activeAnchorId = anchorId
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return activeAnchorId
|
|
42
|
+
}
|
|
@@ -3,6 +3,8 @@ import { useStore } from 'vuex'
|
|
|
3
3
|
import { useRouter, useRoute } from 'vue-router'
|
|
4
4
|
import { ref } from 'vue'
|
|
5
5
|
|
|
6
|
+
import { DEFAULT_ACTIVE_ANCHOR_OFFSET, getActiveAnchorId } from './useActiveAnchor'
|
|
7
|
+
|
|
6
8
|
export default function useNavigator() {
|
|
7
9
|
const store = useStore()
|
|
8
10
|
const router = useRouter()
|
|
@@ -46,7 +48,10 @@ export default function useNavigator() {
|
|
|
46
48
|
const select = (id) => {
|
|
47
49
|
const normalized = normalizeStoreAnchorId(id)
|
|
48
50
|
|
|
49
|
-
store.
|
|
51
|
+
if (store.state.page.anchor !== normalized) {
|
|
52
|
+
store.commit('page/setAnchor', normalized)
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
store.commit('page/pushNodesExpanded', normalized)
|
|
51
56
|
}
|
|
52
57
|
|
|
@@ -78,27 +83,29 @@ export default function useNavigator() {
|
|
|
78
83
|
return
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
const
|
|
82
|
-
|
|
86
|
+
const activeAnchorId = getActiveAnchorId({
|
|
87
|
+
anchors: store.state.page.anchors,
|
|
88
|
+
scrollTop: scroll?.position?.top,
|
|
89
|
+
scrollOffset: DEFAULT_ACTIVE_ANCHOR_OFFSET,
|
|
90
|
+
rootAnchorId: 0,
|
|
91
|
+
getAnchorOffsetTop: (anchorId) => {
|
|
92
|
+
const domAnchorId = normalizeDomAnchorId(anchorId)
|
|
83
93
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
if (domAnchorId === '') {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
continue
|
|
90
|
-
}
|
|
98
|
+
const Anchor = document.getElementById(domAnchorId)
|
|
91
99
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
AnchorOffsetTop = Anchor.offsetTop
|
|
96
|
-
}
|
|
100
|
+
if (Anchor !== null && typeof Anchor === 'object') {
|
|
101
|
+
return Anchor.offsetTop
|
|
102
|
+
}
|
|
97
103
|
|
|
98
|
-
|
|
99
|
-
select(anchorId)
|
|
104
|
+
return undefined
|
|
100
105
|
}
|
|
101
|
-
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
select(activeAnchorId)
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
const navigate = (value, toAnchor = true) => {
|
|
@@ -39,7 +39,7 @@ The root node (from DH1) shows the page title from i18n when no label is set:
|
|
|
39
39
|
|
|
40
40
|
## Scroll Synchronization
|
|
41
41
|
|
|
42
|
-
When the user scrolls the page content, the `DPage` scroll observer calls `useNavigator().scrolling()`, which
|
|
42
|
+
When the user scrolls the page content, the `DPage` scroll observer calls `useNavigator().scrolling()`, which selects the last registered heading that crossed the content threshold. Missing or stale anchors are ignored so the table of contents stays in sync with the visible section instead of jumping ahead.
|
|
43
43
|
|
|
44
44
|
## Lifecycle
|
|
45
45
|
|
|
@@ -39,7 +39,7 @@ O nó raiz (do DH1) mostra o título da página do i18n quando nenhum label é d
|
|
|
39
39
|
|
|
40
40
|
## Sincronização de Scroll
|
|
41
41
|
|
|
42
|
-
Quando o usuário faz scroll no conteúdo da página, o observador de scroll do `DPage` chama `useNavigator().scrolling()`, que
|
|
42
|
+
Quando o usuário faz scroll no conteúdo da página, o observador de scroll do `DPage` chama `useNavigator().scrolling()`, que seleciona o último heading registrado que cruzou o limite superior da área de conteúdo. Âncoras ausentes ou obsoletas são ignoradas para manter o índice sincronizado com a seção realmente visível, sem saltar adiante.
|
|
43
43
|
|
|
44
44
|
## Ciclo de Vida
|
|
45
45
|
|
package/src/quasar.factory.js
CHANGED
|
@@ -1504,7 +1504,7 @@ async function fetchRemoteMarkdown (url, timeoutMs = 8000) {
|
|
|
1504
1504
|
}
|
|
1505
1505
|
}
|
|
1506
1506
|
|
|
1507
|
-
async function resolveHomePageSources (projectRoot, config = {}, options = {}) {
|
|
1507
|
+
export async function resolveHomePageSources (projectRoot, config = {}, options = {}) {
|
|
1508
1508
|
const { logPrefix = '[docsector]' } = options
|
|
1509
1509
|
const pagesDir = resolve(projectRoot, 'src', 'pages')
|
|
1510
1510
|
const { defaultLang, langs } = getConfiguredLanguages(config)
|
|
@@ -1551,18 +1551,17 @@ async function resolveHomePageSources (projectRoot, config = {}, options = {}) {
|
|
|
1551
1551
|
function createHomePageOverridePlugin (projectRoot) {
|
|
1552
1552
|
const virtualId = 'virtual:docsector-homepage-override'
|
|
1553
1553
|
const resolvedId = '\0' + virtualId
|
|
1554
|
-
let
|
|
1554
|
+
let homePageSources = null
|
|
1555
1555
|
let loadPromise = null
|
|
1556
1556
|
|
|
1557
1557
|
const ensureSources = async () => {
|
|
1558
|
-
if (
|
|
1558
|
+
if (homePageSources) return homePageSources
|
|
1559
1559
|
if (!loadPromise) {
|
|
1560
1560
|
loadPromise = (async () => {
|
|
1561
1561
|
const configUrl = pathToFileURL(resolve(projectRoot, 'docsector.config.js')).href
|
|
1562
1562
|
const { default: config } = await import(configUrl)
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
return byLang
|
|
1563
|
+
homePageSources = await resolveHomePageSources(projectRoot, config, { logPrefix: '[docsector]' })
|
|
1564
|
+
return homePageSources
|
|
1566
1565
|
})().finally(() => {
|
|
1567
1566
|
loadPromise = null
|
|
1568
1567
|
})
|
|
@@ -1586,18 +1585,21 @@ function createHomePageOverridePlugin (projectRoot) {
|
|
|
1586
1585
|
},
|
|
1587
1586
|
async load (id) {
|
|
1588
1587
|
if (id === resolvedId) {
|
|
1589
|
-
await ensureSources()
|
|
1590
|
-
return
|
|
1588
|
+
const sources = await ensureSources()
|
|
1589
|
+
return [
|
|
1590
|
+
`export const homePageSourceMode = ${JSON.stringify(sources?.mode || 'local')}`,
|
|
1591
|
+
`export default ${JSON.stringify(sources?.byLang || {})}`
|
|
1592
|
+
].join('\n')
|
|
1591
1593
|
}
|
|
1592
1594
|
|
|
1593
|
-
await ensureSources()
|
|
1594
|
-
if (!byLang) return null
|
|
1595
|
+
const sources = await ensureSources()
|
|
1596
|
+
if (!sources?.byLang) return null
|
|
1595
1597
|
|
|
1596
1598
|
const match = id.match(/Homepage\.([A-Za-z0-9-]+)\.md\?raw(?:$|&)/)
|
|
1597
1599
|
if (!match) return null
|
|
1598
1600
|
|
|
1599
1601
|
const lang = match[1]
|
|
1600
|
-
const content = byLang[lang]
|
|
1602
|
+
const content = sources.byLang[lang]
|
|
1601
1603
|
if (typeof content !== 'string') return null
|
|
1602
1604
|
|
|
1603
1605
|
return `export default ${JSON.stringify(content)}`
|
package/src/store/Page.js
CHANGED
|
@@ -46,7 +46,7 @@ export default {
|
|
|
46
46
|
pushAnchors (state, value) {
|
|
47
47
|
if (value === false) {
|
|
48
48
|
state.anchors = []
|
|
49
|
-
} else {
|
|
49
|
+
} else if (!state.anchors.includes(value)) {
|
|
50
50
|
// index: id
|
|
51
51
|
state.anchors.push(value)
|
|
52
52
|
}
|
|
@@ -82,7 +82,9 @@ export default {
|
|
|
82
82
|
state.nodesExpanded = [0]
|
|
83
83
|
},
|
|
84
84
|
pushNodesExpanded (state, nodeId) {
|
|
85
|
-
state.nodesExpanded.
|
|
85
|
+
if (!state.nodesExpanded.includes(nodeId)) {
|
|
86
|
+
state.nodesExpanded.push(nodeId)
|
|
87
|
+
}
|
|
86
88
|
},
|
|
87
89
|
|
|
88
90
|
setScrolling (state, val) {
|