@heroiclands/package-build 22.1.1 → 22.3.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/CHANGELOG.md +18 -0
- package/CONTENT.md +27 -32
- package/bin/content-build.mjs +0 -3
- package/bin/package-build.mjs +9 -26
- package/config.mjs +7 -2
- package/content-config.mjs +73 -41
- package/docs/api.md +26 -25
- package/docs/commands.md +22 -22
- package/docs/configuration.md +78 -46
- package/docs/content-format.md +36 -6
- package/engine/foundry-entries.mjs +12 -1
- package/engine/frontmatter-lint.mjs +2 -3
- package/engine/helpers.mjs +4 -1
- package/engine/ids.mjs +12 -0
- package/engine/note-claims.mjs +21 -0
- package/engine/pack-config.mjs +14 -1
- package/engine/pack-router.mjs +82 -5
- package/engine/pdf-build.mjs +2 -2
- package/engine/retired-fields.mjs +2 -3
- package/engine/schema-check.mjs +6 -6
- package/engine/site-build.mjs +59 -218
- package/engine/site-config.mjs +22 -22
- package/engine/site-index.mjs +18 -62
- package/engine/web-wikilinks.mjs +2 -16
- package/engine/wikilinks.mjs +35 -14
- package/manifest.mjs +16 -10
- package/package.json +1 -1
- package/release.mjs +41 -8
- package/sohl/kb-passes.mjs +14 -85
- package/types/config.d.mts +17 -0
- package/types/content-config.d.mts +6 -6
- package/types/engine/ids.d.mts +11 -0
- package/types/engine/pack-router.d.mts +20 -2
- package/types/engine/schema-check.d.mts +5 -5
- package/types/engine/site-build.d.mts +12 -62
- package/types/engine/site-config.d.mts +6 -8
- package/types/engine/site-index.d.mts +3 -28
- package/types/engine/web-wikilinks.d.mts +1 -1
- package/types/engine/wikilinks.d.mts +6 -3
- package/types/manifest.d.mts +5 -3
- package/types/release.d.mts +7 -6
- package/types/sohl/kb-passes.d.mts +5 -36
package/engine/pdf-build.mjs
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* ## `publish.site` is the switch, and it is the only switch
|
|
36
36
|
*
|
|
37
37
|
* `homepage` mode fences the content surfaces off: the tree is not walked and
|
|
38
|
-
* `sections
|
|
38
|
+
* `sections` and `landing` emit nothing however they are declared. A
|
|
39
39
|
* PDF of the content tree is a content surface by any reading — arguably the
|
|
40
40
|
* most portable one there is — so it is fenced on exactly the same terms, by
|
|
41
41
|
* asking the same {@link module:content-config.publishesContentPages} the
|
|
@@ -481,7 +481,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
481
481
|
* @returns {string} Typst markup.
|
|
482
482
|
*/
|
|
483
483
|
const renderPage = (page, headingOffset, anchorPrefix) => {
|
|
484
|
-
const src = page.relPath ?? page.
|
|
484
|
+
const src = page.relPath ?? page.base;
|
|
485
485
|
const wikiErrors = [];
|
|
486
486
|
const { markdown, errors } = expandContentTables(page.body, {
|
|
487
487
|
docs: universe.get(page.pkg) ?? [],
|
|
@@ -326,9 +326,8 @@ export function assertNoSectionField(fm, { file, absPath } = {}) {
|
|
|
326
326
|
if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "section")) return;
|
|
327
327
|
|
|
328
328
|
const err = new Error(`${sectionRetiredMessage(file)}.`);
|
|
329
|
-
// Anchored at column 1: `
|
|
330
|
-
//
|
|
331
|
-
// this field — a finding about the top-level one must not open on it.
|
|
329
|
+
// Anchored at column 1: a nested `section:` inside some other block is
|
|
330
|
+
// not this field — a finding about the top-level one must not open on it.
|
|
332
331
|
const position = locateFrontmatterKey(absPath, "section", undefined, { topLevel: true });
|
|
333
332
|
if (position) err.position = position;
|
|
334
333
|
throw err;
|
package/engine/schema-check.mjs
CHANGED
|
@@ -466,8 +466,8 @@ export function compareEmittedSystem({
|
|
|
466
466
|
* Two places to find it, because a system checks itself against source it owns
|
|
467
467
|
* while a module checks against a dependency it fetched:
|
|
468
468
|
*
|
|
469
|
-
* - **A system**: its own `schema.json`, generated from its `src/`
|
|
470
|
-
*
|
|
469
|
+
* - **A system**: its own `schema.json`, generated from its `src/` into
|
|
470
|
+
* `build/` by `package-build schema`.
|
|
471
471
|
* - **A module**: the copy cached by `content-build deps fetch`, from the
|
|
472
472
|
* archive of the version it pins — which is what makes the comparison happen
|
|
473
473
|
* at `verified` rather than against whatever the system's `main` holds today.
|
|
@@ -505,7 +505,7 @@ export function resolveSchemaArtifact(config, system = undefined) {
|
|
|
505
505
|
|
|
506
506
|
// The system checking itself, against the schema its own build published.
|
|
507
507
|
if (config.packageKind === "systems" && config.foundryPackage === systemId) {
|
|
508
|
-
const own = path.join(config.rootDir, SCHEMA_ARTIFACT_FILE);
|
|
508
|
+
const own = path.join(config.rootDir, "build", SCHEMA_ARTIFACT_FILE);
|
|
509
509
|
return fs.existsSync(own) ? read(own) : null;
|
|
510
510
|
}
|
|
511
511
|
|
|
@@ -683,9 +683,9 @@ export function checkAuthoredSystemData(
|
|
|
683
683
|
* subtype declares.
|
|
684
684
|
*
|
|
685
685
|
* The build-time face of {@link compareEmittedSystem}: it resolves the schema
|
|
686
|
-
* the way every other check here does — the system's own
|
|
687
|
-
* the cached one from the release a module pins — and attaches the message
|
|
688
|
-
* reader sees.
|
|
686
|
+
* the way every other check here does — the system's own published artifact,
|
|
687
|
+
* or the cached one from the release a module pins — and attaches the message
|
|
688
|
+
* a reader sees.
|
|
689
689
|
*
|
|
690
690
|
* **Silent where there is nothing to check against**, exactly as its two
|
|
691
691
|
* siblings are: a module pinning a system version released before the artifact
|
package/engine/site-build.mjs
CHANGED
|
@@ -33,13 +33,12 @@
|
|
|
33
33
|
* a function.
|
|
34
34
|
*
|
|
35
35
|
* **Every gate reports; none exits.** The integrity checks a site build needs —
|
|
36
|
-
* a wikilink authored in frontmatter, a name that yields no slug,
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* these cases can be tested at all.
|
|
36
|
+
* a wikilink authored in frontmatter, a name that yields no slug, an unusable
|
|
37
|
+
* or unaddressable foreign manifest, a table directive that cannot be
|
|
38
|
+
* honoured, a dead wikilink — were inline `process.exit` calls in both
|
|
39
|
+
* scripts, with no test between them. Here each returns its findings and the
|
|
40
|
+
* command decides. That is the rule `engine/site-index.mjs` already follows,
|
|
41
|
+
* and it is the only reason these cases can be tested at all.
|
|
43
42
|
*
|
|
44
43
|
* @module
|
|
45
44
|
*/
|
|
@@ -49,7 +48,6 @@ import path from "node:path";
|
|
|
49
48
|
import { createRequire } from "node:module";
|
|
50
49
|
import matter from "gray-matter";
|
|
51
50
|
|
|
52
|
-
import { slugify } from "./content-slug.mjs";
|
|
53
51
|
import { addressSlug } from "./content-address.mjs";
|
|
54
52
|
import { protectCode } from "./code-fences.mjs";
|
|
55
53
|
import { expandContentTables } from "./content-tables.mjs";
|
|
@@ -81,37 +79,6 @@ import { HUGO_CONTENT } from "./site-config.mjs";
|
|
|
81
79
|
|
|
82
80
|
const require = createRequire(import.meta.url);
|
|
83
81
|
|
|
84
|
-
/**
|
|
85
|
-
* Every `.md` file under `dir`, depth-first in directory order.
|
|
86
|
-
*
|
|
87
|
-
* Deliberately *not* {@link walkMarkdownTree}, whose stack-based walk yields a
|
|
88
|
-
* tree in reverse. Order was load-bearing here when the address index carried
|
|
89
|
-
* first-writer-wins fallbacks for a page's name, filename and slug — reversing
|
|
90
|
-
* the walk silently changed which page an ambiguous name resolved to. Those
|
|
91
|
-
* fallbacks are gone with the bare `[[Name]]` form, so this is now
|
|
92
|
-
* ordinary reading order rather than a dependency; it is kept because a site's
|
|
93
|
-
* emitted pages should not reorder for no reason.
|
|
94
|
-
*
|
|
95
|
-
* @param {string} dir - Directory to walk.
|
|
96
|
-
* @param {readonly string[]} skip - Directory names to ignore at any depth.
|
|
97
|
-
* @returns {string[]} Absolute paths.
|
|
98
|
-
*/
|
|
99
|
-
export function walkSiteTree(dir, skip = []) {
|
|
100
|
-
const out = [];
|
|
101
|
-
if (!fs.existsSync(dir)) return out;
|
|
102
|
-
const skipped = new Set(skip);
|
|
103
|
-
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
104
|
-
const full = path.join(dir, e.name);
|
|
105
|
-
if (e.isDirectory()) {
|
|
106
|
-
if (skipped.has(e.name)) continue;
|
|
107
|
-
out.push(...walkSiteTree(full, skip));
|
|
108
|
-
} else if (e.isFile() && e.name.endsWith(".md")) {
|
|
109
|
-
out.push(full);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return out;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
82
|
/**
|
|
116
83
|
* The content-tree files this build publishes from, in the order it emits them.
|
|
117
84
|
*
|
|
@@ -135,10 +102,6 @@ export function walkSiteTree(dir, skip = []) {
|
|
|
135
102
|
* is a fact about the filesystem, not about the content, so it can differ
|
|
136
103
|
* between two checkouts of one tree; this order cannot.
|
|
137
104
|
*
|
|
138
|
-
* `collectTreePages` is deliberately **not** converted: it walks an auxiliary
|
|
139
|
-
* tree (`site.trees`, the developer docs), which is not the content tree and
|
|
140
|
-
* appears in no record.
|
|
141
|
-
*
|
|
142
105
|
* @param {string} contentBase - Root of the content tree.
|
|
143
106
|
* @param {object} ctx - The build context. `ctx.records` is the corpus when the
|
|
144
107
|
* caller already derived it — the site build derives one and hands it to both
|
|
@@ -267,73 +230,6 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
267
230
|
return { pages, addressFindings, fmLinkFindings };
|
|
268
231
|
}
|
|
269
232
|
|
|
270
|
-
/**
|
|
271
|
-
* An extra tree's pages — a documentation tree published alongside the content.
|
|
272
|
-
*
|
|
273
|
-
* These preserve their **source layout** below the section rather than being
|
|
274
|
-
* addressed by type and slug: they are a book with chapters, and a reader
|
|
275
|
-
* follows their paths. A `README` is its directory's landing.
|
|
276
|
-
*
|
|
277
|
-
* **The section is the tree's, never the note's.** `tree.section` is the
|
|
278
|
-
* mount point a `trees` entry configures — fixed, physical, and the same
|
|
279
|
-
* value `site-index.mjs` indexes a tree page's address under. A note's own
|
|
280
|
-
* `subType` is a genre and reaches no address, the same contract
|
|
281
|
-
* `packageAddress()` holds for a content page: reading it here would move a
|
|
282
|
-
* page's URL, its file destination (`pageDestination`) and the address a
|
|
283
|
-
* wikilink cites it by, every time an author classified it.
|
|
284
|
-
*
|
|
285
|
-
* @param {object} tree - `{ from, rel, section, route }`.
|
|
286
|
-
* @param {object} ctx - `{ mount }`.
|
|
287
|
-
* @returns {{pages: object[], fmLinkFindings: object[]}}
|
|
288
|
-
*/
|
|
289
|
-
export function collectTreePages(tree, ctx) {
|
|
290
|
-
const pages = [];
|
|
291
|
-
const fmLinkFindings = [];
|
|
292
|
-
|
|
293
|
-
for (const file of walkSiteTree(tree.from)) {
|
|
294
|
-
const note = readNote(file);
|
|
295
|
-
if (!note) continue;
|
|
296
|
-
const { fm, body } = note;
|
|
297
|
-
|
|
298
|
-
for (const hit of frontmatterWikilinks(fm)) {
|
|
299
|
-
fmLinkFindings.push({ file, ...hit });
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const rel = path.relative(tree.from, file).replace(/\\/g, "/");
|
|
303
|
-
const base = path.basename(rel);
|
|
304
|
-
const isReadme = base.toLowerCase() === "readme.md";
|
|
305
|
-
const sec = tree.section;
|
|
306
|
-
const h1 = /^#\s+(.+?)\s*$/m.exec(body);
|
|
307
|
-
const h1Title = h1 ? h1[1].replace(/\{@link\s+[^}]*\}/g, "").trim() : null;
|
|
308
|
-
const name = fm.name?.full ?? fm.title ?? h1Title ?? path.basename(base, ".md");
|
|
309
|
-
const slug = fm.slug ?? slugify(path.basename(base, ".md"));
|
|
310
|
-
const relNoExt = rel.slice(0, -3).toLowerCase();
|
|
311
|
-
const dir = path.posix.dirname(relNoExt);
|
|
312
|
-
pages.push({
|
|
313
|
-
kind: "tree",
|
|
314
|
-
tree,
|
|
315
|
-
fm,
|
|
316
|
-
// As above: the source file, for a located link diagnostic.
|
|
317
|
-
file,
|
|
318
|
-
// The H1 is stripped: the page title renders it.
|
|
319
|
-
body: body.replace(/^\s*#\s+.*$\r?\n?/m, ""),
|
|
320
|
-
name,
|
|
321
|
-
slug,
|
|
322
|
-
base,
|
|
323
|
-
rel,
|
|
324
|
-
sec,
|
|
325
|
-
url:
|
|
326
|
-
isReadme ?
|
|
327
|
-
dir === "." ?
|
|
328
|
-
`${ctx.mount}${sec}/`
|
|
329
|
-
: `${ctx.mount}${sec}/${dir}/`
|
|
330
|
-
: `${ctx.mount}${sec}/${relNoExt}/`,
|
|
331
|
-
isReadme,
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
return { pages, fmLinkFindings };
|
|
335
|
-
}
|
|
336
|
-
|
|
337
233
|
/**
|
|
338
234
|
* The package's homepage notes — the authored page at `/<contentPackage>/`.
|
|
339
235
|
*
|
|
@@ -454,7 +350,6 @@ export function siteGates(pages, findings, { config }) {
|
|
|
454
350
|
addressErrors: findings.addressFindings ?? [],
|
|
455
351
|
staleManifests: [],
|
|
456
352
|
unaddressable: [],
|
|
457
|
-
conflicts: [],
|
|
458
353
|
index: null,
|
|
459
354
|
foreign: null,
|
|
460
355
|
manifests: null,
|
|
@@ -477,14 +372,10 @@ export function siteGates(pages, findings, { config }) {
|
|
|
477
372
|
out.unaddressable = unaddressableForeignPackages(foreign.index);
|
|
478
373
|
if (out.unaddressable.length) return out;
|
|
479
374
|
|
|
480
|
-
|
|
375
|
+
out.index = buildSiteIndex(pages, {
|
|
481
376
|
foreignIndex: foreign.index,
|
|
482
377
|
noIndexPackages: noContentIndexPackages(config),
|
|
483
378
|
});
|
|
484
|
-
out.conflicts = index.conflicts;
|
|
485
|
-
if (out.conflicts.length) return out;
|
|
486
|
-
|
|
487
|
-
out.index = index;
|
|
488
379
|
return out;
|
|
489
380
|
}
|
|
490
381
|
|
|
@@ -505,7 +396,6 @@ export function emptyGates() {
|
|
|
505
396
|
addressErrors: [],
|
|
506
397
|
staleManifests: [],
|
|
507
398
|
unaddressable: [],
|
|
508
|
-
conflicts: [],
|
|
509
399
|
index: null,
|
|
510
400
|
foreign: null,
|
|
511
401
|
manifests: null,
|
|
@@ -519,8 +409,7 @@ export function gatesFailed(gates) {
|
|
|
519
409
|
gates.frontmatterLinks.length ||
|
|
520
410
|
gates.addressErrors.length ||
|
|
521
411
|
gates.staleManifests.length ||
|
|
522
|
-
gates.unaddressable.length
|
|
523
|
-
gates.conflicts.length,
|
|
412
|
+
gates.unaddressable.length,
|
|
524
413
|
);
|
|
525
414
|
}
|
|
526
415
|
|
|
@@ -622,15 +511,8 @@ export function sectionFrontmatter(meta) {
|
|
|
622
511
|
* self-describing and makes sweeping the field out of a content tree
|
|
623
512
|
* output-preserving for a site as it already is for the packs.
|
|
624
513
|
*
|
|
625
|
-
* A **tree** page is the one that still reads `readmeSections`: a `trees` entry
|
|
626
|
-
* keeps its source layout below a named section, so its own `README` is that
|
|
627
|
-
* section's landing and takes the title and hero the section declares.
|
|
628
|
-
*
|
|
629
514
|
* @param {object} page - The page.
|
|
630
515
|
* @param {object} options
|
|
631
|
-
* @param {Record<string, object>} [options.readmeSections] - The sections a
|
|
632
|
-
* published tree declares, which a tree page's own `README` is the landing
|
|
633
|
-
* for.
|
|
634
516
|
* @param {(data: object, page: object) => void} [options.decorate] - Called
|
|
635
517
|
* with each page's frontmatter, for whatever a consumer's own pass adds.
|
|
636
518
|
* @param {(value: unknown, type: string) => string|null} [options.artSrc] -
|
|
@@ -642,37 +524,25 @@ export function sectionFrontmatter(meta) {
|
|
|
642
524
|
* same way.
|
|
643
525
|
* @returns {object} The frontmatter to write.
|
|
644
526
|
*/
|
|
645
|
-
export function pageFrontmatter(page, {
|
|
646
|
-
const { fm, name, slug
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
};
|
|
665
|
-
if (decorate) decorate(data, page);
|
|
666
|
-
} else {
|
|
667
|
-
// A tree's own landing describes the *mount*, and nothing beneath it. A
|
|
668
|
-
// nested README is a sub-section's landing, and reading the section's
|
|
669
|
-
// entry for it would title every one of them alike and hang the section
|
|
670
|
-
// hero on each. Its title comes from its H1, like any other page's.
|
|
671
|
-
const isSectionRoot = path.posix.dirname(page.rel) === ".";
|
|
672
|
-
const meta = isReadme && isSectionRoot ? readmeSections[sec] : null;
|
|
673
|
-
data = { ...fm, title: meta?.title ?? fm.title ?? name };
|
|
674
|
-
if (meta) Object.assign(data, sectionFrontmatter(meta));
|
|
675
|
-
}
|
|
527
|
+
export function pageFrontmatter(page, { decorate, webSrc, artSrc }) {
|
|
528
|
+
const { fm, name, slug } = page;
|
|
529
|
+
const data = {
|
|
530
|
+
...fm,
|
|
531
|
+
// Spread after the note's own frontmatter. Guarded because
|
|
532
|
+
// `package: undefined` is not a value YAML can carry.
|
|
533
|
+
...(page.pkg ? { package: page.pkg } : {}),
|
|
534
|
+
// The address, stated — site-root relative, because Hugo prefixes
|
|
535
|
+
// the site's own base to it. `slug` is written beside it
|
|
536
|
+
// because it is the last segment of that address and Hugo's own key
|
|
537
|
+
// for one; it decides nothing while `url` is present, but a page
|
|
538
|
+
// that carried only `url` would report a slug Hugo had inferred
|
|
539
|
+
// from the filename.
|
|
540
|
+
slug,
|
|
541
|
+
url: `/${slug}/`,
|
|
542
|
+
title: fm.title ?? name,
|
|
543
|
+
kbfolder: page.folder,
|
|
544
|
+
};
|
|
545
|
+
if (decorate) decorate(data, page);
|
|
676
546
|
delete data.aliases;
|
|
677
547
|
if (webSrc && artSrc) resolveArtFields(data, webSrc, artSrc);
|
|
678
548
|
return data;
|
|
@@ -733,16 +603,11 @@ function isPlainObject(value) {
|
|
|
733
603
|
* the same as another note's `type`, and `doc-gear.md` and `weapongear-gear.md`
|
|
734
604
|
* are distinct whatever the sections.
|
|
735
605
|
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
* addressed by their path — so a `README` there is still its directory's
|
|
739
|
-
* `_index.md`.
|
|
606
|
+
* @param {object} page - The page.
|
|
607
|
+
* @returns {string} The file, relative to the mount.
|
|
740
608
|
*/
|
|
741
609
|
export function pageDestination(page) {
|
|
742
|
-
|
|
743
|
-
const rel =
|
|
744
|
-
page.isReadme ? path.posix.join(path.posix.dirname(page.rel), "_index.md") : page.rel;
|
|
745
|
-
return path.join(page.sec, rel);
|
|
610
|
+
return `${page.slug}.md`;
|
|
746
611
|
}
|
|
747
612
|
|
|
748
613
|
/**
|
|
@@ -755,10 +620,10 @@ export function pageDestination(page) {
|
|
|
755
620
|
* authored as a fenced `dataview` block, which `protectCode` would otherwise
|
|
756
621
|
* stash away before the expander saw it. Expanding first leaves an ordinary
|
|
757
622
|
* markdown table to walk, with every other fence still protected.
|
|
758
|
-
* 2. **Then, inside protection**: the consumer's `beforeLinks` pass,
|
|
759
|
-
* resolution
|
|
760
|
-
*
|
|
761
|
-
*
|
|
623
|
+
* 2. **Then, inside protection**: the consumer's `beforeLinks` pass, then
|
|
624
|
+
* wikilink resolution. A `{@link}` tag may sit in prose a wikilink also
|
|
625
|
+
* touches, so the repository's own rewrite runs before the shared one
|
|
626
|
+
* rather than replacing it.
|
|
762
627
|
*
|
|
763
628
|
* @param {object[]} pages - Every page.
|
|
764
629
|
* @param {object} options - Everything the render needs.
|
|
@@ -772,7 +637,6 @@ export function renderPages(pages, options) {
|
|
|
772
637
|
foreign,
|
|
773
638
|
universe,
|
|
774
639
|
pass = {},
|
|
775
|
-
readmeSections,
|
|
776
640
|
decorate,
|
|
777
641
|
linkable = (d) => Boolean(d.fm.shortcode),
|
|
778
642
|
sqlTables,
|
|
@@ -834,11 +698,10 @@ export function renderPages(pages, options) {
|
|
|
834
698
|
};
|
|
835
699
|
|
|
836
700
|
for (const page of pages) {
|
|
837
|
-
// The page's path in the tree an author edits
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
|
|
841
|
-
const src = page.relPath ?? page.rel ?? page.base;
|
|
701
|
+
// The page's path in the tree an author edits, below the content
|
|
702
|
+
// root. It is not composed as `<section>/<basename>`, which named a
|
|
703
|
+
// directory that was never the note's.
|
|
704
|
+
const src = page.relPath ?? page.base;
|
|
842
705
|
const ctx = wikiContext(index, {
|
|
843
706
|
src,
|
|
844
707
|
file: page.file,
|
|
@@ -854,7 +717,6 @@ export function renderPages(pages, options) {
|
|
|
854
717
|
let t = text;
|
|
855
718
|
if (pass.beforeLinks) t = pass.beforeLinks(t, page);
|
|
856
719
|
t = resolveWebWikilinks(t, ctx);
|
|
857
|
-
if (pass.afterLinks) t = pass.afterLinks(t, page);
|
|
858
720
|
// Last, so a consumer's own rewrites see the image as the note
|
|
859
721
|
// wrote it rather than as a figure. Hugo is handed markdown, not a
|
|
860
722
|
// rendered page, so a `{…}` directive left in the body would reach
|
|
@@ -862,26 +724,22 @@ export function renderPages(pages, options) {
|
|
|
862
724
|
return renderImageFigures(t, webSrc);
|
|
863
725
|
};
|
|
864
726
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
});
|
|
880
|
-
tableErrors.push(...errors);
|
|
881
|
-
body = markdown;
|
|
882
|
-
}
|
|
727
|
+
const { markdown: body, errors } = expandContentTables(page.body, {
|
|
728
|
+
docs: universe.get(page.pkg) ?? [],
|
|
729
|
+
linkable,
|
|
730
|
+
source: src,
|
|
731
|
+
// Prepared before this render began — DuckDB is async and this
|
|
732
|
+
// is not. Keyed by the note's own file, absolute here as in
|
|
733
|
+
// every other pass, so the three cannot disagree about a note.
|
|
734
|
+
sqlTables: sqlTables?.get(page.file),
|
|
735
|
+
self: {
|
|
736
|
+
fm: searchableFrontmatter(page.fm, page.pkg),
|
|
737
|
+
path: page.relPath,
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
tableErrors.push(...errors);
|
|
883
741
|
|
|
884
|
-
const data = pageFrontmatter(page, {
|
|
742
|
+
const data = pageFrontmatter(page, { decorate, webSrc, artSrc });
|
|
885
743
|
const dest = path.join(outRoot, pageDestination(page));
|
|
886
744
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
887
745
|
fs.writeFileSync(dest, matter.stringify(protectCode(body, resolve), data));
|
|
@@ -915,9 +773,9 @@ export function renderPages(pages, options) {
|
|
|
915
773
|
* or its own address publishes nothing. Hugo generates a section page
|
|
916
774
|
* automatically only for a *top-level* content directory; below that, a
|
|
917
775
|
* directory without an `_index.md` is not a section, so its URL 404s while its
|
|
918
|
-
* children publish normally. With content pages flat,
|
|
919
|
-
*
|
|
920
|
-
*
|
|
776
|
+
* children publish normally. With content pages flat, no note creates a
|
|
777
|
+
* directory below the mount, so this reaches only what something else
|
|
778
|
+
* placed there.
|
|
921
779
|
*
|
|
922
780
|
* **A section listing is not a page listing any more.** A layout that reads
|
|
923
781
|
* `.Pages` off a section it declares here will find nothing, because no file is
|
|
@@ -1016,7 +874,7 @@ const SITE_PASSES = Object.freeze({
|
|
|
1016
874
|
*
|
|
1017
875
|
* @param {string|undefined} name - The configured name.
|
|
1018
876
|
* @param {object} options - The configured options, plus `repoRoot`.
|
|
1019
|
-
* @returns {{beforeLinks?: Function
|
|
877
|
+
* @returns {{beforeLinks?: Function}} The bundle.
|
|
1020
878
|
*/
|
|
1021
879
|
export function resolveSitePass(name, options) {
|
|
1022
880
|
if (!name) return {};
|
|
@@ -1177,8 +1035,7 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1177
1035
|
}
|
|
1178
1036
|
|
|
1179
1037
|
const content = collectContentPages(resolved.paths.content, ctx);
|
|
1180
|
-
const pages =
|
|
1181
|
-
const fmLinkFindings = [...content.fmLinkFindings];
|
|
1038
|
+
const { pages } = content;
|
|
1182
1039
|
|
|
1183
1040
|
// The homepage is **indexed but not rendered**. Now that it has an
|
|
1184
1041
|
// address, `[[homepage-root|Text]]` is an ordinary wikilink and has to
|
|
@@ -1197,22 +1054,7 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1197
1054
|
url: `${base}${addressSlug(page.fm)}/`,
|
|
1198
1055
|
}));
|
|
1199
1056
|
|
|
1200
|
-
const
|
|
1201
|
-
...t,
|
|
1202
|
-
from: path.resolve(resolved.rootDir, t.from),
|
|
1203
|
-
route: `${mount}${t.section}/`,
|
|
1204
|
-
}));
|
|
1205
|
-
for (const tree of trees) {
|
|
1206
|
-
const got = collectTreePages(tree, ctx);
|
|
1207
|
-
pages.push(...got.pages);
|
|
1208
|
-
fmLinkFindings.push(...got.fmLinkFindings);
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
const gates = siteGates(
|
|
1212
|
-
[...pages, ...homepageEntries],
|
|
1213
|
-
{ ...content, fmLinkFindings },
|
|
1214
|
-
{ config: resolved },
|
|
1215
|
-
);
|
|
1057
|
+
const gates = siteGates([...pages, ...homepageEntries], content, { config: resolved });
|
|
1216
1058
|
if (gatesFailed(gates)) {
|
|
1217
1059
|
return {
|
|
1218
1060
|
gates,
|
|
@@ -1238,7 +1080,6 @@ export function buildSite({ config, sqlTables } = {}) {
|
|
|
1238
1080
|
foreign: gates.foreign,
|
|
1239
1081
|
universe: tableUniverse(pages),
|
|
1240
1082
|
pass,
|
|
1241
|
-
readmeSections: site.readmeSections,
|
|
1242
1083
|
// What counts as a being is the toolchain's to say, not a consumer's.
|
|
1243
1084
|
// Asking in a consumer's script is how one came to still be checking
|
|
1244
1085
|
// `character` and `creature` months after they were retired, and to
|
package/engine/site-config.mjs
CHANGED
|
@@ -44,7 +44,7 @@ import fs from "node:fs";
|
|
|
44
44
|
import path from "node:path";
|
|
45
45
|
import { stringify as stringifyToml } from "smol-toml";
|
|
46
46
|
|
|
47
|
-
import { checkHomepage } from "../config.mjs";
|
|
47
|
+
import { checkHomepage, fail } from "../config.mjs";
|
|
48
48
|
import { slugify } from "./content-slug.mjs";
|
|
49
49
|
|
|
50
50
|
/** The Hugo source directory, relative to the repository root. */
|
|
@@ -381,7 +381,6 @@ function deepMerge(base, overrides) {
|
|
|
381
381
|
*
|
|
382
382
|
* @param {object} options - The sources.
|
|
383
383
|
* @param {object} options.config - The resolved build configuration.
|
|
384
|
-
* @param {string} [options.description] - `package.json`'s `description`.
|
|
385
384
|
* @param {readonly NavigationEntry[]} options.navigation - The navigation.
|
|
386
385
|
* @param {string} options.themesDir - From {@link resolveThemesDir}.
|
|
387
386
|
* @param {boolean} [options.hasTags] - Whether any note the site build walked
|
|
@@ -389,9 +388,10 @@ function deepMerge(base, overrides) {
|
|
|
389
388
|
* `hasTags`. Defaults to `false` — no tagged note, no taxonomy pages.
|
|
390
389
|
* @returns {Record<string, any>} The configuration Hugo reads.
|
|
391
390
|
* @throws {TypeError} When `homepage` fails `checkHomepage`, or the
|
|
392
|
-
* configuration declares no `packageBuild.manifest.title
|
|
391
|
+
* configuration declares no `packageBuild.manifest.title`, no
|
|
392
|
+
* `site.description`, or no `site.assets`.
|
|
393
393
|
*/
|
|
394
|
-
export function hugoConfig({ config,
|
|
394
|
+
export function hugoConfig({ config, navigation, themesDir, hasTags = false }) {
|
|
395
395
|
checkHomepage(config.homepage, config.contentPackage);
|
|
396
396
|
|
|
397
397
|
const title = config.packageBuild?.manifest?.title;
|
|
@@ -401,10 +401,23 @@ export function hugoConfig({ config, description, navigation, themesDir, hasTags
|
|
|
401
401
|
"and the site's `title` reads from it.",
|
|
402
402
|
);
|
|
403
403
|
}
|
|
404
|
+
if (!config.site.description) {
|
|
405
|
+
fail(
|
|
406
|
+
"site.description",
|
|
407
|
+
'is not declared, and the site\'s `<meta name="description">` reads from it',
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
if (!config.site.assets) {
|
|
411
|
+
fail(
|
|
412
|
+
"site.assets",
|
|
413
|
+
"is not declared, and a site build needs one — it is the host every " +
|
|
414
|
+
"package's imagery is served from, and the theme resolves every " +
|
|
415
|
+
"relative asset against it",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
404
418
|
|
|
405
419
|
/** @type {Record<string, unknown>} */
|
|
406
|
-
const params = {};
|
|
407
|
-
if (typeof description === "string" && description.trim()) params.description = description;
|
|
420
|
+
const params = { description: config.site.description };
|
|
408
421
|
if (config.author?.name) params.author = config.author.name;
|
|
409
422
|
if (config.site.assets) params.cdnBaseURL = config.site.assets;
|
|
410
423
|
params.brand = { ...BRAND };
|
|
@@ -449,24 +462,12 @@ export function hugoToml(generated) {
|
|
|
449
462
|
);
|
|
450
463
|
}
|
|
451
464
|
|
|
452
|
-
/**
|
|
453
|
-
* `package.json`'s `description`, or `undefined` when it declares none.
|
|
454
|
-
*
|
|
455
|
-
* @param {string} rootDir - The repository root.
|
|
456
|
-
* @returns {string|undefined} The description.
|
|
457
|
-
*/
|
|
458
|
-
function packageDescription(rootDir) {
|
|
459
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
|
460
|
-
return typeof pkg.description === "string" ? pkg.description : undefined;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
465
|
/**
|
|
464
466
|
* The Hugo configuration, every source read from the repository.
|
|
465
467
|
*
|
|
466
|
-
* Reads
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
* intact.
|
|
468
|
+
* Reads the cached navigation and the installed theme's location, and
|
|
469
|
+
* composes them with {@link hugoConfig}. Nothing is written, so a caller can
|
|
470
|
+
* run this before touching the output tree and fail with it intact.
|
|
470
471
|
*
|
|
471
472
|
* @param {object} config - The resolved build configuration.
|
|
472
473
|
* @param {object} [options] - Options.
|
|
@@ -480,7 +481,6 @@ function packageDescription(rootDir) {
|
|
|
480
481
|
export function generateHugoConfig(config, { hasTags = false } = {}) {
|
|
481
482
|
return hugoConfig({
|
|
482
483
|
config,
|
|
483
|
-
description: packageDescription(config.rootDir),
|
|
484
484
|
navigation: readCachedNavigation(config),
|
|
485
485
|
themesDir: resolveThemesDir(config.rootDir),
|
|
486
486
|
hasTags,
|