@docsector/docsector-reader 4.0.0 → 4.0.1

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
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
  <h1 align="center">Docsector Reader 📖</h1>
5
5
  <p align="center">
6
- <i>A documentation rendering engine built with Vue 3, Quasar v2 and Vite.</i>
6
+ <i>A documentation rendering engine built with Vue 3, Quasar v2 and Vite with AI features.</i>
7
7
  </p>
8
8
  <p align="center">
9
9
  <a href="https://www.npmjs.com/package/@docsector/docsector-reader">
@@ -50,7 +50,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
50
50
  - 🚨 **GitHub-Style Alerts** — Native support for `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, and `[!CAUTION]`
51
51
  - 🌍 **Internationalization (i18n)** — Multi-language support with HJSON locale files and per-page translations
52
52
  - 🌗 **Dark/Light Mode** — Automatic theme switching with Quasar Dark Plugin
53
- - 🔗 **Anchor Navigation** — Right-side Table of Contents tree with scroll tracking and auto-scroll to active section
53
+ - 🔗 **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
54
  - 🖱️ **Active Menu Item UX** — Active menu entries keep pointer cursor, clear URL hash without redundant navigation, and prevent accidental label text selection
55
55
  - 🔎 **Search** — Menu search across all documentation content and tags
56
56
  - 📱 **Responsive** — Mobile-friendly with collapsible sidebar and drawers
@@ -65,7 +65,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
65
65
  - 📊 **Translation Progress** — Automatic translation percentage based on header coverage
66
66
  - 🌐 **Accurate Available Translations** — Locale availability counter now uses actual localized page source presence, avoiding false negatives when metadata is equal
67
67
  - 🏠 **Markdown Home at Root** — Homepage is rendered from `src/pages/Homepage.{lang}.md` directly at `/`
68
- - 🌍 **Remote README as Home** — Optional build-time remote README source for homepage with automatic local fallback
68
+ - 🌍 **Remote README as Home** — Optional build-time remote README source for homepage with automatic local fallback and automatic primary-title handoff when the remote README already provides the project heading
69
69
  - 🔗 **GitHub-Compatible Heading Anchors** — Markdown headings use GitHub-style slugs so standard README Table of Contents links work inside Docsector
70
70
  - 🧬 **Scaffolded Homepage Override Wiring** — New consumer projects automatically wire `virtual:docsector-homepage-override` into i18n message building
71
71
  - 📖 **Expandable Markdown Sections** — Use `<d-block-expandable title="...">...</d-block-expandable>` to collapse secondary content while keeping rich Markdown support inside the body
@@ -360,7 +360,8 @@ You can configure Docsector Reader to use a remote README as homepage content.
360
360
 
361
361
  - Fetch happens at build-time.
362
362
  - The same README content is used for all configured languages.
363
- - If fetch fails, it falls back to local `src/pages/Homepage.{lang}.md` by default.
363
+ - When the remote README resolves successfully, Docsector hides the autogenerated homepage title and uses the README's own primary heading in the rendered content.
364
+ - If fetch fails, it falls back to local `src/pages/Homepage.{lang}.md` by default and keeps the usual autogenerated homepage title.
364
365
  - Standard GitHub-style heading links and README Table of Contents fragments keep working in the rendered homepage.
365
366
 
366
367
  ### Configure
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 = '4.0.0'
26
+ const VERSION = '4.0.1'
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": "4.0.0",
3
+ "version": "4.0.1",
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",
@@ -59,6 +59,7 @@ body.body--dark
59
59
  .q-item
60
60
  min-height: 0
61
61
  padding: 0
62
+ height: 35px
62
63
 
63
64
  .q-item__section--side
64
65
  padding-left: 0.55rem
@@ -11,6 +11,10 @@ defineProps({
11
11
  id: {
12
12
  type: Number,
13
13
  required: true
14
+ },
15
+ renderPrimaryHeading: {
16
+ type: Boolean,
17
+ default: false
14
18
  }
15
19
  })
16
20
 
@@ -32,6 +36,7 @@ const tokenized = computed(() => {
32
36
  <section>
33
37
  <d-page-tokens
34
38
  :id="id"
39
+ :render-primary-heading="renderPrimaryHeading"
35
40
  :tokens="tokenized"
36
41
  />
37
42
  </section>
@@ -8,6 +8,10 @@ defineProps({
8
8
  type: Number,
9
9
  default: 0
10
10
  },
11
+ renderPrimaryHeading: {
12
+ type: Boolean,
13
+ default: false
14
+ },
11
15
  tokens: {
12
16
  type: Array,
13
17
  default: () => []
@@ -34,8 +38,13 @@ import DBlockStepper from './DBlockStepper.vue'
34
38
 
35
39
  <template>
36
40
  <template v-for="(token, index) in tokens" :key="`${token.tag}-${index}`">
41
+ <h1
42
+ v-if="token.tag === 'h1' && renderPrimaryHeading"
43
+ :id="token.anchorId"
44
+ v-html="token.content"
45
+ ></h1>
37
46
  <d-h2
38
- v-if="token.tag === 'h2'"
47
+ v-else-if="token.tag === 'h2'"
39
48
  :id="token.anchorId"
40
49
  :value="token.content"
41
50
  />
@@ -1,13 +1,17 @@
1
1
  <script setup>
2
2
  import { computed } from 'vue'
3
3
  import { useRoute } from 'vue-router'
4
+ import { useStore } from 'vuex'
5
+ import { homePageSourceMode } from 'virtual:docsector-homepage-override'
4
6
  // components
5
7
  import DPage from "./DPage.vue";
6
8
  import DPageBar from "./DPageBar.vue";
7
9
  import DH1 from "./DH1.vue";
8
10
  import DPageSection from "./DPageSection.vue";
11
+ import { usesRemoteReadmeHomeContent } from '../home-page-mode'
9
12
 
10
13
  const route = useRoute()
14
+ const store = useStore()
11
15
 
12
16
  const id = computed(() => {
13
17
  const path = route.path
@@ -19,6 +23,13 @@ const id = computed(() => {
19
23
 
20
24
  return hash >>> 0
21
25
  })
26
+
27
+ const usesRemoteReadmeHome = computed(() => {
28
+ return usesRemoteReadmeHomeContent({
29
+ pageBase: store.state.page.base,
30
+ homePageSourceMode
31
+ })
32
+ })
22
33
  </script>
23
34
 
24
35
  <template>
@@ -26,11 +37,12 @@ const id = computed(() => {
26
37
  <header>
27
38
  <d-page-bar />
28
39
  <hr />
29
- <d-h1 :id="0" />
40
+ <d-h1 v-if="!usesRemoteReadmeHome" :id="0" />
41
+ <span v-else id="0" aria-hidden="true"></span>
30
42
  </header>
31
43
 
32
44
  <main>
33
- <d-page-section :id="id" />
45
+ <d-page-section :id="id" :render-primary-heading="usesRemoteReadmeHome" />
34
46
  </main>
35
47
  </d-page>
36
48
  </template>
@@ -0,0 +1,42 @@
1
+ export const DEFAULT_ACTIVE_ANCHOR_OFFSET = 50
2
+
3
+ const toFiniteNumber = (value) => {
4
+ const normalized = Number(value)
5
+ return Number.isFinite(normalized) ? normalized : 0
6
+ }
7
+
8
+ export function getActiveAnchorId({
9
+ anchors = [],
10
+ scrollTop = 0,
11
+ scrollOffset = DEFAULT_ACTIVE_ANCHOR_OFFSET,
12
+ rootAnchorId = 0,
13
+ getAnchorOffsetTop = () => undefined
14
+ } = {}) {
15
+ const thresholdTop = Math.max(0, toFiniteNumber(scrollTop)) + Math.max(0, toFiniteNumber(scrollOffset))
16
+ let activeAnchorId = rootAnchorId
17
+ const seenAnchors = new Set()
18
+
19
+ for (const anchorId of Array.isArray(anchors) ? anchors : []) {
20
+ if (anchorId === null || anchorId === undefined || anchorId === false || anchorId === 0 || anchorId === '0') {
21
+ continue
22
+ }
23
+
24
+ if (seenAnchors.has(anchorId)) {
25
+ continue
26
+ }
27
+
28
+ seenAnchors.add(anchorId)
29
+
30
+ const resolvedOffsetTop = Number(getAnchorOffsetTop(anchorId))
31
+
32
+ if (!Number.isFinite(resolvedOffsetTop)) {
33
+ continue
34
+ }
35
+
36
+ if (thresholdTop >= resolvedOffsetTop) {
37
+ activeAnchorId = anchorId
38
+ }
39
+ }
40
+
41
+ return activeAnchorId
42
+ }
@@ -3,6 +3,8 @@ import { useStore } from 'vuex'
3
3
  import { useRouter, useRoute } from 'vue-router'
4
4
  import { ref } from 'vue'
5
5
 
6
+ import { DEFAULT_ACTIVE_ANCHOR_OFFSET, getActiveAnchorId } from './useActiveAnchor'
7
+
6
8
  export default function useNavigator() {
7
9
  const store = useStore()
8
10
  const router = useRouter()
@@ -46,7 +48,10 @@ export default function useNavigator() {
46
48
  const select = (id) => {
47
49
  const normalized = normalizeStoreAnchorId(id)
48
50
 
49
- store.commit('page/setAnchor', normalized)
51
+ if (store.state.page.anchor !== normalized) {
52
+ store.commit('page/setAnchor', normalized)
53
+ }
54
+
50
55
  store.commit('page/pushNodesExpanded', normalized)
51
56
  }
52
57
 
@@ -78,27 +83,29 @@ export default function useNavigator() {
78
83
  return
79
84
  }
80
85
 
81
- const scrollPositionTop = scroll.position.top + 50
82
- const anchors = store.state.page.anchors
86
+ const activeAnchorId = getActiveAnchorId({
87
+ anchors: store.state.page.anchors,
88
+ scrollTop: scroll?.position?.top,
89
+ scrollOffset: DEFAULT_ACTIVE_ANCHOR_OFFSET,
90
+ rootAnchorId: 0,
91
+ getAnchorOffsetTop: (anchorId) => {
92
+ const domAnchorId = normalizeDomAnchorId(anchorId)
83
93
 
84
- for (let i = 0; i < anchors.length; i++) {
85
- const anchorId = anchors[i]
86
- const domAnchorId = normalizeDomAnchorId(anchorId)
94
+ if (domAnchorId === '') {
95
+ return undefined
96
+ }
87
97
 
88
- if (domAnchorId === '0') {
89
- continue
90
- }
98
+ const Anchor = document.getElementById(domAnchorId)
91
99
 
92
- const Anchor = document.getElementById(domAnchorId)
93
- let AnchorOffsetTop = 20
94
- if (Anchor !== null && typeof Anchor === 'object') {
95
- AnchorOffsetTop = Anchor.offsetTop
96
- }
100
+ if (Anchor !== null && typeof Anchor === 'object') {
101
+ return Anchor.offsetTop
102
+ }
97
103
 
98
- if (scrollPositionTop >= AnchorOffsetTop) {
99
- select(anchorId)
104
+ return undefined
100
105
  }
101
- }
106
+ })
107
+
108
+ select(activeAnchorId)
102
109
  }
103
110
 
104
111
  const navigate = (value, toAnchor = true) => {
@@ -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
 
@@ -1504,7 +1504,7 @@ async function fetchRemoteMarkdown (url, timeoutMs = 8000) {
1504
1504
  }
1505
1505
  }
1506
1506
 
1507
- async function resolveHomePageSources (projectRoot, config = {}, options = {}) {
1507
+ export async function resolveHomePageSources (projectRoot, config = {}, options = {}) {
1508
1508
  const { logPrefix = '[docsector]' } = options
1509
1509
  const pagesDir = resolve(projectRoot, 'src', 'pages')
1510
1510
  const { defaultLang, langs } = getConfiguredLanguages(config)
@@ -1551,18 +1551,17 @@ async function resolveHomePageSources (projectRoot, config = {}, options = {}) {
1551
1551
  function createHomePageOverridePlugin (projectRoot) {
1552
1552
  const virtualId = 'virtual:docsector-homepage-override'
1553
1553
  const resolvedId = '\0' + virtualId
1554
- let byLang = null
1554
+ let homePageSources = null
1555
1555
  let loadPromise = null
1556
1556
 
1557
1557
  const ensureSources = async () => {
1558
- if (byLang) return byLang
1558
+ if (homePageSources) return homePageSources
1559
1559
  if (!loadPromise) {
1560
1560
  loadPromise = (async () => {
1561
1561
  const configUrl = pathToFileURL(resolve(projectRoot, 'docsector.config.js')).href
1562
1562
  const { default: config } = await import(configUrl)
1563
- const sources = await resolveHomePageSources(projectRoot, config, { logPrefix: '[docsector]' })
1564
- byLang = sources.byLang
1565
- return byLang
1563
+ homePageSources = await resolveHomePageSources(projectRoot, config, { logPrefix: '[docsector]' })
1564
+ return homePageSources
1566
1565
  })().finally(() => {
1567
1566
  loadPromise = null
1568
1567
  })
@@ -1586,18 +1585,21 @@ function createHomePageOverridePlugin (projectRoot) {
1586
1585
  },
1587
1586
  async load (id) {
1588
1587
  if (id === resolvedId) {
1589
- await ensureSources()
1590
- return `export default ${JSON.stringify(byLang || {})}`
1588
+ const sources = await ensureSources()
1589
+ return [
1590
+ `export const homePageSourceMode = ${JSON.stringify(sources?.mode || 'local')}`,
1591
+ `export default ${JSON.stringify(sources?.byLang || {})}`
1592
+ ].join('\n')
1591
1593
  }
1592
1594
 
1593
- await ensureSources()
1594
- if (!byLang) return null
1595
+ const sources = await ensureSources()
1596
+ if (!sources?.byLang) return null
1595
1597
 
1596
1598
  const match = id.match(/Homepage\.([A-Za-z0-9-]+)\.md\?raw(?:$|&)/)
1597
1599
  if (!match) return null
1598
1600
 
1599
1601
  const lang = match[1]
1600
- const content = byLang[lang]
1602
+ const content = sources.byLang[lang]
1601
1603
  if (typeof content !== 'string') return null
1602
1604
 
1603
1605
  return `export default ${JSON.stringify(content)}`
package/src/store/Page.js CHANGED
@@ -46,7 +46,7 @@ export default {
46
46
  pushAnchors (state, value) {
47
47
  if (value === false) {
48
48
  state.anchors = []
49
- } else {
49
+ } else if (!state.anchors.includes(value)) {
50
50
  // index: id
51
51
  state.anchors.push(value)
52
52
  }
@@ -82,7 +82,9 @@ export default {
82
82
  state.nodesExpanded = [0]
83
83
  },
84
84
  pushNodesExpanded (state, nodeId) {
85
- state.nodesExpanded.push(nodeId)
85
+ if (!state.nodesExpanded.includes(nodeId)) {
86
+ state.nodesExpanded.push(nodeId)
87
+ }
86
88
  },
87
89
 
88
90
  setScrolling (state, val) {