@uniweb/build 0.11.2 → 0.11.4
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/package.json +5 -4
- package/src/content/index.js +30 -5
- package/src/site/collection-processor.js +61 -11
- package/src/site/content-collector.js +35 -20
- package/src/site/xref-registry.js +153 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -51,14 +51,15 @@
|
|
|
51
51
|
"jest": "^29.7.0"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
+
"@citestyle/bibtex": "^1.0.0",
|
|
54
55
|
"esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0",
|
|
55
56
|
"js-yaml": "^4.1.0",
|
|
56
57
|
"sharp": "^0.33.2",
|
|
57
58
|
"@uniweb/theming": "0.1.3"
|
|
58
59
|
},
|
|
59
60
|
"optionalDependencies": {
|
|
60
|
-
"@uniweb/
|
|
61
|
-
"@uniweb/
|
|
61
|
+
"@uniweb/content-reader": "1.1.7",
|
|
62
|
+
"@uniweb/runtime": "0.8.6",
|
|
62
63
|
"@uniweb/schemas": "0.2.1"
|
|
63
64
|
},
|
|
64
65
|
"peerDependencies": {
|
|
@@ -68,7 +69,7 @@
|
|
|
68
69
|
"@tailwindcss/vite": "^4.0.0",
|
|
69
70
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
70
71
|
"vite-plugin-svgr": "^4.0.0",
|
|
71
|
-
"@uniweb/core": "0.7.
|
|
72
|
+
"@uniweb/core": "0.7.5"
|
|
72
73
|
},
|
|
73
74
|
"peerDependenciesMeta": {
|
|
74
75
|
"vite": {
|
package/src/content/index.js
CHANGED
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Clean Content Entry Point
|
|
3
3
|
*
|
|
4
|
-
* Exposes
|
|
5
|
-
* (Node builtins, js-yaml, @uniweb/theming). No Vite,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Exposes content-collection helpers and their clean dependency chain
|
|
5
|
+
* (Node builtins, js-yaml, @uniweb/theming, @uniweb/core). No Vite,
|
|
6
|
+
* sharp, or React. Used by Studio's sidecar (Bun-compiled binary) and
|
|
7
|
+
* by @uniweb/unipress (CLI compile path) where those heavy peer/native
|
|
8
|
+
* dependencies aren't available.
|
|
9
9
|
*
|
|
10
10
|
* @module @uniweb/build/content
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
export { collectSiteContent } from '../site/content-collector.js'
|
|
14
|
+
|
|
15
|
+
// Collections + fetch resolution. Pure functions on the clean dep
|
|
16
|
+
// chain — re-exported here so headless callers (unipress, sidecars)
|
|
17
|
+
// can resolve `collections:` declarations without importing
|
|
18
|
+
// `@uniweb/build/site` (which pulls Vite via its plugin index).
|
|
19
|
+
export {
|
|
20
|
+
processCollections,
|
|
21
|
+
writeCollectionFiles,
|
|
22
|
+
getCollectionLastModified,
|
|
23
|
+
} from '../site/collection-processor.js'
|
|
24
|
+
export {
|
|
25
|
+
parseFetchConfig,
|
|
26
|
+
executeFetch,
|
|
27
|
+
applyFilter,
|
|
28
|
+
applySort,
|
|
29
|
+
applyPostProcessing,
|
|
30
|
+
mergeDataIntoContent,
|
|
31
|
+
singularize,
|
|
32
|
+
} from '../site/data-fetcher.js'
|
|
33
|
+
|
|
34
|
+
// Cross-reference registry. Walks the document tree, finds every
|
|
35
|
+
// block-level element with a {#id} attribute, registers the id with
|
|
36
|
+
// its inferred kind + counter. Consumed by the framework's <Ref>
|
|
37
|
+
// component to render `[#id]` cross-references.
|
|
38
|
+
export { buildXrefRegistry } from '../site/xref-registry.js'
|
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
* Collections are defined in site.yml and processed at build time.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
8
|
-
* - Discovers markdown (.md), data (.yml/.yaml),
|
|
9
|
-
*
|
|
8
|
+
* - Discovers markdown (.md), data (.yml/.yaml), JSON (.json), and BibTeX (.bib)
|
|
9
|
+
* files in collection folders
|
|
10
|
+
* - Parses frontmatter for metadata (markdown), full YAML or JSON (data items),
|
|
11
|
+
* or BibTeX → CSL-JSON (bibliography items)
|
|
12
|
+
* - Pure-data formats (YAML, JSON, BibTeX) accept either one record per file
|
|
13
|
+
* (mapping at the top, slug from filename) or many records per file (array
|
|
14
|
+
* at the top, each item carries its own slug; BibTeX always produces an
|
|
15
|
+
* array, with the cite key as slug). Multiple files in the same folder
|
|
16
|
+
* merge — the loader flattens one level after collecting them.
|
|
10
17
|
* - Converts markdown body to ProseMirror JSON
|
|
11
18
|
* - Supports filtering, sorting, and limiting
|
|
12
19
|
* - Auto-generates excerpts and extracts first images (markdown items only)
|
|
@@ -29,6 +36,7 @@ import { readFile, readdir, stat, writeFile, mkdir, copyFile } from 'node:fs/pro
|
|
|
29
36
|
import { join, basename, extname, dirname, relative, resolve } from 'node:path'
|
|
30
37
|
import { existsSync } from 'node:fs'
|
|
31
38
|
import yaml from 'js-yaml'
|
|
39
|
+
import { parseBibtex } from '@citestyle/bibtex'
|
|
32
40
|
import { applyFilter, applySort } from './data-fetcher.js'
|
|
33
41
|
import { resolveAssetPath, walkContentAssets, isLocalAssetPath } from './assets.js'
|
|
34
42
|
|
|
@@ -393,21 +401,35 @@ async function processDataItemAssets(data, itemPath, siteRoot, collectionName, b
|
|
|
393
401
|
* Process a single data item from a YAML file
|
|
394
402
|
*
|
|
395
403
|
* YAML items are pure data — no ProseMirror conversion, no body, no excerpt,
|
|
396
|
-
* no image extraction, no lastModified.
|
|
404
|
+
* no image extraction, no lastModified.
|
|
405
|
+
*
|
|
406
|
+
* A YAML file containing a top-level array returns all items (single-file
|
|
407
|
+
* collection); each item must carry its own `slug`. A YAML file containing
|
|
408
|
+
* a mapping returns a single item with `slug` derived from the filename.
|
|
409
|
+
* Mirrors `processJsonItem` for parity across pure-data formats.
|
|
397
410
|
*
|
|
398
411
|
* @param {string} dir - Collection directory path
|
|
399
412
|
* @param {string} filename - YAML filename (.yml or .yaml)
|
|
400
|
-
* @returns {Promise<Object|null>} Processed item or null if unpublished
|
|
413
|
+
* @returns {Promise<Object|Array|null>} Processed item(s) or null if unpublished
|
|
401
414
|
*/
|
|
402
415
|
async function processDataItem(dir, filename, siteRoot, collectionName, basePath) {
|
|
403
416
|
const filepath = join(dir, filename)
|
|
404
417
|
const raw = await readFile(filepath, 'utf-8')
|
|
405
|
-
const slug = basename(filename, extname(filename))
|
|
406
418
|
const data = yaml.load(raw) || {}
|
|
407
419
|
|
|
408
|
-
//
|
|
409
|
-
if (data
|
|
420
|
+
// Array → multiple items (single-file collection)
|
|
421
|
+
if (Array.isArray(data)) {
|
|
422
|
+
for (const item of data) {
|
|
423
|
+
if (item && typeof item === 'object') {
|
|
424
|
+
await processDataItemAssets(item, filepath, siteRoot, collectionName, basePath)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return data
|
|
428
|
+
}
|
|
410
429
|
|
|
430
|
+
// Mapping → single item
|
|
431
|
+
if (data.published === false) return null
|
|
432
|
+
const slug = basename(filename, extname(filename))
|
|
411
433
|
const item = { slug, ...data }
|
|
412
434
|
await processDataItemAssets(item, filepath, siteRoot, collectionName, basePath)
|
|
413
435
|
return item
|
|
@@ -447,6 +469,29 @@ async function processJsonItem(dir, filename, siteRoot, collectionName, basePath
|
|
|
447
469
|
return item
|
|
448
470
|
}
|
|
449
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Process a single BibTeX file into an array of CSL-JSON bibliography items.
|
|
474
|
+
*
|
|
475
|
+
* Each `@entry{key, ...}` becomes one item. The BibTeX cite key is preserved
|
|
476
|
+
* as `id` (CSL-JSON convention) and copied to `slug` so per-record file
|
|
477
|
+
* emission and runtime lookups behave the same as for other formats.
|
|
478
|
+
*
|
|
479
|
+
* No asset processing — bibliography records reference URLs and DOIs, not
|
|
480
|
+
* local files.
|
|
481
|
+
*
|
|
482
|
+
* @param {string} dir - Collection directory path
|
|
483
|
+
* @param {string} filename - BibTeX filename (.bib)
|
|
484
|
+
* @returns {Promise<Array<Object>>} Array of CSL-JSON items, each with `slug`
|
|
485
|
+
*/
|
|
486
|
+
async function processBibtexItem(dir, filename) {
|
|
487
|
+
const filepath = join(dir, filename)
|
|
488
|
+
const raw = await readFile(filepath, 'utf-8')
|
|
489
|
+
const entries = parseBibtex(raw)
|
|
490
|
+
return entries
|
|
491
|
+
.filter(entry => entry && entry.id)
|
|
492
|
+
.map(entry => ({ slug: entry.id, ...entry }))
|
|
493
|
+
}
|
|
494
|
+
|
|
450
495
|
/**
|
|
451
496
|
* Process a single content item from a markdown file
|
|
452
497
|
*
|
|
@@ -512,12 +557,16 @@ async function collectItems(siteDir, config, collectionsBase, basePath) {
|
|
|
512
557
|
const files = await readdir(collectionDir)
|
|
513
558
|
const itemFiles = files.filter(f =>
|
|
514
559
|
!f.startsWith('_') &&
|
|
515
|
-
(f.endsWith('.md') || f.endsWith('.yml') || f.endsWith('.yaml') || f.endsWith('.json'))
|
|
560
|
+
(f.endsWith('.md') || f.endsWith('.yml') || f.endsWith('.yaml') || f.endsWith('.json') || f.endsWith('.bib'))
|
|
516
561
|
)
|
|
517
562
|
|
|
518
|
-
// Process all collection files (markdown → content items, YAML/JSON → data
|
|
563
|
+
// Process all collection files (markdown → content items, YAML/JSON → data
|
|
564
|
+
// items, BibTeX → CSL-JSON bibliography items).
|
|
519
565
|
let items = await Promise.all(
|
|
520
566
|
itemFiles.map(file => {
|
|
567
|
+
if (file.endsWith('.bib')) {
|
|
568
|
+
return processBibtexItem(collectionDir, file)
|
|
569
|
+
}
|
|
521
570
|
if (file.endsWith('.json')) {
|
|
522
571
|
return processJsonItem(collectionDir, file, siteDir, config.name, basePath)
|
|
523
572
|
}
|
|
@@ -528,7 +577,8 @@ async function collectItems(siteDir, config, collectionsBase, basePath) {
|
|
|
528
577
|
})
|
|
529
578
|
)
|
|
530
579
|
|
|
531
|
-
// Flatten
|
|
580
|
+
// Flatten one level: array-form YAML/JSON files and every .bib file
|
|
581
|
+
// contribute their entries individually.
|
|
532
582
|
items = items.flat()
|
|
533
583
|
|
|
534
584
|
// Filter out nulls (unpublished items)
|
|
@@ -673,7 +723,7 @@ export async function getCollectionLastModified(siteDir, config) {
|
|
|
673
723
|
const files = await readdir(collectionDir)
|
|
674
724
|
const itemFiles = files.filter(f =>
|
|
675
725
|
!f.startsWith('_') &&
|
|
676
|
-
(f.endsWith('.md') || f.endsWith('.yml') || f.endsWith('.yaml') || f.endsWith('.json'))
|
|
726
|
+
(f.endsWith('.md') || f.endsWith('.yml') || f.endsWith('.yaml') || f.endsWith('.json') || f.endsWith('.bib'))
|
|
677
727
|
)
|
|
678
728
|
|
|
679
729
|
let lastModified = null
|
|
@@ -181,13 +181,19 @@ async function readYamlFile(filePath) {
|
|
|
181
181
|
/**
|
|
182
182
|
* Extract inset references from a ProseMirror document.
|
|
183
183
|
*
|
|
184
|
-
* Walks
|
|
185
|
-
* for `{params}`
|
|
186
|
-
*
|
|
187
|
-
*
|
|
184
|
+
* Walks the document recursively for `inset_ref` nodes (produced by
|
|
185
|
+
* content-reader for the `{params}` /
|
|
186
|
+
* `[text](@ComponentName){params}` / `[@key]{params}` forms). Each ref
|
|
187
|
+
* is removed and replaced in-place with an `inset_placeholder` node
|
|
188
|
+
* carrying a unique refId. The extracted refs are returned as an array.
|
|
189
|
+
*
|
|
190
|
+
* Inline insets (mid-paragraph) are kept as inline placeholders so the
|
|
191
|
+
* paragraph's text flow is preserved; block-level insets (own line)
|
|
192
|
+
* stay at the document root. Both share the same refId/getInset(refId)
|
|
193
|
+
* lookup machinery — only the position differs.
|
|
188
194
|
*
|
|
189
195
|
* @param {Object} doc - ProseMirror document (mutated in place)
|
|
190
|
-
* @returns {Array} Array of { refId, type, params, description }
|
|
196
|
+
* @returns {Array} Array of { refId, type, params, description, embedKind }
|
|
191
197
|
*/
|
|
192
198
|
function extractInsets(doc) {
|
|
193
199
|
if (!doc?.content || !Array.isArray(doc.content)) return []
|
|
@@ -195,25 +201,34 @@ function extractInsets(doc) {
|
|
|
195
201
|
const insets = []
|
|
196
202
|
let refIndex = 0
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
204
|
+
function visit(nodes) {
|
|
205
|
+
if (!Array.isArray(nodes)) return
|
|
206
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
207
|
+
const node = nodes[i]
|
|
208
|
+
if (!node) continue
|
|
209
|
+
if (node.type === 'inset_ref') {
|
|
210
|
+
const { component, alt, embedKind, ...params } = node.attrs || {}
|
|
211
|
+
const refId = `inset_${refIndex++}`
|
|
212
|
+
insets.push({
|
|
213
|
+
refId,
|
|
214
|
+
type: component,
|
|
215
|
+
embedKind: embedKind || 'visual',
|
|
216
|
+
params: Object.keys(params).length > 0 ? params : {},
|
|
217
|
+
title: alt || null,
|
|
218
|
+
})
|
|
219
|
+
nodes[i] = {
|
|
220
|
+
type: 'inset_placeholder',
|
|
221
|
+
attrs: { refId, embedKind: embedKind || 'visual' },
|
|
222
|
+
}
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(node.content)) {
|
|
226
|
+
visit(node.content)
|
|
213
227
|
}
|
|
214
228
|
}
|
|
215
229
|
}
|
|
216
230
|
|
|
231
|
+
visit(doc.content)
|
|
217
232
|
return insets
|
|
218
233
|
}
|
|
219
234
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-reference registry — populated at content-collection time.
|
|
3
|
+
*
|
|
4
|
+
* Walks the parsed document tree to find every block-level element
|
|
5
|
+
* carrying a {#id} attribute, infers the element's kind from its node
|
|
6
|
+
* type, and records the entry with a per-kind counter. The result
|
|
7
|
+
* attaches to siteContent.xref and is consumed by the framework's
|
|
8
|
+
* <Ref> component to render `[#id]` cross-references.
|
|
9
|
+
*
|
|
10
|
+
* Built-in kinds:
|
|
11
|
+
* - heading → 'section' (hierarchical counter: 1, 1.1, 1.1.1, 2, …)
|
|
12
|
+
* - image → 'figure' (flat arabic counter)
|
|
13
|
+
* - math_display → 'equation' (flat arabic counter)
|
|
14
|
+
* - table → 'table' (flat arabic counter)
|
|
15
|
+
*
|
|
16
|
+
* Foundation extensions: foundations may declare additional kinds via
|
|
17
|
+
* `foundation.xref.kinds`. The id-collection pass uses the foundation's
|
|
18
|
+
* declared `prefix` (e.g. `thm` → `theorem`) to classify ids that don't
|
|
19
|
+
* land on a built-in element type.
|
|
20
|
+
*
|
|
21
|
+
* The registry is per-document, not per-page: counters span the whole
|
|
22
|
+
* document so authors get consistent numbering even when their document
|
|
23
|
+
* spans multiple pages. (For page-local counter resets, foundations can
|
|
24
|
+
* declare `resetOn: 'chapter'` on a kind — applied during render, not
|
|
25
|
+
* here.)
|
|
26
|
+
*
|
|
27
|
+
* Output shape:
|
|
28
|
+
* {
|
|
29
|
+
* entries: {
|
|
30
|
+
* <id>: {
|
|
31
|
+
* id, // the id itself
|
|
32
|
+
* kind, // 'figure' | 'equation' | 'section' | 'table' | <foundation-declared>
|
|
33
|
+
* counter, // number for flat kinds, dotted string for hierarchical
|
|
34
|
+
* counterText, // displayable counter ('3', '3.2')
|
|
35
|
+
* sourcePath, // page route the id was declared on (for back-refs)
|
|
36
|
+
* },
|
|
37
|
+
* },
|
|
38
|
+
* }
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const KIND_BY_TYPE = {
|
|
42
|
+
heading: 'section',
|
|
43
|
+
image: 'figure',
|
|
44
|
+
math_display: 'equation',
|
|
45
|
+
table: 'table',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildXrefRegistry(siteContent, options = {}) {
|
|
49
|
+
const { foundationKinds = {}, onWarn = (msg) => console.warn(msg) } = options
|
|
50
|
+
|
|
51
|
+
const entries = {}
|
|
52
|
+
const flatCounters = {} // kind -> next integer counter (figure, equation, table, foundation-declared kinds)
|
|
53
|
+
const sectionStack = [] // hierarchical counters per heading level
|
|
54
|
+
|
|
55
|
+
// Index foundation prefixes for id-prefix inference.
|
|
56
|
+
const prefixToKind = {}
|
|
57
|
+
for (const [kind, meta] of Object.entries(foundationKinds)) {
|
|
58
|
+
if (meta?.prefix) prefixToKind[meta.prefix] = kind
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function nextFlat(kind) {
|
|
62
|
+
flatCounters[kind] = (flatCounters[kind] || 0) + 1
|
|
63
|
+
return flatCounters[kind]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Track the shallowest heading depth seen — chapter content commonly
|
|
67
|
+
// starts at h2 (chapter title is rendered separately as h1 by the
|
|
68
|
+
// foundation), so the first encountered level becomes the document's
|
|
69
|
+
// top counter level. Re-anchoring: if a later heading appears at a
|
|
70
|
+
// shallower level than `topLevel`, drop topLevel down to match.
|
|
71
|
+
let topLevel = null
|
|
72
|
+
|
|
73
|
+
function nextHierarchical(level) {
|
|
74
|
+
if (topLevel == null || level < topLevel) topLevel = level
|
|
75
|
+
const depth = level - topLevel + 1 // 1-based depth from the document's top level
|
|
76
|
+
|
|
77
|
+
// Trim deeper levels when surfacing back to a shallower heading.
|
|
78
|
+
while (sectionStack.length >= depth) sectionStack.pop()
|
|
79
|
+
while (sectionStack.length < depth - 1) sectionStack.push(1) // implicit parents start at 1
|
|
80
|
+
sectionStack.push((sectionStack[depth - 1] || 0) + 1)
|
|
81
|
+
sectionStack.length = depth
|
|
82
|
+
return sectionStack.slice().join('.')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function inferKind(node) {
|
|
86
|
+
// 1. Node-type-based: built-ins.
|
|
87
|
+
const builtin = KIND_BY_TYPE[node.type]
|
|
88
|
+
if (builtin) return builtin
|
|
89
|
+
// 2. Explicit kind attribute on the node ({.kind} or kind=…).
|
|
90
|
+
if (node.attrs?.kind && (KIND_BY_TYPE[node.attrs.kind] || foundationKinds[node.attrs.kind])) {
|
|
91
|
+
return node.attrs.kind
|
|
92
|
+
}
|
|
93
|
+
// 3. Foundation-declared prefix on the id.
|
|
94
|
+
const id = node.attrs?.id
|
|
95
|
+
if (id && id.includes('-')) {
|
|
96
|
+
const prefix = id.slice(0, id.indexOf('-'))
|
|
97
|
+
if (prefixToKind[prefix]) return prefixToKind[prefix]
|
|
98
|
+
}
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function visit(node, sourcePath) {
|
|
103
|
+
if (!node || typeof node !== 'object') return
|
|
104
|
+
|
|
105
|
+
// Headings get a hierarchical counter regardless of whether they
|
|
106
|
+
// carry an id — the {#id} just labels them; the counter itself is
|
|
107
|
+
// assigned by tree position.
|
|
108
|
+
let counter = null
|
|
109
|
+
let counterText = null
|
|
110
|
+
if (node.type === 'heading') {
|
|
111
|
+
const level = Math.max(1, Math.min(6, node.attrs?.level || 1))
|
|
112
|
+
counterText = nextHierarchical(level)
|
|
113
|
+
counter = counterText
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const id = node.attrs?.id
|
|
117
|
+
if (id) {
|
|
118
|
+
const kind = inferKind(node)
|
|
119
|
+
if (!kind) {
|
|
120
|
+
onWarn(`[xref] {#${id}} on unrecognized element type "${node.type}" — ignored`)
|
|
121
|
+
} else if (entries[id]) {
|
|
122
|
+
onWarn(`[xref] duplicate id "${id}" — keeping first registration`)
|
|
123
|
+
} else {
|
|
124
|
+
if (counter == null) {
|
|
125
|
+
counter = nextFlat(kind)
|
|
126
|
+
counterText = String(counter)
|
|
127
|
+
}
|
|
128
|
+
entries[id] = { id, kind, counter, counterText, sourcePath: sourcePath || '' }
|
|
129
|
+
}
|
|
130
|
+
} else if (node.type === 'heading') {
|
|
131
|
+
// Heading without an id still advances the counter — but we don't
|
|
132
|
+
// register it (no way to reference it). The advance is needed so
|
|
133
|
+
// that the *next* labeled heading gets the right hierarchical
|
|
134
|
+
// counter.
|
|
135
|
+
// counter already computed above; just no entry.
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (Array.isArray(node.content)) {
|
|
139
|
+
for (const child of node.content) visit(child, sourcePath)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const page of siteContent.pages || []) {
|
|
144
|
+
for (const section of page.sections || []) {
|
|
145
|
+
const content = section.content
|
|
146
|
+
if (content?.type === 'doc' && Array.isArray(content.content)) {
|
|
147
|
+
for (const child of content.content) visit(child, page.route)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { entries }
|
|
153
|
+
}
|