@docsector/docsector-reader 4.0.0 → 4.1.0

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.
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <div class="inline-notice-example q-pa-md">
3
+ <q-banner rounded class="inline-notice-example__banner">
4
+ <template #avatar>
5
+ <q-icon name="tips_and_updates" color="primary"></q-icon>
6
+ </template>
7
+
8
+ <div class="text-weight-medium">{{ title }}</div>
9
+ <div class="text-body2">{{ message }}</div>
10
+
11
+ <template #action>
12
+ <q-btn flat color="primary" label="Dismiss" @click="dismiss"></q-btn>
13
+ </template>
14
+ </q-banner>
15
+
16
+ <div v-if="dismissed" class="inline-notice-example__status text-caption">
17
+ The notice was dismissed.
18
+ </div>
19
+ </div>
20
+ </template>
21
+
22
+ <script>
23
+ import { ref } from 'vue'
24
+
25
+ export default {
26
+ setup () {
27
+ const title = 'Documentation tip'
28
+ const message = 'Use expanded examples when the source code is part of the lesson.'
29
+ const dismissed = ref(false)
30
+
31
+ function dismiss () {
32
+ dismissed.value = true
33
+ }
34
+
35
+ return {
36
+ dismissed,
37
+ dismiss,
38
+ message,
39
+ title
40
+ }
41
+ }
42
+ }
43
+ </script>
44
+
45
+ <style scoped>
46
+ .inline-notice-example {
47
+ display: grid;
48
+ gap: 10px;
49
+ }
50
+
51
+ .inline-notice-example__banner {
52
+ background: rgba(25, 118, 210, 0.08);
53
+ border: 1px solid rgba(25, 118, 210, 0.18);
54
+ }
55
+
56
+ .inline-notice-example__status {
57
+ color: #607d68;
58
+ padding-left: 8px;
59
+ }
60
+ </style>
@@ -0,0 +1,5 @@
1
+ export const REMOTE_README_HOME_PAGE_MODE = 'remote-readme'
2
+
3
+ export function usesRemoteReadmeHomeContent ({ pageBase = '', homePageSourceMode = 'local' } = {}) {
4
+ return pageBase === 'home' && homePageSourceMode === REMOTE_README_HOME_PAGE_MODE
5
+ }
@@ -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 iterates over registered anchors and selects the one closest to the current scroll position. This keeps the table of contents in sync with the visible content.
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 itera sobre as âncoras registradas e seleciona a mais próxima da posição de scroll atual. Isso mantém o índice sincronizado com o conteúdo visível.
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
 
@@ -0,0 +1,56 @@
1
+ ## Overview
2
+
3
+ Code example blocks render project Vue SFCs as live previews inside Markdown pages.
4
+
5
+ They are useful when documentation needs to show the real behavior of a component and still let readers inspect the exact source behind the preview.
6
+
7
+ The block is authored with the custom Markdown element `<d-block-code-example>`.
8
+
9
+ ## Example Files
10
+
11
+ Place example components under `src/examples/**/*.vue` in the project using Docsector.
12
+
13
+ Docsector discovers those files at build time through Vite. The `src` value is normalized to kebab-case, so this block:
14
+
15
+ ```html
16
+ <d-block-code-example src="manual/code-examples/basic-counter" title="Basic counter" />
17
+ ```
18
+
19
+ resolves this file:
20
+
21
+ ```text
22
+ src/examples/manual/code-examples/BasicCounter.vue
23
+ ```
24
+
25
+ You can also use `file` instead of `src` when migrating examples from Quasar Docs patterns.
26
+
27
+ ## Attributes
28
+
29
+ | Attribute | Purpose |
30
+ |-----------|---------|
31
+ | `src` | Example id under `src/examples/**/*.vue` |
32
+ | `file` | Alias for `src` |
33
+ | `title` | Header title shown above the preview |
34
+ | `expanded` | Opens the source panel by default when set to `true` |
35
+ | `codepen` | Shows the CodePen action unless set to `false` |
36
+ | `scrollable` | Gives the preview a fixed scrollable height |
37
+ | `overflow` | Allows both horizontal and vertical overflow in the preview |
38
+ | `height` | Sets a custom preview height, such as `360` or `420px` |
39
+
40
+ ## Source Panel
41
+
42
+ The source button opens the Vue SFC split into Template, Script, Style, and All tabs when those sections are present.
43
+
44
+ The source panel reuses the standard Docsector code block renderer, so readers get syntax highlighting, copy support, and the same dark/light treatment as regular code blocks.
45
+
46
+ ## GitHub Source Link
47
+
48
+ The GitHub button opens the example SFC in the project repository when Docsector can derive a repository URL from `github.editBaseUrl` or `links.github` in `docsector.config.js`.
49
+
50
+ ## CodePen Export
51
+
52
+ The CodePen button is available when the source can be transformed safely for a browser-only demo.
53
+
54
+ The first implementation supports plain Vue SFCs with a template, optional style, and an Options API `export default` script. Named imports from `vue` and `quasar` are converted to browser globals. Examples that use `<script setup>`, TypeScript scripts, or local imports still render in Docsector, but the CodePen action is disabled.
55
+
56
+ Set `codepen="false"` when an example is intentionally not meant to be exported.
@@ -0,0 +1,56 @@
1
+ ## Visão geral
2
+
3
+ Blocos de exemplos de código renderizam SFCs Vue do projeto como previews ao vivo dentro das páginas Markdown.
4
+
5
+ Eles são úteis quando a documentação precisa mostrar o comportamento real de um componente e ainda permitir que leitores inspecionem o código fonte exato por trás do preview.
6
+
7
+ O bloco é escrito com o elemento Markdown customizado `<d-block-code-example>`.
8
+
9
+ ## Arquivos de exemplo
10
+
11
+ Coloque os componentes de exemplo em `src/examples/**/*.vue` no projeto que usa o Docsector.
12
+
13
+ O Docsector descobre esses arquivos durante o build com Vite. O valor de `src` é normalizado para kebab-case, então este bloco:
14
+
15
+ ```html
16
+ <d-block-code-example src="manual/code-examples/basic-counter" title="Basic counter" />
17
+ ```
18
+
19
+ resolve este arquivo:
20
+
21
+ ```text
22
+ src/examples/manual/code-examples/BasicCounter.vue
23
+ ```
24
+
25
+ Também é possível usar `file` no lugar de `src` ao migrar exemplos inspirados nos padrões do Quasar Docs.
26
+
27
+ ## Atributos
28
+
29
+ | Atributo | Finalidade |
30
+ |----------|------------|
31
+ | `src` | Id do exemplo dentro de `src/examples/**/*.vue` |
32
+ | `file` | Alias de `src` |
33
+ | `title` | Título exibido acima do preview |
34
+ | `expanded` | Abre o painel de fonte por padrão quando definido como `true` |
35
+ | `codepen` | Mostra a ação do CodePen, exceto quando definido como `false` |
36
+ | `scrollable` | Dá ao preview uma altura fixa com rolagem |
37
+ | `overflow` | Permite overflow horizontal e vertical no preview |
38
+ | `height` | Define uma altura customizada para o preview, como `360` ou `420px` |
39
+
40
+ ## Painel de fonte
41
+
42
+ O botão de fonte abre o SFC Vue dividido em abas Template, Script, Style e All quando essas seções existem.
43
+
44
+ O painel reutiliza o renderizador padrão de blocos de código do Docsector, então leitores recebem syntax highlighting, suporte a cópia e o mesmo tratamento visual em temas claro e escuro.
45
+
46
+ ## Link de fonte no GitHub
47
+
48
+ O botão do GitHub abre o SFC do exemplo no repositório do projeto quando o Docsector consegue derivar a URL a partir de `github.editBaseUrl` ou `links.github` em `docsector.config.js`.
49
+
50
+ ## Exportação para CodePen
51
+
52
+ O botão do CodePen fica disponível quando o código pode ser transformado com segurança para uma demo somente no navegador.
53
+
54
+ A primeira implementação suporta SFCs Vue simples com template, style opcional e script Options API com `export default`. Imports nomeados de `vue` e `quasar` são convertidos para globais do navegador. Exemplos que usam `<script setup>`, scripts TypeScript ou imports locais continuam renderizando no Docsector, mas a ação do CodePen fica desativada.
55
+
56
+ Use `codepen="false"` quando um exemplo intencionalmente não deve ser exportado.
@@ -0,0 +1,38 @@
1
+ ## Showcase
2
+
3
+ ### Basic Counter
4
+
5
+ This example is rendered from a real Vue SFC under `src/examples/manual/code-examples/BasicCounter.vue`.
6
+
7
+ <d-block-code-example src="manual/code-examples/basic-counter" title="Basic counter">
8
+ Use the source button to inspect the SFC sections, or open the compatible demo in CodePen.
9
+ </d-block-code-example>
10
+
11
+ ### Expanded Source by Default
12
+
13
+ Use `expanded="true"` when the source code is part of the lesson and should be visible as soon as the reader reaches the example.
14
+
15
+ <d-block-code-example src="manual/code-examples/inline-notice" title="Expanded source example" expanded="true">
16
+ This example intentionally starts with the source panel open.
17
+ </d-block-code-example>
18
+
19
+ ## Authoring Syntax
20
+
21
+ ```html
22
+ <d-block-code-example src="manual/code-examples/basic-counter" title="Basic counter">
23
+ Optional caption rendered as inline Markdown.
24
+ </d-block-code-example>
25
+
26
+ <d-block-code-example src="manual/code-examples/inline-notice" title="Expanded source example" expanded="true">
27
+ The source panel starts open.
28
+ </d-block-code-example>
29
+ ```
30
+
31
+ ## Features Visible Above
32
+
33
+ - **Live preview** rendered from the bundled Vue component
34
+ - **Source toggle** with Template, Script, Style, and All tabs
35
+ - **CodePen action** for compatible examples
36
+ - **GitHub action** pointing to the example SFC
37
+ - **Expanded source state** with `expanded="true"`
38
+ - **Inline Markdown caption** below the preview
@@ -0,0 +1,38 @@
1
+ ## Showcase
2
+
3
+ ### Contador básico
4
+
5
+ Este exemplo é renderizado a partir de um SFC Vue real em `src/examples/manual/code-examples/BasicCounter.vue`.
6
+
7
+ <d-block-code-example src="manual/code-examples/basic-counter" title="Basic counter">
8
+ Use o botão de fonte para inspecionar as seções do SFC, ou abra a demo compatível no CodePen.
9
+ </d-block-code-example>
10
+
11
+ ### Fonte expandida por padrão
12
+
13
+ Use `expanded="true"` quando o código fonte faz parte da explicação e deve aparecer assim que o leitor chegar ao exemplo.
14
+
15
+ <d-block-code-example src="manual/code-examples/inline-notice" title="Expanded source example" expanded="true">
16
+ Este exemplo intencionalmente começa com o painel de fonte aberto.
17
+ </d-block-code-example>
18
+
19
+ ## Sintaxe de autoria
20
+
21
+ ```html
22
+ <d-block-code-example src="manual/code-examples/basic-counter" title="Basic counter">
23
+ Legenda opcional renderizada como Markdown inline.
24
+ </d-block-code-example>
25
+
26
+ <d-block-code-example src="manual/code-examples/inline-notice" title="Expanded source example" expanded="true">
27
+ O painel de fonte começa aberto.
28
+ </d-block-code-example>
29
+ ```
30
+
31
+ ## Recursos visíveis acima
32
+
33
+ - **Preview ao vivo** renderizado a partir do componente Vue empacotado
34
+ - **Alternância de fonte** com abas Template, Script, Style e All
35
+ - **Ação do CodePen** para exemplos compatíveis
36
+ - **Ação do GitHub** apontando para o SFC do exemplo
37
+ - **Estado de fonte expandida** com `expanded="true"`
38
+ - **Legenda em Markdown inline** abaixo do preview
@@ -514,6 +514,34 @@ export default {
514
514
  }
515
515
  },
516
516
 
517
+ '/content/blocks/code-examples': {
518
+ config: {
519
+ icon: 'integration_instructions',
520
+ status: 'new',
521
+ meta: {
522
+ description: {
523
+ 'en-US': 'Code examples — Documentation of Docsector Reader',
524
+ 'pt-BR': 'Exemplos de código — Documentacao do Docsector Reader'
525
+ }
526
+ },
527
+ book: 'manual',
528
+ menu: {},
529
+ subpages: {
530
+ showcase: true
531
+ }
532
+ },
533
+ data: {
534
+ 'en-US': { title: 'Code examples' },
535
+ 'pt-BR': { title: 'Exemplos de código' }
536
+ },
537
+ metadata: {
538
+ tags: {
539
+ 'en-US': 'code examples live preview vue sfc source codepen component demo',
540
+ 'pt-BR': 'exemplos código preview ao vivo vue sfc fonte codepen componente demo'
541
+ }
542
+ }
543
+ },
544
+
517
545
  '/content/blocks/mermaid-diagrams': {
518
546
  config: {
519
547
  icon: 'account_tree',
@@ -1144,6 +1144,82 @@ function createBooksPlugin (projectRoot) {
1144
1144
  }
1145
1145
  }
1146
1146
 
1147
+ function buildVirtualCodeExamplesModule () {
1148
+ return `const componentModules = import.meta.glob('/src/examples/**/*.vue')
1149
+ const sourceModules = import.meta.glob('/src/examples/**/*.vue', { query: '?raw', import: 'default' })
1150
+
1151
+ const trimSlashes = (value) => String(value || '').replace(/\\\\/g, '/').replace(/^\\/+|\\/+$/g, '')
1152
+ const toKebabSegment = (value) => String(value || '')
1153
+ .replace(/\\.vue$/i, '')
1154
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
1155
+ .replace(/[\\s_]+/g, '-')
1156
+ .replace(/-+/g, '-')
1157
+ .toLowerCase()
1158
+
1159
+ export const normalizeCodeExampleId = (value) => trimSlashes(value)
1160
+ .replace(/^src\\/examples\\//i, '')
1161
+ .replace(/^examples\\//i, '')
1162
+ .replace(/\\.vue$/i, '')
1163
+ .split('/')
1164
+ .filter(Boolean)
1165
+ .map(toKebabSegment)
1166
+ .join('/')
1167
+
1168
+ export const codeExamples = Object.keys(componentModules).reduce((examples, filePath) => {
1169
+ const id = normalizeCodeExampleId(filePath)
1170
+
1171
+ examples[id] = {
1172
+ id,
1173
+ filePath,
1174
+ loadComponent: componentModules[filePath],
1175
+ loadSource: sourceModules[filePath]
1176
+ }
1177
+
1178
+ return examples
1179
+ }, {})
1180
+
1181
+ export const codeExampleIds = Object.keys(codeExamples).sort()
1182
+
1183
+ export const resolveCodeExample = (value) => {
1184
+ const id = normalizeCodeExampleId(value)
1185
+ const entry = codeExamples[id]
1186
+
1187
+ if (!entry) {
1188
+ return {
1189
+ id,
1190
+ filePath: '',
1191
+ exists: false,
1192
+ loadComponent: null,
1193
+ loadSource: null
1194
+ }
1195
+ }
1196
+
1197
+ return {
1198
+ ...entry,
1199
+ exists: true
1200
+ }
1201
+ }
1202
+
1203
+ export default codeExamples
1204
+ `
1205
+ }
1206
+
1207
+ function createCodeExamplesPlugin () {
1208
+ const virtualId = 'virtual:docsector-code-examples'
1209
+ const resolvedId = '\0' + virtualId
1210
+
1211
+ return {
1212
+ name: 'docsector-code-examples',
1213
+ resolveId (id) {
1214
+ if (id === virtualId) return resolvedId
1215
+ },
1216
+ load (id) {
1217
+ if (id !== resolvedId) return null
1218
+ return buildVirtualCodeExamplesModule()
1219
+ }
1220
+ }
1221
+ }
1222
+
1147
1223
  /**
1148
1224
  * Create the HJSON Vite plugin for loading .hjson files as ES modules.
1149
1225
  */
@@ -1504,7 +1580,7 @@ async function fetchRemoteMarkdown (url, timeoutMs = 8000) {
1504
1580
  }
1505
1581
  }
1506
1582
 
1507
- async function resolveHomePageSources (projectRoot, config = {}, options = {}) {
1583
+ export async function resolveHomePageSources (projectRoot, config = {}, options = {}) {
1508
1584
  const { logPrefix = '[docsector]' } = options
1509
1585
  const pagesDir = resolve(projectRoot, 'src', 'pages')
1510
1586
  const { defaultLang, langs } = getConfiguredLanguages(config)
@@ -1551,18 +1627,17 @@ async function resolveHomePageSources (projectRoot, config = {}, options = {}) {
1551
1627
  function createHomePageOverridePlugin (projectRoot) {
1552
1628
  const virtualId = 'virtual:docsector-homepage-override'
1553
1629
  const resolvedId = '\0' + virtualId
1554
- let byLang = null
1630
+ let homePageSources = null
1555
1631
  let loadPromise = null
1556
1632
 
1557
1633
  const ensureSources = async () => {
1558
- if (byLang) return byLang
1634
+ if (homePageSources) return homePageSources
1559
1635
  if (!loadPromise) {
1560
1636
  loadPromise = (async () => {
1561
1637
  const configUrl = pathToFileURL(resolve(projectRoot, 'docsector.config.js')).href
1562
1638
  const { default: config } = await import(configUrl)
1563
- const sources = await resolveHomePageSources(projectRoot, config, { logPrefix: '[docsector]' })
1564
- byLang = sources.byLang
1565
- return byLang
1639
+ homePageSources = await resolveHomePageSources(projectRoot, config, { logPrefix: '[docsector]' })
1640
+ return homePageSources
1566
1641
  })().finally(() => {
1567
1642
  loadPromise = null
1568
1643
  })
@@ -1586,18 +1661,21 @@ function createHomePageOverridePlugin (projectRoot) {
1586
1661
  },
1587
1662
  async load (id) {
1588
1663
  if (id === resolvedId) {
1589
- await ensureSources()
1590
- return `export default ${JSON.stringify(byLang || {})}`
1664
+ const sources = await ensureSources()
1665
+ return [
1666
+ `export const homePageSourceMode = ${JSON.stringify(sources?.mode || 'local')}`,
1667
+ `export default ${JSON.stringify(sources?.byLang || {})}`
1668
+ ].join('\n')
1591
1669
  }
1592
1670
 
1593
- await ensureSources()
1594
- if (!byLang) return null
1671
+ const sources = await ensureSources()
1672
+ if (!sources?.byLang) return null
1595
1673
 
1596
1674
  const match = id.match(/Homepage\.([A-Za-z0-9-]+)\.md\?raw(?:$|&)/)
1597
1675
  if (!match) return null
1598
1676
 
1599
1677
  const lang = match[1]
1600
- const content = byLang[lang]
1678
+ const content = sources.byLang[lang]
1601
1679
  if (typeof content !== 'string') return null
1602
1680
 
1603
1681
  return `export default ${JSON.stringify(content)}`
@@ -2917,6 +2995,7 @@ export function createQuasarConfig (options = {}) {
2917
2995
 
2918
2996
  vitePlugins: [
2919
2997
  createBooksPlugin(projectRoot),
2998
+ createCodeExamplesPlugin(),
2920
2999
  createHjsonPlugin(),
2921
3000
  createHomePageOverridePlugin(projectRoot),
2922
3001
  createGitDatesPlugin(projectRoot),
package/src/store/Page.js CHANGED
@@ -1,3 +1,23 @@
1
+ const getNodePath = (nodes, targetId, ancestry = []) => {
2
+ for (const node of nodes) {
3
+ const nextAncestry = [...ancestry, node.id]
4
+
5
+ if (node.id === targetId) {
6
+ return nextAncestry
7
+ }
8
+
9
+ if (Array.isArray(node.children) && node.children.length > 0) {
10
+ const childPath = getNodePath(node.children, targetId, nextAncestry)
11
+
12
+ if (childPath !== null) {
13
+ return childPath
14
+ }
15
+ }
16
+ }
17
+
18
+ return null
19
+ }
20
+
1
21
  export default {
2
22
  namespaced: true,
3
23
 
@@ -46,7 +66,7 @@ export default {
46
66
  pushAnchors (state, value) {
47
67
  if (value === false) {
48
68
  state.anchors = []
49
- } else {
69
+ } else if (!state.anchors.includes(value)) {
50
70
  // index: id
51
71
  state.anchors.push(value)
52
72
  }
@@ -82,7 +102,13 @@ export default {
82
102
  state.nodesExpanded = [0]
83
103
  },
84
104
  pushNodesExpanded (state, nodeId) {
85
- state.nodesExpanded.push(nodeId)
105
+ const nodePath = getNodePath(state.nodes, nodeId) || [nodeId]
106
+
107
+ for (const pathNodeId of nodePath) {
108
+ if (!state.nodesExpanded.includes(pathNodeId)) {
109
+ state.nodesExpanded.push(pathNodeId)
110
+ }
111
+ }
86
112
  },
87
113
 
88
114
  setScrolling (state, val) {