@heroiclands/package-build 8.1.0 → 10.0.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 +748 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +479 -123
- package/bin/package-build.mjs +27 -69
- package/bin/report.mjs +1 -2
- package/bundle.mjs +2 -10
- package/config.mjs +31 -106
- package/container.mjs +13 -57
- package/content-config.mjs +300 -188
- package/coverage.mjs +14 -55
- package/deploy.mjs +4 -13
- package/docs/content-format.md +1418 -0
- package/e2e.mjs +16 -55
- package/engine/address-charset.mjs +62 -0
- package/engine/address-diff.mjs +1 -4
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +203 -31
- package/engine/code-fences.mjs +4 -13
- package/engine/compendiums.mjs +13 -37
- package/engine/content-address.mjs +6 -10
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +144 -99
- package/engine/content-lint.mjs +12 -16
- package/engine/content-slug.mjs +2 -6
- package/engine/content-tables.mjs +26 -79
- package/engine/diagnostics.mjs +37 -15
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-reference.mjs +6 -20
- package/engine/field-spec.mjs +49 -45
- package/engine/foreign-catalog.mjs +7 -22
- package/engine/foreign-manifests.mjs +1 -4
- package/engine/frontmatter-lint.mjs +347 -43
- package/engine/frontmatter.mjs +3 -8
- package/engine/generate.mjs +36 -20
- package/engine/helpers.mjs +54 -81
- package/engine/homepage.mjs +4 -15
- package/engine/ids.mjs +21 -12
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -9
- package/engine/journals.mjs +4 -14
- package/engine/kb-manifest.mjs +41 -24
- package/engine/macros.mjs +2 -10
- package/engine/manifest-emit.mjs +6 -17
- package/engine/map-notes.mjs +53 -87
- package/engine/note-claims.mjs +383 -0
- package/engine/note-package.mjs +1 -4
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +56 -60
- package/engine/pack-router.mjs +18 -8
- package/engine/prose-config.mjs +20 -4
- package/engine/prose-lint.mjs +61 -17
- package/engine/region-events.mjs +1 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scene-levels.mjs +8 -22
- package/engine/scenes.mjs +31 -47
- package/engine/schema-check.mjs +348 -7
- package/engine/schema-extract.mjs +11 -39
- package/engine/site-build.mjs +13 -38
- package/engine/site-index.mjs +40 -35
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +115 -92
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +147 -183
- package/index.mjs +1 -5
- package/lang.mjs +1 -3
- package/manifest.mjs +10 -37
- package/markdownlint-config.mjs +1 -5
- package/package.json +6 -2
- package/sohl/actors.mjs +251 -68
- package/sohl/being-info.mjs +3 -6
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +4 -6
- package/sohl/item-builders.mjs +1 -3
- package/sohl/item-fields.mjs +16 -34
- package/sohl/items.mjs +111 -17
- package/sohl/kb-passes.mjs +29 -39
- package/sohl/note-schemas.mjs +11 -7
- package/sohl/skill-base.mjs +7 -23
- package/stage.mjs +3 -13
- package/templates.mjs +4 -15
- package/types/bundle.d.mts +1 -1
- package/types/container.d.mts +2 -2
- package/types/content-config.d.mts +48 -4
- package/types/coverage.d.mts +1 -1
- package/types/e2e.d.mts +4 -4
- package/types/engine/address-charset.d.mts +45 -0
- package/types/engine/alias-index.d.mts +122 -0
- package/types/engine/base-compiler.d.mts +132 -4
- package/types/engine/content-address.d.mts +2 -2
- package/types/engine/content-format-check.d.mts +163 -0
- package/types/engine/content-format.d.mts +101 -0
- package/types/engine/content-links.d.mts +16 -1
- package/types/engine/content-lint.d.mts +6 -0
- package/types/engine/diagnostics.d.mts +29 -0
- package/types/engine/document-subtypes.d.mts +233 -0
- package/types/engine/field-spec.d.mts +76 -23
- package/types/engine/frontmatter-lint.d.mts +47 -2
- package/types/engine/generate.d.mts +15 -2
- package/types/engine/helpers.d.mts +22 -14
- package/types/engine/ids.d.mts +10 -0
- package/types/engine/index.d.mts +5 -0
- package/types/engine/item-registry.d.mts +21 -2
- package/types/engine/kb-manifest.d.mts +35 -8
- package/types/engine/map-notes.d.mts +21 -11
- package/types/engine/note-claims.d.mts +113 -0
- package/types/engine/note-vocabulary.d.mts +251 -0
- package/types/engine/pack-config.d.mts +4 -3
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/prose-lint.d.mts +6 -2
- package/types/engine/retired-fields.d.mts +73 -2
- package/types/engine/schema-check.d.mts +182 -0
- package/types/engine/schema-extract.d.mts +1 -1
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/system-block.d.mts +281 -0
- package/types/engine/web-wikilinks.d.mts +23 -12
- package/types/engine/wikilink-syntax.d.mts +29 -0
- package/types/manifest.d.mts +1 -1
- package/types/sohl/actors.d.mts +62 -6
- package/types/sohl/document-subtypes.d.mts +14 -0
- package/types/sohl/index.d.mts +1 -0
- package/types/sohl/items.d.mts +21 -0
package/engine/generate.mjs
CHANGED
|
@@ -48,16 +48,12 @@ import { Journals } from "./journals.mjs";
|
|
|
48
48
|
import { Actors } from "../sohl/actors.mjs";
|
|
49
49
|
import { Macros } from "./macros.mjs";
|
|
50
50
|
import { Scenes } from "./scenes.mjs";
|
|
51
|
-
import {
|
|
52
|
-
statsForPack,
|
|
53
|
-
loadFolders,
|
|
54
|
-
buildFolderResolver,
|
|
55
|
-
writeFolderDocs,
|
|
56
|
-
} from "./helpers.mjs";
|
|
51
|
+
import { statsForPack, loadFolders, buildFolderResolver, writeFolderDocs } from "./helpers.mjs";
|
|
57
52
|
import { countContentNotes } from "./content-tree.mjs";
|
|
58
53
|
import { emitDiagnostic } from "./diagnostics.mjs";
|
|
59
54
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
60
55
|
import { routerFor } from "./pack-router.mjs";
|
|
56
|
+
import { unclaimedNoteFindings } from "./note-claims.mjs";
|
|
61
57
|
|
|
62
58
|
/**
|
|
63
59
|
* The compiler class for each Foundry document type a pack may hold.
|
|
@@ -99,16 +95,30 @@ export const packJsonDir = (name, config = loadPackConfig()) =>
|
|
|
99
95
|
* order among packs of one type — and every one of them is written before the
|
|
100
96
|
* actors pass that reads them.
|
|
101
97
|
*
|
|
98
|
+
* **Scoped to one system when the caller has one (#58).** A being addresses an
|
|
99
|
+
* item by `(type, shortcode)`, and that address is unique within one system and
|
|
100
|
+
* not across two: `skill:sword` is an HM3 skill *and* a SoHL skill, with
|
|
101
|
+
* different data models behind them. The reference itself is unambiguous — it
|
|
102
|
+
* sits inside a system block, so position says which it means — but the
|
|
103
|
+
* resolver has to know which catalogue it is searching, or it resolves the pair
|
|
104
|
+
* by whichever pack was read first. So an Actor pass reads the Item packs of
|
|
105
|
+
* **its own** system plus the system-neutral ones, which belong to every
|
|
106
|
+
* system. Asking for no system reads them all, which is every single-system
|
|
107
|
+
* build and the behaviour this always had.
|
|
108
|
+
*
|
|
102
109
|
* @param {object} [config] - The resolved build configuration. Defaults to this
|
|
103
110
|
* repository's.
|
|
111
|
+
* @param {string|null} [system] - The system whose catalogue is wanted. Omitted
|
|
112
|
+
* or `null`, every Item pack is read.
|
|
104
113
|
* @returns {string[]} Each Item pack's JSON directory. Empty when the
|
|
105
114
|
* repository ships no items at all, which is a legitimate package: the actors
|
|
106
115
|
* pass accepts an empty list and reports an item it cannot resolve per
|
|
107
116
|
* `(type, shortcode)` instead, naming the being (#49).
|
|
108
117
|
*/
|
|
109
|
-
export function itemPackJsonDirs(config = loadPackConfig()) {
|
|
118
|
+
export function itemPackJsonDirs(config = loadPackConfig(), system = null) {
|
|
110
119
|
return config.packs
|
|
111
120
|
.filter((pack) => pack.type === "Item")
|
|
121
|
+
.filter((pack) => system == null || !pack.system || pack.system === system)
|
|
112
122
|
.map((pack) => packJsonDir(pack.name, config));
|
|
113
123
|
}
|
|
114
124
|
|
|
@@ -170,10 +180,7 @@ export function orderPassesByDependency(packs) {
|
|
|
170
180
|
readsOutputOf(pack.type).every(
|
|
171
181
|
(dependency) =>
|
|
172
182
|
!declared.has(dependency) ||
|
|
173
|
-
packs.every(
|
|
174
|
-
(other) =>
|
|
175
|
-
other.type !== dependency || emitted.has(other.name),
|
|
176
|
-
),
|
|
183
|
+
packs.every((other) => other.type !== dependency || emitted.has(other.name)),
|
|
177
184
|
);
|
|
178
185
|
|
|
179
186
|
while (remaining.length) {
|
|
@@ -269,8 +276,7 @@ async function generatePack(
|
|
|
269
276
|
let folderList;
|
|
270
277
|
let resolver;
|
|
271
278
|
try {
|
|
272
|
-
folderList =
|
|
273
|
-
folders ? loadFolders(path.join(contentBase, folders)) : [];
|
|
279
|
+
folderList = folders ? loadFolders(path.join(contentBase, folders)) : [];
|
|
274
280
|
({ resolver } = buildFolderResolver(folderList));
|
|
275
281
|
} catch (err) {
|
|
276
282
|
log.error(`${name} ${folders} validation failed: ${err.message}`);
|
|
@@ -305,7 +311,9 @@ async function generatePack(
|
|
|
305
311
|
// the Item packs, so the dependency is stated rather than assumed
|
|
306
312
|
// (#1508) — and it is every Item pack, since a repository may ship more
|
|
307
313
|
// than one (#1566).
|
|
308
|
-
|
|
314
|
+
// Scoped to this pack's system, so a being resolves `(type, shortcode)`
|
|
315
|
+
// against its own system's catalogue and the neutral one (#58).
|
|
316
|
+
itemsSourceDirs: itemPackJsonDirs(config, system ?? null),
|
|
309
317
|
// The catalogue of a package this repository depends on but does
|
|
310
318
|
// not contain, for a repository that authors beings without
|
|
311
319
|
// holding the items they are assembled from. Cache-only: a cold
|
|
@@ -365,10 +373,7 @@ export function emptyPassErrors(passes) {
|
|
|
365
373
|
* @throws {Error} If the configured Foundry package id has drifted from the
|
|
366
374
|
* shipped manifest's `id` (see `package-manifest.mjs`).
|
|
367
375
|
*/
|
|
368
|
-
export async function generatePacksJson({
|
|
369
|
-
only,
|
|
370
|
-
config = loadPackConfig(),
|
|
371
|
-
} = {}) {
|
|
376
|
+
export async function generatePacksJson({ only, config = loadPackConfig() } = {}) {
|
|
372
377
|
// Before anything is generated: every UUID written below is addressed to
|
|
373
378
|
// the configured `foundryPackage`, so a value that has drifted from the shipped
|
|
374
379
|
// manifest's `id` produces a whole pack of links that resolve nowhere.
|
|
@@ -393,6 +398,17 @@ export async function generatePacksJson({
|
|
|
393
398
|
return 1;
|
|
394
399
|
}
|
|
395
400
|
log.info(`Content tree: ${noteCount} note(s) at ${contentBase}`);
|
|
401
|
+
|
|
402
|
+
// A note whose `type:` no configured pack claims compiles into nothing, and
|
|
403
|
+
// used to say nothing (#146) — no pass got far enough to reject it, so the
|
|
404
|
+
// silence had no owner. Asked once, of the whole configuration, because
|
|
405
|
+
// that is the only place it can be answered: a per-pass check would report
|
|
406
|
+
// every type a system deliberately does not map, which is exactly the
|
|
407
|
+
// silence #79 requires. Independent of `only`, since it is a fact about the
|
|
408
|
+
// configured pack list rather than about which passes this run executes.
|
|
409
|
+
const unclaimed = unclaimedNoteFindings(config);
|
|
410
|
+
for (const finding of unclaimed) emitDiagnostic(finding);
|
|
411
|
+
|
|
396
412
|
fs.mkdirSync(config.paths.packJson, { recursive: true });
|
|
397
413
|
|
|
398
414
|
// A companion pack has no pass of its own — naming it selects the pass that
|
|
@@ -431,10 +447,10 @@ export async function generatePacksJson({
|
|
|
431
447
|
for (const message of unsatisfied) {
|
|
432
448
|
emitDiagnostic({ severity: "error", message });
|
|
433
449
|
}
|
|
434
|
-
return unsatisfied.length;
|
|
450
|
+
return unsatisfied.length + unclaimed.length;
|
|
435
451
|
}
|
|
436
452
|
|
|
437
|
-
let totalErrors =
|
|
453
|
+
let totalErrors = unclaimed.length;
|
|
438
454
|
const passes = [];
|
|
439
455
|
for (const pack of ordered) {
|
|
440
456
|
const { errors, compiled } = await generatePack(
|
package/engine/helpers.mjs
CHANGED
|
@@ -39,6 +39,8 @@ import { contentPackage, foundryPackageId } from "./content-package.mjs";
|
|
|
39
39
|
import { searchableFrontmatter } from "./note-package.mjs";
|
|
40
40
|
import { loadForeignManifests, PACKAGE_BASE } from "./kb-manifest.mjs";
|
|
41
41
|
import { buildWikilinkIndex, convertWikilinks } from "./wikilinks.mjs";
|
|
42
|
+
// The alias sources every index shares (#131).
|
|
43
|
+
import { aliasesOf } from "./alias-index.mjs";
|
|
42
44
|
import { expandContentTables } from "./content-tables.mjs";
|
|
43
45
|
import { emitDiagnostic, positionInBody } from "./diagnostics.mjs";
|
|
44
46
|
// The pure `sohl:` frontmatter readers live in a leaf module so the item-type
|
|
@@ -89,8 +91,7 @@ export function parseMarkdownFile(filePath) {
|
|
|
89
91
|
// be reported as a file position (#17). The frontmatter's lines and the
|
|
90
92
|
// blank lines `trim()` removes both sit in between, and the trim can take
|
|
91
93
|
// indentation off the first line as well — hence a column, not just a line.
|
|
92
|
-
const bodyStart =
|
|
93
|
-
content.length - raw.length + (raw.length - raw.trimStart().length);
|
|
94
|
+
const bodyStart = content.length - raw.length + (raw.length - raw.trimStart().length);
|
|
94
95
|
const before = content.slice(0, bodyStart);
|
|
95
96
|
const bodyLine = before.split("\n").length;
|
|
96
97
|
const bodyColumn = bodyStart - before.lastIndexOf("\n");
|
|
@@ -148,8 +149,8 @@ export function* walkMarkdownTree(
|
|
|
148
149
|
|
|
149
150
|
/**
|
|
150
151
|
* Resolve the required `sohl.archetype` frontmatter for an Item/Actor entry
|
|
151
|
-
* (
|
|
152
|
-
*
|
|
152
|
+
* (the archetype contract, #604). The property is a nullable number that
|
|
153
|
+
* authors must state explicitly:
|
|
153
154
|
* - a number → the document is an archetype of that priority.
|
|
154
155
|
* - `null` → the document is not an archetype.
|
|
155
156
|
* - absent → an authoring error (throws), so "not an archetype" is never
|
|
@@ -183,30 +184,29 @@ export function resolveArchetype(fm, label) {
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
187
|
+
* The value a document's `system.archetype` carries, from the required
|
|
188
|
+
* `sohl.archetype` frontmatter (#126, sohl#1780).
|
|
189
|
+
*
|
|
190
|
+
* A **schema field**, so the tri-state is written out in full rather than
|
|
191
|
+
* expressed by a key's presence: a number is an archetype at that priority,
|
|
192
|
+
* and `null` is not an archetype. This is where {@link resolveArchetype}'s
|
|
193
|
+
* `undefined` becomes the field's `null` — an emitted `undefined` would be
|
|
194
|
+
* dropped by `JSON.stringify`, leaving the compiled document with no
|
|
195
|
+
* `archetype` at all and the tri-state readable as two.
|
|
196
|
+
*
|
|
197
|
+
* **`0` is an archetype.** It is the priority SoHL's own archetypes ship at,
|
|
198
|
+
* and it is falsy, so this returns it unchanged and every caller must ask
|
|
199
|
+
* `typeof v === "number"` rather than testing truthiness.
|
|
200
|
+
*
|
|
201
|
+
* @param {object} fm Parsed frontmatter.
|
|
202
|
+
* @param {string} label Human-readable context for error messages.
|
|
203
|
+
* @returns {number|null} The archetype priority, or `null` for a document
|
|
204
|
+
* that is not an archetype.
|
|
196
205
|
* @throws {Error} When `sohl.archetype` is absent or invalid.
|
|
197
206
|
*/
|
|
198
|
-
export function
|
|
207
|
+
export function systemArchetype(fm, label) {
|
|
199
208
|
const archetype = resolveArchetype(fm, label);
|
|
200
|
-
|
|
201
|
-
const sohl = { ...(out.sohl || {}) };
|
|
202
|
-
if (archetype === undefined) {
|
|
203
|
-
delete sohl.docArchetype;
|
|
204
|
-
} else {
|
|
205
|
-
sohl.docArchetype = archetype;
|
|
206
|
-
}
|
|
207
|
-
if (Object.keys(sohl).length > 0) out.sohl = sohl;
|
|
208
|
-
else delete out.sohl;
|
|
209
|
-
return out;
|
|
209
|
+
return archetype === undefined ? null : archetype;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
|
@@ -321,10 +321,7 @@ export function supportedCoreVersion(config = loadPackConfig()) {
|
|
|
321
321
|
* The resolved build configuration. Defaults to this repository's.
|
|
322
322
|
* @returns {object} The `_stats` block.
|
|
323
323
|
*/
|
|
324
|
-
export function buildStats(
|
|
325
|
-
systemVersion = undefined,
|
|
326
|
-
config = loadPackConfig(),
|
|
327
|
-
) {
|
|
324
|
+
export function buildStats(systemVersion = undefined, config = loadPackConfig()) {
|
|
328
325
|
return {
|
|
329
326
|
systemId: config.stats.systemId,
|
|
330
327
|
systemVersion: systemVersion ?? config.stats.systemVersion,
|
|
@@ -453,12 +450,10 @@ export function buildContentLinkIndex(contentBase, router = packRouter()) {
|
|
|
453
450
|
docPack: router.resolveOrNull(fm, "JournalEntry"),
|
|
454
451
|
shortcode: fm.shortcode ?? null,
|
|
455
452
|
name: fm.name?.full ?? base,
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
base,
|
|
461
|
-
].filter(Boolean),
|
|
453
|
+
// The shared alias sources (#131): `aliases`, `name.aliases` and
|
|
454
|
+
// `name.full`, and deliberately not the filename — see
|
|
455
|
+
// {@link aliasesOf} for what that admitted and why it went.
|
|
456
|
+
aliases: aliasesOf(fm),
|
|
462
457
|
});
|
|
463
458
|
}
|
|
464
459
|
// Packages this build links *into* but does not publish. Their manifests
|
|
@@ -473,9 +468,7 @@ export function buildContentLinkIndex(contentBase, router = packRouter()) {
|
|
|
473
468
|
);
|
|
474
469
|
if (stale.length) {
|
|
475
470
|
for (const st of stale) {
|
|
476
|
-
log.error(
|
|
477
|
-
`Unusable link manifest for "${st.package}": ${st.reason}`,
|
|
478
|
-
);
|
|
471
|
+
log.error(`Unusable link manifest for "${st.package}": ${st.reason}`);
|
|
479
472
|
}
|
|
480
473
|
throw new Error(
|
|
481
474
|
"Cross-package links cannot be resolved from a stale manifest; " +
|
|
@@ -486,12 +479,7 @@ export function buildContentLinkIndex(contentBase, router = packRouter()) {
|
|
|
486
479
|
`Wikilink index: ${docs.length} local document(s), ` +
|
|
487
480
|
`${foreign.size} foreign address(es)`,
|
|
488
481
|
);
|
|
489
|
-
return buildWikilinkIndex(
|
|
490
|
-
docs,
|
|
491
|
-
foundryPackageId(),
|
|
492
|
-
foreign,
|
|
493
|
-
contentPackage(),
|
|
494
|
-
);
|
|
482
|
+
return buildWikilinkIndex(docs, foundryPackageId(), foreign, contentPackage());
|
|
495
483
|
}
|
|
496
484
|
|
|
497
485
|
/**
|
|
@@ -518,18 +506,7 @@ export function buildContentLinkIndex(contentBase, router = packRouter()) {
|
|
|
518
506
|
*/
|
|
519
507
|
export function convertNoteWikilinks(
|
|
520
508
|
body,
|
|
521
|
-
{
|
|
522
|
-
type,
|
|
523
|
-
id,
|
|
524
|
-
pack,
|
|
525
|
-
docPack,
|
|
526
|
-
index,
|
|
527
|
-
name,
|
|
528
|
-
file,
|
|
529
|
-
bodyLine,
|
|
530
|
-
bodyColumn,
|
|
531
|
-
lineMap,
|
|
532
|
-
},
|
|
509
|
+
{ type, id, pack, docPack, index, name, file, bodyLine, bodyColumn, lineMap },
|
|
533
510
|
) {
|
|
534
511
|
const result = convertWikilinks(body ?? "", {
|
|
535
512
|
type,
|
|
@@ -580,9 +557,7 @@ export function convertNoteWikilinks(
|
|
|
580
557
|
const claims = u.candidates ?? [];
|
|
581
558
|
const named =
|
|
582
559
|
claims.length ?
|
|
583
|
-
claims
|
|
584
|
-
.map((c) => `"${c.name}" (${c.type}-${c.shortcode})`)
|
|
585
|
-
.join(" and ")
|
|
560
|
+
claims.map((c) => `"${c.name}" (${c.type}-${c.shortcode})`).join(" and ")
|
|
586
561
|
: "two or more notes";
|
|
587
562
|
fail(
|
|
588
563
|
u,
|
|
@@ -591,6 +566,19 @@ export function convertNoteWikilinks(
|
|
|
591
566
|
`as [[type-shortcode|Text]].`,
|
|
592
567
|
);
|
|
593
568
|
}
|
|
569
|
+
// The author wrote a pipe, so they meant an address — and this target
|
|
570
|
+
// is not one. Its own message, because the correction is its own: a
|
|
571
|
+
// note *name* has to become an address, which is not the same job as
|
|
572
|
+
// fixing a shortcode that resolves nowhere (#131).
|
|
573
|
+
if (u.reason === "not-an-address") {
|
|
574
|
+
fail(
|
|
575
|
+
u,
|
|
576
|
+
`wikilink ${u.link} in "${name}" is written as an address — ` +
|
|
577
|
+
`the "|" says so — but "${u.target}" is not one. Write ` +
|
|
578
|
+
`[[type-shortcode|Text]], or drop the "|" to name it as ` +
|
|
579
|
+
`an alias within this note's own type.`,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
594
582
|
// A qualified address resolving nowhere is a typo, now that every
|
|
595
583
|
// linkable package is either built here or vendored (#1499) — so it
|
|
596
584
|
// fails the note rather than degrading to text. A bare alias stays a
|
|
@@ -613,9 +601,7 @@ export function convertNoteWikilinks(
|
|
|
613
601
|
`unresolved wikilink ${u.link} (${u.reason}) in "${name}"` +
|
|
614
602
|
// A link this build wrote is not at any authored position, so
|
|
615
603
|
// say where it came from instead of implying an edit site.
|
|
616
|
-
(at.generated ?
|
|
617
|
-
" — emitted by the content table on this line"
|
|
618
|
-
: ""),
|
|
604
|
+
(at.generated ? " — emitted by the content table on this line" : ""),
|
|
619
605
|
});
|
|
620
606
|
}
|
|
621
607
|
return result;
|
|
@@ -668,8 +654,7 @@ export function collectContentDocs(contentBase) {
|
|
|
668
654
|
* unlinkable; a note missing either renders as plain text rather than shipping a
|
|
669
655
|
* literal wikilink into a journal.
|
|
670
656
|
*/
|
|
671
|
-
const packLinkable = (doc) =>
|
|
672
|
-
Boolean(doc.fm?.shortcode) && Boolean(doc.fm?.type);
|
|
657
|
+
const packLinkable = (doc) => Boolean(doc.fm?.shortcode) && Boolean(doc.fm?.type);
|
|
673
658
|
|
|
674
659
|
/**
|
|
675
660
|
* Expand the fenced `dataview` tables in one note's markdown, before wikilinks
|
|
@@ -710,9 +695,7 @@ export function expandNoteTables(body, { docs, name, fm, bodyLine }) {
|
|
|
710
695
|
self,
|
|
711
696
|
});
|
|
712
697
|
if (errors.length) {
|
|
713
|
-
const err = new Error(
|
|
714
|
-
errors.map((e) => `content table — ${e.reason}`).join("; "),
|
|
715
|
-
);
|
|
698
|
+
const err = new Error(errors.map((e) => `content table — ${e.reason}`).join("; "));
|
|
716
699
|
// The first failing directive's line. Reporting one position for a
|
|
717
700
|
// message that may name several is honest here: a caller opens the
|
|
718
701
|
// file at the first thing to fix, and the message lists the rest.
|
|
@@ -735,18 +718,14 @@ export function expandNoteTables(body, { docs, name, fm, bodyLine }) {
|
|
|
735
718
|
*/
|
|
736
719
|
export function loadFolders(foldersFile) {
|
|
737
720
|
if (!fs.existsSync(foldersFile)) {
|
|
738
|
-
log.warn(
|
|
739
|
-
`No folders.yaml at ${foldersFile}; no folders will be emitted`,
|
|
740
|
-
);
|
|
721
|
+
log.warn(`No folders.yaml at ${foldersFile}; no folders will be emitted`);
|
|
741
722
|
return [];
|
|
742
723
|
}
|
|
743
724
|
const raw = fs.readFileSync(foldersFile, "utf8");
|
|
744
725
|
const parsed = yaml.parse(raw);
|
|
745
726
|
if (parsed == null) return [];
|
|
746
727
|
if (!Array.isArray(parsed)) {
|
|
747
|
-
throw new Error(
|
|
748
|
-
`folders.yaml must contain a YAML list; got ${typeof parsed}`,
|
|
749
|
-
);
|
|
728
|
+
throw new Error(`folders.yaml must contain a YAML list; got ${typeof parsed}`);
|
|
750
729
|
}
|
|
751
730
|
return parsed;
|
|
752
731
|
}
|
|
@@ -818,10 +797,7 @@ export function buildFolderResolver(folders) {
|
|
|
818
797
|
* underscores.
|
|
819
798
|
*/
|
|
820
799
|
export function folderFilename(name, id) {
|
|
821
|
-
return (
|
|
822
|
-
`folder_${unidecode(name)}_${id}`.replace(/[^0-9a-zA-Z]+/g, "_") +
|
|
823
|
-
".json"
|
|
824
|
-
);
|
|
800
|
+
return `folder_${unidecode(name)}_${id}`.replace(/[^0-9a-zA-Z]+/g, "_") + ".json";
|
|
825
801
|
}
|
|
826
802
|
|
|
827
803
|
/**
|
|
@@ -843,10 +819,7 @@ export function writeFolderDocs(folders, stats, destDir, documentType) {
|
|
|
843
819
|
_stats: stats,
|
|
844
820
|
_key: `!folders!${folder.id}`,
|
|
845
821
|
};
|
|
846
|
-
const outPath = path.join(
|
|
847
|
-
destDir,
|
|
848
|
-
folderFilename(folder.name, folder.id),
|
|
849
|
-
);
|
|
822
|
+
const outPath = path.join(destDir, folderFilename(folder.name, folder.id));
|
|
850
823
|
fs.writeFileSync(outPath, JSON.stringify(doc, null, 2), "utf8");
|
|
851
824
|
}
|
|
852
825
|
log.info(`Emitted ${folders.length} folder document(s) to ${destDir}`);
|
package/engine/homepage.mjs
CHANGED
|
@@ -262,9 +262,7 @@ export function checkHomepageCount(found, { contentBase, contentPackage }) {
|
|
|
262
262
|
if (pages.length === 1) return [];
|
|
263
263
|
|
|
264
264
|
return pages.map((page) => {
|
|
265
|
-
const others = pages
|
|
266
|
-
.filter((p) => p !== page)
|
|
267
|
-
.map((p) => formatLocator({ file: p.file }));
|
|
265
|
+
const others = pages.filter((p) => p !== page).map((p) => formatLocator({ file: p.file }));
|
|
268
266
|
return {
|
|
269
267
|
file: page.file,
|
|
270
268
|
...positionOfType(page.file),
|
|
@@ -292,11 +290,7 @@ export function checkHomepageCount(found, { contentBase, contentPackage }) {
|
|
|
292
290
|
*/
|
|
293
291
|
function positionOfType(file) {
|
|
294
292
|
try {
|
|
295
|
-
return positionInFrontmatter(
|
|
296
|
-
fs.readFileSync(file, "utf8"),
|
|
297
|
-
"type",
|
|
298
|
-
HOMEPAGE_TYPE,
|
|
299
|
-
);
|
|
293
|
+
return positionInFrontmatter(fs.readFileSync(file, "utf8"), "type", HOMEPAGE_TYPE);
|
|
300
294
|
} catch {
|
|
301
295
|
return {};
|
|
302
296
|
}
|
|
@@ -325,9 +319,7 @@ export function homepageTitle(fm, config) {
|
|
|
325
319
|
config?.packageBuild?.manifest
|
|
326
320
|
);
|
|
327
321
|
const title = manifest?.title;
|
|
328
|
-
return typeof title === "string" && title.trim() ?
|
|
329
|
-
title
|
|
330
|
-
: config.contentPackage;
|
|
322
|
+
return typeof title === "string" && title.trim() ? title : config.contentPackage;
|
|
331
323
|
}
|
|
332
324
|
|
|
333
325
|
/**
|
|
@@ -394,10 +386,7 @@ export const HOMEPAGE_ADDRESS_KEYS = Object.freeze(new Set(["url", "href"]));
|
|
|
394
386
|
*/
|
|
395
387
|
function collectProse(text, field, kind, out, skipCode = false) {
|
|
396
388
|
const pattern = new RegExp(MARKDOWN_LINK.source, "g");
|
|
397
|
-
const matches =
|
|
398
|
-
skipCode ?
|
|
399
|
-
matchAllOutsideCode(text, pattern)
|
|
400
|
-
: [...text.matchAll(pattern)];
|
|
389
|
+
const matches = skipCode ? matchAllOutsideCode(text, pattern) : [...text.matchAll(pattern)];
|
|
401
390
|
for (const m of matches) out.push({ field, url: m[1], kind });
|
|
402
391
|
}
|
|
403
392
|
|
package/engine/ids.mjs
CHANGED
|
@@ -36,11 +36,7 @@ import crypto from "crypto";
|
|
|
36
36
|
* @returns {string} A 16-character hexadecimal Foundry id.
|
|
37
37
|
*/
|
|
38
38
|
export function makeId(namespace, value) {
|
|
39
|
-
return crypto
|
|
40
|
-
.createHash("sha1")
|
|
41
|
-
.update(`${namespace}:${value}`)
|
|
42
|
-
.digest("hex")
|
|
43
|
-
.slice(0, 16);
|
|
39
|
+
return crypto.createHash("sha1").update(`${namespace}:${value}`).digest("hex").slice(0, 16);
|
|
44
40
|
}
|
|
45
41
|
|
|
46
42
|
/**
|
|
@@ -55,9 +51,18 @@ export function makeId(namespace, value) {
|
|
|
55
51
|
*
|
|
56
52
|
* @type {ReadonlySet<string>}
|
|
57
53
|
*/
|
|
58
|
-
export const MAP_TYPES = Object.freeze(
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
export const MAP_TYPES = Object.freeze(new Set(["map"]));
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The map subTypes, which differ only in the canvas defaults derived for them.
|
|
58
|
+
*
|
|
59
|
+
* They were three *types* until #174, which cost three entries in the pack
|
|
60
|
+
* router, three in the claims set and three in every consumer's section config
|
|
61
|
+
* — for one idea that the specification had always described as one type.
|
|
62
|
+
*
|
|
63
|
+
* @type {readonly string[]}
|
|
64
|
+
*/
|
|
65
|
+
export const MAP_SUBTYPES = Object.freeze(["battlemap", "localmap", "regionalmap"]);
|
|
61
66
|
|
|
62
67
|
/**
|
|
63
68
|
* Content type → the pack its documents compile into, and the document type
|
|
@@ -77,10 +82,7 @@ export const PACK_BY_TYPE = Object.freeze({
|
|
|
77
82
|
macro: { pack: "macros", docType: "Macro" },
|
|
78
83
|
being: { pack: "actors", docType: "Actor" },
|
|
79
84
|
...Object.fromEntries(
|
|
80
|
-
[...MAP_TYPES].map((type) => [
|
|
81
|
-
type,
|
|
82
|
-
{ pack: "scenes", docType: "Scene" },
|
|
83
|
-
]),
|
|
85
|
+
[...MAP_TYPES].map((type) => [type, { pack: "scenes", docType: "Scene" }]),
|
|
84
86
|
),
|
|
85
87
|
});
|
|
86
88
|
|
|
@@ -104,6 +106,13 @@ export const PACK_BY_TYPE = Object.freeze({
|
|
|
104
106
|
export const RETIRED_TYPES = Object.freeze({
|
|
105
107
|
character: "being",
|
|
106
108
|
creature: "being",
|
|
109
|
+
// The three map spellings, retired in favour of the single `map` whose
|
|
110
|
+
// subType they became (#174). Recorded rather than deleted for the same
|
|
111
|
+
// reason as the two above: an unnamed type falls through to the open item
|
|
112
|
+
// set, so a note or link left on one would be routed to the items pack.
|
|
113
|
+
battlemap: "map",
|
|
114
|
+
localmap: "map",
|
|
115
|
+
regionalmap: "map",
|
|
107
116
|
});
|
|
108
117
|
|
|
109
118
|
/**
|
package/engine/index.mjs
CHANGED
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
/** Deterministic document ids, the conventional pack map, and compendium UUIDs. */
|
|
37
37
|
export * as ids from "./ids.mjs";
|
|
38
38
|
|
|
39
|
+
/** The per-system frontmatter block, and the declared shared→system mapping. */
|
|
40
|
+
export * as systemBlock from "./system-block.mjs";
|
|
41
|
+
|
|
39
42
|
/** Fenced-code detection, so a rewrite never edits a code block. */
|
|
40
43
|
export * as codeFences from "./code-fences.mjs";
|
|
41
44
|
|
|
@@ -51,6 +54,9 @@ export * as packConfig from "./pack-config.mjs";
|
|
|
51
54
|
/** Which pack of a document type a note's document lands in (#1566). */
|
|
52
55
|
export * as packRouter from "./pack-router.mjs";
|
|
53
56
|
|
|
57
|
+
/** Which note types a configuration compiles, and the notes nothing claims (#146). */
|
|
58
|
+
export * as noteClaims from "./note-claims.mjs";
|
|
59
|
+
|
|
54
60
|
/** The content package a build compiles, and the Foundry package it ships in. */
|
|
55
61
|
export * as contentPackage from "./content-package.mjs";
|
|
56
62
|
|
|
@@ -66,6 +72,9 @@ export * as homepage from "./homepage.mjs";
|
|
|
66
72
|
/** The note types the engine itself declares, whatever a consumer registers. */
|
|
67
73
|
export * as noteSchemas from "./note-schemas.mjs";
|
|
68
74
|
|
|
75
|
+
/** The closed frontmatter regions: the `data:` container and each type's `subType`. */
|
|
76
|
+
export * as noteVocabulary from "./note-vocabulary.mjs";
|
|
77
|
+
|
|
69
78
|
/** The shipped Foundry manifest: locating it, reading it, guarding its id. */
|
|
70
79
|
|
|
71
80
|
/** The URL a content note is published at — the one web-address rule. */
|
|
@@ -104,6 +113,9 @@ export * as helpers from "./helpers.mjs";
|
|
|
104
113
|
/** The consumer's resolved item-type registry: the whitelist and its builders. */
|
|
105
114
|
export * as itemRegistry from "./item-registry.mjs";
|
|
106
115
|
|
|
116
|
+
/** The per-system note-type → document-subtype map, and looking a note up in it. */
|
|
117
|
+
export * as documentSubtypes from "./document-subtypes.mjs";
|
|
118
|
+
|
|
107
119
|
/** Which types carry documentation of their own, and where it is addressed. */
|
|
108
120
|
export * as itemDocs from "./item-docs.mjs";
|
|
109
121
|
|
|
@@ -113,6 +125,9 @@ export * as wikilinks from "./wikilinks.mjs";
|
|
|
113
125
|
/** What a `[[…]]` is, before either resolver decides where it points. */
|
|
114
126
|
export * as wikilinkSyntax from "./wikilink-syntax.mjs";
|
|
115
127
|
|
|
128
|
+
/** The alias namespace: what a note may be called, and who may claim a name. */
|
|
129
|
+
export * as aliasIndex from "./alias-index.mjs";
|
|
130
|
+
|
|
116
131
|
/** The address index a site build resolves its wikilinks against. */
|
|
117
132
|
export * as siteIndex from "./site-index.mjs";
|
|
118
133
|
|
package/engine/item-registry.mjs
CHANGED
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
* Both are read from the one resolved configuration, so they are literally the
|
|
19
19
|
* same object's keys and values: a type cannot be whitelisted for compilation
|
|
20
20
|
* without the builder that compiles it, which is the guarantee #1504 exists
|
|
21
|
-
* for.
|
|
21
|
+
* for. Where a consumer declares **several** registries, one per system (#58),
|
|
22
|
+
* the vocabulary is their union and every lookup below takes the system that is
|
|
23
|
+
* asking — a type both systems declare has two builders, and answering with one
|
|
24
|
+
* of them because it was declared first is the silent-wrong-output failure this
|
|
25
|
+
* package spends its time removing. The Item compiler dispatches through {@link itemBuilder}, so the table a
|
|
22
26
|
* consumer configured is the table its notes compile with — the whitelist and
|
|
23
27
|
* the dispatch used to come from different places, and a consumer supplying its
|
|
24
28
|
* own registry got the types it asked for and the builders it did not (#1563).
|
|
@@ -60,6 +64,43 @@ export function itemTypes() {
|
|
|
60
64
|
return loadPackConfig().itemTypes;
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Look one type up in the table a system declared, or in the flat one.
|
|
69
|
+
*
|
|
70
|
+
* The flat table is the union with the first declaring registry winning, which
|
|
71
|
+
* is the right answer for every single-system build and no answer at all where
|
|
72
|
+
* two systems declare the type. So a contested type without a system **throws**
|
|
73
|
+
* rather than resolving: the alternative is a document built by one system's
|
|
74
|
+
* builder and stamped with another's, which is exactly the shape of defect the
|
|
75
|
+
* document-subtype map exists to stop (#79).
|
|
76
|
+
*
|
|
77
|
+
* @param {string} what - What is being looked up, for the message.
|
|
78
|
+
* @param {"itemBuilders"|"itemArt"|"itemFields"} table - Which table.
|
|
79
|
+
* @param {string} type - The item type.
|
|
80
|
+
* @param {string} [system] - The system asking, where a build has more than one.
|
|
81
|
+
* @returns {any} The entry, or `undefined`.
|
|
82
|
+
* @throws {Error} When several systems declare the type and none is named.
|
|
83
|
+
*/
|
|
84
|
+
function lookup(what, table, type, system) {
|
|
85
|
+
const config = loadPackConfig();
|
|
86
|
+
if (system !== undefined) {
|
|
87
|
+
const perSystem = /** @type {Record<string, Record<string, unknown>>} */ (
|
|
88
|
+
config[`${table}BySystem`]
|
|
89
|
+
);
|
|
90
|
+
// A consumer declaring one, system-less registry keeps answering for
|
|
91
|
+
// every system: it *is* the only vocabulary in the build.
|
|
92
|
+
if (Object.keys(perSystem).length) return perSystem[system]?.[type];
|
|
93
|
+
} else if (config.itemTypesBySeveralSystems.has(type)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`More than one declared registry defines the item type "${type}", so ` +
|
|
96
|
+
`the ${what} of a "${type}" depends on which system is asking. ` +
|
|
97
|
+
`Name the system at the call site, or stop declaring the type ` +
|
|
98
|
+
`twice.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return /** @type {Record<string, unknown>} */ (config[table])[type];
|
|
102
|
+
}
|
|
103
|
+
|
|
63
104
|
/**
|
|
64
105
|
* The builder the consuming repository registered for an item type.
|
|
65
106
|
*
|
|
@@ -68,13 +109,16 @@ export function itemTypes() {
|
|
|
68
109
|
* failing as an anonymous `is not a function` (#1504).
|
|
69
110
|
*
|
|
70
111
|
* @param {string} type - The note's `type` frontmatter.
|
|
112
|
+
* @param {string} [system] - The system compiling it, where a build declares
|
|
113
|
+
* more than one registry. Omitted, a type only one registry declares still
|
|
114
|
+
* resolves; a contested one throws rather than picking a side.
|
|
71
115
|
* @returns {(fm: object) => object} The builder for that type.
|
|
72
116
|
* @throws {Error} When the configuration registers no builder for `type`.
|
|
73
117
|
*/
|
|
74
|
-
export function itemBuilder(type) {
|
|
75
|
-
const builder = /** @type {
|
|
76
|
-
|
|
77
|
-
)
|
|
118
|
+
export function itemBuilder(type, system) {
|
|
119
|
+
const builder = /** @type {Function|undefined} */ (
|
|
120
|
+
lookup("builder", "itemBuilders", type, system)
|
|
121
|
+
);
|
|
78
122
|
if (typeof builder !== "function") {
|
|
79
123
|
throw new Error(
|
|
80
124
|
`No builder registered for item type "${type}" — add one to the ` +
|
|
@@ -85,6 +129,25 @@ export function itemBuilder(type) {
|
|
|
85
129
|
return /** @type {(fm: object) => object} */ (builder);
|
|
86
130
|
}
|
|
87
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The frontmatter fields a type's registry entry declares, if any.
|
|
134
|
+
*
|
|
135
|
+
* Sparse by design: a type whose entry declares none compiles normally and is
|
|
136
|
+
* simply undocumented (#22). What reads it is the `system`-block passthrough,
|
|
137
|
+
* which has to know which paths a declared field already writes before it
|
|
138
|
+
* writes the rest (#58).
|
|
139
|
+
*
|
|
140
|
+
* @param {string} type - The item type.
|
|
141
|
+
* @param {string} [system] - The system compiling it, where a build declares
|
|
142
|
+
* more than one registry.
|
|
143
|
+
* @returns {readonly object[]|undefined} The declaration, or `undefined`.
|
|
144
|
+
*/
|
|
145
|
+
export function itemFields(type, system) {
|
|
146
|
+
return /** @type {readonly object[]|undefined} */ (
|
|
147
|
+
lookup("field declaration", "itemFields", type, system)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
88
151
|
/**
|
|
89
152
|
* The default art for an item type — the image a note of that type is given
|
|
90
153
|
* when it carries no `img:` of its own.
|
|
@@ -108,13 +171,13 @@ export function itemBuilder(type) {
|
|
|
108
171
|
* One spelling, one meaning, wherever it is written.
|
|
109
172
|
*
|
|
110
173
|
* @param {string} type - the item type.
|
|
174
|
+
* @param {string} [system] - The system compiling it, where a build declares
|
|
175
|
+
* more than one registry.
|
|
111
176
|
* @returns {string} The default image path for that type.
|
|
112
177
|
* @throws {Error} When the type's registry entry pairs no `img`.
|
|
113
178
|
*/
|
|
114
|
-
export function itemArt(type) {
|
|
115
|
-
const art = /** @type {
|
|
116
|
-
loadPackConfig().itemArt
|
|
117
|
-
)[type];
|
|
179
|
+
export function itemArt(type, system) {
|
|
180
|
+
const art = /** @type {string|undefined} */ (lookup("default art", "itemArt", type, system));
|
|
118
181
|
if (!art) {
|
|
119
182
|
throw new Error(
|
|
120
183
|
`No default art for item type "${type}" — the note carries no ` +
|