@heroiclands/package-build 22.14.1 → 22.14.3
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 +26 -0
- package/bin/content-build.mjs +6 -6
- package/bin/package-build.mjs +10 -0
- package/config.mjs +18 -1
- package/content-config.mjs +2 -2
- package/docs/api.md +63 -35
- package/docs/configuration.md +16 -7
- package/docs/content-format.md +157 -181
- package/docs/getting-started.md +42 -11
- package/docs/project-setup.md +5 -6
- package/engine/address.mjs +3 -5
- package/engine/asset-bindings.mjs +66 -0
- package/engine/calendar-notes.mjs +74 -0
- package/engine/changelog-lint.mjs +2 -2
- package/engine/content-index.mjs +63 -36
- package/engine/content-lint.mjs +1 -2
- package/engine/content-package.mjs +2 -2
- package/engine/content-tables.mjs +4 -8
- package/engine/folder-notes.mjs +1 -2
- package/engine/foundry-entries.mjs +21 -6
- package/engine/frontmatter-lint.mjs +3 -4
- package/engine/frontmatter.mjs +5 -8
- package/engine/helpers.mjs +3 -4
- package/engine/index.mjs +3 -0
- package/engine/infobox.mjs +67 -2
- package/engine/item-registry.mjs +2 -2
- package/engine/note-vocabulary.mjs +92 -6
- package/engine/pack-config.mjs +2 -2
- package/engine/pack-router.mjs +2 -2
- package/engine/pdf-render.mjs +1 -1
- package/engine/pdf-toc.mjs +3 -3
- package/engine/retired-fields.mjs +1 -1
- package/engine/scene-levels.mjs +4 -4
- package/engine/schema-check.mjs +1 -1
- package/engine/site-build.mjs +134 -121
- package/engine/site-preview.mjs +293 -0
- package/engine/site-root.mjs +20 -2
- package/engine/social-tie-terms.mjs +11 -0
- package/engine/social-ties.mjs +84 -0
- package/engine/sql-tables.mjs +4 -9
- package/hm3/actors.mjs +1 -1
- package/package.json +3 -3
- package/sohl/item-fields.mjs +2 -2
- package/types/config.d.mts +8 -0
- package/types/content-config.d.mts +1 -1
- package/types/engine/address.d.mts +3 -5
- package/types/engine/asset-bindings.d.mts +6 -0
- package/types/engine/calendar-notes.d.mts +12 -0
- package/types/engine/content-index.d.mts +11 -0
- package/types/engine/content-package.d.mts +2 -2
- package/types/engine/folder-notes.d.mts +1 -2
- package/types/engine/frontmatter-lint.d.mts +1 -2
- package/types/engine/frontmatter.d.mts +2 -4
- package/types/engine/helpers.d.mts +3 -4
- package/types/engine/index.d.mts +1 -0
- package/types/engine/item-registry.d.mts +2 -2
- package/types/engine/note-vocabulary.d.mts +5 -6
- package/types/engine/pack-config.d.mts +1 -1
- package/types/engine/pack-router.d.mts +1 -1
- package/types/engine/pdf-render.d.mts +1 -1
- package/types/engine/pdf-toc.d.mts +1 -1
- package/types/engine/site-build.d.mts +36 -3
- package/types/engine/site-preview.d.mts +34 -0
- package/types/engine/social-tie-terms.d.mts +5 -0
- package/types/engine/social-ties.d.mts +10 -0
package/engine/frontmatter.mjs
CHANGED
|
@@ -137,11 +137,10 @@ export function sohlSystemField(fm, to, defaultValue = undefined, { legacyKey =
|
|
|
137
137
|
* Read a frontmatter property that is authored as a **map**, returning its
|
|
138
138
|
* entries — or `null` when the note authors none.
|
|
139
139
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* as `[]`, not `{}`. The two spellings mean the same thing — this note authors
|
|
140
|
+
* An empty map may be written as `{}` or `[]`. Both spellings mean the same
|
|
141
|
+
* thing — this note authors
|
|
143
142
|
* no entries — and a build that accepted only one of them failed on notes the
|
|
144
|
-
* editor itself had produced (
|
|
143
|
+
* editor itself had produced ( 44 affiliation notes in `sohl-thalorna`).
|
|
145
144
|
* Normalizing the notes would fix only today's tree: the next editor touch puts
|
|
146
145
|
* the empty list back.
|
|
147
146
|
*
|
|
@@ -213,8 +212,7 @@ export function resolveCharges(fm) {
|
|
|
213
212
|
* kept — an element a sign leaves untouched still beats one another sign
|
|
214
213
|
* hinders, so it carries real weight when maps merge.
|
|
215
214
|
*
|
|
216
|
-
* An absent property, an empty map, and
|
|
217
|
-
* editor writes for a cleared map all mean the same thing — see
|
|
215
|
+
* An absent property, an empty map, and an empty list all mean the same thing — see
|
|
218
216
|
* {@link readMapEntries}.
|
|
219
217
|
*
|
|
220
218
|
* @param {object} fm - The item frontmatter.
|
|
@@ -249,8 +247,7 @@ export function resolveSkillAptitudes(fm, ctx = "item") {
|
|
|
249
247
|
* and be dropped silently, shipping an affiliation whose authored hostility had
|
|
250
248
|
* quietly become neutrality — so it is a build error instead.
|
|
251
249
|
*
|
|
252
|
-
* An absent property, an empty map, and
|
|
253
|
-
* editor writes for a cleared map all mean the same thing — neutral toward
|
|
250
|
+
* An absent property, an empty map, and an empty list all mean the same thing — neutral toward
|
|
254
251
|
* everyone. See {@link readMapEntries}.
|
|
255
252
|
*
|
|
256
253
|
* @param {object} fm - The item frontmatter.
|
package/engine/helpers.mjs
CHANGED
|
@@ -249,9 +249,8 @@ export function assertSuppliedCorpus(records, who) {
|
|
|
249
249
|
* is warned about and skipped.
|
|
250
250
|
*
|
|
251
251
|
* Directory names in `skipDirectories` are ignored wherever they appear. The
|
|
252
|
-
* walk itself knows nothing about what they mean: `Templates/` is
|
|
253
|
-
*
|
|
254
|
-
* of a content tree, so it is stated by the caller rather than hard-coded.
|
|
252
|
+
* walk itself knows nothing about what they mean: `Templates/` is authoring
|
|
253
|
+
* scaffolding rather than content, so the caller names it explicitly.
|
|
255
254
|
*
|
|
256
255
|
* @param {string} rootDir - Root of the tree to walk.
|
|
257
256
|
* @param {object} opts
|
|
@@ -652,7 +651,7 @@ let cachedDefaultStats;
|
|
|
652
651
|
* Each compiler used to hoist `const STATS = buildStats()` at module scope,
|
|
653
652
|
* which read the shipped package manifest the moment the module was imported —
|
|
654
653
|
* so importing a compiler required a manifest to exist even when nothing was
|
|
655
|
-
* going to be compiled
|
|
654
|
+
* going to be compiled. Deferred to first use and memoised here, the
|
|
656
655
|
* cost and the identity are what they always were; only the moment moved.
|
|
657
656
|
*
|
|
658
657
|
* @returns {object} The default `_stats` block, shared by every compiler.
|
package/engine/index.mjs
CHANGED
|
@@ -184,6 +184,9 @@ export * as packagebuildIndex from "./packagebuild-index.mjs";
|
|
|
184
184
|
/** Publishing a content tree as a website: the pass, and its integrity gates. */
|
|
185
185
|
export * as siteBuild from "./site-build.mjs";
|
|
186
186
|
|
|
187
|
+
/** In-memory page rendering for an editor preview. */
|
|
188
|
+
export * as sitePreview from "./site-preview.mjs";
|
|
189
|
+
|
|
187
190
|
/** Address rules every content tree is linted against: shape, uniqueness, alias. */
|
|
188
191
|
export * as contentLint from "./content-lint.mjs";
|
|
189
192
|
|
package/engine/infobox.mjs
CHANGED
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
* @module
|
|
113
113
|
*/
|
|
114
114
|
|
|
115
|
-
import { isAddressTuple, renderAddress } from "./address.mjs";
|
|
115
|
+
import { isAddressTuple, parseAddress, renderAddress } from "./address.mjs";
|
|
116
116
|
import { getFrontmatter } from "./frontmatter.mjs";
|
|
117
117
|
import { currentType } from "./ids.mjs";
|
|
118
118
|
import { NOTE_VOCABULARY, dataFields } from "./note-vocabulary.mjs";
|
|
@@ -274,6 +274,7 @@ export const NOTE_FIELD_PRESENTATION = Object.freeze({
|
|
|
274
274
|
bgImage: Object.freeze({ withheld: "an image, which the box never carries" }),
|
|
275
275
|
banner: Object.freeze({ withheld: "an image, which the box never carries" }),
|
|
276
276
|
overlay: Object.freeze({ withheld: "an image, which the box never carries" }),
|
|
277
|
+
"lore.event": Object.freeze({ withheld: "chronology machinery, not a summary row" }),
|
|
277
278
|
|
|
278
279
|
assocSkill: Object.freeze({ label: "Skill" }),
|
|
279
280
|
assocAffiliation: Object.freeze({ label: "Affiliation" }),
|
|
@@ -555,6 +556,58 @@ function rowValue(kind, raw, resolve, hint) {
|
|
|
555
556
|
return presentValue(raw);
|
|
556
557
|
}
|
|
557
558
|
|
|
559
|
+
/** Expand structured references using the existing linked-row shape. */
|
|
560
|
+
function structuredRows(field, raw, resolve, label, fm) {
|
|
561
|
+
if (Array.isArray(raw) && raw.some(isMapping)) {
|
|
562
|
+
const links = raw
|
|
563
|
+
.filter((entry) => isMapping(entry) && hasValue(entry.to))
|
|
564
|
+
.map((entry) => {
|
|
565
|
+
const link = linkValue(entry.to, resolve, { type: field.ref ?? "place" });
|
|
566
|
+
const details = Object.entries(entry)
|
|
567
|
+
.filter(([key, value]) => key !== "to" && hasValue(value))
|
|
568
|
+
.map(([key, value]) => `${humanizeFieldName(key)}: ${presentValue(value)}`);
|
|
569
|
+
return {
|
|
570
|
+
...link,
|
|
571
|
+
text: details.length ? `${link.text} (${details.join(", ")})` : link.text,
|
|
572
|
+
};
|
|
573
|
+
});
|
|
574
|
+
return links.length ? [{ label, kind: "links", value: links }] : [];
|
|
575
|
+
}
|
|
576
|
+
if (isMapping(raw) && !isAddressTuple(raw)) {
|
|
577
|
+
const groups = new Map();
|
|
578
|
+
for (const [target, relation] of Object.entries(raw)) {
|
|
579
|
+
if (!hasValue(target) || !hasValue(relation) || isMapping(relation)) continue;
|
|
580
|
+
const term = String(relation);
|
|
581
|
+
if (field.terms && !field.terms.some((entry) => entry.term === term)) continue;
|
|
582
|
+
if (field.terms && field.keyKind === "address") {
|
|
583
|
+
const address = parseAddress(
|
|
584
|
+
target,
|
|
585
|
+
{
|
|
586
|
+
package: fm.package ?? "local",
|
|
587
|
+
system: "note",
|
|
588
|
+
types: new Set(field.accepts),
|
|
589
|
+
},
|
|
590
|
+
{ declared: true },
|
|
591
|
+
);
|
|
592
|
+
if (address.reason || (field.accepts && !field.accepts.includes(address.type)))
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const links = groups.get(term) ?? [];
|
|
596
|
+
links.push(linkValue(target, resolve, { type: field.ref }));
|
|
597
|
+
groups.set(term, links);
|
|
598
|
+
}
|
|
599
|
+
const terms = field.terms ? field.terms.map(({ term }) => term) : [...groups.keys()];
|
|
600
|
+
return terms
|
|
601
|
+
.filter((term) => groups.has(term))
|
|
602
|
+
.map((term) => ({
|
|
603
|
+
label: presentValue(term),
|
|
604
|
+
kind: "links",
|
|
605
|
+
value: groups.get(term),
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
|
|
558
611
|
/**
|
|
559
612
|
* One row, with its unit on it.
|
|
560
613
|
*
|
|
@@ -664,7 +717,19 @@ function noteBox(
|
|
|
664
717
|
// The current name wins, which is the whole of the retirement window's
|
|
665
718
|
// behaviour and is `readAliasedField`'s answer, not a second one.
|
|
666
719
|
const own = getFrontmatter(data, field.name, undefined);
|
|
667
|
-
const raw =
|
|
720
|
+
const raw = own !== undefined ? own : readAliasedField(fm, field.name, { inData: true });
|
|
721
|
+
const structured = structuredRows(
|
|
722
|
+
field,
|
|
723
|
+
raw,
|
|
724
|
+
resolve,
|
|
725
|
+
overlay.label ?? humanizeFieldName(field.name),
|
|
726
|
+
fm,
|
|
727
|
+
);
|
|
728
|
+
if (structured) {
|
|
729
|
+
rows.push(...structured);
|
|
730
|
+
if (structured.length) shown.add(field.name);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
668
733
|
if (!hasValue(raw)) continue;
|
|
669
734
|
|
|
670
735
|
if (overlay.group) {
|
package/engine/item-registry.mjs
CHANGED
|
@@ -57,7 +57,7 @@ import { resolveImg } from "./helpers.mjs";
|
|
|
57
57
|
* compile.
|
|
58
58
|
*
|
|
59
59
|
* An accessor rather than a hoisted constant, so that importing this module
|
|
60
|
-
* needs no configuration
|
|
60
|
+
* needs no configuration.
|
|
61
61
|
*
|
|
62
62
|
* @returns {ReadonlySet<string>} The configured item types.
|
|
63
63
|
*/
|
|
@@ -163,7 +163,7 @@ export function itemFields(type, system) {
|
|
|
163
163
|
* SoHL-owned table could never hold. Art is not looked up in
|
|
164
164
|
* `sohl/default-item-art.mjs` instead: a type was configurable while its
|
|
165
165
|
* default art was not, so a second consumer's items compiled only if every one
|
|
166
|
-
* of its notes set `img
|
|
166
|
+
* of its notes set `img:`.
|
|
167
167
|
*
|
|
168
168
|
* **Still fail-fast.** A type with neither a note-level `img:` nor paired art
|
|
169
169
|
* aborts the pack build rather than shipping a mismatched icon — the contract
|
|
@@ -96,12 +96,16 @@ import {
|
|
|
96
96
|
// package's declared present compute — the closed `data:` container's own
|
|
97
97
|
// question, asked the way `checkPlace` and `checkCalendarNote` are.
|
|
98
98
|
import { checkBeingAge } from "./being-age.mjs";
|
|
99
|
+
import { checkSocialTies } from "./social-ties.mjs";
|
|
100
|
+
import { SOCIAL_TIES } from "./social-tie-terms.mjs";
|
|
101
|
+
import { parseNoteDate } from "./note-dates.mjs";
|
|
99
102
|
import { checkHeld } from "./holdings.mjs";
|
|
100
103
|
import { checkCitedPopulations, checkPopulation } from "./populations.mjs";
|
|
101
104
|
// What trade a settlement supports — the scale and the check that holds a
|
|
102
105
|
// value to it, read rather than restated, so the reference below and the
|
|
103
106
|
// finding an author meets state one list.
|
|
104
107
|
import { MARKET_CLASSES, checkMarket } from "./market-class.mjs";
|
|
108
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
105
109
|
|
|
106
110
|
/**
|
|
107
111
|
* One `data:` key a note type may carry.
|
|
@@ -365,6 +369,51 @@ function checkPlace(note, opts) {
|
|
|
365
369
|
return [...checkHeld(note, opts), ...checkWorldFacts(note, opts)];
|
|
366
370
|
}
|
|
367
371
|
|
|
372
|
+
/** Validate a being's authored date using the shared note-date grammar. */
|
|
373
|
+
function checkBeingDate(key) {
|
|
374
|
+
return (note) =>
|
|
375
|
+
parseNoteDate(note.fm?.data?.[key], {
|
|
376
|
+
field: `data.${key}`,
|
|
377
|
+
file: note.file,
|
|
378
|
+
raw: note.raw,
|
|
379
|
+
keyPath: ["data", key],
|
|
380
|
+
}).findings;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** A place's purpose selects one of its own character tags. */
|
|
384
|
+
function checkPlacePurpose(note) {
|
|
385
|
+
const value = note.fm?.data?.purpose;
|
|
386
|
+
if (value === undefined || value === null) return [];
|
|
387
|
+
const at = () => ({
|
|
388
|
+
file: note.file,
|
|
389
|
+
...positionOfFrontmatterPath(note.raw ?? "", ["data", "purpose"]),
|
|
390
|
+
severity: "error",
|
|
391
|
+
});
|
|
392
|
+
const subType = note.fm?.subType;
|
|
393
|
+
if (!["settlement", "site", "structure"].includes(subType))
|
|
394
|
+
return [
|
|
395
|
+
{
|
|
396
|
+
...at(),
|
|
397
|
+
message: `data.purpose applies to a settlement, site, or structure; this place is ${subType ?? "untyped"}`,
|
|
398
|
+
},
|
|
399
|
+
];
|
|
400
|
+
if (!DECLARED_TAGS.placeCharacter.tags.includes(value))
|
|
401
|
+
return [
|
|
402
|
+
{
|
|
403
|
+
...at(),
|
|
404
|
+
message: `data.purpose ${JSON.stringify(value)} is not a placeCharacter tag; choose one of ${DECLARED_TAGS.placeCharacter.tags.join(", ")}`,
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
if (hasTag(note.fm, value)) return [];
|
|
408
|
+
const tags = note.fm?.tags ?? note.fm?.tag;
|
|
409
|
+
return [
|
|
410
|
+
{
|
|
411
|
+
...at(),
|
|
412
|
+
message: `data.purpose ${JSON.stringify(value)} must select one of this place's tags; it carries ${JSON.stringify(tags ?? [])}`,
|
|
413
|
+
},
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
|
|
368
417
|
/* --------------------------------------------------------------------- */
|
|
369
418
|
/* The vocabulary */
|
|
370
419
|
/* --------------------------------------------------------------------- */
|
|
@@ -485,6 +534,10 @@ export const DECLARED_TAGS = Object.freeze({
|
|
|
485
534
|
"holy",
|
|
486
535
|
"sacred",
|
|
487
536
|
"free",
|
|
537
|
+
"ford",
|
|
538
|
+
"portage",
|
|
539
|
+
"pass",
|
|
540
|
+
"well",
|
|
488
541
|
]),
|
|
489
542
|
}),
|
|
490
543
|
/** A place's scale, where the subtype does not distinguish it. */
|
|
@@ -564,14 +617,13 @@ export function exclusiveTagGroups(type, groups = DECLARED_TAGS) {
|
|
|
564
617
|
/**
|
|
565
618
|
* Whether a note carries a given tag, however the author wrote it.
|
|
566
619
|
*
|
|
567
|
-
* `tags:`
|
|
620
|
+
* `tags:` accepts common YAML forms: a single tag
|
|
568
621
|
* may be a scalar rather than a list, a value may carry the leading `#` it is
|
|
569
622
|
* written with in prose, and case and surrounding space are not significant.
|
|
570
623
|
* The spelling of the tag *itself* still is — a near miss is a near miss, and
|
|
571
624
|
* the frontmatter lint is what reports it; nothing here guesses.
|
|
572
625
|
*
|
|
573
|
-
* Reads `tags` and
|
|
574
|
-
* also accepts.
|
|
626
|
+
* Reads `tags` and the singular `tag` spelling.
|
|
575
627
|
*
|
|
576
628
|
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
577
629
|
* @param {string} tag - The tag to look for, in its declared spelling.
|
|
@@ -663,6 +715,16 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
663
715
|
accepts: ["affiliation"],
|
|
664
716
|
describe: "Affiliations the being belongs to — traditions, polities, and the rest.",
|
|
665
717
|
},
|
|
718
|
+
{
|
|
719
|
+
name: "socialTies",
|
|
720
|
+
kind: "map",
|
|
721
|
+
keyKind: "address",
|
|
722
|
+
accepts: ["being", "affiliation"],
|
|
723
|
+
terms: SOCIAL_TIES,
|
|
724
|
+
shape: "a map keyed by Address",
|
|
725
|
+
check: checkSocialTies,
|
|
726
|
+
describe: `Defining ties directed from this being to others: ${SOCIAL_TIES.map(({ term, meaning }) => `\`${term}\` (${meaning})`).join("; ")}`,
|
|
727
|
+
},
|
|
666
728
|
{ name: "gender", ...TEXT, describe: "`male`, `female` or `other`." },
|
|
667
729
|
{
|
|
668
730
|
name: "species",
|
|
@@ -674,6 +736,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
674
736
|
{
|
|
675
737
|
name: "born",
|
|
676
738
|
...TEXT,
|
|
739
|
+
check: checkBeingDate("born"),
|
|
677
740
|
describe:
|
|
678
741
|
"When the being was born — a date, or `unknown` where the birth is " +
|
|
679
742
|
"unrecorded. Absent, the being was never born.",
|
|
@@ -681,6 +744,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
681
744
|
{
|
|
682
745
|
name: "died",
|
|
683
746
|
...TEXT,
|
|
747
|
+
check: checkBeingDate("died"),
|
|
684
748
|
describe:
|
|
685
749
|
"When the being died — a date, or `unknown` where the death is " +
|
|
686
750
|
"unrecorded. Absent, the being is alive.",
|
|
@@ -1097,13 +1161,20 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1097
1161
|
|
|
1098
1162
|
doc: Object.freeze({
|
|
1099
1163
|
stubbable: true,
|
|
1100
|
-
//
|
|
1164
|
+
// A genre is all this field carries: what kind of page
|
|
1101
1165
|
// it is, never who reads it. An audience term alongside them would give
|
|
1102
1166
|
// a developer how-to two valid values and no rule for choosing.
|
|
1103
1167
|
//
|
|
1104
1168
|
// `userguide` and `howto`, not `user-guide` and `how-to`: a subType is
|
|
1105
1169
|
// held to the address charset, and a segment carries no hyphen.
|
|
1106
|
-
subTypes: Object.freeze([
|
|
1170
|
+
subTypes: Object.freeze([
|
|
1171
|
+
"rules",
|
|
1172
|
+
"userguide",
|
|
1173
|
+
"reference",
|
|
1174
|
+
"howto",
|
|
1175
|
+
"concept",
|
|
1176
|
+
"settingguide",
|
|
1177
|
+
]),
|
|
1107
1178
|
check: checkCitedPopulations,
|
|
1108
1179
|
data: Object.freeze([]),
|
|
1109
1180
|
}),
|
|
@@ -1189,7 +1260,15 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1189
1260
|
// because `DataFieldSpec` declares the keys a type accepts and not the
|
|
1190
1261
|
// subType that may write them.
|
|
1191
1262
|
check: checkCalendarNote,
|
|
1192
|
-
data:
|
|
1263
|
+
data: Object.freeze([
|
|
1264
|
+
...CALENDAR_FIELDS,
|
|
1265
|
+
{
|
|
1266
|
+
name: "event",
|
|
1267
|
+
kind: "map",
|
|
1268
|
+
shape: "event metadata map",
|
|
1269
|
+
describe: "A dated occurrence and its relationships to other events and places.",
|
|
1270
|
+
},
|
|
1271
|
+
]),
|
|
1193
1272
|
}),
|
|
1194
1273
|
|
|
1195
1274
|
place: Object.freeze({
|
|
@@ -1210,6 +1289,13 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1210
1289
|
...TEXT,
|
|
1211
1290
|
describe: "What a person from this place is called — a Vylarian.",
|
|
1212
1291
|
},
|
|
1292
|
+
{
|
|
1293
|
+
name: "purpose",
|
|
1294
|
+
...TEXT,
|
|
1295
|
+
check: checkPlacePurpose,
|
|
1296
|
+
describe:
|
|
1297
|
+
"The reason this settlement, site, or structure exists, selected from its placeCharacter tags.",
|
|
1298
|
+
},
|
|
1213
1299
|
{
|
|
1214
1300
|
name: "lore",
|
|
1215
1301
|
...LINKS,
|
package/engine/pack-config.mjs
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* only the derivations differ, and they differ because a code file can do its
|
|
38
38
|
* own I/O while `defineConfig` deliberately does none.
|
|
39
39
|
*
|
|
40
|
-
* **Resolved on first read, never at import
|
|
40
|
+
* **Resolved on first read, never at import.** {@link loadPackConfig}
|
|
41
41
|
* is a function rather than a module-level constant, so importing this module —
|
|
42
42
|
* or the `engine` barrel, or a leaf module that happens to sit downstream of it
|
|
43
43
|
* — costs nothing and requires nothing. A repository with no configuration can
|
|
@@ -705,7 +705,7 @@ let loadedFrom;
|
|
|
705
705
|
*
|
|
706
706
|
* Every engine module that needs a configured value calls this, rather than
|
|
707
707
|
* hoisting one at import: that is what keeps the library importable without a
|
|
708
|
-
* configuration
|
|
708
|
+
* configuration. The result is memoised, so calling it in a default
|
|
709
709
|
* parameter — the usual spelling here — costs one property read per call.
|
|
710
710
|
*
|
|
711
711
|
* @returns {import("../content-config.mjs").ContentBuildConfig} The frozen configuration.
|
package/engine/pack-router.mjs
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* - A declared name that no pack answers to — or that names a pack holding
|
|
44
44
|
* another document type, or a companion pack, which no note may address —
|
|
45
45
|
* fails the build, naming the note and what it asked for. A silent fall-back
|
|
46
|
-
* to the default would
|
|
46
|
+
* to the default would publish an address into the wrong compendium.
|
|
47
47
|
* - A note's **derived** documents are routed by the default of *their* type,
|
|
48
48
|
* not by the note's declaration: an item note's prose compiles into a
|
|
49
49
|
* JournalEntry, and `pack:` names where the *item* goes.
|
|
@@ -501,7 +501,7 @@ export function routerFor(config) {
|
|
|
501
501
|
* asks where a note's document lives.
|
|
502
502
|
*
|
|
503
503
|
* An accessor rather than a hoisted constant, so that importing this module
|
|
504
|
-
* needs no configuration
|
|
504
|
+
* needs no configuration. {@link routerFor} keeps one router per
|
|
505
505
|
* configuration object, so repeated calls return the same instance.
|
|
506
506
|
*
|
|
507
507
|
* @returns {ReturnType<typeof createPackRouter>} This repository's router.
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -956,7 +956,7 @@ export function bookTypstPreamble() {
|
|
|
956
956
|
/**
|
|
957
957
|
* The whole book, as one Typst document.
|
|
958
958
|
*
|
|
959
|
-
* **Pure, and that is the point.**
|
|
959
|
+
* **Pure, and that is the point.** The book's structural properties
|
|
960
960
|
* about structure — the outline's shape, the anchors, which links went inward,
|
|
961
961
|
* the order entries print in — is decided here from a plan and a map of bodies,
|
|
962
962
|
* with no filesystem and no compiler. {@link module:engine/pdf-build} supplies
|
package/engine/pdf-toc.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* The document tree a PDF is built from, and the plan it resolves to
|
|
15
|
+
* The document tree a PDF is built from, and the plan it resolves to.
|
|
16
16
|
*
|
|
17
17
|
* The packs and the website both render the *whole* content tree: every note
|
|
18
18
|
* becomes a document and a page, and the three surfaces agreeing about what the
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* handed back as `selections`, the way `expandContentTables` is handed its
|
|
40
40
|
* `sqlTables` because DuckDB is async and the pass is not. That is what lets the
|
|
41
41
|
* whole structure of a 2,500-entry book be asserted in a unit test with no
|
|
42
|
-
* renderer present,
|
|
42
|
+
* renderer present, covering the structure before presentation is applied.
|
|
43
43
|
*
|
|
44
44
|
* **Every gate reports; none exits.** A filter that will not parse, a `file:`
|
|
45
45
|
* that resolves nowhere and a filter reaching another package's schema are
|
|
@@ -415,7 +415,7 @@ export async function runTreeFilters(nodes, db, { keep = () => true, text } = {}
|
|
|
415
415
|
* The plan is an ordered list of entries — `section`, `prose`, `note` — and it
|
|
416
416
|
* is the artifact worth having. Order, depth, the outline, the table of
|
|
417
417
|
* contents, anchor uniqueness and every link destination are all readable from
|
|
418
|
-
* it, so
|
|
418
|
+
* it, so the book's structure can be asserted here, on data,
|
|
419
419
|
* without a renderer or a PDF. Only how the result *looks* needs eyes.
|
|
420
420
|
*
|
|
421
421
|
* **Notes are sorted, prose is not.** A section's entries come out in
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
* {@link RETIRED_FIELD_ALIASES}.
|
|
85
85
|
*
|
|
86
86
|
* **The third step is deletion, and it needs no code.** `image`, a map's
|
|
87
|
-
* background art,
|
|
87
|
+
* background art, illustrates all three states: reported,
|
|
88
88
|
* swept, then dropped from the
|
|
89
89
|
* table. Removing the entry is the whole of it — with no alias, the spelling is
|
|
90
90
|
* an ordinary unknown key in the `sohl:` block, which the frontmatter lint
|
package/engine/scene-levels.mjs
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* Plain ESM with no Foundry, so the rule itself is a pure function over records
|
|
44
44
|
* and is unit-tested directly.
|
|
45
45
|
*
|
|
46
|
-
* **What a report has to say
|
|
46
|
+
* **What a report has to say.** The likeliest way to meet any of this is a
|
|
47
47
|
* compendium CLI older than {@link LEVEL_SPLITTING_CLI}, which does not split
|
|
48
48
|
* Scene Levels into the sublevel at all — so the guard fires on every scene at
|
|
49
49
|
* once, and each `levels` entry is an inline Level *object* rather than an id.
|
|
@@ -71,7 +71,7 @@ const ADVENTURE_PREFIX = "!adventures!";
|
|
|
71
71
|
/**
|
|
72
72
|
* The compendium CLI release that began splitting Scene Levels into the
|
|
73
73
|
* `!scenes.levels!` sublevel. Anything older writes them inline on the Scene
|
|
74
|
-
* document, which is the shape the reports below name
|
|
74
|
+
* document, which is the shape the reports below name.
|
|
75
75
|
*/
|
|
76
76
|
const LEVEL_SPLITTING_CLI = "3.0.3";
|
|
77
77
|
|
|
@@ -184,7 +184,7 @@ function predatesLevelSplitting(version) {
|
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
186
|
* What to do about it, decided from the version actually resolved — so the
|
|
187
|
-
* report ends in an instruction rather than a lead to follow
|
|
187
|
+
* report ends in an instruction rather than a lead to follow.
|
|
188
188
|
*
|
|
189
189
|
* @param {string | undefined} cliVersion - The resolved compendium CLI
|
|
190
190
|
* version, when it is known.
|
|
@@ -222,7 +222,7 @@ function describeRemedy(cliVersion) {
|
|
|
222
222
|
* it on that record's write path; a pack that has none never split them, which
|
|
223
223
|
* is what a compendium CLI older than {@link LEVEL_SPLITTING_CLI} does — two
|
|
224
224
|
* different diagnoses, so the message names whichever shape it found rather
|
|
225
|
-
* than guessing
|
|
225
|
+
* than guessing.
|
|
226
226
|
*
|
|
227
227
|
* @param {Array<[string, object]>} declaring - `[sceneId, scene]` for every
|
|
228
228
|
* scene declaring at least one level.
|
package/engine/schema-check.mjs
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* of `sohl-kethira-basic`'s deities, is not defined at the version that module
|
|
27
27
|
* targets, so the divine/arcane split evaporates on load.
|
|
28
28
|
* - **Declared, not emitted.** The mirror image, fixed by hand in
|
|
29
|
-
*
|
|
29
|
+
* the shared build.
|
|
30
30
|
*
|
|
31
31
|
* Neither was found by tooling. Both were found by set-subtracting compiled
|
|
32
32
|
* documents' `system` keys against `defineSchema()` **by hand**, which is how
|