@docsector/docsector-reader 4.3.1 → 4.3.3

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 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, resize-safe drawer state, 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.1'
27
+ const VERSION = '4.3.3'
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.1",
3
+ "version": "4.3.3",
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",
@@ -15,6 +15,8 @@ const { t } = useI18n()
15
15
  const { navigate, anchor, selected: navigatorSelected } = useNavigator()
16
16
 
17
17
  const scrolling = ref(null)
18
+ const enableScrollingTimeout = ref(null)
19
+ const initialAnchorTimeout = ref(null)
18
20
 
19
21
  const nodes = computed(() => store.getters['page/nodes'])
20
22
  const expanded = computed({
@@ -77,13 +79,13 @@ watch(selected, () => {
77
79
  onMounted(() => {
78
80
  store.commit('layout/setMetaToggle', true)
79
81
 
80
- setTimeout(() => {
82
+ enableScrollingTimeout.value = setTimeout(() => {
81
83
  store.commit('page/setScrolling', true)
82
84
  }, 1000)
83
85
 
84
86
  const id = route.hash.replace(/^#+/g, '')
85
87
  if (id) {
86
- setTimeout(() => {
88
+ initialAnchorTimeout.value = setTimeout(() => {
87
89
  anchor(route.hash)
88
90
  }, 500)
89
91
  }
@@ -94,13 +96,15 @@ onBeforeUnmount(() => {
94
96
  clearTimeout(scrolling.value)
95
97
  }
96
98
 
97
- store.commit('layout/setMetaToggle', false)
99
+ if (enableScrollingTimeout.value) {
100
+ clearTimeout(enableScrollingTimeout.value)
101
+ }
98
102
 
99
- store.commit('page/resetAnchor')
100
- store.commit('page/resetAnchors')
101
- store.commit('page/resetNodes')
103
+ if (initialAnchorTimeout.value) {
104
+ clearTimeout(initialAnchorTimeout.value)
105
+ }
102
106
 
103
- store.commit('page/setScrolling', false)
107
+ store.commit('layout/setMetaToggle', false)
104
108
  })
105
109
  </script>
106
110
 
@@ -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. As each heading component (DH1–DH6) mounts, it registers itself in the `page/nodes` store via `useNavigator`
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's numeric `id`, and the label is the heading text.
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
 
@@ -44,7 +44,7 @@ When the user scrolls the page content, the `DPage` scroll observer calls `useNa
44
44
  ## Lifecycle
45
45
 
46
46
  - **onMounted** — Enables meta toggle, starts scroll tracking after 1s delay, anchors to URL hash if present
47
- - **onBeforeUnmount** — Resets anchors, nodes, and disables scroll tracking
47
+ - **onBeforeUnmount** — Clears local timers and disables the meta drawer toggle without clearing the page Table of Contents state during responsive UI unmounts
48
48
 
49
49
  ## Styling
50
50
 
@@ -6,7 +6,7 @@ Na implementação, esse comportamento é sustentado por `DPageAnchor`.
6
6
 
7
7
  ## Como Funciona
8
8
 
9
- 1. Conforme cada componente de título (DH1–DH6) é montado, ele se registra no store `page/nodes` via `useNavigator`
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` numérico do título, e o label é o texto do título.
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
 
@@ -44,7 +44,7 @@ Quando o usuário faz scroll no conteúdo da página, o observador de scroll do
44
44
  ## Ciclo de Vida
45
45
 
46
46
  - **onMounted** — Habilita toggle de meta, inicia rastreamento de scroll após 1s de delay, ancora no hash da URL se presente
47
- - **onBeforeUnmount** — Reseta âncoras, nós e desabilita rastreamento de scroll
47
+ - **onBeforeUnmount** — Limpa timers locais e desabilita o toggle do drawer meta sem apagar o estado do índice da página durante desmontagens responsivas da UI
48
48
 
49
49
  ## Estilização
50
50
 
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[0].children.find(x => x.id === node.id)
139
+ const found = findNode(state.nodes, node.id)
85
140
 
86
- if (!found) {
87
- const value = {
88
- id: node.id,
89
- label: node.label,
90
- children: node.children
91
- }
141
+ if (found) {
142
+ found.label = node.label
143
+ return
144
+ }
92
145
 
93
- const children = state.nodes[0].children
94
- if (node.child && children.length) {
95
- children.at(-1).children.push(value)
96
- } else {
97
- state.nodes[0].children.push(value)
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) {