@docsector/docsector-reader 4.3.1 → 4.3.2
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 +1 -1
- package/bin/docsector.js +1 -1
- package/package.json +1 -1
- package/src/components/DPageSection.vue +6 -1
- package/src/components/page-anchor-tree.js +45 -0
- package/src/pages/manual/basic/d-page-anchor.overview.en-US.md +2 -2
- package/src/pages/manual/basic/d-page-anchor.overview.pt-BR.md +2 -2
- package/src/store/Page.js +92 -25
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
52
52
|
- 🌍 **Internationalization (i18n)** — Multi-language support with HJSON locale files and per-page translations
|
|
53
53
|
- 🌗 **Dark/Light Mode** — Automatic theme switching with Quasar Dark Plugin
|
|
54
54
|
- 🧰 **Docsector CLI Skill Installer** — Install the built-in authoring skill into older scaffolds with `docsector install-skill`
|
|
55
|
-
- 🔗 **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
|
|
55
|
+
- 🔗 **Anchor Navigation** — Right-side source-ordered 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
|
|
56
56
|
- 🖱️ **Active Menu Item UX** — Active menu entries keep pointer cursor, clear URL hash without redundant navigation, and prevent accidental label text selection
|
|
57
57
|
- 🔎 **Search** — Menu search across all documentation content and tags
|
|
58
58
|
- 📱 **Responsive** — Mobile-friendly with collapsible sidebar and drawers
|
package/bin/docsector.js
CHANGED
|
@@ -24,7 +24,7 @@ const packageRoot = resolve(__dirname, '..')
|
|
|
24
24
|
const args = process.argv.slice(2)
|
|
25
25
|
const command = args[0]
|
|
26
26
|
|
|
27
|
-
const VERSION = '4.3.
|
|
27
|
+
const VERSION = '4.3.2'
|
|
28
28
|
const AUTHORING_SKILL_NAME = 'docsector-documentation-authoring'
|
|
29
29
|
const AUTHORING_SKILL_DESCRIPTION = 'Author Docsector documentation with Markdown, custom blocks, MCP, and WebMCP.'
|
|
30
30
|
const AUTHORING_SKILL_PUBLIC_PATH = `/.well-known/agent-skills/${AUTHORING_SKILL_NAME}/SKILL.md`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docsector/docsector-reader",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.2",
|
|
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",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { computed, watch } from 'vue'
|
|
3
3
|
import { useStore } from 'vuex'
|
|
4
4
|
import { useI18n } from "vue-i18n"
|
|
5
5
|
|
|
6
6
|
import DPageTokens from './DPageTokens.vue'
|
|
7
7
|
import { pageValueI18nPath } from '../i18n/path'
|
|
8
|
+
import { buildPageAnchorTree } from './page-anchor-tree'
|
|
8
9
|
import { tokenizePageSectionSource } from './page-section-tokens'
|
|
9
10
|
|
|
10
11
|
defineProps({
|
|
@@ -30,6 +31,10 @@ const tokenized = computed(() => {
|
|
|
30
31
|
|
|
31
32
|
return tokenizePageSectionSource(t(pageValueI18nPath(absolute, 'source')))
|
|
32
33
|
})
|
|
34
|
+
|
|
35
|
+
watch(tokenized, (tokens) => {
|
|
36
|
+
store.commit('page/setAnchorTree', buildPageAnchorTree(tokens))
|
|
37
|
+
}, { immediate: true })
|
|
33
38
|
</script>
|
|
34
39
|
|
|
35
40
|
<template>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const isTreeHeadingToken = (token) => token?.tag === 'h2' || token?.tag === 'h3'
|
|
2
|
+
|
|
3
|
+
const hasAnchorId = (value) => value !== null && value !== undefined && value !== false && value !== ''
|
|
4
|
+
|
|
5
|
+
const createRootNode = (children = []) => ({
|
|
6
|
+
id: 0,
|
|
7
|
+
children
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const createTreeNode = (token) => ({
|
|
11
|
+
id: token.anchorId,
|
|
12
|
+
label: token.content,
|
|
13
|
+
children: []
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const buildPageAnchorTree = (tokens = []) => {
|
|
17
|
+
const anchors = []
|
|
18
|
+
const rootNode = createRootNode()
|
|
19
|
+
const seenAnchors = new Set()
|
|
20
|
+
let currentParentNode = null
|
|
21
|
+
|
|
22
|
+
for (const token of Array.isArray(tokens) ? tokens : []) {
|
|
23
|
+
if (!isTreeHeadingToken(token) || !hasAnchorId(token.anchorId) || seenAnchors.has(token.anchorId)) {
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
seenAnchors.add(token.anchorId)
|
|
28
|
+
anchors.push(token.anchorId)
|
|
29
|
+
|
|
30
|
+
const node = createTreeNode(token)
|
|
31
|
+
|
|
32
|
+
if (token.tag === 'h3' && currentParentNode !== null) {
|
|
33
|
+
currentParentNode.children.push(node)
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
rootNode.children.push(node)
|
|
38
|
+
currentParentNode = token.tag === 'h2' ? node : null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
anchors,
|
|
43
|
+
nodes: [rootNode]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -6,7 +6,7 @@ Under the hood, this behavior is powered by `DPageAnchor`.
|
|
|
6
6
|
|
|
7
7
|
## How It Works
|
|
8
8
|
|
|
9
|
-
1.
|
|
9
|
+
1. `DPageSection` tokenizes the current subpage and rebuilds the heading tree in source order
|
|
10
10
|
2. `DPageAnchor` reads the `page/nodes` getter to render the tree
|
|
11
11
|
3. When the user scrolls, the scroll observer in `DPage` updates the selected anchor
|
|
12
12
|
4. Clicking a tree node navigates to the corresponding heading
|
|
@@ -22,7 +22,7 @@ The implementation interacts with these store state/getters:
|
|
|
22
22
|
|
|
23
23
|
## Tree Rendering
|
|
24
24
|
|
|
25
|
-
Uses Quasar's `QTree` component with `default-expand-all`. The node key is the heading
|
|
25
|
+
Uses Quasar's `QTree` component with `default-expand-all`. The node key is the heading `id`, and the label is the heading text. H2 tokens become top-level tree entries, and H3 tokens nest under the nearest preceding H2.
|
|
26
26
|
|
|
27
27
|
The root node (from DH1) shows the page title from i18n when no label is set:
|
|
28
28
|
|
|
@@ -6,7 +6,7 @@ Na implementação, esse comportamento é sustentado por `DPageAnchor`.
|
|
|
6
6
|
|
|
7
7
|
## Como Funciona
|
|
8
8
|
|
|
9
|
-
1.
|
|
9
|
+
1. `DPageSection` tokeniza a subpágina atual e reconstrói a árvore de títulos na ordem do conteúdo
|
|
10
10
|
2. `DPageAnchor` lê o getter `page/nodes` para renderizar a árvore
|
|
11
11
|
3. Quando o usuário faz scroll, o observador de scroll no `DPage` atualiza a âncora selecionada
|
|
12
12
|
4. Clicar em um nó da árvore navega até o título correspondente
|
|
@@ -22,7 +22,7 @@ A implementação interage com estes estados/getters da store:
|
|
|
22
22
|
|
|
23
23
|
## Renderização da Árvore
|
|
24
24
|
|
|
25
|
-
Usa o componente `QTree` do Quasar com `default-expand-all`. A chave do nó é o `id`
|
|
25
|
+
Usa o componente `QTree` do Quasar com `default-expand-all`. A chave do nó é o `id` do título, e o label é o texto do título. Tokens H2 viram entradas de primeiro nível, e tokens H3 ficam aninhados no H2 anterior mais próximo.
|
|
26
26
|
|
|
27
27
|
O nó raiz (do DH1) mostra o título da página do i18n quando nenhum label é definido:
|
|
28
28
|
|
package/src/store/Page.js
CHANGED
|
@@ -1,3 +1,50 @@
|
|
|
1
|
+
const createDefaultNodes = () => [
|
|
2
|
+
{
|
|
3
|
+
id: 0,
|
|
4
|
+
children: []
|
|
5
|
+
}
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
const cloneNode = (node) => {
|
|
9
|
+
const clonedNode = {
|
|
10
|
+
id: node.id,
|
|
11
|
+
children: Array.isArray(node.children) ? node.children.map(cloneNode) : []
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (node.label !== undefined) {
|
|
15
|
+
clonedNode.label = node.label
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return clonedNode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const normalizeNodes = (nodes) => {
|
|
22
|
+
if (!Array.isArray(nodes) || nodes.length === 0) {
|
|
23
|
+
return createDefaultNodes()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const [rootNode] = nodes
|
|
27
|
+
|
|
28
|
+
if (rootNode?.id !== 0) {
|
|
29
|
+
return createDefaultNodes()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return [cloneNode(rootNode)]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const uniqueAnchors = (anchors) => {
|
|
36
|
+
const seenAnchors = new Set()
|
|
37
|
+
|
|
38
|
+
return (Array.isArray(anchors) ? anchors : []).filter((anchorId) => {
|
|
39
|
+
if (anchorId === null || anchorId === undefined || anchorId === false || seenAnchors.has(anchorId)) {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
seenAnchors.add(anchorId)
|
|
44
|
+
return true
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
1
48
|
const getNodePath = (nodes, targetId, ancestry = []) => {
|
|
2
49
|
for (const node of nodes) {
|
|
3
50
|
const nextAncestry = [...ancestry, node.id]
|
|
@@ -18,6 +65,24 @@ const getNodePath = (nodes, targetId, ancestry = []) => {
|
|
|
18
65
|
return null
|
|
19
66
|
}
|
|
20
67
|
|
|
68
|
+
const findNode = (nodes, targetId) => {
|
|
69
|
+
for (const node of nodes) {
|
|
70
|
+
if (node.id === targetId) {
|
|
71
|
+
return node
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(node.children) && node.children.length > 0) {
|
|
75
|
+
const found = findNode(node.children, targetId)
|
|
76
|
+
|
|
77
|
+
if (found !== null) {
|
|
78
|
+
return found
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
21
86
|
export default {
|
|
22
87
|
namespaced: true,
|
|
23
88
|
|
|
@@ -26,12 +91,7 @@ export default {
|
|
|
26
91
|
|
|
27
92
|
anchors: [],
|
|
28
93
|
|
|
29
|
-
nodes:
|
|
30
|
-
{
|
|
31
|
-
id: 0,
|
|
32
|
-
children: []
|
|
33
|
-
}
|
|
34
|
-
],
|
|
94
|
+
nodes: createDefaultNodes(),
|
|
35
95
|
nodesExpanded: [0],
|
|
36
96
|
|
|
37
97
|
scrolling: false,
|
|
@@ -73,29 +133,36 @@ export default {
|
|
|
73
133
|
},
|
|
74
134
|
|
|
75
135
|
resetNodes (state) {
|
|
76
|
-
state.nodes =
|
|
77
|
-
{
|
|
78
|
-
id: 0,
|
|
79
|
-
children: []
|
|
80
|
-
}
|
|
81
|
-
]
|
|
136
|
+
state.nodes = createDefaultNodes()
|
|
82
137
|
},
|
|
83
138
|
pushNodes (state, node) {
|
|
84
|
-
const found = state.nodes
|
|
139
|
+
const found = findNode(state.nodes, node.id)
|
|
85
140
|
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
children: node.children
|
|
91
|
-
}
|
|
141
|
+
if (found) {
|
|
142
|
+
found.label = node.label
|
|
143
|
+
return
|
|
144
|
+
}
|
|
92
145
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
146
|
+
const value = {
|
|
147
|
+
id: node.id,
|
|
148
|
+
label: node.label,
|
|
149
|
+
children: node.children
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const children = state.nodes[0].children
|
|
153
|
+
if (node.child && children.length) {
|
|
154
|
+
children.at(-1).children.push(value)
|
|
155
|
+
} else {
|
|
156
|
+
state.nodes[0].children.push(value)
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
setAnchorTree (state, { anchors = [], nodes = createDefaultNodes() } = {}) {
|
|
160
|
+
state.anchors = uniqueAnchors(anchors)
|
|
161
|
+
state.nodes = normalizeNodes(nodes)
|
|
162
|
+
state.nodesExpanded = [0]
|
|
163
|
+
|
|
164
|
+
if (state.anchor !== 0 && !state.anchors.includes(state.anchor)) {
|
|
165
|
+
state.anchor = 0
|
|
99
166
|
}
|
|
100
167
|
},
|
|
101
168
|
resetNodesExpanded (state) {
|