@docsector/docsector-reader 3.0.0 → 3.2.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.
package/README.md CHANGED
@@ -43,6 +43,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
43
43
  - 📝 **Markdown Rendering** — Write docs in Markdown, rendered with syntax highlighting (Prism.js)
44
44
  - 🧱 **Raw HTML in Markdown** — Renders inline and block HTML tags inside markdown sections (including homepage remote README content)
45
45
  - 🧩 **Mermaid Diagrams** — Native support for fenced ` ```mermaid ` blocks, with automatic dark/light theme switching
46
+ - ➗ **Math & KaTeX** — Native support for inline `$...$` and display `$$...$$` formulas rendered with KaTeX
46
47
  - 🚨 **GitHub-Style Alerts** — Native support for `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, and `[!CAUTION]`
47
48
  - 🌍 **Internationalization (i18n)** — Multi-language support with HJSON locale files and per-page translations
48
49
  - 🌗 **Dark/Light Mode** — Automatic theme switching with Quasar Dark Plugin
@@ -54,6 +55,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
54
55
  - 📚 **Book Tabs with Per-State Colors** — Define `*.book.js` tabs with icons, order, and `color.active` / `color.inactive`
55
56
  - 🔀 **Internal Shortcut Pages** — Route entries can redirect with `config.link.to`, keeping localized titles while inheriting icon/status from the destination page
56
57
  - 📐 **Responsive Subpage Toolbar** — Subpage actions align with the content column on desktop and dock to the bottom on mobile
58
+ - ⬆️ **Reading Progress Back to Top** — Documentation subpages can show a floating back-to-top control with circular reading progress that stays above the mobile subpage toolbar
57
59
  - 🏷️ **Status Badges** — Mark pages as `done`, `draft`, `empty`, or `new` with visual indicators
58
60
  - ✏️ **Edit on GitHub** — Direct links to edit pages on your repository
59
61
  - 🧭 **Robust Edit Link Mapping** — Normalizes route paths (including trailing slashes) into `page.subpage.locale.md` source files for reliable GitHub edit URLs
package/bin/docsector.js CHANGED
@@ -23,7 +23,7 @@ const packageRoot = resolve(__dirname, '..')
23
23
  const args = process.argv.slice(2)
24
24
  const command = args[0]
25
25
 
26
- const VERSION = '3.0.0'
26
+ const VERSION = '3.2.0'
27
27
 
28
28
  const HELP = `
29
29
  Docsector Reader v${VERSION}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
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",
@@ -70,8 +70,10 @@
70
70
  "axios": "^1.7.7",
71
71
  "core-js": "^3.6.5",
72
72
  "hjson": "^3.2.2",
73
+ "katex": "^0.17.0",
73
74
  "markdown-it": "^13.0.1",
74
75
  "markdown-it-attrs": "^4.1.6",
76
+ "markdown-it-texmath": "^1.0.0",
75
77
  "mermaid": "^11.0.0",
76
78
  "prismjs": "^1.27.0",
77
79
  "q-colorize-mixin": "^2.0.0-alpha.5"
@@ -5,6 +5,7 @@ import { useRoute, useRouter } from 'vue-router'
5
5
  import { useQuasar } from 'quasar'
6
6
 
7
7
  import useNavigator from '../composables/useNavigator'
8
+ import { getReadingProgressState } from '../composables/useReadingProgress'
8
9
 
9
10
  import DPageAnchor from './DPageAnchor.vue'
10
11
  import DPageMeta from './DPageMeta.vue'
@@ -20,6 +21,10 @@ const props = defineProps({
20
21
  disableNav: {
21
22
  type: Boolean,
22
23
  default: false
24
+ },
25
+ showBackToTopControl: {
26
+ type: Boolean,
27
+ default: false
23
28
  }
24
29
  })
25
30
 
@@ -29,6 +34,26 @@ const submenu = ref(null)
29
34
  const pageMinHeight = ref('calc(100vh - 86px)')
30
35
  const submenuHeight = ref('36px')
31
36
  const pageBottomInset = ref('0px')
37
+ const readingProgress = ref(getReadingProgressState())
38
+
39
+ const getPageScrollContainer = () => {
40
+ return pageScrollArea.value?.$el?.querySelector('.q-scrollarea__container') || null
41
+ }
42
+
43
+ const syncReadingProgress = (scrollTop = null) => {
44
+ const container = getPageScrollContainer()
45
+
46
+ if (!container) {
47
+ readingProgress.value = getReadingProgressState()
48
+ return
49
+ }
50
+
51
+ readingProgress.value = getReadingProgressState({
52
+ scrollTop: scrollTop ?? container.scrollTop,
53
+ scrollHeight: container.scrollHeight,
54
+ clientHeight: container.clientHeight
55
+ })
56
+ }
32
57
 
33
58
  const updatePageMinHeight = () => {
34
59
  const pageContainerEl = pageContainer.value?.$el || pageContainer.value
@@ -47,6 +72,7 @@ const updatePageMinHeight = () => {
47
72
  pageMinHeight.value = `calc(100vh - ${totalOffset}px)`
48
73
  submenuHeight.value = `${Math.max(36, Math.round(measuredSubmenuHeight))}px`
49
74
  pageBottomInset.value = isMobile ? submenuHeight.value : '0px'
75
+ syncReadingProgress()
50
76
  }
51
77
 
52
78
  const schedulePageMinHeightUpdate = () => {
@@ -80,6 +106,12 @@ const main = computed(() => {
80
106
  return 'overview'
81
107
  }
82
108
  })
109
+ const shouldShowBackToTopControl = computed(() => {
110
+ return props.showBackToTopControl && readingProgress.value.hasOverflow && readingProgress.value.isVisible
111
+ })
112
+ const backToTopRightOffset = computed(() => {
113
+ return layoutMeta.value && !$q.screen.lt.md ? '332px' : '24px'
114
+ })
83
115
 
84
116
  const toggleSectionsTree = () => {
85
117
  layoutMeta.value = !layoutMeta.value
@@ -133,10 +165,7 @@ const resetPageScroll = () => {
133
165
  if (pageScrollArea.value !== null) {
134
166
  pageScrollArea.value.setScrollPosition('vertical', 0, 0)
135
167
  }
136
- }
137
-
138
- const getPageScrollContainer = () => {
139
- return pageScrollArea.value?.$el?.querySelector('.q-scrollarea__container') || null
168
+ syncReadingProgress(0)
140
169
  }
141
170
 
142
171
  const isEditableTarget = (target) => {
@@ -208,6 +237,15 @@ const handleMainScrollKeys = (event) => {
208
237
  container.scrollTop = nextTop
209
238
  }
210
239
 
240
+ const handlePageScroll = (scrollState) => {
241
+ scrolling(scrollState)
242
+ syncReadingProgress(scrollState?.position?.top)
243
+ }
244
+
245
+ const scrollToTop = () => {
246
+ navigate(0)
247
+ }
248
+
211
249
  onMounted(() => {
212
250
  window.addEventListener('keydown', handleMainScrollKeys)
213
251
  window.addEventListener('resize', schedulePageMinHeightUpdate)
@@ -236,6 +274,7 @@ onBeforeUnmount(() => {
236
274
  watch(() => route.fullPath, () => {
237
275
  nextTick(() => {
238
276
  schedulePageMinHeightUpdate()
277
+ syncReadingProgress(0)
239
278
  })
240
279
  })
241
280
  </script>
@@ -292,10 +331,38 @@ watch(() => route.fullPath, () => {
292
331
  <slot />
293
332
  </div>
294
333
  <d-page-meta v-if="!disableNav" />
295
- <q-scroll-observer @scroll="scrolling" :debounce="300" />
334
+ <q-scroll-observer @scroll="handlePageScroll" :debounce="300" />
296
335
  </q-scroll-area>
297
336
  </q-page>
298
337
 
338
+ <div
339
+ v-if="shouldShowBackToTopControl"
340
+ class="d-back-to-top"
341
+ :style="{ '--d-back-to-top-right': backToTopRightOffset }"
342
+ >
343
+ <q-circular-progress
344
+ class="d-back-to-top__progress"
345
+ :value="readingProgress.progressPercent"
346
+ size="58px"
347
+ :thickness="0.16"
348
+ color="primary"
349
+ track-color="grey-5"
350
+ />
351
+ <q-btn
352
+ class="d-back-to-top__button"
353
+ round
354
+ dense
355
+ unelevated
356
+ color="dark"
357
+ text-color="white"
358
+ icon="north"
359
+ :aria-label="$t('system.backToTop')"
360
+ @click="scrollToTop"
361
+ >
362
+ <q-tooltip anchor="top middle" self="bottom middle" :offset="[10, 10]">{{ $t('system.backToTop') }}</q-tooltip>
363
+ </q-btn>
364
+ </div>
365
+
299
366
  <q-drawer elevated show-if-above side="right" v-model="layoutMeta">
300
367
  <d-page-anchor id="anchor" />
301
368
  </q-drawer>
@@ -321,6 +388,25 @@ watch(() => route.fullPath, () => {
321
388
  #page
322
389
  min-height: var(--d-page-min-height, calc(100vh - 86px)) !important
323
390
 
391
+ .d-back-to-top
392
+ position: fixed
393
+ right: var(--d-back-to-top-right, 24px)
394
+ bottom: calc(24px + var(--d-page-bottom-inset, 0px) + env(safe-area-inset-bottom, 0px))
395
+ width: 58px
396
+ height: 58px
397
+ z-index: 1200
398
+ filter: drop-shadow(0 8px 18px rgba(0,0,0,0.2))
399
+
400
+ .d-back-to-top__progress,
401
+ .d-back-to-top__button
402
+ position: absolute
403
+ inset: 0
404
+
405
+ .d-back-to-top__button
406
+ margin: auto
407
+ width: 40px
408
+ height: 40px
409
+
324
410
  #scroll-container
325
411
  width: 100%
326
412
  max-width: 1200px
@@ -425,4 +511,12 @@ body.mobile.body--dark
425
511
  body.mobile
426
512
  .q-drawer--right
427
513
  background: rgba(255, 255, 255, 0.7)
514
+
515
+ body.body--light
516
+ .d-back-to-top__progress
517
+ color: var(--q-primary)
518
+
519
+ body.body--dark
520
+ .d-back-to-top__progress
521
+ color: #58d1a8
428
522
  </style>
@@ -22,7 +22,7 @@ const id = computed(() => {
22
22
  </script>
23
23
 
24
24
  <template>
25
- <d-page>
25
+ <d-page show-back-to-top-control>
26
26
  <header>
27
27
  <d-page-bar />
28
28
  <hr />
@@ -1,5 +1,7 @@
1
1
  import MarkdownIt from 'markdown-it'
2
2
  import attrs from 'markdown-it-attrs'
3
+ import katex from 'katex'
4
+ import texmath from 'markdown-it-texmath'
3
5
 
4
6
  const ALERT_MESSAGE_TYPES = new Set([
5
7
  'note',
@@ -12,6 +14,10 @@ const ALERT_MESSAGE_TYPES = new Set([
12
14
  const QUICK_LINKS_MARKER_PREFIX = '@@DOCSECTOR_QUICK_LINKS_'
13
15
  const EXPANDABLE_MARKER_PREFIX = '@@DOCSECTOR_EXPANDABLE_'
14
16
  const CODE_SEGMENT_MARKER_PREFIX = '@@DOCSECTOR_CODE_SEGMENT_'
17
+ const MATH_KATEX_OPTIONS = {
18
+ throwOnError: false,
19
+ strict: 'ignore'
20
+ }
15
21
 
16
22
  const parseAlertMarker = (rawContent = '') => {
17
23
  const match = String(rawContent).trim().match(/^\[!\s*([A-Za-z]+)\s*\]\s*(.*)$/s)
@@ -100,7 +106,7 @@ const restoreShieldedCodeSegments = (source = '', codeSegmentsMap = new Map()) =
100
106
  let restored = String(source)
101
107
 
102
108
  codeSegmentsMap.forEach((content, marker) => {
103
- restored = restored.replaceAll(marker, content)
109
+ restored = restored.replaceAll(marker, () => content)
104
110
  })
105
111
 
106
112
  return restored
@@ -299,10 +305,24 @@ const pushSourceCodeToken = (tokens, element, parserState) => {
299
305
  })
300
306
  }
301
307
 
308
+ const installMathSupport = (markdown) => {
309
+ markdown.use(texmath, {
310
+ engine: katex,
311
+ delimiters: 'dollars',
312
+ katexOptions: MATH_KATEX_OPTIONS
313
+ })
314
+
315
+ return markdown
316
+ }
317
+
318
+ const renderBlockToken = (markdown, element, env) => {
319
+ return markdown.renderer.render([element], markdown.options, env).trim()
320
+ }
321
+
302
322
  const createMarkdownBlockParser = () => {
303
- const markdown = new MarkdownIt({
323
+ const markdown = installMathSupport(new MarkdownIt({
304
324
  html: true
305
- })
325
+ }))
306
326
 
307
327
  markdown.use(attrs, {
308
328
  leftDelimiter: ':',
@@ -314,9 +334,9 @@ const createMarkdownBlockParser = () => {
314
334
  }
315
335
 
316
336
  const createMarkdownInlineParser = () => {
317
- return new MarkdownIt({
337
+ return installMathSupport(new MarkdownIt({
318
338
  html: true
319
- })
339
+ }))
320
340
  }
321
341
 
322
342
  const normalizePageSectionSource = (source = '') => {
@@ -444,6 +464,11 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
444
464
  return
445
465
  }
446
466
 
467
+ if (element.type === 'math_block') {
468
+ blockquote.content += renderBlockToken(markdown, element, markdownEnv)
469
+ return
470
+ }
471
+
447
472
  if (element.type.endsWith('_open')) {
448
473
  appendBlockquoteTag(element, true)
449
474
  return
@@ -519,6 +544,13 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
519
544
  pushSourceCodeToken(tokens, element, parserState)
520
545
  break
521
546
 
547
+ case 'math_block':
548
+ tokens.push({
549
+ tag: 'html',
550
+ content: renderBlockToken(markdown, element, markdownEnv)
551
+ })
552
+ break
553
+
522
554
  case 'html_block':
523
555
  tokens.push({
524
556
  tag: 'html',
@@ -574,6 +606,9 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
574
606
  case 'inline':
575
607
  parent.content += element.content
576
608
  break
609
+ case 'math_block':
610
+ parent.content += renderBlockToken(markdown, element, markdownEnv)
611
+ break
577
612
  case 'html_inline':
578
613
  case 'html_block':
579
614
  parent.content += element.content
@@ -0,0 +1,46 @@
1
+ export const DEFAULT_READING_PROGRESS_THRESHOLD = 0.12
2
+
3
+ const toFiniteNumber = (value) => {
4
+ const normalized = Number(value)
5
+ return Number.isFinite(normalized) ? normalized : 0
6
+ }
7
+
8
+ const clamp = (value, min, max) => {
9
+ return Math.min(max, Math.max(min, value))
10
+ }
11
+
12
+ export function getReadingProgressState({
13
+ scrollTop = 0,
14
+ scrollHeight = 0,
15
+ clientHeight = 0,
16
+ visibilityThreshold = DEFAULT_READING_PROGRESS_THRESHOLD
17
+ } = {}) {
18
+ const normalizedClientHeight = Math.max(0, toFiniteNumber(clientHeight))
19
+ const normalizedScrollHeight = Math.max(0, toFiniteNumber(scrollHeight))
20
+ const maxScrollTop = Math.max(0, normalizedScrollHeight - normalizedClientHeight)
21
+
22
+ if (maxScrollTop === 0) {
23
+ return {
24
+ hasOverflow: false,
25
+ maxScrollTop: 0,
26
+ scrollTop: 0,
27
+ progressPercent: 0,
28
+ visibleOffset: 0,
29
+ isVisible: false
30
+ }
31
+ }
32
+
33
+ const normalizedScrollTop = clamp(toFiniteNumber(scrollTop), 0, maxScrollTop)
34
+ const normalizedThreshold = clamp(toFiniteNumber(visibilityThreshold), 0, 1)
35
+ const visibleOffset = Math.round(maxScrollTop * normalizedThreshold)
36
+ const progressPercent = Math.round((normalizedScrollTop / maxScrollTop) * 100)
37
+
38
+ return {
39
+ hasOverflow: true,
40
+ maxScrollTop,
41
+ scrollTop: normalizedScrollTop,
42
+ progressPercent,
43
+ visibleOffset,
44
+ isVisible: normalizedScrollTop >= visibleOffset && normalizedScrollTop > 0
45
+ }
46
+ }
package/src/css/app.sass CHANGED
@@ -1,3 +1,5 @@
1
+ @import 'katex/dist/katex.min.css'
2
+
1
3
  /* --- Docsector Reader --- */
2
4
  @font-face
3
5
  font-family: "Fira Code Nerd Font"
@@ -187,6 +189,16 @@ body.body--dark
187
189
  display: inline
188
190
  line-height: 0.85em
189
191
 
192
+ .katex
193
+ color: inherit
194
+ max-width: 100%
195
+
196
+ .katex-display
197
+ max-width: 100%
198
+ overflow-x: auto
199
+ overflow-y: hidden
200
+ padding: 0.35rem 0.1rem
201
+
190
202
  a
191
203
  text-decoration: none
192
204
  outline: 0
@@ -61,6 +61,25 @@ You can render Mermaid diagrams using fenced blocks with the `mermaid` language
61
61
  ```
62
62
  &#96;&#96;&#96;mermaid
63
63
  flowchart TD
64
+ A[Start] --> B[End]
65
+ &#96;&#96;&#96;
66
+ ```
67
+
68
+ ### Math and TeX
69
+
70
+ Docsector supports KaTeX-style math in standard Markdown flows, including paragraphs, alerts, and expandable content.
71
+
72
+ Use single dollar delimiters for inline math such as $E = mc^2$.
73
+
74
+ Use double dollar delimiters for display math:
75
+
76
+ ```markdown
77
+ $$
78
+ \int_0^1 x^2 dx
79
+ $$
80
+ ```
81
+
82
+ Math delimiters remain literal inside inline code and fenced code blocks, so syntax examples can be documented without rendering the equation.
64
83
 
65
84
  ### GitHub Alerts
66
85
 
@@ -75,9 +94,6 @@ Docsector also supports GitHub-style alert blockquotes:
75
94
 
76
95
  Supported types are `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, and `CAUTION`.
77
96
  Regular blockquotes (without `[!TYPE]`) continue to work as normal.
78
- A[Start] --> B[End]
79
- &#96;&#96;&#96;
80
- ```
81
97
 
82
98
  ### Custom Attributes
83
99
 
@@ -65,6 +65,22 @@ flowchart TD
65
65
  &#96;&#96;&#96;
66
66
  ```
67
67
 
68
+ ### Math e TeX
69
+
70
+ O Docsector suporta fórmulas com KaTeX nos fluxos normais de Markdown, incluindo parágrafos, alertas e conteúdo expansível.
71
+
72
+ Use delimitadores com um dólar para fórmulas inline, como $E = mc^2$.
73
+
74
+ Use delimitadores com dois dólares para fórmulas em bloco:
75
+
76
+ ```markdown
77
+ $$
78
+ \int_0^1 x^2 dx
79
+ $$
80
+ ```
81
+
82
+ Os delimitadores matemáticos permanecem literais dentro de código inline e fenced code, então exemplos de sintaxe podem ser documentados sem renderizar a equação.
83
+
68
84
  ### Alertas do GitHub
69
85
 
70
86
  O Docsector tambem suporta blockquotes de alerta no estilo do GitHub:
@@ -9,6 +9,7 @@ Every documentation page is rendered inside a `DPage` instance.
9
9
  | Prop | Type | Default | Description |
10
10
  |------|------|---------|-------------|
11
11
  | `disableNav` | `Boolean` | `false` | Hides the DPageMeta navigation footer |
12
+ | `showBackToTopControl` | `Boolean` | `false` | Enables the floating back-to-top control with circular reading progress |
12
13
 
13
14
  ## Template Structure
14
15
 
@@ -46,6 +47,8 @@ DPage reads the route's `meta.subpages` configuration to determine which tabs to
46
47
 
47
48
  DPage resets the scroll position on route changes via `router.beforeEach`. The scroll observer monitors vertical scroll position and updates the anchor selection via the `useNavigator` composable.
48
49
 
50
+ When `showBackToTopControl` is enabled, DPage also derives reading progress from the same scroll container. The floating control stays hidden at the top, appears after a small amount of scroll, shows circular progress, and returns to anchor `0` when clicked.
51
+
49
52
  ## Store Integration
50
53
 
51
54
  DPage reads from and writes to several Vuex store modules:
@@ -9,6 +9,7 @@ Toda página de documentação é renderizada dentro de uma instância de `DPage
9
9
  | Prop | Tipo | Padrão | Descrição |
10
10
  |------|------|--------|-----------|
11
11
  | `disableNav` | `Boolean` | `false` | Oculta o rodapé de navegação DPageMeta |
12
+ | `showBackToTopControl` | `Boolean` | `false` | Habilita o controle flutuante de voltar ao topo com progresso circular de leitura |
12
13
 
13
14
  ## Estrutura do Template
14
15
 
@@ -46,6 +47,8 @@ DPage lê a configuração `meta.subpages` da rota para determinar quais abas ex
46
47
 
47
48
  DPage reseta a posição de scroll nas mudanças de rota via `router.beforeEach`. O observador de scroll monitora a posição vertical e atualiza a seleção de âncora via composable `useNavigator`.
48
49
 
50
+ Quando `showBackToTopControl` está habilitada, DPage também deriva o progresso de leitura a partir da mesma área de scroll. O controle flutuante fica oculto no topo, aparece após uma pequena rolagem, exibe progresso circular e retorna para a âncora `0` ao ser clicado.
51
+
49
52
  ## Integração com Store
50
53
 
51
54
  DPage lê e escreve em vários módulos da Vuex store:
@@ -9,7 +9,7 @@ DSubpage generates a deterministic numeric ID from the current route path using
9
9
  ## Template
10
10
 
11
11
  ```html
12
- <d-page>
12
+ <d-page show-back-to-top-control>
13
13
  <header>
14
14
  <d-h1 :id="0" />
15
15
  </header>
@@ -54,3 +54,7 @@ DSubpage is automatically used by the router for all documentation pages. You do
54
54
  - `DSubpage` **uses** `DPage` as its container
55
55
  - `DPage` handles layout (scroll, toolbar, drawer)
56
56
  - `DSubpage` handles content composition (H1 + sections)
57
+
58
+ ## Built-in Back to Top Control
59
+
60
+ Routed documentation subpages enable DPage's floating back-to-top control automatically. The control is only shown when the content actually overflows, becomes visible after the reader scrolls a little, and visualizes the current reading progress with a circular indicator.
@@ -9,7 +9,7 @@ DSubpage gera um ID numérico determinístico a partir do caminho da rota atual
9
9
  ## Template
10
10
 
11
11
  ```html
12
- <d-page>
12
+ <d-page show-back-to-top-control>
13
13
  <header>
14
14
  <d-h1 :id="0" />
15
15
  </header>
@@ -54,3 +54,7 @@ DSubpage é usado automaticamente pelo roteador para todas as páginas de docume
54
54
  - `DSubpage` **usa** `DPage` como container
55
55
  - `DPage` cuida do layout (scroll, toolbar, drawer)
56
56
  - `DSubpage` cuida da composição de conteúdo (H1 + seções)
57
+
58
+ ## Controle Integrado de Voltar ao Topo
59
+
60
+ As sub-páginas de documentação roteadas habilitam automaticamente o controle flutuante de voltar ao topo do DPage. O controle só é exibido quando o conteúdo realmente tem overflow, fica visível após uma pequena rolagem do leitor e mostra o progresso atual de leitura com um indicador circular.