@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 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.6.0'
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`
@@ -88,10 +88,10 @@ export default {
88
88
  matchThreshold: 0.4,
89
89
  contextExpansion: 1,
90
90
  queryRewrite: {
91
- enabled: true
91
+ enabled: false
92
92
  },
93
93
  reranking: {
94
- enabled: true,
94
+ enabled: false,
95
95
  model: '@cf/baai/bge-reranker-base',
96
96
  matchThreshold: 0.4
97
97
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "4.6.0",
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
- return tokenizePageSectionSource(t(pageValueI18nPath(absolute, 'source')))
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) => {
@@ -88,6 +88,7 @@ import DBlockApi from './DBlockApi.vue'
88
88
  <div
89
89
  v-else-if="token.tag === 'table'"
90
90
  class="d-table-wrapper"
91
+ :class="{ 'd-table-wrapper--vs': token.highlight }"
91
92
  >
92
93
  <d-page-rich-content
93
94
  tag="table"
@@ -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
@@ -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 === true) {
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 === true) {
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': &#123;
18
+ config: &#123;
19
+ subpages: &#123;
20
+ vs: &#123; template: 'vs' &#125; // enable the `vs` subpage with the `vs` template
21
+ &#125;
22
+ &#125;
23
+ &#125;
24
+ ```
25
+
26
+ The boolean shorthand (`showcase: true`) still means "enabled, freestyle". The object form `&#123; template: '<name>' &#125;` 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': &#123;
18
+ config: &#123;
19
+ subpages: &#123;
20
+ vs: &#123; template: 'vs' &#125; // habilita a subpágina `vs` com o template `vs`
21
+ &#125;
22
+ &#125;
23
+ &#125;
24
+ ```
25
+
26
+ O atalho booleano (`showcase: true`) continua significando "habilitada, freestyle". A forma de objeto `&#123; template: '<nome>' &#125;` 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
  ```
@@ -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 === true,
89
- vs: config?.subpages?.vs === true
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