@docsector/docsector-reader 4.6.0 → 4.7.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 +2 -0
- package/bin/docsector.js +1 -1
- package/docsector.config.js +2 -2
- package/package.json +1 -1
- package/src/components/DPageSection.vue +17 -3
- package/src/components/DPageTokens.vue +1 -0
- package/src/components/DSubpage.vue +9 -1
- package/src/components/page-template-sections.js +171 -0
- package/src/css/app.sass +37 -2
- package/src/i18n/helpers.js +2 -2
- package/src/page-template.js +118 -0
- package/src/pages/manual/content/structures/subpage.overview.en-US.md +38 -1
- package/src/pages/manual/content/structures/subpage.overview.pt-BR.md +38 -1
- package/src/router/routes.js +9 -2
package/README.md
CHANGED
|
@@ -67,6 +67,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
67
67
|
- 🦶 **Global Branding Footer** — Built-in `Powered by Docsector` footer renders across documentation and system pages, while respecting each page's own scroll container for full-width layout integration without double scrollbars
|
|
68
68
|
- 🔀 **Internal Shortcut Pages** — Route entries can redirect with `config.link.to`, keeping localized titles while inheriting icon/status from the destination page
|
|
69
69
|
- 📐 **Responsive Subpage Toolbar** — Subpage actions align with the content column on desktop and dock to the bottom on mobile
|
|
70
|
+
- 🆚 **Subpage Templates** — Subpages opt into a structured template via `vs: { template: 'vs' }`; the managed/strict `vs` template owns the order and localized titles of its **Features**, **Performance** and **Security** sections (one `##` heading per section, missing sections dropped gracefully), auto-colorizes `✓`/`✗`/`➕` comparison marks, and highlights the column whose header matches the consumer's `branding.name`
|
|
70
71
|
- ⬆️ **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
|
|
71
72
|
- 🏷️ **Status Badges** — Mark pages as `done`, `draft`, `empty`, or `new` with visual indicators
|
|
72
73
|
- ✏️ **Edit on GitHub** — Direct links to edit pages on your repository
|
|
@@ -1137,6 +1138,7 @@ Notes:
|
|
|
1137
1138
|
- In `manual.index.js`, route keys are relative to the `manual` book (for example `'/my-section/my-page'` becomes `/manual/my-section/my-page/...`).
|
|
1138
1139
|
- You only need to set `config.book` when overriding the inferred book from the registry file.
|
|
1139
1140
|
- When `showcase` or `vs` are enabled, the subpage toolbar aligns with the content width on desktop and becomes a bottom action bar on mobile.
|
|
1141
|
+
- A subpage can opt into a **structured template** with the object form `vs: { template: 'vs' }` (the boolean `true` stays `freestyle`). The built-in `vs` template is managed/strict: it owns the order and localized titles of its **Features**, **Performance** and **Security** sections — write one `##` heading per section in the Markdown and omitted sections are dropped gracefully.
|
|
1140
1142
|
|
|
1141
1143
|
2️⃣ Create Markdown files:
|
|
1142
1144
|
|
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.
|
|
27
|
+
const VERSION = '4.7.0'
|
|
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/docsector.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docsector/docsector-reader",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.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",
|
|
@@ -7,8 +7,10 @@ import DPageTokens from './DPageTokens.vue'
|
|
|
7
7
|
import { pageValueI18nPath } from '../i18n/path'
|
|
8
8
|
import { buildPageAnchorTree } from './page-anchor-tree'
|
|
9
9
|
import { tokenizePageSectionSource } from './page-section-tokens'
|
|
10
|
+
import { applyTemplateSections } from './page-template-sections'
|
|
11
|
+
import docsectorConfig from 'docsector.config.js'
|
|
10
12
|
|
|
11
|
-
defineProps({
|
|
13
|
+
const props = defineProps({
|
|
12
14
|
id: {
|
|
13
15
|
type: Number,
|
|
14
16
|
required: true
|
|
@@ -16,11 +18,15 @@ defineProps({
|
|
|
16
18
|
renderPrimaryHeading: {
|
|
17
19
|
type: Boolean,
|
|
18
20
|
default: false
|
|
21
|
+
},
|
|
22
|
+
template: {
|
|
23
|
+
type: Object,
|
|
24
|
+
default: null
|
|
19
25
|
}
|
|
20
26
|
})
|
|
21
27
|
|
|
22
28
|
const store = useStore()
|
|
23
|
-
const { t } = useI18n()
|
|
29
|
+
const { t, locale } = useI18n()
|
|
24
30
|
|
|
25
31
|
const tokenized = computed(() => {
|
|
26
32
|
const absolute = store.state.i18n.absolute
|
|
@@ -29,7 +35,15 @@ const tokenized = computed(() => {
|
|
|
29
35
|
return []
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
const tokens = tokenizePageSectionSource(t(pageValueI18nPath(absolute, 'source')))
|
|
39
|
+
|
|
40
|
+
if (Array.isArray(props.template?.sections) && props.template.sections.length > 0) {
|
|
41
|
+
return applyTemplateSections(tokens, props.template, locale.value, {
|
|
42
|
+
highlightColumn: docsectorConfig?.branding?.name
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return tokens
|
|
33
47
|
})
|
|
34
48
|
|
|
35
49
|
watch(tokenized, (tokens) => {
|
|
@@ -9,10 +9,18 @@ import DPageBar from "./DPageBar.vue";
|
|
|
9
9
|
import DH1 from "./DH1.vue";
|
|
10
10
|
import DPageSection from "./DPageSection.vue";
|
|
11
11
|
import { usesRemoteReadmeHomeContent } from '../home-page-mode'
|
|
12
|
+
import { getTemplate } from '../page-template'
|
|
12
13
|
|
|
13
14
|
const route = useRoute()
|
|
14
15
|
const store = useStore()
|
|
15
16
|
|
|
17
|
+
const template = computed(() => {
|
|
18
|
+
const relative = store.state.page.relative
|
|
19
|
+
const subpage = relative ? relative.replace(/^\//, '') : 'overview'
|
|
20
|
+
const templates = route.matched?.[0]?.meta?.subpageTemplates
|
|
21
|
+
return getTemplate(templates?.[subpage])
|
|
22
|
+
})
|
|
23
|
+
|
|
16
24
|
const id = computed(() => {
|
|
17
25
|
const path = route.path
|
|
18
26
|
|
|
@@ -42,7 +50,7 @@ const usesRemoteReadmeHome = computed(() => {
|
|
|
42
50
|
</header>
|
|
43
51
|
|
|
44
52
|
<main>
|
|
45
|
-
<d-page-section :id="id" :render-primary-heading="usesRemoteReadmeHome" />
|
|
53
|
+
<d-page-section :id="id" :render-primary-heading="usesRemoteReadmeHome" :template="template" />
|
|
46
54
|
</main>
|
|
47
55
|
</d-page>
|
|
48
56
|
</template>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const stripHtml = (value) => String(value ?? '').replace(/<[^>]*>/g, '')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a heading/key into a comparable slug: strip HTML and diacritics,
|
|
5
|
+
* lowercase, and collapse non-alphanumerics into single hyphens.
|
|
6
|
+
*/
|
|
7
|
+
const normalizeKey = (value) => stripHtml(value)
|
|
8
|
+
.normalize('NFD')
|
|
9
|
+
.replace(/[̀-ͯ]/g, '')
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.trim()
|
|
12
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
13
|
+
.replace(/^-+|-+$/g, '')
|
|
14
|
+
|
|
15
|
+
const warnUnknownSection = (templateName, heading, allowed) => {
|
|
16
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
17
|
+
console.warn(
|
|
18
|
+
`[docsector] Unknown "${templateName}" template section: "${heading}". ` +
|
|
19
|
+
`Allowed sections: ${allowed.join(', ')}.`
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ? Comparison marks colorized in rendered content (✓ yes, ✗ no, ➕ add-on)
|
|
25
|
+
const MARK_CLASS = { '✓': 'vs-mark--yes', '✗': 'vs-mark--no', '➕': 'vs-mark--dep' }
|
|
26
|
+
const MARKABLE_TAGS = new Set(['p', 'table', 'ul', 'ol'])
|
|
27
|
+
const MARK_PATTERN = /[✓✗➕]/
|
|
28
|
+
|
|
29
|
+
const colorizeMarks = (html) => String(html ?? '').replace(
|
|
30
|
+
/[✓✗➕]/g,
|
|
31
|
+
(glyph) => `<span class="vs-mark ${MARK_CLASS[glyph]}">${glyph}</span>`
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// ? Detect tables whose 2nd header matches the consumer-configured highlight column
|
|
35
|
+
const isHighlightColumnTable = (html, label) => {
|
|
36
|
+
if (!label) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const header = html.match(/<thead[\s\S]*?<\/thead>/i)?.[0] ?? html.match(/<tr[\s\S]*?<\/tr>/i)?.[0] ?? ''
|
|
41
|
+
const cells = [...header.matchAll(/<th[^>]*>([\s\S]*?)<\/th>/gi)].map(cell => cell[1].replace(/<[^>]*>/g, '').trim())
|
|
42
|
+
|
|
43
|
+
return cells[1] === label
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const transformToken = (token, highlightColumn) => {
|
|
47
|
+
if (!token || typeof token.content !== 'string') {
|
|
48
|
+
return token
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let content = token.content
|
|
52
|
+
if (MARKABLE_TAGS.has(token.tag) && MARK_PATTERN.test(content)) {
|
|
53
|
+
content = colorizeMarks(content)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ? Flag (not inject) — the <table> element is created by DPageTokens, not in token.content
|
|
57
|
+
const highlight = token.tag === 'table' && isHighlightColumnTable(token.content, highlightColumn)
|
|
58
|
+
|
|
59
|
+
if (content === token.content && !highlight) {
|
|
60
|
+
return token
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return highlight ? { ...token, content, highlight: true } : { ...token, content }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Apply a structured subpage template to a flat token stream (managed/strict).
|
|
68
|
+
*
|
|
69
|
+
* The template owns the section structure: each canonical section is rendered in
|
|
70
|
+
* the template's declared order, with the template's localized title overriding
|
|
71
|
+
* the markdown heading. Sections absent from the markdown are gracefully omitted.
|
|
72
|
+
* Markdown content before the first `h2` is kept as an intro. Unknown top-level
|
|
73
|
+
* `h2` sections are warned about and appended after the canonical sections so no
|
|
74
|
+
* authored content is lost.
|
|
75
|
+
*
|
|
76
|
+
* Freestyle templates (no `sections`) return the tokens unchanged.
|
|
77
|
+
*
|
|
78
|
+
* @param {Array} tokens - Tokenized page section source.
|
|
79
|
+
* @param {Object} template - Resolved template preset.
|
|
80
|
+
* @param {string} [locale] - Active locale for section titles.
|
|
81
|
+
* @param {Object} [options] - Render options.
|
|
82
|
+
* @param {string} [options.highlightColumn] - Header label whose comparison column is emphasized (consumer-provided, e.g. the project name).
|
|
83
|
+
* @returns {Array} Reordered/relabeled token stream.
|
|
84
|
+
*/
|
|
85
|
+
export function applyTemplateSections (tokens, template, locale = 'en-US', options = {}) {
|
|
86
|
+
const sections = template?.sections
|
|
87
|
+
const highlightColumn = options.highlightColumn
|
|
88
|
+
|
|
89
|
+
if (!Array.isArray(sections) || sections.length === 0) {
|
|
90
|
+
return Array.isArray(tokens) ? tokens : []
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ? Map every accepted slug (canonical key + each localized title) to its section index
|
|
94
|
+
const indexBySlug = new Map()
|
|
95
|
+
sections.forEach((section, index) => {
|
|
96
|
+
indexBySlug.set(normalizeKey(section.key), index)
|
|
97
|
+
|
|
98
|
+
for (const title of Object.values(section.title || {})) {
|
|
99
|
+
const slug = normalizeKey(title)
|
|
100
|
+
if (slug && !indexBySlug.has(slug)) {
|
|
101
|
+
indexBySlug.set(slug, index)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// ! Partition tokens into intro, canonical buckets and unknown sections
|
|
107
|
+
const intro = []
|
|
108
|
+
const buckets = sections.map(() => null)
|
|
109
|
+
const unknowns = []
|
|
110
|
+
let current = null
|
|
111
|
+
|
|
112
|
+
// @ Walk the flat stream, splitting at h2 boundaries
|
|
113
|
+
for (const token of Array.isArray(tokens) ? tokens : []) {
|
|
114
|
+
if (token?.tag === 'h2') {
|
|
115
|
+
const slug = normalizeKey(token.anchorId ?? token.content)
|
|
116
|
+
const index = indexBySlug.has(slug) ? indexBySlug.get(slug) : -1
|
|
117
|
+
|
|
118
|
+
if (index >= 0) {
|
|
119
|
+
if (buckets[index] === null) {
|
|
120
|
+
buckets[index] = []
|
|
121
|
+
}
|
|
122
|
+
current = { type: 'section', index }
|
|
123
|
+
} else {
|
|
124
|
+
const unknown = { token, body: [] }
|
|
125
|
+
unknowns.push(unknown)
|
|
126
|
+
current = { type: 'unknown', unknown }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (current === null) {
|
|
133
|
+
intro.push(token)
|
|
134
|
+
} else if (current.type === 'section') {
|
|
135
|
+
buckets[current.index].push(token)
|
|
136
|
+
} else {
|
|
137
|
+
current.unknown.body.push(token)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// @@ Rebuild in canonical order
|
|
142
|
+
const output = [...intro]
|
|
143
|
+
|
|
144
|
+
sections.forEach((section, index) => {
|
|
145
|
+
const body = buckets[index]
|
|
146
|
+
if (body === null) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
output.push({
|
|
151
|
+
tag: 'h2',
|
|
152
|
+
anchorId: section.key,
|
|
153
|
+
content: section.title?.[locale] || section.title?.['en-US'] || section.key,
|
|
154
|
+
icon: section.icon
|
|
155
|
+
})
|
|
156
|
+
output.push(...body)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// ? Append unknown sections (non-destructive) after warning
|
|
160
|
+
if (unknowns.length > 0) {
|
|
161
|
+
const allowed = sections.map(section => section.key)
|
|
162
|
+
|
|
163
|
+
for (const unknown of unknowns) {
|
|
164
|
+
warnUnknownSection(template.name, stripHtml(unknown.token.content), allowed)
|
|
165
|
+
output.push(unknown.token, ...unknown.body)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// : colorize comparison marks (✓/✗/➕) + highlight the configured column
|
|
170
|
+
return output.map(token => transformToken(token, highlightColumn))
|
|
171
|
+
}
|
package/src/css/app.sass
CHANGED
|
@@ -84,6 +84,20 @@ body.body--dark
|
|
|
84
84
|
color: #B19248
|
|
85
85
|
section
|
|
86
86
|
color: var(--q-light-in-dark-2)
|
|
87
|
+
// VS comparison marks (dark)
|
|
88
|
+
.vs-mark--yes
|
|
89
|
+
color: #3fb950
|
|
90
|
+
.vs-mark--no
|
|
91
|
+
color: #ff7b72
|
|
92
|
+
.vs-mark--dep
|
|
93
|
+
color: #d29922
|
|
94
|
+
// VS comparison: highlight the configured column (dark)
|
|
95
|
+
.d-table-wrapper--vs
|
|
96
|
+
th:nth-child(2),
|
|
97
|
+
td:nth-child(2)
|
|
98
|
+
background: rgba(193, 166, 103, 0.12)
|
|
99
|
+
border-left-color: rgba(193, 166, 103, 0.45)
|
|
100
|
+
border-right-color: rgba(193, 166, 103, 0.45)
|
|
87
101
|
// token
|
|
88
102
|
code:not(pre > code)
|
|
89
103
|
color: var(--q-primary-in-dark-bg) !important
|
|
@@ -102,7 +116,8 @@ body.body--dark
|
|
|
102
116
|
border-bottom: 1px dotted #1b4a6c
|
|
103
117
|
|
|
104
118
|
&.overview,
|
|
105
|
-
&.showcase
|
|
119
|
+
&.showcase,
|
|
120
|
+
&.vs
|
|
106
121
|
blockquote
|
|
107
122
|
border-left-color: #3d444d
|
|
108
123
|
background-color: #161b22 !important
|
|
@@ -157,6 +172,25 @@ body.body--dark
|
|
|
157
172
|
--big-play-button-background: #000 !important
|
|
158
173
|
--big-play-button-background-dark: #000 !important
|
|
159
174
|
|
|
175
|
+
// VS comparison marks
|
|
176
|
+
.vs-mark
|
|
177
|
+
font-weight: 700
|
|
178
|
+
.vs-mark--yes
|
|
179
|
+
color: #1a7f37
|
|
180
|
+
.vs-mark--no
|
|
181
|
+
color: #cf222e
|
|
182
|
+
.vs-mark--dep
|
|
183
|
+
color: #9a6700
|
|
184
|
+
|
|
185
|
+
// VS comparison: highlight the configured column
|
|
186
|
+
.d-table-wrapper--vs
|
|
187
|
+
th:nth-child(2),
|
|
188
|
+
td:nth-child(2)
|
|
189
|
+
background: rgba(105, 86, 43, 0.07)
|
|
190
|
+
font-weight: 600
|
|
191
|
+
border-left: 2px solid rgba(105, 86, 43, 0.35)
|
|
192
|
+
border-right: 2px solid rgba(105, 86, 43, 0.35)
|
|
193
|
+
|
|
160
194
|
h1, h2, h3, h4, h5, h6
|
|
161
195
|
font-weight: 600
|
|
162
196
|
padding: 6px
|
|
@@ -281,7 +315,8 @@ body.body--dark
|
|
|
281
315
|
transition: all 0.3s ease
|
|
282
316
|
|
|
283
317
|
&.overview,
|
|
284
|
-
&.showcase
|
|
318
|
+
&.showcase,
|
|
319
|
+
&.vs
|
|
285
320
|
blockquote
|
|
286
321
|
margin: 1.5em 0
|
|
287
322
|
padding: 0.85em 1.1em
|
package/src/i18n/helpers.js
CHANGED
|
@@ -448,11 +448,11 @@ export function buildMessages ({ langModules, mdModules, pages, books, pageEntri
|
|
|
448
448
|
// Overview
|
|
449
449
|
_.overview.source = load(topPage, path, 'overview', lang, sourceRoot)
|
|
450
450
|
// showcase
|
|
451
|
-
if (config.subpages?.showcase
|
|
451
|
+
if (config.subpages?.showcase) {
|
|
452
452
|
_.showcase.source = load(topPage, path, 'showcase', lang, sourceRoot)
|
|
453
453
|
}
|
|
454
454
|
// Vs
|
|
455
|
-
if (config.subpages?.vs
|
|
455
|
+
if (config.subpages?.vs) {
|
|
456
456
|
_.vs.source = load(topPage, path, 'vs', lang, sourceRoot)
|
|
457
457
|
}
|
|
458
458
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export const PAGE_TEMPLATE_FREESTYLE = 'freestyle'
|
|
2
|
+
export const PAGE_TEMPLATE_VS = 'vs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Subpage template presets.
|
|
6
|
+
*
|
|
7
|
+
* A template defines the fixed structure a subpage must follow. `freestyle`
|
|
8
|
+
* (the default) imposes no structure — the markdown renders exactly as written.
|
|
9
|
+
* Structured templates declare an ordered `sections` list; the renderer slots
|
|
10
|
+
* the markdown bodies into that fixed structure (canonical order, template-owned
|
|
11
|
+
* localized titles, graceful omission of absent sections).
|
|
12
|
+
*/
|
|
13
|
+
const PAGE_TEMPLATE_PRESETS = Object.freeze({
|
|
14
|
+
[PAGE_TEMPLATE_FREESTYLE]: Object.freeze({
|
|
15
|
+
name: PAGE_TEMPLATE_FREESTYLE,
|
|
16
|
+
sections: null
|
|
17
|
+
}),
|
|
18
|
+
[PAGE_TEMPLATE_VS]: Object.freeze({
|
|
19
|
+
name: PAGE_TEMPLATE_VS,
|
|
20
|
+
sections: Object.freeze([
|
|
21
|
+
Object.freeze({
|
|
22
|
+
key: 'features',
|
|
23
|
+
icon: 'checklist',
|
|
24
|
+
title: Object.freeze({ 'en-US': 'Features', 'pt-BR': 'Recursos' })
|
|
25
|
+
}),
|
|
26
|
+
Object.freeze({
|
|
27
|
+
key: 'performance',
|
|
28
|
+
icon: 'speed',
|
|
29
|
+
title: Object.freeze({ 'en-US': 'Performance', 'pt-BR': 'Desempenho' })
|
|
30
|
+
}),
|
|
31
|
+
Object.freeze({
|
|
32
|
+
key: 'security',
|
|
33
|
+
icon: 'security',
|
|
34
|
+
title: Object.freeze({ 'en-US': 'Security', 'pt-BR': 'Segurança' })
|
|
35
|
+
})
|
|
36
|
+
])
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const normalizeTemplateName = (value) => {
|
|
41
|
+
const normalized = String(value || '')
|
|
42
|
+
.trim()
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.replace(/[\s_-]+/g, '-')
|
|
45
|
+
|
|
46
|
+
if (normalized && Object.prototype.hasOwnProperty.call(PAGE_TEMPLATE_PRESETS, normalized)) {
|
|
47
|
+
return normalized
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return PAGE_TEMPLATE_FREESTYLE
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const firstDefined = (source, keys) => {
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
if (source?.[key] !== undefined) {
|
|
56
|
+
return source[key]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const toTemplateName = (source) => {
|
|
64
|
+
if (source === undefined || source === null) {
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof source === 'boolean') {
|
|
69
|
+
return source ? PAGE_TEMPLATE_FREESTYLE : undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (typeof source === 'string') {
|
|
73
|
+
return normalizeTemplateName(source)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (typeof source === 'object' && !Array.isArray(source)) {
|
|
77
|
+
const name = firstDefined(source, ['template', 'name', 'type', 'preset'])
|
|
78
|
+
return name === undefined ? undefined : normalizeTemplateName(name)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether a subpage config value enables that subpage.
|
|
86
|
+
*
|
|
87
|
+
* Accepts the boolean shorthand (`true`) or the object form (`{ template }`).
|
|
88
|
+
*/
|
|
89
|
+
export function isSubpageEnabled (value) {
|
|
90
|
+
return value === true || (typeof value === 'object' && value !== null && !Array.isArray(value))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a subpage template from one or more config sources.
|
|
95
|
+
*
|
|
96
|
+
* Sources may be a template name string, the subpage config value
|
|
97
|
+
* (`true` | `{ template }`), or undefined. Later sources win; the default is
|
|
98
|
+
* `freestyle`. Returns the resolved template preset object.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveSubpageTemplate (...sources) {
|
|
101
|
+
let name = PAGE_TEMPLATE_FREESTYLE
|
|
102
|
+
|
|
103
|
+
for (const source of sources) {
|
|
104
|
+
const resolved = toTemplateName(source)
|
|
105
|
+
if (resolved !== undefined) {
|
|
106
|
+
name = resolved
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return PAGE_TEMPLATE_PRESETS[name] || PAGE_TEMPLATE_PRESETS[PAGE_TEMPLATE_FREESTYLE]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get a template preset object by name, falling back to `freestyle`.
|
|
115
|
+
*/
|
|
116
|
+
export function getTemplate (name) {
|
|
117
|
+
return PAGE_TEMPLATE_PRESETS[normalizeTemplateName(name)] || PAGE_TEMPLATE_PRESETS[PAGE_TEMPLATE_FREESTYLE]
|
|
118
|
+
}
|
|
@@ -8,6 +8,43 @@ Under the hood, routed documentation uses `DSubpage` for this composition.
|
|
|
8
8
|
|
|
9
9
|
The implementation generates a deterministic numeric ID from the current route path using a hash function. This ID is passed to `DPageSection` to keep per-page renderer indexes stable across page navigations.
|
|
10
10
|
|
|
11
|
+
## Subpage templates
|
|
12
|
+
|
|
13
|
+
Subpages render free-form Markdown by default (the `freestyle` template). A subpage can instead opt into a **structured template** that owns a fixed set of sections, declared per subpage in the page registry:
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
// src/pages/manual.index.js
|
|
17
|
+
'/WPI/HTTP/HTTP_Server_CLI': {
|
|
18
|
+
config: {
|
|
19
|
+
subpages: {
|
|
20
|
+
vs: { template: 'vs' } // enable the `vs` subpage with the `vs` template
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The boolean shorthand (`showcase: true`) still means "enabled, freestyle". The object form `{ template: '<name>' }` selects a template.
|
|
27
|
+
|
|
28
|
+
### Built-in templates
|
|
29
|
+
|
|
30
|
+
| Template | Structure |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `freestyle` (default) | No fixed structure — Markdown renders as written |
|
|
33
|
+
| `vs` | Fixed comparison sections: Features, Performance, Security |
|
|
34
|
+
|
|
35
|
+
### Managed (strict) rendering
|
|
36
|
+
|
|
37
|
+
Structured templates are **managed**: the template owns the section titles, icons and order. In the Markdown you write one `##` heading per section (its slug must match a section key — or one of its localized titles); the renderer then:
|
|
38
|
+
|
|
39
|
+
- renders sections in the template's canonical order,
|
|
40
|
+
- replaces each heading with the template's localized title,
|
|
41
|
+
- gracefully omits sections you do not include (so a page with partial data just leaves them out),
|
|
42
|
+
- warns in the dev console about unknown headings and appends them after the canonical sections.
|
|
43
|
+
|
|
44
|
+
Every `vs` subpage therefore shares the same structure. Templates are resolved by `resolveSubpageTemplate()` (`src/page-template.js`) and applied by `applyTemplateSections()` (`src/components/page-template-sections.js`).
|
|
45
|
+
|
|
46
|
+
Inside comparison tables the engine colorizes the marks `✓` / `✗` / `➕` and highlights the column whose header matches your project's `branding.name`. The engine stays product-agnostic — the highlighted column is configured by the consumer (via `branding.name` in `docsector.config.js`), not hardcoded.
|
|
47
|
+
|
|
11
48
|
## Template
|
|
12
49
|
|
|
13
50
|
```html
|
|
@@ -16,7 +53,7 @@ The implementation generates a deterministic numeric ID from the current route p
|
|
|
16
53
|
<d-h1 :id="0" />
|
|
17
54
|
</header>
|
|
18
55
|
<main>
|
|
19
|
-
<d-page-section :id="id" />
|
|
56
|
+
<d-page-section :id="id" :template="template" />
|
|
20
57
|
</main>
|
|
21
58
|
</d-page>
|
|
22
59
|
```
|
|
@@ -8,6 +8,43 @@ Na implementação, a documentação roteada usa `DSubpage` para essa composiç
|
|
|
8
8
|
|
|
9
9
|
A implementação gera um ID numérico determinístico a partir do caminho da rota atual usando uma função hash. Esse ID é passado ao `DPageSection` para manter estáveis os índices internos do renderer em cada página.
|
|
10
10
|
|
|
11
|
+
## Templates de subpágina
|
|
12
|
+
|
|
13
|
+
As subpáginas renderizam Markdown livre por padrão (o template `freestyle`). Uma subpágina pode, em vez disso, optar por um **template estruturado** que controla um conjunto fixo de seções, declarado por subpágina no registro de páginas:
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
// src/pages/manual.index.js
|
|
17
|
+
'/WPI/HTTP/HTTP_Server_CLI': {
|
|
18
|
+
config: {
|
|
19
|
+
subpages: {
|
|
20
|
+
vs: { template: 'vs' } // habilita a subpágina `vs` com o template `vs`
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
O atalho booleano (`showcase: true`) continua significando "habilitada, freestyle". A forma de objeto `{ template: '<nome>' }` seleciona um template.
|
|
27
|
+
|
|
28
|
+
### Templates nativos
|
|
29
|
+
|
|
30
|
+
| Template | Estrutura |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `freestyle` (padrão) | Sem estrutura fixa — o Markdown renderiza como foi escrito |
|
|
33
|
+
| `vs` | Seções de comparação fixas: Recursos, Desempenho, Segurança |
|
|
34
|
+
|
|
35
|
+
### Renderização gerenciada (estrita)
|
|
36
|
+
|
|
37
|
+
Templates estruturados são **gerenciados**: o template controla os títulos, ícones e a ordem das seções. No Markdown você escreve um heading `##` por seção (cujo slug deve corresponder à chave da seção — ou a um de seus títulos localizados); o renderer então:
|
|
38
|
+
|
|
39
|
+
- renderiza as seções na ordem canônica do template,
|
|
40
|
+
- substitui cada heading pelo título localizado do template,
|
|
41
|
+
- omite com elegância as seções que você não inclui (uma página com dados parciais simplesmente as deixa de fora),
|
|
42
|
+
- avisa no console de dev sobre headings desconhecidos e os anexa após as seções canônicas.
|
|
43
|
+
|
|
44
|
+
Toda subpágina `vs` compartilha, portanto, a mesma estrutura. Os templates são resolvidos por `resolveSubpageTemplate()` (`src/page-template.js`) e aplicados por `applyTemplateSections()` (`src/components/page-template-sections.js`).
|
|
45
|
+
|
|
46
|
+
Dentro das tabelas de comparação o engine coloriza as marcas `✓` / `✗` / `➕` e destaca a coluna cujo header corresponde ao `branding.name` do seu projeto. O engine permanece agnóstico ao produto — a coluna destacada é configurada pelo consumidor (via `branding.name` no `docsector.config.js`), não fica hardcoded.
|
|
47
|
+
|
|
11
48
|
## Template
|
|
12
49
|
|
|
13
50
|
```html
|
|
@@ -16,7 +53,7 @@ A implementação gera um ID numérico determinístico a partir do caminho da ro
|
|
|
16
53
|
<d-h1 :id="0" />
|
|
17
54
|
</header>
|
|
18
55
|
<main>
|
|
19
|
-
<d-page-section :id="id" />
|
|
56
|
+
<d-page-section :id="id" :template="template" />
|
|
20
57
|
</main>
|
|
21
58
|
</d-page>
|
|
22
59
|
```
|
package/src/router/routes.js
CHANGED
|
@@ -2,6 +2,7 @@ import { pageEntries, versions } from 'virtual:docsector-books'
|
|
|
2
2
|
import boot from 'pages/boot'
|
|
3
3
|
import docsectorConfig from 'docsector.config.js'
|
|
4
4
|
import { resolveHomePageLayout, resolvePageLayout } from '../page-layout'
|
|
5
|
+
import { isSubpageEnabled, resolveSubpageTemplate } from '../page-template'
|
|
5
6
|
|
|
6
7
|
const normalizeInternalLink = (linkTo) => {
|
|
7
8
|
const normalized = String(linkTo || '').trim()
|
|
@@ -85,8 +86,13 @@ for (const entry of pageEntries || []) {
|
|
|
85
86
|
const config = rawConfig || {}
|
|
86
87
|
const menu = (typeof config.menu === 'object' && config.menu !== null) ? config.menu : {}
|
|
87
88
|
const subpages = {
|
|
88
|
-
showcase: config?.subpages?.showcase
|
|
89
|
-
vs: config?.subpages?.vs
|
|
89
|
+
showcase: isSubpageEnabled(config?.subpages?.showcase),
|
|
90
|
+
vs: isSubpageEnabled(config?.subpages?.vs)
|
|
91
|
+
}
|
|
92
|
+
const subpageTemplates = {
|
|
93
|
+
overview: resolveSubpageTemplate(config?.subpages?.overview).name,
|
|
94
|
+
showcase: resolveSubpageTemplate(config?.subpages?.showcase).name,
|
|
95
|
+
vs: resolveSubpageTemplate(config?.subpages?.vs).name
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
const topPage = config.book ?? config.type ?? entry?.book ?? 'manual'
|
|
@@ -159,6 +165,7 @@ for (const entry of pageEntries || []) {
|
|
|
159
165
|
pageVersion,
|
|
160
166
|
menu,
|
|
161
167
|
subpages,
|
|
168
|
+
subpageTemplates,
|
|
162
169
|
data: page.data,
|
|
163
170
|
book: topPage,
|
|
164
171
|
// legacy compatibility
|