@docsector/docsector-reader 4.3.0 → 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 CHANGED
@@ -51,7 +51,8 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
51
51
  - 🚨 **GitHub-Style Alerts** — Native support for `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, and `[!CAUTION]`
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
- - 🔗 **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
+ - 🧰 **Docsector CLI Skill Installer** — Install the built-in authoring skill into older scaffolds with `docsector install-skill`
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
55
56
  - 🖱️ **Active Menu Item UX** — Active menu entries keep pointer cursor, clear URL hash without redundant navigation, and prevent accidental label text selection
56
57
  - 🔎 **Search** — Menu search across all documentation content and tags
57
58
  - 📱 **Responsive** — Mobile-friendly with collapsible sidebar and drawers
@@ -535,6 +536,14 @@ This repository publishes the built-in Docsector authoring skill at:
535
536
 
536
537
  The skill teaches agents Docsector Markdown authoring, all documented blocks, page/source conventions, MCP lookup, and WebMCP browser tools.
537
538
 
539
+ For projects scaffolded before the built-in skill existed, run:
540
+
541
+ ```bash
542
+ npx docsector install-skill
543
+ ```
544
+
545
+ The helper copies the skill into `.github/skills/` for repository-local assistants and into `public/.well-known/agent-skills/` for published discovery. Existing folders are skipped unless `--force` is passed.
546
+
538
547
  When `digest` is omitted in config, Docsector computes it automatically from the referenced local artifact and writes it as:
539
548
 
540
549
  - `sha256:{hex}`
@@ -1087,6 +1096,8 @@ Notes:
1087
1096
 
1088
1097
  ```bash
1089
1098
  docsector init <name> # Scaffold a new consumer project
1099
+ docsector install-skill # Install the built-in authoring skill
1100
+ docsector install-skill --force # Refresh an existing installed authoring skill
1090
1101
  docsector dev # Start dev server (port 8181)
1091
1102
  docsector dev --port 3000 # Custom port
1092
1103
  docsector build # Build for production (dist/spa/)
package/bin/docsector.js CHANGED
@@ -8,11 +8,12 @@
8
8
  * docsector dev — Start development server with hot-reload
9
9
  * docsector build — Build optimized SPA for production
10
10
  * docsector serve — Serve the production build locally
11
+ * docsector install-skill — Install the built-in authoring skill into a project
11
12
  * docsector help — Show help information
12
13
  */
13
14
 
14
15
  import { spawn } from 'child_process'
15
- import { existsSync, mkdirSync, writeFileSync, copyFileSync } from 'fs'
16
+ import { existsSync, mkdirSync, writeFileSync, copyFileSync, cpSync } from 'fs'
16
17
  import { resolve, dirname } from 'path'
17
18
  import { fileURLToPath } from 'url'
18
19
 
@@ -23,7 +24,25 @@ const packageRoot = resolve(__dirname, '..')
23
24
  const args = process.argv.slice(2)
24
25
  const command = args[0]
25
26
 
26
- const VERSION = '4.3.0'
27
+ const VERSION = '4.3.2'
28
+ const AUTHORING_SKILL_NAME = 'docsector-documentation-authoring'
29
+ const AUTHORING_SKILL_DESCRIPTION = 'Author Docsector documentation with Markdown, custom blocks, MCP, and WebMCP.'
30
+ const AUTHORING_SKILL_PUBLIC_PATH = `/.well-known/agent-skills/${AUTHORING_SKILL_NAME}/SKILL.md`
31
+ const AUTHORING_SKILL_SOURCE_DIR = resolve(packageRoot, 'public', '.well-known', 'agent-skills', AUTHORING_SKILL_NAME)
32
+ const AUTHORING_SKILL_CONFIG_SNIPPET = `\
33
+ agentSkills: {
34
+ enabled: true,
35
+ path: '/.well-known/agent-skills/index.json',
36
+ schema: 'https://schemas.agentskills.io/discovery/0.2.0/schema.json',
37
+ skills: [
38
+ {
39
+ name: '${AUTHORING_SKILL_NAME}',
40
+ type: 'skill-md',
41
+ description: '${AUTHORING_SKILL_DESCRIPTION}',
42
+ url: '${AUTHORING_SKILL_PUBLIC_PATH}'
43
+ }
44
+ ]
45
+ }`
27
46
 
28
47
  const HELP = `
29
48
  Docsector Reader v${VERSION}
@@ -37,11 +56,14 @@ const HELP = `
37
56
  dev Start development server with hot-reload (port 8181)
38
57
  build Build optimized SPA for production (output: dist/spa/)
39
58
  serve Serve the production build locally
59
+ install-skill
60
+ Copy the built-in Docsector authoring skill into this project
40
61
  version Show version number
41
62
  help Show this help message
42
63
 
43
64
  Options:
44
65
  --port <number> Override dev server port (default: 8181)
66
+ --force Overwrite an existing installed authoring skill
45
67
 
46
68
  Examples:
47
69
  docsector init my-docs
@@ -49,6 +71,8 @@ const HELP = `
49
71
  docsector dev --port 3000
50
72
  docsector build
51
73
  docsector serve
74
+ docsector install-skill
75
+ docsector install-skill --force
52
76
 
53
77
  Documentation:
54
78
  https://github.com/docsector/docsector-reader
@@ -235,10 +259,10 @@ export default {
235
259
  // schema: 'https://schemas.agentskills.io/discovery/0.2.0/schema.json',
236
260
  // skills: [
237
261
  // {
238
- // name: 'my-docs-mcp',
262
+ // name: '${AUTHORING_SKILL_NAME}',
239
263
  // type: 'skill-md',
240
- // description: 'Search and read docs pages through MCP.',
241
- // url: '/.well-known/agent-skills/my-docs-mcp/SKILL.md'
264
+ // description: '${AUTHORING_SKILL_DESCRIPTION}',
265
+ // url: '${AUTHORING_SKILL_PUBLIC_PATH}'
242
266
  // }
243
267
  // ]
244
268
  // },
@@ -783,6 +807,75 @@ function run (cmd, cmdArgs = []) {
783
807
  })
784
808
  }
785
809
 
810
+ function copyAuthoringSkillTarget (targetDir, { force = false } = {}) {
811
+ if (!existsSync(AUTHORING_SKILL_SOURCE_DIR)) {
812
+ throw new Error(`Built-in authoring skill not found at ${AUTHORING_SKILL_SOURCE_DIR}`)
813
+ }
814
+
815
+ if (targetDir === AUTHORING_SKILL_SOURCE_DIR) {
816
+ return 'already-source'
817
+ }
818
+
819
+ if (existsSync(targetDir) && !force) {
820
+ return 'skipped'
821
+ }
822
+
823
+ mkdirSync(dirname(targetDir), { recursive: true })
824
+ cpSync(AUTHORING_SKILL_SOURCE_DIR, targetDir, {
825
+ recursive: true,
826
+ force: true
827
+ })
828
+
829
+ return existsSync(targetDir) ? 'installed' : 'failed'
830
+ }
831
+
832
+ function installAuthoringSkill ({ projectRoot = process.cwd(), force = false } = {}) {
833
+ const targets = [
834
+ {
835
+ label: 'Repository-local skill',
836
+ dir: resolve(projectRoot, '.github', 'skills', AUTHORING_SKILL_NAME)
837
+ },
838
+ {
839
+ label: 'Public skill artifact',
840
+ dir: resolve(projectRoot, 'public', '.well-known', 'agent-skills', AUTHORING_SKILL_NAME)
841
+ }
842
+ ]
843
+
844
+ console.log(`\n Installing ${AUTHORING_SKILL_NAME} into ${projectRoot}...\n`)
845
+
846
+ try {
847
+ for (const target of targets) {
848
+ const result = copyAuthoringSkillTarget(target.dir, { force })
849
+ if (result === 'installed') {
850
+ console.log(` created ${target.label}: ${target.dir}`)
851
+ } else if (result === 'already-source') {
852
+ console.log(` using package ${target.label}: ${target.dir}`)
853
+ } else if (result === 'skipped') {
854
+ console.log(` skipped ${target.label}: ${target.dir}`)
855
+ } else {
856
+ throw new Error(`Unable to install ${target.label} at ${target.dir}`)
857
+ }
858
+ }
859
+ } catch (err) {
860
+ console.error(`\n Error: ${err.message}\n`)
861
+ process.exit(1)
862
+ }
863
+
864
+ if (!force) {
865
+ console.log('\n Existing skill folders are left untouched. Use --force to refresh them.')
866
+ }
867
+
868
+ console.log('\n To publish the skill discovery index, add this to docsector.config.js:\n')
869
+ console.log(
870
+ AUTHORING_SKILL_CONFIG_SNIPPET.split('\n')
871
+ .map(line => ` ${line}`)
872
+ .join('\n')
873
+ )
874
+ console.log('\n Then run:')
875
+ console.log(' npx docsector build')
876
+ console.log('')
877
+ }
878
+
786
879
  /**
787
880
  * Scaffold a new Docsector documentation project.
788
881
  */
@@ -916,6 +1009,10 @@ switch (command) {
916
1009
  run('serve', ['dist/spa', '--history', ...args.slice(1)])
917
1010
  break
918
1011
 
1012
+ case 'install-skill':
1013
+ installAuthoringSkill({ force: args.includes('--force') })
1014
+ break
1015
+
919
1016
  case 'version':
920
1017
  case '-v':
921
1018
  case '--version':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "4.3.0",
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
+ }
@@ -52,6 +52,45 @@ Docsector keeps two synchronized copies of the same skill:
52
52
 
53
53
  During build, Docsector copies the public artifact into `dist/spa/.well-known/agent-skills/` and generates the discovery index.
54
54
 
55
+ ## Retrofitting Older Scaffolds
56
+
57
+ Projects scaffolded before this skill existed can import it with the CLI helper:
58
+
59
+ ```bash
60
+ npx docsector install-skill
61
+ ```
62
+
63
+ The command copies the built-in skill into both expected locations:
64
+
65
+ - `.github/skills/docsector-documentation-authoring/`
66
+ - `public/.well-known/agent-skills/docsector-documentation-authoring/`
67
+
68
+ Existing folders are skipped so local edits are not overwritten. Use `--force` when you intentionally want to refresh the installed copy from the package:
69
+
70
+ ```bash
71
+ npx docsector install-skill --force
72
+ ```
73
+
74
+ The helper does not rewrite `docsector.config.js`. After installing the files, enable discovery with:
75
+
76
+ ```javascript
77
+ agentSkills: {
78
+ enabled: true,
79
+ path: '/.well-known/agent-skills/index.json',
80
+ schema: 'https://schemas.agentskills.io/discovery/0.2.0/schema.json',
81
+ skills: [
82
+ {
83
+ name: 'docsector-documentation-authoring',
84
+ type: 'skill-md',
85
+ description: 'Author Docsector documentation with Markdown, custom blocks, MCP, and WebMCP.',
86
+ url: '/.well-known/agent-skills/docsector-documentation-authoring/SKILL.md'
87
+ }
88
+ ]
89
+ }
90
+ ```
91
+
92
+ Then run `npx docsector build` to generate `/.well-known/agent-skills/index.json` with the computed digest.
93
+
55
94
  ## How Agents Should Use It
56
95
 
57
96
  Agents should load `SKILL.md` first, then open reference files only when a task needs more detail.
@@ -52,6 +52,45 @@ O Docsector mantém duas cópias sincronizadas da mesma skill:
52
52
 
53
53
  Durante o build, o Docsector copia o artefato público para `dist/spa/.well-known/agent-skills/` e gera o índice de descoberta.
54
54
 
55
+ ## Retrocompatibilidade Com Scaffolds Antigos
56
+
57
+ Projetos criados antes dessa skill existir podem importá-la com o helper da CLI:
58
+
59
+ ```bash
60
+ npx docsector install-skill
61
+ ```
62
+
63
+ O comando copia a skill embutida para os dois locais esperados:
64
+
65
+ - `.github/skills/docsector-documentation-authoring/`
66
+ - `public/.well-known/agent-skills/docsector-documentation-authoring/`
67
+
68
+ Pastas existentes são ignoradas para não sobrescrever edições locais. Use `--force` quando quiser atualizar a cópia instalada a partir do pacote:
69
+
70
+ ```bash
71
+ npx docsector install-skill --force
72
+ ```
73
+
74
+ O helper não reescreve `docsector.config.js`. Depois de instalar os arquivos, habilite a descoberta com:
75
+
76
+ ```javascript
77
+ agentSkills: {
78
+ enabled: true,
79
+ path: '/.well-known/agent-skills/index.json',
80
+ schema: 'https://schemas.agentskills.io/discovery/0.2.0/schema.json',
81
+ skills: [
82
+ {
83
+ name: 'docsector-documentation-authoring',
84
+ type: 'skill-md',
85
+ description: 'Author Docsector documentation with Markdown, custom blocks, MCP, and WebMCP.',
86
+ url: '/.well-known/agent-skills/docsector-documentation-authoring/SKILL.md'
87
+ }
88
+ ]
89
+ }
90
+ ```
91
+
92
+ Depois execute `npx docsector build` para gerar `/.well-known/agent-skills/index.json` com o digest calculado.
93
+
55
94
  ## Como Agentes Devem Usar
56
95
 
57
96
  Agentes devem carregar primeiro o `SKILL.md` e abrir os arquivos de referência apenas quando a tarefa precisar de mais detalhes.
@@ -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
 
@@ -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
 
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) {