@heroiclands/package-build 20.4.0 → 20.6.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 +288 -0
- package/CONTENT.md +213 -20
- package/README.md +19 -1
- package/bin/content-build.mjs +135 -32
- package/bin/package-build.mjs +46 -13
- package/content-config.mjs +345 -101
- package/docs/api.md +1352 -0
- package/docs/commands.md +1609 -0
- package/docs/configuration.md +1432 -0
- package/docs/content-format.md +16 -6
- package/docs/diagnostics.md +356 -0
- package/docs/getting-started.md +813 -0
- package/docs/project-setup.md +469 -0
- package/engine/actor-compiler.mjs +30 -27
- package/engine/address-diff.mjs +45 -41
- package/engine/base-compiler.mjs +6 -0
- package/engine/bundles.mjs +9 -0
- package/engine/content-address.mjs +9 -9
- package/engine/content-index.mjs +44 -23
- package/engine/content-links.mjs +44 -11
- package/engine/content-lint.mjs +44 -10
- package/engine/content-tables.mjs +32 -27
- package/engine/folder-notes.mjs +4 -2
- package/engine/frontmatter-lint.mjs +35 -38
- package/engine/generate.mjs +5 -0
- package/engine/helpers.mjs +86 -32
- package/engine/index.mjs +12 -2
- package/engine/journals.mjs +9 -0
- package/engine/note-claims.mjs +18 -10
- package/engine/note-schemas.mjs +0 -5
- package/engine/note-vocabulary.mjs +32 -31
- package/engine/pack-config.mjs +26 -12
- package/engine/pack-router.mjs +0 -0
- package/engine/pdf-build.mjs +464 -0
- package/engine/pdf-fonts.mjs +420 -0
- package/engine/pdf-render.mjs +876 -0
- package/engine/pdf-toc.mjs +525 -0
- package/engine/scenes.mjs +14 -5
- package/engine/schema-check.mjs +1 -1
- package/engine/site-build.mjs +21 -3
- package/engine/web-wikilinks.mjs +6 -3
- package/engine/wikilinks.mjs +2 -4
- package/hm3/actors.mjs +8 -0
- package/hm3/items.mjs +8 -0
- package/package.json +1 -1
- package/release.mjs +63 -3
- package/sohl/actors.mjs +8 -0
- package/sohl/items.mjs +8 -0
- package/sohl/note-schemas.mjs +5 -5
- package/types/content-config.d.mts +66 -15
- package/types/engine/actor-compiler.d.mts +34 -30
- package/types/engine/address-diff.d.mts +57 -3
- package/types/engine/base-compiler.d.mts +10 -2
- package/types/engine/bundles.d.mts +9 -0
- package/types/engine/content-address.d.mts +9 -9
- package/types/engine/content-index.d.mts +57 -13
- package/types/engine/content-lint.d.mts +6 -4
- package/types/engine/content-tables.d.mts +49 -18
- package/types/engine/frontmatter-lint.d.mts +3 -2
- package/types/engine/helpers.d.mts +105 -31
- package/types/engine/index.d.mts +4 -0
- package/types/engine/journals.d.mts +9 -0
- package/types/engine/note-claims.d.mts +17 -10
- package/types/engine/note-vocabulary.d.mts +23 -196
- package/types/engine/pack-config.d.mts +4 -4
- package/types/engine/pdf-build.d.mts +42 -0
- package/types/engine/pdf-fonts.d.mts +30 -0
- package/types/engine/pdf-render.d.mts +156 -0
- package/types/engine/pdf-toc.d.mts +114 -0
- package/types/engine/scenes.d.mts +10 -1
- package/types/engine/schema-check.d.mts +2 -2
- package/types/engine/site-build.d.mts +34 -6
- package/types/engine/wikilinks.d.mts +2 -3
- package/types/hm3/actors.d.mts +8 -0
- package/types/hm3/items.d.mts +8 -0
- package/types/release.d.mts +15 -4
- package/types/sohl/actors.d.mts +10 -2
- package/types/sohl/items.d.mts +8 -0
package/engine/bundles.mjs
CHANGED
|
@@ -119,6 +119,15 @@ export function loadBundleSources(sourceDirs) {
|
|
|
119
119
|
return documents;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Adventure pack compiler.
|
|
124
|
+
*
|
|
125
|
+
* Walks the content tree and compiles every `type: bundle` note into one
|
|
126
|
+
* Adventure per system, holding **copies** of the compiled documents its
|
|
127
|
+
* `contents` addresses name. Reading other passes' output is what
|
|
128
|
+
* {@link Bundles.readsPackOutputOf} declares, and what the generator orders the
|
|
129
|
+
* compile by.
|
|
130
|
+
*/
|
|
122
131
|
export class Bundles extends BasePackCompiler {
|
|
123
132
|
static id = "bundles";
|
|
124
133
|
static label = "bundle";
|
|
@@ -81,11 +81,11 @@ export const KB_PREFIX = "kb/";
|
|
|
81
81
|
* consumer deriving a `path` from a key drops the *package and the system*, not
|
|
82
82
|
* the package alone.
|
|
83
83
|
*
|
|
84
|
-
* The hyphen is a separator and never occurs inside a segment: a shortcode
|
|
85
|
-
*
|
|
86
|
-
* and a type is a bare word. That is the same charset
|
|
87
|
-
* parsing rests on, so the address and the key
|
|
88
|
-
* ends and the other begins.
|
|
84
|
+
* The hyphen is a separator and never occurs inside a segment: a shortcode
|
|
85
|
+
* matches `ADDRESS_SEGMENT_PATTERN` (`engine/address-charset.mjs`, enforced by
|
|
86
|
+
* `content-lint.mjs`) and a type is a bare word. That is the same charset
|
|
87
|
+
* guarantee positional key parsing rests on, so the address and the key
|
|
88
|
+
* cannot disagree about where one ends and the other begins.
|
|
89
89
|
*
|
|
90
90
|
* @param {object} fm - Parsed frontmatter.
|
|
91
91
|
* @returns {string} The address segment, e.g. `weapongear-dagger`.
|
|
@@ -311,10 +311,10 @@ export const CANONICAL_KEY_SEGMENTS = 4;
|
|
|
311
311
|
*
|
|
312
312
|
* Parsing is plain positional counting: split on the separator, require
|
|
313
313
|
* {@link CANONICAL_KEY_SEGMENTS} of them, and assign each position its field.
|
|
314
|
-
* **The charset rule is what makes that sound** — every segment
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
314
|
+
* **The charset rule is what makes that sound** — every segment matches
|
|
315
|
+
* `ADDRESS_SEGMENT_PATTERN` (`engine/address-charset.mjs`), so the hyphen is
|
|
316
|
+
* purely a separator and the count alone determines every field. That is
|
|
317
|
+
* enforced at each of the three sources rather than assumed of
|
|
318
318
|
* the data: shortcodes by `content-lint.mjs`, `contentPackage` by
|
|
319
319
|
* `defineConfig`, and types are bare words. Were any of them free to
|
|
320
320
|
* carry a hyphen, no amount of counting would recover the fields and the reader
|
package/engine/content-index.mjs
CHANGED
|
@@ -259,19 +259,6 @@ export function asciiAliases(aliases) {
|
|
|
259
259
|
return aliases.map((alias) => asciiName(alias)).filter((alias) => alias !== null);
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
/**
|
|
263
|
-
* Build one index record from a note's frontmatter and its place in the tree.
|
|
264
|
-
*
|
|
265
|
-
* @param {object} options - Options.
|
|
266
|
-
* @param {Record<string, any>} options.frontmatter - The note's parsed frontmatter.
|
|
267
|
-
* @param {string} options.relPath - Its path below the content root, POSIX-separated.
|
|
268
|
-
* @param {string} options.contentPackage - The package the tree compiles as.
|
|
269
|
-
* @param {string} [options.body] - The note's markdown body, for its anchors.
|
|
270
|
-
* @param {number} [options.bodyLine] - The 1-based file line the body starts on.
|
|
271
|
-
* @returns {Record<string, any>} The record, keys sorted at every depth.
|
|
272
|
-
* @throws {Error} When the note carries a key this module derives, which would
|
|
273
|
-
* otherwise be overwritten without a word.
|
|
274
|
-
*/
|
|
275
262
|
/**
|
|
276
263
|
* This note's Foundry addresses, or `null` where it has none.
|
|
277
264
|
*
|
|
@@ -410,6 +397,29 @@ function assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage) {
|
|
|
410
397
|
}
|
|
411
398
|
}
|
|
412
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Build one index record from a note's frontmatter and its place in the tree.
|
|
402
|
+
*
|
|
403
|
+
* The frontmatter as authored, plus what the index derives from it: the package
|
|
404
|
+
* it compiles as, its address, ASCII folds of its name and aliases, the anchors
|
|
405
|
+
* of its body, its Foundry block, the address of its documentation journal, and
|
|
406
|
+
* where the file sits within the tree.
|
|
407
|
+
*
|
|
408
|
+
* @param {object} options - Options.
|
|
409
|
+
* @param {Record<string, any>} options.frontmatter - The note's parsed frontmatter.
|
|
410
|
+
* @param {string} options.relPath - Its path below the content root, POSIX-separated.
|
|
411
|
+
* @param {string} [options.absPath] - The file, read only on the failing path to
|
|
412
|
+
* locate the offending key.
|
|
413
|
+
* @param {string} options.contentPackage - The package the tree compiles as.
|
|
414
|
+
* @param {string} [options.body] - The note's markdown body, for its anchors.
|
|
415
|
+
* @param {number} [options.bodyLine] - The 1-based file line the body starts on.
|
|
416
|
+
* @param {object} [options.manifest] - The package manifest, which the Foundry
|
|
417
|
+
* entries are derived against.
|
|
418
|
+
* @returns {Record<string, any>} The record, keys sorted at every depth.
|
|
419
|
+
* @throws {Error} When the note carries a key this module derives, which would
|
|
420
|
+
* otherwise be overwritten without a word. `file` and, where the file was
|
|
421
|
+
* read, `position` ride on the error.
|
|
422
|
+
*/
|
|
413
423
|
export function buildIndexRecord({
|
|
414
424
|
frontmatter,
|
|
415
425
|
relPath,
|
|
@@ -466,16 +476,6 @@ export function buildIndexRecord({
|
|
|
466
476
|
);
|
|
467
477
|
}
|
|
468
478
|
|
|
469
|
-
/**
|
|
470
|
-
* Read a content tree into index records, in the order they will be written.
|
|
471
|
-
*
|
|
472
|
-
* @param {string} contentBase - The content tree to walk.
|
|
473
|
-
* @param {object} options - Options.
|
|
474
|
-
* @param {string} options.contentPackage - The package the tree compiles as.
|
|
475
|
-
* @param {Array<string>} [options.skipDirectories] - Directory names to skip.
|
|
476
|
-
* @returns {Array<Record<string, any>>} The records, in a total order that does
|
|
477
|
-
* not depend on directory-read order.
|
|
478
|
-
*/
|
|
479
479
|
/**
|
|
480
480
|
* The record for an item note's **documentation journal**.
|
|
481
481
|
*
|
|
@@ -531,6 +531,27 @@ function buildDocRecord({ frontmatter, address, entry, file, contentPackage, anc
|
|
|
531
531
|
);
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Read a content tree into index records, in the order they will be written.
|
|
536
|
+
*
|
|
537
|
+
* An item note yields two records — the item, and the documentation journal
|
|
538
|
+
* that is a document in its own right.
|
|
539
|
+
*
|
|
540
|
+
* @param {string} contentBase - The content tree to walk.
|
|
541
|
+
* @param {object} options - Options.
|
|
542
|
+
* @param {string} options.contentPackage - The package the tree compiles as.
|
|
543
|
+
* @param {readonly string[]} options.skipDirectories - The walk's scope, stated
|
|
544
|
+
* by the caller. An absent one is the caller's omission, and
|
|
545
|
+
* {@link module:engine/helpers.walkMarkdownTree} throws on it.
|
|
546
|
+
* @param {object} [options.manifest] - The package manifest, which the Foundry
|
|
547
|
+
* entries are derived against.
|
|
548
|
+
* @param {object[]} [options.problems] - Supplied by a **reader**: a note that
|
|
549
|
+
* cannot be recorded is pushed here as a diagnostic and skipped. Omitted, the
|
|
550
|
+
* note throws — the contract the emitter needs, since an index missing a note
|
|
551
|
+
* asserts that it does not exist.
|
|
552
|
+
* @returns {Array<Record<string, any>>} The records, in a total order that does
|
|
553
|
+
* not depend on directory-read order.
|
|
554
|
+
*/
|
|
534
555
|
export function collectContentIndex(
|
|
535
556
|
contentBase,
|
|
536
557
|
{ contentPackage, skipDirectories, manifest, problems },
|
package/engine/content-links.mjs
CHANGED
|
@@ -301,17 +301,6 @@ export function buildLinkIndex(
|
|
|
301
301
|
return out;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
/**
|
|
305
|
-
* The note an **address** names, or `undefined`.
|
|
306
|
-
*
|
|
307
|
-
* The qualifier is read with {@link readQualifier} rather than a second
|
|
308
|
-
* copy of the rule, so this cannot drift from what the builds do — the two
|
|
309
|
-
* separators, the first-hyphen split, and the optional leading package
|
|
310
|
-
* segment.
|
|
311
|
-
*
|
|
312
|
-
* @param {string} target - The link target, anchor already removed.
|
|
313
|
-
* @returns {object|undefined} The note it addresses.
|
|
314
|
-
*/
|
|
315
304
|
/**
|
|
316
305
|
* Every indexed entry an address names, matching only the segments it
|
|
317
306
|
* supplies.
|
|
@@ -351,6 +340,20 @@ export function buildLinkIndex(
|
|
|
351
340
|
return matchAddress([...byKey], q).map(([, v]) => v);
|
|
352
341
|
}
|
|
353
342
|
|
|
343
|
+
/**
|
|
344
|
+
* The note an **address** names, or `undefined`.
|
|
345
|
+
*
|
|
346
|
+
* The qualifier is read with {@link readQualifier} rather than a second
|
|
347
|
+
* copy of the rule, so this cannot drift from what the builds do — the two
|
|
348
|
+
* separators, the first-hyphen split, and the optional leading package
|
|
349
|
+
* segment.
|
|
350
|
+
*
|
|
351
|
+
* @param {string} target - The link target, anchor already removed.
|
|
352
|
+
* @param {string} [keyPath] - The dotted frontmatter key path the link sits
|
|
353
|
+
* under, which supplies the system an omitted segment defaults to — see
|
|
354
|
+
* {@link blockSystem}. Body prose has none.
|
|
355
|
+
* @returns {object|undefined} The note it addresses.
|
|
356
|
+
*/
|
|
354
357
|
function resolveAddress(target, keyPath) {
|
|
355
358
|
const qualified = readQualifier(target, types, packages);
|
|
356
359
|
if (!qualified || qualified.reason) return undefined;
|
|
@@ -379,6 +382,9 @@ export function buildLinkIndex(
|
|
|
379
382
|
* rather than collapsed here.
|
|
380
383
|
*
|
|
381
384
|
* @param {string} target - The link target.
|
|
385
|
+
* @param {string} [keyPath] - The dotted frontmatter key path the link sits
|
|
386
|
+
* under, which supplies the system an omitted segment defaults to — see
|
|
387
|
+
* {@link blockSystem}. Body prose has none.
|
|
382
388
|
* @returns {object[]} The foreign entries, each carrying its `package`.
|
|
383
389
|
*/
|
|
384
390
|
function foreignHits(target, keyPath) {
|
|
@@ -411,6 +417,32 @@ export function buildLinkIndex(
|
|
|
411
417
|
return hits.length === 1 ? hits[0] : null;
|
|
412
418
|
}
|
|
413
419
|
|
|
420
|
+
/**
|
|
421
|
+
* The note or foreign entry a **frontmatter reference** names, or null.
|
|
422
|
+
*
|
|
423
|
+
* A `ref:` field holds a shortcode, not an address. The system persists it
|
|
424
|
+
* verbatim and resolves it at runtime against the items embedded on one
|
|
425
|
+
* actor, where packages do not exist: an actor assembled from several
|
|
426
|
+
* packages carries their items side by side. So a reference resolves when
|
|
427
|
+
* *any* reachable package declares the `(type, shortcode)` pair — package
|
|
428
|
+
* and system wildcarded, local notes first, then the fetched indexes.
|
|
429
|
+
*
|
|
430
|
+
* Distinct from {@link resolveAddress}, which defaults an omitted package
|
|
431
|
+
* to this one. That is the rule for a link, whose target is a document to
|
|
432
|
+
* point at; a reference names an item to stand beside.
|
|
433
|
+
*
|
|
434
|
+
* @param {string} target - The reference as `type-shortcode`.
|
|
435
|
+
* @returns {object|null} The note or foreign entry declaring it.
|
|
436
|
+
*/
|
|
437
|
+
function referenceHit(target) {
|
|
438
|
+
const q = readQualifier(target, types, packages);
|
|
439
|
+
if (!q || q.reason) return null;
|
|
440
|
+
const local = matchAddress([...byKey], q);
|
|
441
|
+
if (local.length) return local[0][1];
|
|
442
|
+
const abroad = matchAddress([...foreign.index], q);
|
|
443
|
+
return abroad.length ? abroad[0][1] : null;
|
|
444
|
+
}
|
|
445
|
+
|
|
414
446
|
return {
|
|
415
447
|
notes,
|
|
416
448
|
frontmatterLinks,
|
|
@@ -434,6 +466,7 @@ export function buildLinkIndex(
|
|
|
434
466
|
resolveAddress,
|
|
435
467
|
manifestHit,
|
|
436
468
|
foreignHits,
|
|
469
|
+
referenceHit,
|
|
437
470
|
/** Whether a target reads as a qualified address at all. */
|
|
438
471
|
isAddress: (target) => Boolean(readQualifier(target, types, packages)),
|
|
439
472
|
};
|
package/engine/content-lint.mjs
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
* disagree without anything detecting it, which the canonical-separator
|
|
23
23
|
* handling already did once on each side.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
25
|
+
* Five rules, all about a note's identity:
|
|
26
26
|
*
|
|
27
|
-
* 1. **Shape** — a `shortcode` is strictly ASCII-alphanumeric. It is
|
|
28
|
-
* identity key referenced from saved world data, and it is half of the
|
|
27
|
+
* 1. **Shape** — a `shortcode` is strictly lowercase ASCII-alphanumeric. It is
|
|
28
|
+
* the identity key referenced from saved world data, and it is half of the
|
|
29
29
|
* `type-shortcode` address, whose parse depends on the separating hyphen
|
|
30
30
|
* being the only hyphen in the string.
|
|
31
31
|
* 2. **Uniqueness** — `(type, shortcode)` names one note.
|
|
@@ -39,6 +39,10 @@
|
|
|
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
|
+
* 5. **The vocabulary a package's kind leaves it** — a package compiling no
|
|
43
|
+
* Foundry documents publishes `doc` and `homepage` notes and nothing else.
|
|
44
|
+
* It is here rather than with the claim check in `note-claims.mjs` because
|
|
45
|
+
* that check runs at compile, and this is the case where no compile runs.
|
|
42
46
|
*
|
|
43
47
|
* **Nothing here writes.** A check reports and an author fixes.
|
|
44
48
|
*
|
|
@@ -61,6 +65,9 @@ import fs from "node:fs";
|
|
|
61
65
|
import path from "node:path";
|
|
62
66
|
|
|
63
67
|
import { ADDRESS_SEGMENT_PATTERN } from "./address-charset.mjs";
|
|
68
|
+
// The kind that compiles nothing, and the vocabulary that leaves a tree with.
|
|
69
|
+
import { DOCUMENTATION_KIND, compilesFoundryDocuments } from "../content-config.mjs";
|
|
70
|
+
import { DOCUMENTATION_NOTE_TYPES } from "./note-claims.mjs";
|
|
64
71
|
import { positionInFrontmatter } from "./diagnostics.mjs";
|
|
65
72
|
import { assertStatedScope } from "./helpers.mjs";
|
|
66
73
|
// The corpus, read from the one pass that derives it.
|
|
@@ -69,7 +76,8 @@ import { checkHomepageCount, isHomepage } from "./homepage.mjs";
|
|
|
69
76
|
import { declaresRenamedFrom, renamedFrom, renamedFromEntries } from "./note-renames.mjs";
|
|
70
77
|
|
|
71
78
|
/**
|
|
72
|
-
* The shape every `shortcode` must match: ASCII letters and digits
|
|
79
|
+
* The shape every `shortcode` must match: lowercase ASCII letters and digits
|
|
80
|
+
* only.
|
|
73
81
|
*
|
|
74
82
|
* This is {@link ADDRESS_SEGMENT_PATTERN}, not a second copy of it. A shortcode
|
|
75
83
|
* is the last segment of a canonical address, and the rule it is held to is the
|
|
@@ -77,9 +85,10 @@ import { declaresRenamedFrom, renamedFrom, renamedFromEntries } from "./note-ren
|
|
|
77
85
|
* free to drift apart. The name survives because this is where the rule
|
|
78
86
|
* is applied to a note.
|
|
79
87
|
*
|
|
80
|
-
* Case is
|
|
81
|
-
*
|
|
82
|
-
*
|
|
88
|
+
* Case is held to that rule with no exception: two shortcodes differing only
|
|
89
|
+
* in case are two names nobody can tell apart, and `canonicalKey` lowercases
|
|
90
|
+
* every address it builds regardless, so a mixed-case shortcode addresses the
|
|
91
|
+
* same document as its lowercase spelling.
|
|
83
92
|
*
|
|
84
93
|
* A consuming system's *runtime* keeps its own copy of this pattern — it cannot
|
|
85
94
|
* import a build-time dependency into shipped code — and is expected to pin the
|
|
@@ -225,8 +234,9 @@ function checkRenamedFrom({ fm, file }, raw) {
|
|
|
225
234
|
...at(value),
|
|
226
235
|
severity: "error",
|
|
227
236
|
message:
|
|
228
|
-
`\`renamedFrom: ${value}\` is not strictly alphanumeric
|
|
229
|
-
`
|
|
237
|
+
`\`renamedFrom: ${value}\` is not strictly alphanumeric — ` +
|
|
238
|
+
`lowercase letters and digits only (${ADDRESS_SEGMENT_PATTERN.source}) — ` +
|
|
239
|
+
`so it is not an address this package ever published — a ` +
|
|
230
240
|
`shortcode is held to one charset whether it is current or past`,
|
|
231
241
|
});
|
|
232
242
|
continue;
|
|
@@ -285,6 +295,12 @@ export function lintContentTree(
|
|
|
285
295
|
) {
|
|
286
296
|
const findings = [];
|
|
287
297
|
const notes = collectNotes(contentBase, { skipDirectories, config, records, problems });
|
|
298
|
+
// Whether this package's note vocabulary is the narrowed one. Asked of the
|
|
299
|
+
// configuration once rather than per note, and defaulted to the wide
|
|
300
|
+
// vocabulary when a caller supplies none — an unconfigured lint holds a
|
|
301
|
+
// tree to the rules every package shares.
|
|
302
|
+
const narrowed =
|
|
303
|
+
Boolean(config) && !compilesFoundryDocuments(/** @type {{packageKind: string}} */ (config));
|
|
288
304
|
|
|
289
305
|
/** @type {Map<string, Array<{file: string, absPath: string}>>} */
|
|
290
306
|
const byKey = new Map();
|
|
@@ -299,6 +315,23 @@ export function lintContentTree(
|
|
|
299
315
|
// tree costs one pass rather than two.
|
|
300
316
|
const raw = () => fs.readFileSync(absPath, "utf8");
|
|
301
317
|
|
|
318
|
+
// Rule 5, and it is the whole of the check for a package that compiles
|
|
319
|
+
// nothing: no pass downstream would report the note, because the pass
|
|
320
|
+
// that reports an unclaimed type is a compile pass and none runs.
|
|
321
|
+
const type = typeof fm.type === "string" ? fm.type.trim() : "";
|
|
322
|
+
if (narrowed && type && !DOCUMENTATION_NOTE_TYPES.has(type)) {
|
|
323
|
+
findings.push({
|
|
324
|
+
file,
|
|
325
|
+
...positionInFrontmatter(raw(), "type", type),
|
|
326
|
+
severity: "error",
|
|
327
|
+
message:
|
|
328
|
+
`\`type: ${type}\` compiles to a Foundry document, and a ` +
|
|
329
|
+
`\`${DOCUMENTATION_KIND}\` package compiles none — so the ` +
|
|
330
|
+
`note has no destination. Its vocabulary is ` +
|
|
331
|
+
`${[...DOCUMENTATION_NOTE_TYPES].map((t) => `\`${t}\``).join(" and ")}`,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
302
335
|
// Before the keyless `continue` below, because a note declaring a
|
|
303
336
|
// rename while carrying no address of its own is exactly one of the
|
|
304
337
|
// things this reports — and reaching it after the skip would mean it
|
|
@@ -332,7 +365,8 @@ export function lintContentTree(
|
|
|
332
365
|
...positionInFrontmatter(raw(), "shortcode", String(shortcode)),
|
|
333
366
|
severity: "error",
|
|
334
367
|
message:
|
|
335
|
-
`shortcode "${shortcode}" is not strictly alphanumeric
|
|
368
|
+
`shortcode "${shortcode}" is not strictly alphanumeric — ` +
|
|
369
|
+
`lowercase letters and digits only (${ADDRESS_SEGMENT_PATTERN.source}); it ` +
|
|
336
370
|
`is the identity key and half of the ` +
|
|
337
371
|
`"${fm.type}-${shortcode}" address, whose parse needs the ` +
|
|
338
372
|
`separator to be the only hyphen`,
|
|
@@ -1059,17 +1059,32 @@ export function renderContentTable(spec, rows, linkable, self) {
|
|
|
1059
1059
|
/* ------------------------------------------------------------------------ */
|
|
1060
1060
|
|
|
1061
1061
|
/**
|
|
1062
|
-
*
|
|
1062
|
+
* The `WHERE` clause of a query, as authored, for a message that has to name
|
|
1063
|
+
* what matched nothing.
|
|
1064
|
+
*
|
|
1065
|
+
* The parsed spec holds an expression tree, and rendering that back to text
|
|
1066
|
+
* would be a second dialect of the query language to keep in step. The authored
|
|
1067
|
+
* line is what the author wrote and what they will edit, so it is what the
|
|
1068
|
+
* finding quotes.
|
|
1069
|
+
*
|
|
1070
|
+
* @param {string} query - The fence's contents.
|
|
1071
|
+
* @returns {string} The clause, or "" when the query has none.
|
|
1072
|
+
*/
|
|
1073
|
+
function whereText(query) {
|
|
1074
|
+
const match = /^\s*WHERE\s+(.+?)\s*$/im.exec(String(query ?? ""));
|
|
1075
|
+
return match ? `\`${match[1]}\`` : "";
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Expand every fenced `dataview` and `sql` block in a markdown body.
|
|
1063
1080
|
*
|
|
1064
1081
|
* A block that cannot be honoured — malformed or unsupported — is left in the
|
|
1065
1082
|
* body verbatim and reported in `errors`, so the failure is visible in the
|
|
1066
1083
|
* output as well as on the console. Every other code fence, and every code
|
|
1067
1084
|
* span, is left alone (that is how the syntax is documented).
|
|
1068
1085
|
*
|
|
1069
|
-
* A query that
|
|
1070
|
-
*
|
|
1071
|
-
* a category with no content yet is a normal state of the corpus rather than a
|
|
1072
|
-
* broken build.
|
|
1086
|
+
* A query that selects **no** note is an error unless the fence says
|
|
1087
|
+
* `allow-empty`, which states that an empty table is the intended result.
|
|
1073
1088
|
*
|
|
1074
1089
|
* @param {string} markdown - The note body, frontmatter already stripped.
|
|
1075
1090
|
* @param {object} ctx
|
|
@@ -1080,30 +1095,20 @@ export function renderContentTable(spec, rows, linkable, self) {
|
|
|
1080
1095
|
* @param {string} [ctx.source] - The note being expanded, for error reports.
|
|
1081
1096
|
* @param {ContentTableDoc} [ctx.self] - The note being expanded, as a searchable
|
|
1082
1097
|
* doc: what a query's `this` reads.
|
|
1098
|
+
* @param {object[]} [ctx.sqlTables] - This note's prepared `sql` results, in
|
|
1099
|
+
* document order, from
|
|
1100
|
+
* {@link module:engine/sql-tables.prepareSqlTables}. An `sql` directive with
|
|
1101
|
+
* no prepared result is an error: nothing here runs a query.
|
|
1083
1102
|
* @returns {{markdown: string, errors: Array<{source: string, directive: string,
|
|
1084
|
-
* reason: string, line: number
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1103
|
+
* reason: string, line: number, column?: number}>,
|
|
1104
|
+
* warnings: Array<{source: string, line: number, column: number,
|
|
1105
|
+
* reason: string}>, lineMap: Array<{line: number, generated: boolean}>}}
|
|
1106
|
+
* `lineMap` is parallel to the emitted lines and says which authored line
|
|
1107
|
+
* each came from, so a diagnostic about the expanded body can name an
|
|
1108
|
+
* authored position. An `errors` entry carries the 0-based line of the
|
|
1109
|
+
* directive that failed, for the same reason. `warnings` holds one entry per
|
|
1110
|
+
* `dataview` directive the body still authors.
|
|
1089
1111
|
*/
|
|
1090
|
-
/**
|
|
1091
|
-
* The `WHERE` clause of a query, as authored, for a message that has to name
|
|
1092
|
-
* what matched nothing.
|
|
1093
|
-
*
|
|
1094
|
-
* The parsed spec holds an expression tree, and rendering that back to text
|
|
1095
|
-
* would be a second dialect of the query language to keep in step. The authored
|
|
1096
|
-
* line is what the author wrote and what they will edit, so it is what the
|
|
1097
|
-
* finding quotes.
|
|
1098
|
-
*
|
|
1099
|
-
* @param {string} query - The fence's contents.
|
|
1100
|
-
* @returns {string} The clause, or "" when the query has none.
|
|
1101
|
-
*/
|
|
1102
|
-
function whereText(query) {
|
|
1103
|
-
const match = /^\s*WHERE\s+(.+?)\s*$/im.exec(String(query ?? ""));
|
|
1104
|
-
return match ? `\`${match[1]}\`` : "";
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
1112
|
export function expandContentTables(
|
|
1108
1113
|
markdown,
|
|
1109
1114
|
{
|
package/engine/folder-notes.mjs
CHANGED
|
@@ -54,7 +54,7 @@ import path from "node:path";
|
|
|
54
54
|
import log from "loglevel";
|
|
55
55
|
|
|
56
56
|
import { NO_SYSTEM, canonicalKey } from "./content-address.mjs";
|
|
57
|
-
import { isAddressSegment } from "./address-charset.mjs";
|
|
57
|
+
import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./address-charset.mjs";
|
|
58
58
|
import { makeId } from "./ids.mjs";
|
|
59
59
|
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
60
60
|
|
|
@@ -242,7 +242,9 @@ export function collectFolderNotes(notes, pkg) {
|
|
|
242
242
|
throw Object.assign(
|
|
243
243
|
new Error(
|
|
244
244
|
`folder shortcode "${shortcode}" is not strictly ` +
|
|
245
|
-
`alphanumeric
|
|
245
|
+
`alphanumeric — lowercase letters and digits only ` +
|
|
246
|
+
`(${ADDRESS_SEGMENT_PATTERN.source}) — so its address ` +
|
|
247
|
+
`would not parse: ${absPath}`,
|
|
246
248
|
),
|
|
247
249
|
{ absPath },
|
|
248
250
|
);
|
|
@@ -583,9 +583,9 @@ function checkKeyedMap(note, { field, segments, entries, raw, packs }) {
|
|
|
583
583
|
*
|
|
584
584
|
* **Two checks, in this order** — the charset, then the closed set. The
|
|
585
585
|
* charset is first because it is the more general statement about
|
|
586
|
-
* the same value: a value outside
|
|
587
|
-
* declares, and only once it is a well-formed term is the
|
|
588
|
-
* reason to refuse it.
|
|
586
|
+
* the same value: a value outside `ADDRESS_SEGMENT_PATTERN` is refused
|
|
587
|
+
* whatever the type declares, and only once it is a well-formed term is the
|
|
588
|
+
* type's own list the reason to refuse it.
|
|
589
589
|
*
|
|
590
590
|
* There is deliberately no third, retired-spelling check ahead of them
|
|
591
591
|
* accepting `user-guide` as a warning naming `userguide`. Every consumer tree
|
|
@@ -674,7 +674,8 @@ function checkSubType(note, { type, entry }) {
|
|
|
674
674
|
*
|
|
675
675
|
* @param {object} note - The note.
|
|
676
676
|
* @param {object} opts
|
|
677
|
-
* @param {
|
|
677
|
+
* @param {string} opts.type - The note's declared `type`, which scopes the
|
|
678
|
+
* groups checked against.
|
|
678
679
|
* @returns {object[]} Findings.
|
|
679
680
|
*/
|
|
680
681
|
function checkTags(note, { type }) {
|
|
@@ -729,6 +730,27 @@ const ART_FIELDS = Object.freeze([
|
|
|
729
730
|
Object.freeze({ key: "portrait", inData: true }),
|
|
730
731
|
]);
|
|
731
732
|
|
|
733
|
+
/**
|
|
734
|
+
* The keys a field declaration is authored at **inside a system block**.
|
|
735
|
+
*
|
|
736
|
+
* The first segment of each field's in-block key: `impact.die` is authored as
|
|
737
|
+
* `impact`, and a field whose shared source moved under `data:` is authored at
|
|
738
|
+
* the `legacyKey` it declares rather than at its dotted name. Keying on
|
|
739
|
+
* the name instead would report `sohl.species` as a property no `being` has,
|
|
740
|
+
* against exactly the notes the sweep has not reached yet.
|
|
741
|
+
*
|
|
742
|
+
* Written once and read twice: the note type's own declaration answers for the
|
|
743
|
+
* system whose vocabulary the caller's `schemas` are, and a second system's
|
|
744
|
+
* registry answers for its block. Two derivations of one thing would be
|
|
745
|
+
* free to disagree about which position a note authors.
|
|
746
|
+
*
|
|
747
|
+
* @param {readonly object[]|null|undefined} schema - A type's declarations.
|
|
748
|
+
* @returns {Set<string>} The in-block keys.
|
|
749
|
+
*/
|
|
750
|
+
function inBlockKeys(schema) {
|
|
751
|
+
return new Set(authoredFields(schema ?? []).map((f) => legacyKeyOf(f).split(".")[0]));
|
|
752
|
+
}
|
|
753
|
+
|
|
732
754
|
/**
|
|
733
755
|
* The in-block keys a type's own declarations claim for a *different* quantity.
|
|
734
756
|
*
|
|
@@ -756,27 +778,6 @@ const ART_FIELDS = Object.freeze([
|
|
|
756
778
|
* @returns {Set<string>} The in-block keys that are not the note-level field of
|
|
757
779
|
* the same name.
|
|
758
780
|
*/
|
|
759
|
-
/**
|
|
760
|
-
* The keys a field declaration is authored at **inside a system block**.
|
|
761
|
-
*
|
|
762
|
-
* The first segment of each field's in-block key: `impact.die` is authored as
|
|
763
|
-
* `impact`, and a field whose shared source moved under `data:` is authored at
|
|
764
|
-
* the `legacyKey` it declares rather than at its dotted name. Keying on
|
|
765
|
-
* the name instead would report `sohl.species` as a property no `being` has,
|
|
766
|
-
* against exactly the notes the sweep has not reached yet.
|
|
767
|
-
*
|
|
768
|
-
* Written once and read twice: the note type's own declaration answers for the
|
|
769
|
-
* system whose vocabulary the caller's `schemas` are, and a second system's
|
|
770
|
-
* registry answers for its block. Two derivations of one thing would be
|
|
771
|
-
* free to disagree about which position a note authors.
|
|
772
|
-
*
|
|
773
|
-
* @param {readonly object[]|null|undefined} schema - A type's declarations.
|
|
774
|
-
* @returns {Set<string>} The in-block keys.
|
|
775
|
-
*/
|
|
776
|
-
function inBlockKeys(schema) {
|
|
777
|
-
return new Set(authoredFields(schema ?? []).map((f) => legacyKeyOf(f).split(".")[0]));
|
|
778
|
-
}
|
|
779
|
-
|
|
780
781
|
function collidingBlockKeys(schema) {
|
|
781
782
|
const keys = new Set();
|
|
782
783
|
if (!Array.isArray(schema)) return keys;
|
|
@@ -909,8 +910,9 @@ function checkEmbeddedShortcodes(note, blockName) {
|
|
|
909
910
|
* @param {object} note - A note from the link index (`{fm, file, raw, type}`).
|
|
910
911
|
* @param {object} opts
|
|
911
912
|
* @param {Record<string, readonly object[]>} opts.schemas - Type → declaration.
|
|
912
|
-
* @param {object} [opts.index] - The link index, for the reference check
|
|
913
|
-
* absence skips that check rather than
|
|
913
|
+
* @param {object} [opts.index] - The link index, for the reference check, which
|
|
914
|
+
* runs through its `referenceHit`. Its absence skips that check rather than
|
|
915
|
+
* reporting every reference as dead.
|
|
914
916
|
* @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
|
|
915
917
|
* it declares, as `engine/note-vocabulary.mjs` states them. Supplied
|
|
916
918
|
* by the caller for the same reason `schemas` is: this module validates a
|
|
@@ -1466,19 +1468,14 @@ export function lintNote(
|
|
|
1466
1468
|
continue;
|
|
1467
1469
|
}
|
|
1468
1470
|
|
|
1469
|
-
// A reference names another note by shortcode
|
|
1470
|
-
//
|
|
1471
|
-
//
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1474
|
-
// **As an address, always** — which is now the only namespace there
|
|
1475
|
-
// is. A frontmatter reference is a bare address by construction:
|
|
1476
|
-
// there is no pipe to read intent from, and the field supplies the
|
|
1477
|
-
// type. The resolver once took a namespace argument, and omitting it
|
|
1478
|
-
// read every `ref:` value as an alias, which `type-shortcode` never was.
|
|
1471
|
+
// A reference names another note by shortcode, and the field supplies
|
|
1472
|
+
// the type, so the resolver is handed the whole `type-shortcode` pair.
|
|
1473
|
+
// It resolves in any reachable package: the value is persisted as
|
|
1474
|
+
// written and looked up at runtime among one actor's embedded items,
|
|
1475
|
+
// which come from every package the actor draws on.
|
|
1479
1476
|
if (field.ref && index && typeof value === "string" && value) {
|
|
1480
1477
|
const target = `${field.ref}-${value}`;
|
|
1481
|
-
if (!index.
|
|
1478
|
+
if (!index.referenceHit(target)) {
|
|
1482
1479
|
findings.push({
|
|
1483
1480
|
file: note.file,
|
|
1484
1481
|
...at(head, value),
|
package/engine/generate.mjs
CHANGED
|
@@ -403,6 +403,11 @@ export function unsatisfiedPassDependencies(running, config) {
|
|
|
403
403
|
* document type that routes nowhere. True for the first configured pack of
|
|
404
404
|
* the type, so one unroutable note yields one error rather than one per
|
|
405
405
|
* pack.
|
|
406
|
+
* @param {object} folderNotes - The folder index, which resolves a note's
|
|
407
|
+
* `packFolder` address to a Foundry folder id and its ancestor chain.
|
|
408
|
+
* @param {object} corpus - The corpus this compile derived once, shared by
|
|
409
|
+
* every pass so that no two answer "which files are the corpus?"
|
|
410
|
+
* differently.
|
|
406
411
|
* @returns {Promise<{errors: number, compiled: number}>} The compiler's error
|
|
407
412
|
* count (0 on success) and the number of entries it wrote.
|
|
408
413
|
*/
|