@heroiclands/package-build 22.2.0 → 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 +8 -0
- package/CONTENT.md +27 -32
- package/bin/content-build.mjs +0 -3
- package/content-config.mjs +42 -40
- package/docs/api.md +25 -25
- package/docs/configuration.md +19 -29
- 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-router.mjs +82 -5
- package/engine/pdf-build.mjs +2 -2
- package/engine/retired-fields.mjs +2 -3
- package/engine/site-build.mjs +59 -218
- package/engine/site-index.mjs +18 -62
- package/engine/web-wikilinks.mjs +2 -16
- package/engine/wikilinks.mjs +35 -14
- package/package.json +1 -1
- package/sohl/kb-passes.mjs +14 -85
- 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/site-build.d.mts +12 -62
- 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/sohl/kb-passes.d.mts +5 -36
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-index.mjs
CHANGED
|
@@ -65,20 +65,14 @@ import { isDraftNote } from "./note-vocabulary.mjs";
|
|
|
65
65
|
*
|
|
66
66
|
* @typedef {object} SiteEntry
|
|
67
67
|
* @property {string} kind `"content"` for a note compiled from the content
|
|
68
|
-
* tree
|
|
69
|
-
*
|
|
70
|
-
*
|
|
68
|
+
* tree. Only content entries take part in
|
|
69
|
+
* type-scoped indexing; anything else is carried
|
|
70
|
+
* through unindexed.
|
|
71
71
|
* @property {object} fm The note's frontmatter.
|
|
72
72
|
* @property {string} name Display name.
|
|
73
73
|
* @property {string} slug URL segment.
|
|
74
|
-
* @property {string} [sec] The Hugo section a **tree** page is filed under, and
|
|
75
|
-
* the first segment of the `<sec>/<slug>` address it
|
|
76
|
-
* is reachable by. A content page has none: it is
|
|
77
|
-
* addressed by `(type, shortcode)` and emitted flat.
|
|
78
74
|
* @property {string} base Source file's basename, e.g. `Climbing.md`.
|
|
79
75
|
* @property {string} url The page's published address.
|
|
80
|
-
* @property {boolean} [isReadme] Whether a tree page is its directory's
|
|
81
|
-
* landing.
|
|
82
76
|
*/
|
|
83
77
|
|
|
84
78
|
/**
|
|
@@ -94,23 +88,18 @@ import { isDraftNote } from "./note-vocabulary.mjs";
|
|
|
94
88
|
* from `index`.
|
|
95
89
|
* @property {Set<string>} contentTypes Every type the resolver should read as
|
|
96
90
|
* an address qualifier, local and foreign.
|
|
97
|
-
* @property {Set<string>} sections Section names, lowercased.
|
|
98
91
|
* @property {Map<string, {name: string, url: string, subType?: string}>} refIndex
|
|
99
92
|
* `type:shortcode` → page, for callers
|
|
100
93
|
* resolving embedded references (a
|
|
101
94
|
* being's items, say).
|
|
102
|
-
* @property {{key: string, package: string}[]} conflicts Addresses claimed by
|
|
103
|
-
* more than one package. Non-empty is a
|
|
104
|
-
* build failure; the caller reports it.
|
|
105
95
|
*/
|
|
106
96
|
|
|
107
97
|
/**
|
|
108
98
|
* Merge the packages this build does not publish into the local index.
|
|
109
99
|
*
|
|
110
100
|
* Every canonical key is globally unique, so a foreign manifest merges straight
|
|
111
|
-
* in — one map, one lookup, no precedence rule.
|
|
112
|
-
*
|
|
113
|
-
* canonical form exists to make detectable.
|
|
101
|
+
* in — one map, one lookup, no precedence rule. It runs before the local pass
|
|
102
|
+
* writes a single key, so a local page always ends up owning its own address.
|
|
114
103
|
*
|
|
115
104
|
* The short `type/shortcode` form is merged too, because a bare `[[doc-xyz]]`
|
|
116
105
|
* carries no package and must still find a foreign note when exactly one
|
|
@@ -122,22 +111,15 @@ import { isDraftNote } from "./note-vocabulary.mjs";
|
|
|
122
111
|
*
|
|
123
112
|
* @param {Map<string, object>} index - The local index, mutated.
|
|
124
113
|
* @param {Map<string, {package: string, type?: string}>} foreignIndex - Merged in.
|
|
125
|
-
* @returns {{
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* left out of the index, so a resolver can say *ambiguous* rather than
|
|
129
|
-
* *nothing answers*.
|
|
114
|
+
* @returns {{ambiguous: Set<string>}} The short `type/shortcode` forms two
|
|
115
|
+
* foreign packages claim — those are left out of the index, so a resolver
|
|
116
|
+
* can say *ambiguous* rather than *nothing answers*.
|
|
130
117
|
*/
|
|
131
118
|
function mergeForeign(index, foreignIndex) {
|
|
132
|
-
const conflicts = [];
|
|
133
119
|
const short = new Map();
|
|
134
120
|
const ambiguous = new Set();
|
|
135
121
|
|
|
136
122
|
for (const [key, value] of foreignIndex) {
|
|
137
|
-
if (index.has(key)) {
|
|
138
|
-
conflicts.push({ key, package: value.package });
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
123
|
index.set(key, value);
|
|
142
124
|
|
|
143
125
|
const parts = readCanonicalKey(key);
|
|
@@ -154,7 +136,7 @@ function mergeForeign(index, foreignIndex) {
|
|
|
154
136
|
for (const [key, value] of short) {
|
|
155
137
|
if (!index.has(key)) index.set(key, value);
|
|
156
138
|
}
|
|
157
|
-
return {
|
|
139
|
+
return { ambiguous };
|
|
158
140
|
}
|
|
159
141
|
|
|
160
142
|
/**
|
|
@@ -178,7 +160,6 @@ export function buildSiteIndex(
|
|
|
178
160
|
) {
|
|
179
161
|
const index = new Map();
|
|
180
162
|
const contentTypes = new Set();
|
|
181
|
-
const sections = new Set();
|
|
182
163
|
const refIndex = new Map();
|
|
183
164
|
// Every package an address may name: this build's own, plus every one a
|
|
184
165
|
// vendored manifest speaks for. Without it `readQualifier` cannot see the
|
|
@@ -187,30 +168,12 @@ export function buildSiteIndex(
|
|
|
187
168
|
const ownPackage = contentPackage();
|
|
188
169
|
const packages = new Set(ownPackage ? [ownPackage] : []);
|
|
189
170
|
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
// A page's name, filename and bare slug were indexed here too, as
|
|
198
|
-
// collision-aware fallbacks the bare `[[Name]]` form looked up; that form is
|
|
199
|
-
// retired and nothing consults them, so they are gone and with them the rule
|
|
200
|
-
// that two pages of a type may not share a name.
|
|
201
|
-
for (const e of entries) {
|
|
202
|
-
if (typeof e.sec !== "string" || !e.sec) continue;
|
|
203
|
-
sections.add(e.sec.toLowerCase());
|
|
204
|
-
// `draft` rides on every key a page is addressable by, because a link
|
|
205
|
-
// into a draft note renders marked whichever of them the author wrote.
|
|
206
|
-
// It decides nothing about resolution: the page is indexed and
|
|
207
|
-
// published as any other.
|
|
208
|
-
index.set(`${e.sec}/${e.slug}`.toLowerCase(), {
|
|
209
|
-
url: e.url,
|
|
210
|
-
name: e.name,
|
|
211
|
-
draft: isDraftNote(e.fm),
|
|
212
|
-
});
|
|
213
|
-
}
|
|
171
|
+
// A page is addressed by `(type, shortcode)` and nothing else. Its name,
|
|
172
|
+
// filename and bare slug are not keys: the bare `[[Name]]` form that
|
|
173
|
+
// looked them up is retired, and with it the rule that two pages of a
|
|
174
|
+
// type may not share a name. A page carries no section either — a
|
|
175
|
+
// section is a listing the configuration declares, not part of any
|
|
176
|
+
// address.
|
|
214
177
|
|
|
215
178
|
// A foreign package may use a type this build has never seen. Seeding those
|
|
216
179
|
// is what lets the resolver recognise `polity-xyz` as an address at all —
|
|
@@ -226,15 +189,11 @@ export function buildSiteIndex(
|
|
|
226
189
|
// the local packages, so a manifest should never carry one — this is what
|
|
227
190
|
// makes that a belt-and-braces rather than the only thing standing between
|
|
228
191
|
// a stale vendored manifest and a shadowed local page.
|
|
229
|
-
|
|
230
|
-
// The corollary is that a conflict can only be reported against the keys
|
|
231
|
-
// that exist at this point — the addressing ones, `section/slug` and the
|
|
232
|
-
// bare fallbacks — which is precisely the overlap worth refusing.
|
|
233
|
-
const { conflicts, ambiguous } = mergeForeign(index, foreignIndex);
|
|
192
|
+
const { ambiguous } = mergeForeign(index, foreignIndex);
|
|
234
193
|
|
|
235
194
|
for (const e of entries) {
|
|
236
|
-
//
|
|
237
|
-
//
|
|
195
|
+
// Only a content note is addressable; anything else is carried
|
|
196
|
+
// through and takes no part in type-scoped indexing.
|
|
238
197
|
if (e.kind !== "content") continue;
|
|
239
198
|
const type = String(e.fm.type).toLowerCase();
|
|
240
199
|
contentTypes.add(type);
|
|
@@ -303,11 +262,9 @@ export function buildSiteIndex(
|
|
|
303
262
|
index,
|
|
304
263
|
ambiguous,
|
|
305
264
|
contentTypes,
|
|
306
|
-
sections,
|
|
307
265
|
packages,
|
|
308
266
|
noIndexPackages,
|
|
309
267
|
refIndex,
|
|
310
|
-
conflicts,
|
|
311
268
|
};
|
|
312
269
|
}
|
|
313
270
|
|
|
@@ -352,7 +309,6 @@ export function wikiContext(
|
|
|
352
309
|
// a URL and a name, an asset's carries the path to a file.
|
|
353
310
|
assets,
|
|
354
311
|
collide: built.ambiguous,
|
|
355
|
-
sections: built.sections,
|
|
356
312
|
contentTypes: built.contentTypes,
|
|
357
313
|
packages: built.packages,
|
|
358
314
|
noIndexPackages: built.noIndexPackages,
|
package/engine/web-wikilinks.mjs
CHANGED
|
@@ -312,7 +312,7 @@ function isPlainMap(value) {
|
|
|
312
312
|
* for the website, and what the book reads its staging list out of.
|
|
313
313
|
*
|
|
314
314
|
* @param {string} body - The markdown body.
|
|
315
|
-
* @param {object} ctx - `{ index, assets, collide,
|
|
315
|
+
* @param {object} ctx - `{ index, assets, collide, contentTypes,
|
|
316
316
|
* packages, noIndexPackages, foreign, type, errors, src, file }`.
|
|
317
317
|
* `packages` is every package an address may name, without which the leading
|
|
318
318
|
* package segment of a canonical address reads as an unknown type;
|
|
@@ -415,10 +415,6 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
415
415
|
const rawKey = target.toLowerCase();
|
|
416
416
|
const hit =
|
|
417
417
|
lookupRead(ctx.index, read, ctx.contentPackage) ??
|
|
418
|
-
// `section/slug` is the site's own address for a page, and it is in
|
|
419
|
-
// the same map. Admitted only when the target carries a slash, so
|
|
420
|
-
// a page's bare slug cannot answer for an address.
|
|
421
|
-
(rawKey.includes("/") ? ctx.index.get(rawKey) : undefined) ??
|
|
422
418
|
// A manifest entry carries the same `{ url, name }` shape as a
|
|
423
419
|
// local one, so a cross-package hit needs no special case
|
|
424
420
|
// below. Local wins: a live build is authoritative and a vendored
|
|
@@ -449,16 +445,6 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
449
445
|
return hit.draft ? draftLink(link) : link;
|
|
450
446
|
}
|
|
451
447
|
|
|
452
|
-
const slash = target.indexOf("/");
|
|
453
|
-
const prefix = slash === -1 ? null : target.slice(0, slash).toLowerCase();
|
|
454
|
-
// A slash-qualified target whose prefix is a real KB **section** is an
|
|
455
|
-
// address in the site's own `section/slug` space, which the lookup
|
|
456
|
-
// above already consulted. It parses as no `type/shortcode`, but it did
|
|
457
|
-
// address something and nothing answered — so it is unresolved, not
|
|
458
|
-
// unaddressable. (A prefix that is a content *type* never reaches here:
|
|
459
|
-
// it parses as an address.)
|
|
460
|
-
const siteAddress = prefix !== null && ctx.sections.has(prefix);
|
|
461
|
-
|
|
462
448
|
// **An address resolving nowhere is a failure, unconditionally**.
|
|
463
449
|
//
|
|
464
450
|
// It was gated on a manifest-completeness check — while any linkable package was
|
|
@@ -485,7 +471,7 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
485
471
|
ctx.collide?.has(collideKey) ? "ambiguous"
|
|
486
472
|
// "It parsed as an address" is a property of the parse, not of
|
|
487
473
|
// a key: a partial address has no single key to be non-null.
|
|
488
|
-
:
|
|
474
|
+
: read && !read.reason ? "unresolved"
|
|
489
475
|
: read?.reason === "unknown-type" ? "unknown-type"
|
|
490
476
|
: read?.reason === "no-content-index" ? "no-content-index"
|
|
491
477
|
// Every link is an address, and this is not one. Distinct from
|