@heroiclands/package-build 20.3.0 → 20.4.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 +97 -0
- package/CONTENT.md +111 -92
- package/README.md +2 -9
- package/bin/content-build.mjs +70 -61
- package/bin/package-build.mjs +2 -2
- package/config.mjs +6 -8
- package/content-config.mjs +187 -64
- package/docs/content-format.md +57 -68
- package/e2e.mjs +4 -4
- package/engine/actor-compiler.mjs +36 -36
- package/engine/address-charset.mjs +6 -6
- package/engine/address-diff.mjs +12 -11
- package/engine/anchored-sections.mjs +1 -1
- package/engine/anchors.mjs +1 -1
- package/engine/base-compiler.mjs +36 -38
- package/engine/bundle-notes.mjs +4 -4
- package/engine/bundles.mjs +8 -8
- package/engine/code-fences.mjs +3 -3
- package/engine/compendiums.mjs +8 -8
- package/engine/compile-corpus.mjs +4 -5
- package/engine/content-address.mjs +15 -15
- package/engine/content-charset.mjs +1 -1
- package/engine/content-format-check.mjs +24 -25
- package/engine/content-format.mjs +8 -8
- package/engine/content-html.mjs +202 -0
- package/engine/content-icons.mjs +225 -98
- package/engine/content-index.mjs +17 -17
- package/engine/content-links.mjs +30 -30
- package/engine/content-lint.mjs +20 -24
- package/engine/content-package.mjs +7 -9
- package/engine/content-slug.mjs +2 -2
- package/engine/content-tables.mjs +7 -8
- package/engine/content-tree.mjs +1 -1
- package/engine/derived-fields.mjs +174 -0
- package/engine/diagnostics.mjs +3 -4
- package/engine/document-subtypes.mjs +6 -7
- package/engine/field-reference.mjs +8 -8
- package/engine/field-spec.mjs +29 -30
- package/engine/folder-notes.mjs +14 -14
- package/engine/foreign-catalog.mjs +8 -9
- package/engine/foundry-entries.mjs +19 -20
- package/engine/frontmatter-lint.mjs +64 -69
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +39 -39
- package/engine/helpers.mjs +63 -54
- package/engine/homepage.mjs +20 -22
- package/engine/ids.mjs +15 -15
- package/engine/index-records.mjs +4 -4
- package/engine/index.mjs +14 -8
- package/engine/item-compiler.mjs +54 -29
- package/engine/item-docs.mjs +11 -13
- package/engine/item-registry.mjs +13 -13
- package/engine/journals.mjs +15 -15
- package/engine/macros.mjs +3 -3
- package/engine/map-notes.mjs +6 -6
- package/engine/metadata-index.mjs +5 -5
- package/engine/note-claims.mjs +36 -34
- package/engine/note-ids.mjs +5 -5
- package/engine/note-package.mjs +4 -4
- package/engine/note-renames.mjs +3 -3
- package/engine/note-schemas.mjs +3 -3
- package/engine/note-vocabulary.mjs +27 -26
- package/engine/pack-config.mjs +13 -13
- package/engine/pack-router.mjs +0 -0
- package/engine/prose-config.mjs +9 -9
- package/engine/prose-lint.mjs +12 -13
- package/engine/region-events.mjs +2 -2
- package/engine/retired-fields.mjs +24 -22
- package/engine/runtime-only-fields.mjs +1 -1
- package/engine/scene-levels.mjs +1 -1
- package/engine/scenes.mjs +19 -20
- package/engine/schema-check.mjs +20 -20
- package/engine/schema-extract.mjs +1 -1
- package/engine/site-build.mjs +45 -47
- package/engine/site-index.mjs +17 -18
- package/engine/sql-tables.mjs +21 -13
- package/engine/subtype-registry.mjs +5 -4
- package/engine/system-block.mjs +18 -19
- package/engine/systems.mjs +2 -2
- package/engine/web-wikilinks.mjs +35 -35
- package/engine/wikilink-syntax.mjs +16 -16
- package/engine/wikilinks.mjs +39 -39
- package/hm3/actors.mjs +22 -22
- package/hm3/document-subtypes.mjs +5 -5
- package/hm3/index.mjs +2 -2
- package/hm3/item-builders.mjs +1 -1
- package/hm3/item-fields.mjs +3 -3
- package/hm3/items.mjs +20 -8
- package/hm3/template-priority.mjs +2 -2
- package/lang.mjs +3 -3
- package/manifest.mjs +17 -20
- package/package.json +1 -2
- package/release.mjs +3 -3
- package/sohl/actors.mjs +28 -28
- package/sohl/affiliation-standings.mjs +2 -2
- package/sohl/being-info.mjs +5 -5
- package/sohl/default-item-art.mjs +5 -5
- package/sohl/document-subtypes.mjs +5 -5
- package/sohl/index.mjs +3 -3
- package/sohl/item-builders.mjs +7 -7
- package/sohl/item-fields.mjs +8 -9
- package/sohl/items.mjs +20 -6
- package/sohl/kb-passes.mjs +5 -5
- package/sohl/note-schemas.mjs +7 -7
- package/sohl/skill-base.mjs +1 -1
- package/types/content-config.d.mts +25 -24
- package/types/e2e.d.mts +3 -3
- package/types/engine/actor-compiler.d.mts +14 -13
- package/types/engine/address-charset.d.mts +6 -6
- package/types/engine/address-diff.d.mts +3 -3
- package/types/engine/base-compiler.d.mts +17 -19
- package/types/engine/bundle-notes.d.mts +3 -3
- package/types/engine/bundles.d.mts +1 -1
- package/types/engine/code-fences.d.mts +3 -3
- package/types/engine/compendiums.d.mts +3 -3
- package/types/engine/compile-corpus.d.mts +1 -1
- package/types/engine/content-address.d.mts +11 -11
- package/types/engine/content-format-check.d.mts +6 -6
- package/types/engine/content-format.d.mts +2 -2
- package/types/engine/content-html.d.mts +78 -0
- package/types/engine/content-icons.d.mts +150 -62
- package/types/engine/content-index.d.mts +2 -2
- package/types/engine/content-links.d.mts +8 -8
- package/types/engine/content-lint.d.mts +2 -2
- package/types/engine/content-package.d.mts +6 -8
- package/types/engine/derived-fields.d.mts +101 -0
- package/types/engine/diagnostics.d.mts +2 -2
- package/types/engine/document-subtypes.d.mts +3 -3
- package/types/engine/field-spec.d.mts +37 -39
- package/types/engine/folder-notes.d.mts +4 -5
- package/types/engine/foreign-catalog.d.mts +3 -3
- package/types/engine/foundry-entries.d.mts +6 -7
- package/types/engine/frontmatter-lint.d.mts +7 -7
- package/types/engine/frontmatter.d.mts +7 -7
- package/types/engine/generate.d.mts +7 -7
- package/types/engine/helpers.d.mts +25 -27
- package/types/engine/homepage.d.mts +16 -18
- package/types/engine/ids.d.mts +13 -13
- package/types/engine/index-records.d.mts +3 -3
- package/types/engine/index.d.mts +2 -0
- package/types/engine/item-compiler.d.mts +21 -5
- package/types/engine/item-docs.d.mts +2 -2
- package/types/engine/item-registry.d.mts +6 -6
- package/types/engine/journals.d.mts +3 -3
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/metadata-index.d.mts +4 -4
- package/types/engine/note-claims.d.mts +16 -15
- package/types/engine/note-ids.d.mts +4 -4
- package/types/engine/note-package.d.mts +1 -1
- package/types/engine/note-renames.d.mts +3 -3
- package/types/engine/note-vocabulary.d.mts +9 -8
- package/types/engine/pack-config.d.mts +3 -3
- package/types/engine/pack-router.d.mts +1 -1
- package/types/engine/prose-config.d.mts +9 -9
- package/types/engine/prose-lint.d.mts +3 -4
- package/types/engine/region-events.d.mts +2 -2
- package/types/engine/retired-fields.d.mts +10 -9
- package/types/engine/schema-check.d.mts +11 -11
- package/types/engine/site-build.d.mts +18 -19
- package/types/engine/site-index.d.mts +3 -4
- package/types/engine/sql-tables.d.mts +11 -5
- package/types/engine/subtype-registry.d.mts +3 -3
- package/types/engine/system-block.d.mts +3 -3
- package/types/engine/web-wikilinks.d.mts +7 -7
- package/types/engine/wikilink-syntax.d.mts +17 -17
- package/types/engine/wikilinks.d.mts +13 -12
- package/types/hm3/actors.d.mts +1 -1
- package/types/hm3/document-subtypes.d.mts +1 -1
- package/types/hm3/items.d.mts +1 -2
- package/types/hm3/template-priority.d.mts +1 -1
- package/types/manifest.d.mts +8 -8
- package/types/sohl/actors.d.mts +2 -2
- package/types/sohl/affiliation-standings.d.mts +2 -2
- package/types/sohl/being-info.d.mts +2 -2
- package/types/sohl/document-subtypes.d.mts +1 -1
- package/types/sohl/items.d.mts +1 -2
- package/types/sohl/note-schemas.d.mts +1 -1
- package/MIGRATING.md +0 -608
package/engine/content-links.mjs
CHANGED
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
* written target is a *partial* address — the segments it omits are
|
|
28
28
|
* wildcards, and the package it omits is this one — so a target resolving to
|
|
29
29
|
* *several* notes is an ambiguity rather than a first match, and is reported
|
|
30
|
-
* naming every candidate
|
|
30
|
+
* naming every candidate.
|
|
31
31
|
* 3. **An unlabelled link.** `[[x]]` addresses nothing: the alias namespace it
|
|
32
|
-
*
|
|
32
|
+
* once named is retired, and a shortcode is an address rather than
|
|
33
33
|
* prose, so the link has neither a resolvable target nor text to show. The
|
|
34
34
|
* correction is always `[[type-shortcode|Text]]`.
|
|
35
35
|
* 4. **A wikilink authored in frontmatter.** Both builds walk a note's *body*
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
* document is reachable from the book's root" — is a statement about what one
|
|
50
50
|
* package publishes, not about the note format, so it belongs with the
|
|
51
51
|
* publishing it describes; so does a retired hostname. Both are served by the
|
|
52
|
-
* link graph returned here rather than implemented here
|
|
52
|
+
* link graph returned here rather than implemented here.
|
|
53
53
|
*
|
|
54
54
|
* @module
|
|
55
55
|
*/
|
|
@@ -61,7 +61,7 @@ import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
|
61
61
|
import { expandContentTables } from "./content-tables.mjs";
|
|
62
62
|
import { collectAnchors } from "./anchors.mjs";
|
|
63
63
|
// The corpus, and everything derived from it, read from the one place that
|
|
64
|
-
// derives it
|
|
64
|
+
// derives it. Nothing in the index's own import graph reaches this
|
|
65
65
|
// module, so this is a plain static import rather than the deferred one
|
|
66
66
|
// `sql-tables` needs to keep out of the compilers' cycle.
|
|
67
67
|
import { authoredFrontmatter, indexRecordsFor, isNoteRecord, noteFile } from "./content-index.mjs";
|
|
@@ -88,7 +88,7 @@ import { readQualifier } from "./wikilinks.mjs";
|
|
|
88
88
|
* Every `{#anchor}` a note declares on a heading.
|
|
89
89
|
*
|
|
90
90
|
* **Read from the content index's reader, not a second one.** This module kept
|
|
91
|
-
* its own
|
|
91
|
+
* its own, and the two disagreed: it matched `{#([a-z0-9-]+)}` while
|
|
92
92
|
* {@link module:engine/content-index.collectAnchors} matches `{#([^}]+)}`, so
|
|
93
93
|
* an anchor with a capital in it — `{#CalendarFormat}` — existed for the index
|
|
94
94
|
* and for the compiler and did not exist for the link checker. Nothing links to
|
|
@@ -110,7 +110,7 @@ export function anchorsOf(body) {
|
|
|
110
110
|
* Read a content tree into the index a link resolves against.
|
|
111
111
|
*
|
|
112
112
|
* **The corpus comes from the content index, not from a walk of this module's
|
|
113
|
-
* own
|
|
113
|
+
* own**. Every pass used to answer "which files are the content?" for
|
|
114
114
|
* itself and throw the answer away; this one now reads
|
|
115
115
|
* {@link module:engine/content-index.indexRecordsFor}, which is the same
|
|
116
116
|
* derivation the published artifact and the compilers are driven from. So a
|
|
@@ -118,7 +118,7 @@ export function anchorsOf(body) {
|
|
|
118
118
|
* anchors it resolves against are the ones every other pass will emit — rather
|
|
119
119
|
* than a second derivation that agrees with them only by inspection. That was
|
|
120
120
|
* not hypothetical: this module carried its own anchor reader until the anchor
|
|
121
|
-
* half
|
|
121
|
+
* anchor half, and the two disagreed about which anchors existed.
|
|
122
122
|
*
|
|
123
123
|
* **The file is opened for its bytes and nothing else.** The index deliberately
|
|
124
124
|
* carries no note *body*, and a link lives in the body — so each note is read
|
|
@@ -138,7 +138,7 @@ export function anchorsOf(body) {
|
|
|
138
138
|
* @param {string} contentBase - Root of the content tree.
|
|
139
139
|
* @param {object} [opts]
|
|
140
140
|
* @param {object} [opts.config] - The resolved build configuration, whose
|
|
141
|
-
* fetched dependency indexes foreign addresses resolve through
|
|
141
|
+
* fetched dependency indexes foreign addresses resolve through, and
|
|
142
142
|
* whose `contentPackage` every local address is built from. Omitted, the
|
|
143
143
|
* ambient configuration is resolved and no cross-package address resolves.
|
|
144
144
|
* @param {readonly string[]} [opts.skipDirectories] - The walk's scope, passed
|
|
@@ -162,10 +162,10 @@ export function buildLinkIndex(
|
|
|
162
162
|
|
|
163
163
|
// The one package every note in this tree belongs to. Taken from the
|
|
164
164
|
// configuration this build resolved — never from a note (`package:` is
|
|
165
|
-
// retired, so there is no second source an address could disagree with
|
|
166
|
-
//
|
|
165
|
+
// retired, so there is no second source an address could disagree with)
|
|
166
|
+
// and never from the ambient one, which is a different configuration
|
|
167
167
|
// whenever a test injects one, `PACKAGE_BUILD_CONFIG` names one, or the
|
|
168
|
-
// command runs from a worktree
|
|
168
|
+
// command runs from a worktree.
|
|
169
169
|
const resolved = config ?? loadPackConfig();
|
|
170
170
|
const pkg = resolved.contentPackage;
|
|
171
171
|
|
|
@@ -197,12 +197,12 @@ export function buildLinkIndex(
|
|
|
197
197
|
notes.push(note);
|
|
198
198
|
|
|
199
199
|
// The anchors the index recorded, rather than a second reading of the
|
|
200
|
-
// same headings — the disagreement
|
|
200
|
+
// same headings — the disagreement the anchor half removes.
|
|
201
201
|
anchors.set(note, new Set((record.anchors ?? []).map((a) => a.slug)));
|
|
202
202
|
|
|
203
203
|
if (typeof fm.shortcode === "string" && fm.shortcode) {
|
|
204
204
|
// Canonical addresses only. Every written target expands to one
|
|
205
|
-
// before lookup
|
|
205
|
+
// before lookup, so there is nothing left for a short key to
|
|
206
206
|
// answer — and the short key was harmful: `type/shortcode` is
|
|
207
207
|
// system-blind, set with a plain `Map.set`, so two notes in one
|
|
208
208
|
// package sharing a `(type, shortcode)` across systems silently
|
|
@@ -245,7 +245,7 @@ export function buildLinkIndex(
|
|
|
245
245
|
/** The searchable universe a `dataview` table draws its rows from. */
|
|
246
246
|
const tableDocs = notes.map((n) => ({
|
|
247
247
|
// Package present for a `WHERE … package = "…"` clause, synthesised
|
|
248
|
-
// rather than authored — see {@link searchableFrontmatter}
|
|
248
|
+
// rather than authored — see {@link searchableFrontmatter}.
|
|
249
249
|
fm: searchableFrontmatter(n.fm, pkg),
|
|
250
250
|
path: n.rel,
|
|
251
251
|
tld: n.rel.split("/")[0],
|
|
@@ -259,14 +259,14 @@ export function buildLinkIndex(
|
|
|
259
259
|
* @returns {Array<{target: string, anchor: string, text: string,
|
|
260
260
|
* occurrence: number, labelled: boolean}>} `target` is `""` for a
|
|
261
261
|
* same-page `[[#anchor]]`; `labelled` says whether the link carries the
|
|
262
|
-
* `|` every link must have
|
|
262
|
+
* `|` every link must have.
|
|
263
263
|
*/
|
|
264
264
|
function linksOf(note) {
|
|
265
265
|
let body = note.body;
|
|
266
266
|
if (/^[ \t]*(?:`{3,}|~{3,})[ \t]*(?:dataview|sql)\b/im.test(body)) {
|
|
267
267
|
body = expandContentTables(body, {
|
|
268
268
|
// Unfiltered: every note in the tree is this package's, so
|
|
269
|
-
// there is no other package's note to exclude
|
|
269
|
+
// there is no other package's note to exclude.
|
|
270
270
|
docs: tableDocs,
|
|
271
271
|
linkable: (d) => Boolean(d.fm.shortcode),
|
|
272
272
|
source: note.file,
|
|
@@ -316,7 +316,7 @@ export function buildLinkIndex(
|
|
|
316
316
|
* Every indexed entry an address names, matching only the segments it
|
|
317
317
|
* supplies.
|
|
318
318
|
*
|
|
319
|
-
* This is the whole
|
|
319
|
+
* This is the whole resolution rule in one place: a written
|
|
320
320
|
* address is a *partial* one, unsupplied segments are wildcards, and the
|
|
321
321
|
* caller requires exactly one hit. Nothing here decides an ambiguity — zero
|
|
322
322
|
* and many are different findings with different fixes, so the count is
|
|
@@ -354,7 +354,7 @@ export function buildLinkIndex(
|
|
|
354
354
|
function resolveAddress(target, keyPath) {
|
|
355
355
|
const qualified = readQualifier(target, types, packages);
|
|
356
356
|
if (!qualified || qualified.reason) return undefined;
|
|
357
|
-
// Every omitted segment defaults from where the link is written
|
|
357
|
+
// Every omitted segment defaults from where the link is written,
|
|
358
358
|
// so the target expands to exactly one canonical address and this is a
|
|
359
359
|
// plain lookup. There is no candidate set, and therefore no single-hit
|
|
360
360
|
// rule and no ambiguity to report.
|
|
@@ -370,13 +370,13 @@ export function buildLinkIndex(
|
|
|
370
370
|
* Every foreign manifest entry an address names, in package order.
|
|
371
371
|
*
|
|
372
372
|
* A written target is a **partial** address, so this matches on the
|
|
373
|
-
* segments it supplies and wildcards the rest
|
|
373
|
+
* segments it supplies and wildcards the rest. A target naming a
|
|
374
374
|
* package necessarily names its system too — omission runs left to right —
|
|
375
375
|
* so the fully qualified form matches at most one entry; a shorter one
|
|
376
376
|
* names no package, and resolves against any foreign package that
|
|
377
377
|
* publishes it. Either way only exactly one hit resolves. Two claimants make it ambiguous, which is a different finding
|
|
378
378
|
* from resolving nowhere and has a different fix, so the count is returned
|
|
379
|
-
* rather than collapsed here
|
|
379
|
+
* rather than collapsed here.
|
|
380
380
|
*
|
|
381
381
|
* @param {string} target - The link target.
|
|
382
382
|
* @returns {object[]} The foreign entries, each carrying its `package`.
|
|
@@ -384,7 +384,7 @@ export function buildLinkIndex(
|
|
|
384
384
|
function foreignHits(target, keyPath) {
|
|
385
385
|
const q = readQualifier(target, types, packages);
|
|
386
386
|
if (!q || q.reason) return [];
|
|
387
|
-
// An omitted package means *this* package
|
|
387
|
+
// An omitted package means *this* package, so a short form
|
|
388
388
|
// addresses nothing foreign and never reaches a dependency's index.
|
|
389
389
|
// Reaching another package is the fully qualified form's job, and
|
|
390
390
|
// saying so is the whole point: a link that resolved into `sohl` only
|
|
@@ -428,7 +428,7 @@ export function buildLinkIndex(
|
|
|
428
428
|
/**
|
|
429
429
|
* Resolve a link target the way both builds do, or `undefined`. Every
|
|
430
430
|
* link is an address, so this is {@link resolveAddress} under the name
|
|
431
|
-
* the walkers use
|
|
431
|
+
* the walkers use.
|
|
432
432
|
*/
|
|
433
433
|
resolve: resolveAddress,
|
|
434
434
|
resolveAddress,
|
|
@@ -453,7 +453,7 @@ export function buildLinkIndex(
|
|
|
453
453
|
const SITE_HOST = /^(?:[a-z0-9-]+\.)*heroiclands\.org$/i;
|
|
454
454
|
|
|
455
455
|
/**
|
|
456
|
-
* Every package landing this build can name, as `package` → base
|
|
456
|
+
* Every package landing this build can name, as `package` → base.
|
|
457
457
|
*
|
|
458
458
|
* **A landing needs no manifest, and that is what makes it work.** The link
|
|
459
459
|
* manifest indexes content notes, and a homepage is deliberately not one — it
|
|
@@ -578,13 +578,13 @@ function readAddress(url, packages) {
|
|
|
578
578
|
* **What is checkable, stated plainly.** Only an address into this site is, and
|
|
579
579
|
* only against facts this build already holds:
|
|
580
580
|
*
|
|
581
|
-
* - A **retired content type** in the path. The engine knows
|
|
581
|
+
* - A **retired content type** in the path. The engine knows the retired names
|
|
582
582
|
* and what replaced it, so this is a fact rather than a guess — and it is
|
|
583
583
|
* exactly the SoHL defect.
|
|
584
584
|
* - A **hardcoded absolute URL** into this package's own prefix, or into one a
|
|
585
585
|
* a fetched index names. Every one of them has a better form to write, which
|
|
586
586
|
* is why every one is reported — including a bare `/<package>/`, which names
|
|
587
|
-
* another package's landing
|
|
587
|
+
* another package's landing.
|
|
588
588
|
*
|
|
589
589
|
* That last case was exempt until the better form was identified, on the
|
|
590
590
|
* reasoning that a landing is in no link manifest so nothing could resolve it.
|
|
@@ -658,7 +658,7 @@ export function auditHomepageLinks(index) {
|
|
|
658
658
|
// Landings first, and by the roster rather than by the manifest
|
|
659
659
|
// package set: a landing is addressable in a repository that
|
|
660
660
|
// has fetched no index at all, which is the case the fence creates
|
|
661
|
-
// and the case this rule exists for
|
|
661
|
+
// and the case this rule exists for.
|
|
662
662
|
const landing = landingTarget(url, bases);
|
|
663
663
|
if (landing) {
|
|
664
664
|
report(
|
|
@@ -749,7 +749,7 @@ export function auditHomepageLinks(index) {
|
|
|
749
749
|
*
|
|
750
750
|
* **How the link is *written* is a separate finding from where it points**, and
|
|
751
751
|
* the two are kept apart because the corrections differ. An unlabelled link
|
|
752
|
-
*
|
|
752
|
+
* has to become `[[type-shortcode|Text]]`; a labelled one whose target
|
|
753
753
|
* resolves nowhere has a shortcode to fix. Reporting a bare `[[Name]]` as a
|
|
754
754
|
* dead address would send an author hunting for a note that was never named.
|
|
755
755
|
*
|
|
@@ -761,7 +761,7 @@ export function auditHomepageLinks(index) {
|
|
|
761
761
|
* carries a `reason` from {@link LINK_FINDING_REASONS} —
|
|
762
762
|
* `"not-an-address"`, `"unknown-type"`, `"ambiguous"` (with the claiming
|
|
763
763
|
* `packages`), or `"unresolved"` — and every one of them is an **error**:
|
|
764
|
-
* the three resolvers agree on severity for every class
|
|
764
|
+
* the three resolvers agree on severity for every class.
|
|
765
765
|
*/
|
|
766
766
|
export function auditLinks(index) {
|
|
767
767
|
const { notes, anchors, linksOf, resolve, manifestHit, isAddress } = index;
|
|
@@ -792,7 +792,7 @@ export function auditLinks(index) {
|
|
|
792
792
|
for (const note of notes) {
|
|
793
793
|
for (const { target, anchor, text, occurrence, labelled } of linksOf(note)) {
|
|
794
794
|
// The label is required whatever the link part is, an anchor
|
|
795
|
-
// included — so this is tested before the same-page form
|
|
795
|
+
// included — so this is tested before the same-page form.
|
|
796
796
|
if (!labelled) {
|
|
797
797
|
unlabelledLinks.push({
|
|
798
798
|
note,
|
|
@@ -824,7 +824,7 @@ export function auditLinks(index) {
|
|
|
824
824
|
// Two packages publish the short address, so it names neither.
|
|
825
825
|
// Reported as its own class: "no document has that identity" is
|
|
826
826
|
// false here — two do — and the fix is the qualified form
|
|
827
|
-
// rather than a corrected shortcode
|
|
827
|
+
// rather than a corrected shortcode.
|
|
828
828
|
deadAddresses.push({
|
|
829
829
|
...at,
|
|
830
830
|
reason: "ambiguous",
|
package/engine/content-lint.mjs
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* Linting a content tree's **addresses** — the rules every package's notes are
|
|
16
16
|
* authored against, wherever those notes live.
|
|
17
17
|
*
|
|
18
|
-
* These rules
|
|
19
|
-
* consequences and no upside
|
|
18
|
+
* These rules do not live in a consumer's `utils/`, which has two
|
|
19
|
+
* consequences and no upside. `thalorna` and `kethira` notes were checked
|
|
20
20
|
* by nothing at all, so the packages most likely to carry authoring mistakes
|
|
21
21
|
* were the ones nothing inspected. And one rule with two implementations can
|
|
22
22
|
* disagree without anything detecting it, which the canonical-separator
|
|
@@ -30,28 +30,24 @@
|
|
|
30
30
|
* being the only hyphen in the string.
|
|
31
31
|
* 2. **Uniqueness** — `(type, shortcode)` names one note.
|
|
32
32
|
* 3. **The package's own address** — exactly one note claims `/<package>/`,
|
|
33
|
-
* which is {@link checkHomepageCount}
|
|
33
|
+
* which is {@link checkHomepageCount}. It belongs here for the same
|
|
34
34
|
* reason the other two do: it is a statement about which note holds which
|
|
35
35
|
* address, it needs no `site:` configuration to decide, and a package with
|
|
36
36
|
* no front page is misconfigured whether or not anyone runs a site build.
|
|
37
37
|
* 4. **Vacated addresses** — a `renamedFrom:` entry names an address this note
|
|
38
|
-
*
|
|
38
|
+
* once held and nothing holds now. It is the same statement as
|
|
39
39
|
* rule 2 read backwards, and it needs the same whole-tree view: an entry can
|
|
40
40
|
* only be checked against every *other* note's address, and two notes
|
|
41
41
|
* claiming one predecessor is the uniqueness rule applied to the past.
|
|
42
42
|
*
|
|
43
43
|
* **Nothing here writes.** A check reports and an author fixes.
|
|
44
44
|
*
|
|
45
|
-
* **
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* qualifier themselves. The
|
|
50
|
-
*
|
|
51
|
-
* field itself is retired now (#180), refused from `retired-fields.mjs`. Removing it was
|
|
52
|
-
* verified output-neutral beforehand: across 1,735 stripped notes,
|
|
53
|
-
* `package compile` produced byte-identical `build/packs-json` and the site
|
|
54
|
-
* build byte-identical `site/content`.
|
|
45
|
+
* **There is deliberately no third rule** requiring every note to repeat its
|
|
46
|
+
* own `type-shortcode` address in a top-level `aliases:` list. It would serve
|
|
47
|
+
* exactly one reader — **Obsidian**, so `[[type-shortcode]]` resolves in the
|
|
48
|
+
* editor — and nothing in the build reads it: both resolvers parse the hyphen
|
|
49
|
+
* qualifier themselves. The field is retired, refused from
|
|
50
|
+
* `retired-fields.mjs`.
|
|
55
51
|
*
|
|
56
52
|
* **What is deliberately absent.** Corpus reachability — "every Rules document
|
|
57
53
|
* is reachable from the book's root" — is a statement about what one package
|
|
@@ -67,7 +63,7 @@ import path from "node:path";
|
|
|
67
63
|
import { ADDRESS_SEGMENT_PATTERN } from "./address-charset.mjs";
|
|
68
64
|
import { positionInFrontmatter } from "./diagnostics.mjs";
|
|
69
65
|
import { assertStatedScope } from "./helpers.mjs";
|
|
70
|
-
// The corpus, read from the one pass that derives it
|
|
66
|
+
// The corpus, read from the one pass that derives it.
|
|
71
67
|
import { authoredFrontmatter, indexRecordsFor, isNoteRecord, noteFile } from "./content-index.mjs";
|
|
72
68
|
import { checkHomepageCount, isHomepage } from "./homepage.mjs";
|
|
73
69
|
import { declaresRenamedFrom, renamedFrom, renamedFromEntries } from "./note-renames.mjs";
|
|
@@ -78,7 +74,7 @@ import { declaresRenamedFrom, renamedFrom, renamedFromEntries } from "./note-ren
|
|
|
78
74
|
* This is {@link ADDRESS_SEGMENT_PATTERN}, not a second copy of it. A shortcode
|
|
79
75
|
* is the last segment of a canonical address, and the rule it is held to is the
|
|
80
76
|
* rule *every* segment is held to — so the two are one constant rather than two
|
|
81
|
-
* free to drift apart
|
|
77
|
+
* free to drift apart. The name survives because this is where the rule
|
|
82
78
|
* is applied to a note.
|
|
83
79
|
*
|
|
84
80
|
* Case is deliberately **not** constrained: hundreds of authored shortcodes are
|
|
@@ -112,7 +108,7 @@ export function isValidShortcode(value) {
|
|
|
112
108
|
* `Templates/`, a `README`, a repository's own `CLAUDE.md` — has no type, is
|
|
113
109
|
* neither addressed nor addressable, and would fail rules it can never satisfy.
|
|
114
110
|
*
|
|
115
|
-
* **Read from the index, not from a walk of this pass's own
|
|
111
|
+
* **Read from the index, not from a walk of this pass's own**. The
|
|
116
112
|
* `lint` command already derives the index — its link check and its `sql`
|
|
117
113
|
* tables are built from it — and then walked the tree a second time to get
|
|
118
114
|
* here, so one command held two answers to "which files are the corpus?" and
|
|
@@ -162,7 +158,7 @@ function collectNotes(contentBase, { skipDirectories, config, records, problems
|
|
|
162
158
|
}
|
|
163
159
|
|
|
164
160
|
/**
|
|
165
|
-
* What one note's `renamedFrom:` says, checked against itself
|
|
161
|
+
* What one note's `renamedFrom:` says, checked against itself.
|
|
166
162
|
*
|
|
167
163
|
* The entries a note can be wrong about on its own: a value that is not a
|
|
168
164
|
* shortcode, one naming the address the note holds *now*, one written twice.
|
|
@@ -276,7 +272,7 @@ function checkRenamedFrom({ fm, file }, raw) {
|
|
|
276
272
|
* @param {object} [opts.config] - The resolved build configuration, which the
|
|
277
273
|
* corpus is derived against.
|
|
278
274
|
* @param {readonly object[]} [opts.records] - Index records the caller already
|
|
279
|
-
* derived, so a command reads one corpus
|
|
275
|
+
* derived, so a command reads one corpus.
|
|
280
276
|
* @param {object[]} [opts.problems] - Collects the notes the index cannot
|
|
281
277
|
* record, instead of letting one of them silence the lint.
|
|
282
278
|
* @returns {{findings: Array<{file: string, line?: number, column?: number,
|
|
@@ -348,17 +344,17 @@ export function lintContentTree(
|
|
|
348
344
|
// what a tree that failed to check out produces — so the lint would go
|
|
349
345
|
// green on the one state it most needs to catch.
|
|
350
346
|
//
|
|
351
|
-
// The state that catches is an **empty walk**, not an empty key set
|
|
347
|
+
// The state that catches is an **empty walk**, not an empty key set.
|
|
352
348
|
// Notes may be keyless: a folder document carries no `shortcode`, and a
|
|
353
349
|
// tree of them is populated, correct, and unkeyed. Reporting that as a
|
|
354
350
|
// missing checkout trains its author to stop reading the output — the one
|
|
355
351
|
// thing this guard needs them to do. A tree holding notes is therefore a
|
|
356
352
|
// tree; only a tree holding none is the absent one.
|
|
357
353
|
//
|
|
358
|
-
// The homepage
|
|
354
|
+
// The homepage is not the headline example, though it was once addressed
|
|
359
355
|
// by the package rather than by a slug — so a `publish.site: homepage`
|
|
360
356
|
// package had a tree with exactly one note and no key at all. It carries an
|
|
361
|
-
// address like every other note now
|
|
357
|
+
// address like every other note now; the guard is unchanged, because
|
|
362
358
|
// what it reads was never the key count.
|
|
363
359
|
if (notes.length === 0) {
|
|
364
360
|
findings.push({
|
|
@@ -401,8 +397,8 @@ export function lintContentTree(
|
|
|
401
397
|
}
|
|
402
398
|
}
|
|
403
399
|
|
|
404
|
-
// The two questions about a declared rename that need the whole tree
|
|
405
|
-
//
|
|
400
|
+
// The two questions about a declared rename that need the whole tree.
|
|
401
|
+
// Both are the uniqueness rule above, applied to the past: an
|
|
406
402
|
// address has one holder, so it has one successor and it cannot be both
|
|
407
403
|
// vacated and occupied.
|
|
408
404
|
for (const [claim, claimants] of claimedPredecessors) {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* ships them — read from the repository's `package-build.config.yaml`.
|
|
17
17
|
*
|
|
18
18
|
* Both values are **derived**, not declared: `config.mjs` is the single place
|
|
19
|
-
* the configuration is resolved
|
|
19
|
+
* the configuration is resolved. This module survives as the import
|
|
20
20
|
* path the link resolver and the compilers have always used, so that the values
|
|
21
21
|
* can still be mocked in one place and so no consumer has to learn a new
|
|
22
22
|
* spelling for them.
|
|
@@ -35,13 +35,11 @@ import { loadPackConfig } from "./pack-config.mjs";
|
|
|
35
35
|
* repository's identity in the address space, not a switch — and never dead
|
|
36
36
|
* configuration, whatever else changes.
|
|
37
37
|
*
|
|
38
|
-
* It
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* retired and declaring it now fails the build; this value stays, here, where
|
|
44
|
-
* it is declared once.
|
|
38
|
+
* It is not a **selector**. Every content tree is single-package — each is
|
|
39
|
+
* single-sourced in the repository that ships it — so a `package:` field on a
|
|
40
|
+
* note would restate this constant once per note, and a value matching nothing
|
|
41
|
+
* would filter the whole tree out in silence. That field is refused, and this
|
|
42
|
+
* value stays here, declared once.
|
|
45
43
|
*
|
|
46
44
|
* Stable across compilation targets. If this content were ever compiled for a
|
|
47
45
|
* second game system, it would still be published as `sohl` — only the Foundry
|
|
@@ -65,7 +63,7 @@ export function contentPackage() {
|
|
|
65
63
|
* here: a note is published under `sohl` and its documents are addressed as
|
|
66
64
|
* `Compendium.sohl.<pack>.<Type>.<id>`. In `sohl-thalorna` the two differ
|
|
67
65
|
* (`thalorna` vs `sohl-thalorna`), which is why they are separate values rather
|
|
68
|
-
* than one — treating them as interchangeable is
|
|
66
|
+
* than one — treating them as interchangeable is the mistake.
|
|
69
67
|
*
|
|
70
68
|
* Configured rather than read from the manifest so the link resolver stays
|
|
71
69
|
* filesystem-free and unit-testable. `assertPackageIdMatchesManifestFile` in
|
package/engine/content-slug.mjs
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* {@link slugify} reduces a piece of prose — a heading, a document name — to a
|
|
18
18
|
* URL-safe token. It is **not** how a page is addressed: a note's URL is its
|
|
19
19
|
* address, `type-shortcode`, derived in `engine/content-address.mjs` and
|
|
20
|
-
* touching no display string at all
|
|
20
|
+
* touching no display string at all.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* It is not the other way round: a `contentSlug` here that
|
|
23
23
|
* derived a page's URL from `name.full`, abbreviating it through a table of 200
|
|
24
24
|
* words so the result stayed short, and a `findSlugCollisions` to catch the two
|
|
25
25
|
* notes that would then claim one URL. Its own header justified the readability
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
// The fence syntax is stated once, in `code-fences.mjs`, so the scanner that
|
|
61
61
|
// *skips* code and the one that expands a `dataview` block cannot disagree
|
|
62
|
-
// about where a block begins and ends
|
|
62
|
+
// about where a block begins and ends.
|
|
63
63
|
import { FENCE_LINE } from "./code-fences.mjs";
|
|
64
64
|
|
|
65
65
|
/** What a cell shows when its expression resolves to nothing. */
|
|
@@ -1084,7 +1084,7 @@ export function renderContentTable(spec, rows, linkable, self) {
|
|
|
1084
1084
|
* reason: string, line: number}>, lineMap: Array<{line: number,
|
|
1085
1085
|
* generated: boolean}>}} `lineMap` is parallel to the emitted lines and says
|
|
1086
1086
|
* which authored line each came from, so a diagnostic about the expanded
|
|
1087
|
-
* body can name an authored position
|
|
1087
|
+
* body can name an authored position. An `errors` entry carries the
|
|
1088
1088
|
* 0-based line of the directive that failed, for the same reason.
|
|
1089
1089
|
*/
|
|
1090
1090
|
/**
|
|
@@ -1121,12 +1121,12 @@ export function expandContentTables(
|
|
|
1121
1121
|
let sqlOrdinal = 0;
|
|
1122
1122
|
// One entry per `dataview` directive still authored, so a caller can say how
|
|
1123
1123
|
// much of the corpus is still on the retiring language without this pass
|
|
1124
|
-
// failing a build over it
|
|
1124
|
+
// failing a build over it.
|
|
1125
1125
|
const warnings = [];
|
|
1126
1126
|
const lines = String(markdown ?? "").split("\n");
|
|
1127
1127
|
const out = [];
|
|
1128
1128
|
// Which authored line each emitted line came from, so a later pass can
|
|
1129
|
-
// report an authored position for text this one moved
|
|
1129
|
+
// report an authored position for text this one moved. A generated
|
|
1130
1130
|
// row has no authored line of its own, so it is blamed on the directive
|
|
1131
1131
|
// that produced it — the thing an author can actually edit — and marked,
|
|
1132
1132
|
// because its columns mean nothing.
|
|
@@ -1203,8 +1203,7 @@ export function expandContentTables(
|
|
|
1203
1203
|
line: i,
|
|
1204
1204
|
column: indent.length + 1,
|
|
1205
1205
|
reason:
|
|
1206
|
-
"`dataview` content tables are being replaced by `sql` over the " +
|
|
1207
|
-
"content index (#246)",
|
|
1206
|
+
"`dataview` content tables are being replaced by `sql` over the " + "content index",
|
|
1208
1207
|
});
|
|
1209
1208
|
// `dataview allow-empty` says a table selecting nothing is the intended
|
|
1210
1209
|
// state. Spelled on the fence rather than in the query, because it is a
|
|
@@ -1219,7 +1218,7 @@ export function expandContentTables(
|
|
|
1219
1218
|
// a header with nothing under it makes that indistinguishable from
|
|
1220
1219
|
// a category that is legitimately empty. Eight tables in `sohl`'s
|
|
1221
1220
|
// `Rules/Bestiary.md` published that way for months after the
|
|
1222
|
-
// `creature` → `being` rename, and no build said a word
|
|
1221
|
+
// `creature` → `being` rename, and no build said a word.
|
|
1223
1222
|
if (rows.length === 0 && !allowEmpty) {
|
|
1224
1223
|
errors.push({
|
|
1225
1224
|
source,
|
|
@@ -1239,7 +1238,7 @@ export function expandContentTables(
|
|
|
1239
1238
|
directive: block.join("\n"),
|
|
1240
1239
|
reason: err.message,
|
|
1241
1240
|
// The fence's own line, so a table error is as locatable as
|
|
1242
|
-
// everything else the build reports
|
|
1241
|
+
// everything else the build reports.
|
|
1243
1242
|
line: i,
|
|
1244
1243
|
});
|
|
1245
1244
|
block.forEach((text, k) => emit(text, i + k));
|
package/engine/content-tree.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* notices — the LevelDB packs are written, the deploy mirrors them, and the
|
|
21
21
|
* defect surfaces as "my compendiums are blank" long after the build went green.
|
|
22
22
|
*
|
|
23
|
-
* `assets/content/` is this repository's own source
|
|
23
|
+
* `assets/content/` is this repository's own source, but an empty or
|
|
24
24
|
* partial tree is still possible — a bad checkout, or a sparse clone. This
|
|
25
25
|
* counts what is actually there so the build can refuse.
|
|
26
26
|
*/
|