@heroiclands/package-build 18.2.0 → 20.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 +692 -0
- package/CONTENT.md +81 -10
- package/bin/content-build.mjs +7 -1
- package/ci/ci-docker.mjs +21 -0
- package/content-config.mjs +26 -24
- package/docs/content-format.md +408 -85
- package/engine/actor-compiler.mjs +197 -7
- package/engine/address-charset.mjs +23 -5
- package/engine/base-compiler.mjs +65 -2
- package/engine/bundles.mjs +9 -0
- package/engine/content-address.mjs +92 -1
- package/engine/content-format.mjs +102 -0
- package/engine/content-index.mjs +11 -8
- package/engine/content-links.mjs +37 -21
- package/engine/field-reference.mjs +57 -5
- package/engine/field-spec.mjs +214 -7
- package/engine/folder-notes.mjs +88 -1
- package/engine/foreign-catalog.mjs +4 -1
- package/engine/foundry-entries.mjs +16 -0
- package/engine/frontmatter-lint.mjs +215 -28
- package/engine/frontmatter.mjs +12 -12
- package/engine/generate.mjs +78 -46
- package/engine/helpers.mjs +87 -128
- package/engine/index.mjs +3 -0
- package/engine/item-compiler.mjs +44 -9
- package/engine/journals.mjs +27 -16
- package/engine/macros.mjs +8 -0
- package/engine/map-notes.mjs +7 -7
- package/engine/note-ids.mjs +25 -1
- package/engine/note-vocabulary.mjs +76 -9
- package/engine/retired-fields.mjs +57 -16
- package/engine/runtime-only-fields.mjs +204 -0
- package/engine/scenes.mjs +19 -28
- package/engine/schema-check.mjs +23 -1
- package/engine/site-index.mjs +17 -0
- package/engine/subtype-registry.mjs +30 -0
- package/engine/system-block.mjs +81 -3
- package/engine/web-wikilinks.mjs +33 -27
- package/engine/wikilink-syntax.mjs +7 -0
- package/engine/wikilinks.mjs +74 -16
- package/hm3/actors.mjs +70 -23
- package/package.json +2 -2
- package/sohl/actors.mjs +106 -7
- package/sohl/item-fields.mjs +203 -0
- package/sohl/note-schemas.mjs +6 -3
- package/types/content-config.d.mts +0 -7
- package/types/engine/actor-compiler.d.mts +83 -3
- package/types/engine/address-charset.d.mts +22 -4
- package/types/engine/base-compiler.d.mts +54 -3
- package/types/engine/content-address.d.mts +64 -0
- package/types/engine/content-format.d.mts +9 -0
- package/types/engine/field-spec.d.mts +271 -3
- package/types/engine/folder-notes.d.mts +59 -0
- package/types/engine/foundry-entries.d.mts +6 -0
- package/types/engine/frontmatter-lint.d.mts +18 -2
- package/types/engine/frontmatter.d.mts +11 -11
- package/types/engine/generate.d.mts +27 -0
- package/types/engine/helpers.d.mts +37 -38
- package/types/engine/index.d.mts +1 -0
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-ids.d.mts +14 -0
- package/types/engine/retired-fields.d.mts +29 -13
- package/types/engine/runtime-only-fields.d.mts +102 -0
- package/types/engine/schema-check.d.mts +10 -1
- package/types/engine/subtype-registry.d.mts +21 -0
- package/types/engine/system-block.d.mts +28 -2
- package/types/sohl/actors.d.mts +3 -3
package/engine/helpers.mjs
CHANGED
|
@@ -400,15 +400,92 @@ export function makeFilename(name, id) {
|
|
|
400
400
|
* non-alphanumerics collapsed to single hyphens.
|
|
401
401
|
*/
|
|
402
402
|
|
|
403
|
+
/**
|
|
404
|
+
* The path prefixes that name a package other than the one being compiled.
|
|
405
|
+
*
|
|
406
|
+
* Foundry serves every installed package from a root named for its kind, so a
|
|
407
|
+
* path opening with one of these is already a served address and belongs to
|
|
408
|
+
* somebody else — most often `systems/sohl/assets/…`, where every default this
|
|
409
|
+
* toolchain ships lives, and which a module's content cites as readily as the
|
|
410
|
+
* system's own does.
|
|
411
|
+
*
|
|
412
|
+
* **Two, not "the ones we happen to use".** `worlds/` is left out on purpose: a
|
|
413
|
+
* package may not ship art out of a world, so a note that writes one has made a
|
|
414
|
+
* mistake, and prefixing it yields a plainly broken path rather than a
|
|
415
|
+
* plausible one that fails silently much later.
|
|
416
|
+
*
|
|
417
|
+
* @type {readonly string[]}
|
|
418
|
+
*/
|
|
419
|
+
const FOREIGN_PACKAGE_ROOTS = Object.freeze(["systems/", "modules/"]);
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Whether a path already addresses something this package does not own, and so
|
|
423
|
+
* must be emitted exactly as authored.
|
|
424
|
+
*
|
|
425
|
+
* Three shapes qualify, each a different kind of "not mine":
|
|
426
|
+
*
|
|
427
|
+
* - **Another package** — `systems/…` or `modules/…`, per
|
|
428
|
+
* {@link FOREIGN_PACKAGE_ROOTS}.
|
|
429
|
+
* - **Somewhere off this install** — a URI scheme (`https:`, `data:`) or a
|
|
430
|
+
* protocol-relative `//cdn…`.
|
|
431
|
+
* - **The data root itself** — a leading `/`, which Foundry serves from the
|
|
432
|
+
* install rather than from any package.
|
|
433
|
+
*
|
|
434
|
+
* @param {string} s - A non-empty authored path.
|
|
435
|
+
* @returns {boolean} Whether it passes through untranslated.
|
|
436
|
+
*/
|
|
437
|
+
function addressesAnotherPackage(s) {
|
|
438
|
+
if (FOREIGN_PACKAGE_ROOTS.some((root) => s.startsWith(root))) return true;
|
|
439
|
+
// `//host/x.png` — protocol-relative, so it leaves this origin entirely.
|
|
440
|
+
// Checked before the single-slash case, which would otherwise claim it.
|
|
441
|
+
if (s.startsWith("//")) return true;
|
|
442
|
+
// `/x.png` — rooted at the Foundry data root, not at any package.
|
|
443
|
+
if (s.startsWith("/")) return true;
|
|
444
|
+
// `https://…`, `data:…`, `file:…` — a scheme, so not a path at all.
|
|
445
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(s);
|
|
446
|
+
}
|
|
447
|
+
|
|
403
448
|
/**
|
|
404
449
|
* Translate a content-relative image path into its Foundry-relative form.
|
|
405
450
|
*
|
|
406
451
|
* Content frontmatter (`img` / `portrait`) authors a single path that has to
|
|
407
|
-
* work for Foundry, the knowledgebase, and the website.
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
452
|
+
* work for Foundry, the knowledgebase, and the website. **Its first segment
|
|
453
|
+
* says which package owns the file** (#331), and there are exactly three
|
|
454
|
+
* answers:
|
|
455
|
+
*
|
|
456
|
+
* | Authored path starts with | Owner | Emitted |
|
|
457
|
+
* | ------------------------- | --------------------- | -------------------- |
|
|
458
|
+
* | `systems/` | a separate **system** | unchanged |
|
|
459
|
+
* | `modules/` | a separate **module** | unchanged |
|
|
460
|
+
* | anything else | **this package** | `<assetRoot>/<path>` |
|
|
461
|
+
*
|
|
462
|
+
* So `icons/relic.svg` compiles to `systems/sohl/assets/icons/relic.svg` here
|
|
463
|
+
* and to `modules/sohl-thalorna/assets/icons/relic.svg` in a module — the asset
|
|
464
|
+
* root is derived from the configuration, and is the one place `systems/sohl`
|
|
465
|
+
* is ever spelled (#1508). An authored
|
|
466
|
+
* `systems/sohl/assets/icons/noun/shield.svg` is left exactly as written,
|
|
467
|
+
* whichever package is compiling it.
|
|
468
|
+
*
|
|
469
|
+
* **This is a rule about ownership, not an allowlist of directories.** It used
|
|
470
|
+
* to prefix `icons/…` and `images/…` and pass everything else through — the
|
|
471
|
+
* same answer for every path any tree authors today, and the wrong one for the
|
|
472
|
+
* next directory a package ships. `sohl-kethira-basic` keeps art under
|
|
473
|
+
* `assets/artwork/`, so an authored `artwork/deity.webp` would have shipped
|
|
474
|
+
* unprefixed: a 404 in Foundry, reported by nothing. That a package owns its
|
|
475
|
+
* own tree is the fact; the directory names inside it are that package's
|
|
476
|
+
* business (#331).
|
|
477
|
+
*
|
|
478
|
+
* **Off-install addresses pass through too**, which is the same rule rather
|
|
479
|
+
* than a fourth: a URL, a `data:` URI, or a `/`-rooted path names something no
|
|
480
|
+
* package owns. See {@link addressesAnotherPackage}.
|
|
481
|
+
*
|
|
482
|
+
* **`banner:` does not follow this rule, deliberately (#331).** It is not an
|
|
483
|
+
* asset path inside a Foundry install at all: it reaches no compiled document,
|
|
484
|
+
* and its only consumer is the Hugo theme, which prefixes a relative value with
|
|
485
|
+
* `images/` and joins it onto `params.cdnBaseURL`. The two fields look alike
|
|
486
|
+
* and address different places — `img:` a file Foundry serves, `banner:` a file
|
|
487
|
+
* the CDN serves — so they are documented apart rather than reconciled into one
|
|
488
|
+
* rule that would be true of neither.
|
|
412
489
|
*
|
|
413
490
|
* **Two empties, and they mean opposite things (#218).** `null` — or an absent
|
|
414
491
|
* key, which reaches here as `undefined` — means _unset_: the note names no art
|
|
@@ -453,10 +530,10 @@ export function resolveImg(raw, config = loadPackConfig()) {
|
|
|
453
530
|
const s = String(raw);
|
|
454
531
|
// Blank on purpose — the caller's default must not apply.
|
|
455
532
|
if (s === "") return "";
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return s
|
|
533
|
+
// Somebody else's to serve — emit it exactly as authored.
|
|
534
|
+
if (addressesAnotherPackage(s)) return s;
|
|
535
|
+
// Ours, so root it where Foundry serves this package's files from.
|
|
536
|
+
return `${config.assetRoot}/${s}`;
|
|
460
537
|
}
|
|
461
538
|
|
|
462
539
|
/**
|
|
@@ -913,102 +990,9 @@ export function expandNoteTables(body, { docs, name, fm, bodyLine, sqlTables })
|
|
|
913
990
|
}
|
|
914
991
|
|
|
915
992
|
/* ------------------------------------------------------------------------ */
|
|
916
|
-
/* Folder
|
|
993
|
+
/* Folder document filenames */
|
|
917
994
|
/* ------------------------------------------------------------------------ */
|
|
918
995
|
|
|
919
|
-
/**
|
|
920
|
-
* Loads a folders.yaml file as an array of folder entries. Returns []
|
|
921
|
-
* when the file is missing (logging a warning) so packs without folders
|
|
922
|
-
* can opt out simply by not committing the file.
|
|
923
|
-
*/
|
|
924
|
-
export function loadFolders(foldersFile) {
|
|
925
|
-
if (!fs.existsSync(foldersFile)) {
|
|
926
|
-
log.warn(`No folders.yaml at ${foldersFile}; no folders will be emitted`);
|
|
927
|
-
return [];
|
|
928
|
-
}
|
|
929
|
-
const raw = fs.readFileSync(foldersFile, "utf8");
|
|
930
|
-
const parsed = yaml.parse(raw);
|
|
931
|
-
if (parsed == null) return [];
|
|
932
|
-
if (!Array.isArray(parsed)) {
|
|
933
|
-
throw new Error(`folders.yaml must contain a YAML list; got ${typeof parsed}`);
|
|
934
|
-
}
|
|
935
|
-
return parsed;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
/**
|
|
939
|
-
* Validates folder invariants and returns a resolver function that maps a
|
|
940
|
-
* folder id to the same id (after verifying it exists). Returns `null` for
|
|
941
|
-
* a null/empty input; throws for an unknown id.
|
|
942
|
-
*
|
|
943
|
-
* Invariants:
|
|
944
|
-
* - Every folder must have a non-empty id
|
|
945
|
-
* - Every folder must have a name
|
|
946
|
-
* - Sibling folders (same parentFolderId) must have unique names
|
|
947
|
-
* - Every parentFolderId must match an existing folder id (or be "")
|
|
948
|
-
*
|
|
949
|
-
* Returns { resolver, folders } where folders is the validated list.
|
|
950
|
-
*/
|
|
951
|
-
export function buildFolderResolver(folders) {
|
|
952
|
-
const byId = new Map();
|
|
953
|
-
for (const f of folders) {
|
|
954
|
-
if (!f.id) {
|
|
955
|
-
throw new Error(`Folder missing id: ${JSON.stringify(f)}`);
|
|
956
|
-
}
|
|
957
|
-
if (!f.name) {
|
|
958
|
-
throw new Error(`Folder ${f.id} missing name`);
|
|
959
|
-
}
|
|
960
|
-
if (byId.has(f.id)) {
|
|
961
|
-
throw new Error(`Duplicate folder id ${f.id}`);
|
|
962
|
-
}
|
|
963
|
-
byId.set(f.id, f);
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
const siblingsByParent = new Map();
|
|
967
|
-
for (const f of folders) {
|
|
968
|
-
const parentId = f.parentFolderId || "";
|
|
969
|
-
if (parentId && !byId.has(parentId)) {
|
|
970
|
-
throw new Error(
|
|
971
|
-
`Folder ${f.id} (${f.name}) references unknown parentFolderId ${parentId}`,
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
if (!siblingsByParent.has(parentId)) {
|
|
975
|
-
siblingsByParent.set(parentId, new Set());
|
|
976
|
-
}
|
|
977
|
-
const siblings = siblingsByParent.get(parentId);
|
|
978
|
-
if (siblings.has(f.name)) {
|
|
979
|
-
throw new Error(
|
|
980
|
-
`Sibling folders share name "${f.name}" under parent ${parentId || "(root)"} — names must be unique among siblings`,
|
|
981
|
-
);
|
|
982
|
-
}
|
|
983
|
-
siblings.add(f.name);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/**
|
|
987
|
-
* The folder id a note names, by id.
|
|
988
|
-
*
|
|
989
|
-
* **Only by id.** This resolver answers for `folder:` alone; `packFolder:`
|
|
990
|
-
* names a folder *note* and is resolved through the address index instead
|
|
991
|
-
* (#255). The path lookup that briefly lived here is gone with the path
|
|
992
|
-
* spelling it served — it was never released, so there is nothing to
|
|
993
|
-
* deprecate.
|
|
994
|
-
*
|
|
995
|
-
* @param {string|null|undefined} value - As authored.
|
|
996
|
-
* @returns {string|null} The id, or `null` for an absent value.
|
|
997
|
-
* @throws {Error} When the id is not one this pack declares.
|
|
998
|
-
*/
|
|
999
|
-
function resolver(value) {
|
|
1000
|
-
if (value == null || value === "") return null;
|
|
1001
|
-
const authored = String(value).trim();
|
|
1002
|
-
if (!authored) return null;
|
|
1003
|
-
if (!byId.has(authored)) {
|
|
1004
|
-
throw new Error(`Unknown folder id "${authored}"`);
|
|
1005
|
-
}
|
|
1006
|
-
return authored;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
return { resolver, folders };
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
996
|
/**
|
|
1013
997
|
* Builds a compendium-source filename for a folder JSON document:
|
|
1014
998
|
* `folder_Name_id.json` with non-alphanumeric runs replaced by
|
|
@@ -1017,28 +1001,3 @@ export function buildFolderResolver(folders) {
|
|
|
1017
1001
|
export function folderFilename(name, id) {
|
|
1018
1002
|
return `folder_${unidecode(name)}_${id}`.replace(/[^0-9a-zA-Z]+/g, "_") + ".json";
|
|
1019
1003
|
}
|
|
1020
|
-
|
|
1021
|
-
/**
|
|
1022
|
-
* Writes one JSON document per folder into `destDir`. `documentType`
|
|
1023
|
-
* determines the folder's Foundry `type` field — `"Item"` for the items
|
|
1024
|
-
* pack, `"JournalEntry"` for the journals pack.
|
|
1025
|
-
*/
|
|
1026
|
-
export function writeFolderDocs(folders, stats, destDir, documentType) {
|
|
1027
|
-
for (const folder of folders) {
|
|
1028
|
-
const doc = {
|
|
1029
|
-
name: folder.name,
|
|
1030
|
-
sorting: "a",
|
|
1031
|
-
folder: folder.parentFolderId || null,
|
|
1032
|
-
type: documentType,
|
|
1033
|
-
_id: folder.id,
|
|
1034
|
-
sort: 0,
|
|
1035
|
-
color: folder.color,
|
|
1036
|
-
flags: folder.flags || {},
|
|
1037
|
-
_stats: stats,
|
|
1038
|
-
_key: `!folders!${folder.id}`,
|
|
1039
|
-
};
|
|
1040
|
-
const outPath = path.join(destDir, folderFilename(folder.name, folder.id));
|
|
1041
|
-
fs.writeFileSync(outPath, JSON.stringify(doc, null, 2), "utf8");
|
|
1042
|
-
}
|
|
1043
|
-
log.info(`Emitted ${folders.length} folder document(s) to ${destDir}`);
|
|
1044
|
-
}
|
package/engine/index.mjs
CHANGED
|
@@ -69,6 +69,9 @@ export * as notePackage from "./note-package.mjs";
|
|
|
69
69
|
/** Frontmatter fields a note may no longer declare, and the refusal of them. */
|
|
70
70
|
export * as retiredFields from "./retired-fields.mjs";
|
|
71
71
|
|
|
72
|
+
/** Schema fields a note may never declare, because play writes them. */
|
|
73
|
+
export * as runtimeOnlyFields from "./runtime-only-fields.mjs";
|
|
74
|
+
|
|
72
75
|
/** The package homepage: the note type that compiles to a page, not a document. */
|
|
73
76
|
export * as homepage from "./homepage.mjs";
|
|
74
77
|
|
package/engine/item-compiler.mjs
CHANGED
|
@@ -66,6 +66,9 @@ import { documentSubtype, subtypeRow } from "./document-subtypes.mjs";
|
|
|
66
66
|
// `system` verbatim, and `<system>.img` / `.effects` / `.flags` overriding
|
|
67
67
|
// their shared top-level forms for this system alone (#58).
|
|
68
68
|
import { blockField, blockProperty, claimedPaths, mergeSystemData } from "./system-block.mjs";
|
|
69
|
+
// The other direction of the same declaration: a field the *document* writes in
|
|
70
|
+
// play, which a note may not author and the builder does not emit (#330).
|
|
71
|
+
import { assertNoRuntimeOnlyFields } from "./runtime-only-fields.mjs";
|
|
69
72
|
|
|
70
73
|
/**
|
|
71
74
|
* The description an item carries: a pointer to its **item doc**, the
|
|
@@ -113,6 +116,17 @@ export class SystemItemCompiler extends BasePackCompiler {
|
|
|
113
116
|
*/
|
|
114
117
|
static requiresSystemBlock = true;
|
|
115
118
|
|
|
119
|
+
/**
|
|
120
|
+
* An Item carries an `img` — its icon — which this pass writes from the
|
|
121
|
+
* note's own path, falling back to the type's default art.
|
|
122
|
+
*
|
|
123
|
+
* `portrait` is **not** among them: a portrait is a being's sheet picture,
|
|
124
|
+
* and an item has nowhere to put one.
|
|
125
|
+
*
|
|
126
|
+
* @type {readonly string[]}
|
|
127
|
+
*/
|
|
128
|
+
static emitsArt = Object.freeze(["img"]);
|
|
129
|
+
|
|
116
130
|
/**
|
|
117
131
|
* The note-type → document-subtype map this pass compiles against.
|
|
118
132
|
*
|
|
@@ -174,6 +188,29 @@ export class SystemItemCompiler extends BasePackCompiler {
|
|
|
174
188
|
return !row || row.document === "Item";
|
|
175
189
|
}
|
|
176
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Refuse a note authoring one of its type's **runtime-only** fields (#330).
|
|
193
|
+
*
|
|
194
|
+
* A schema declares fields the document writes for itself — an affliction's
|
|
195
|
+
* `onsetDate` is the world time its onset fired at — and a note authoring
|
|
196
|
+
* one used to compile, because `<system>.system` is a verbatim passthrough
|
|
197
|
+
* and the field really is in the schema. The result was shipped content
|
|
198
|
+
* carrying one world's play state, with the build reporting success.
|
|
199
|
+
*
|
|
200
|
+
* The declaration says which, so nothing here knows a field name; see
|
|
201
|
+
* {@link module:engine/runtime-only-fields}.
|
|
202
|
+
*
|
|
203
|
+
* @param {object} fm - The note's frontmatter.
|
|
204
|
+
* @returns {void}
|
|
205
|
+
* @throws {Error} When the note authors one.
|
|
206
|
+
*/
|
|
207
|
+
assertAuthorable(fm) {
|
|
208
|
+
assertNoRuntimeOnlyFields(fm, itemFields(fm.type, this.system), {
|
|
209
|
+
block: this.system,
|
|
210
|
+
absPath: this.currentNote?.absPath,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
177
214
|
/**
|
|
178
215
|
* The Foundry Item subtype a note compiles into.
|
|
179
216
|
*
|
|
@@ -296,15 +333,13 @@ export class SystemItemCompiler extends BasePackCompiler {
|
|
|
296
333
|
});
|
|
297
334
|
|
|
298
335
|
const effects = blockProperty(fm, system, "effects");
|
|
299
|
-
// Read through the system block like every other item field
|
|
300
|
-
//
|
|
301
|
-
// folder
|
|
302
|
-
//
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
this.folderResolver(packFolderAddress, { isAddress: true })
|
|
307
|
-
: this.folderResolver(blockField(fm, system, "folder", null));
|
|
336
|
+
// Read through the system block like every other item field. There is
|
|
337
|
+
// one spelling: `packFolder` names a folder note by its address, the
|
|
338
|
+
// `folder:` id spelling having been retired with the per-pack YAML it
|
|
339
|
+
// resolved against (#251, #255, #260).
|
|
340
|
+
const folder = this.folderResolver(blockField(fm, system, "packFolder", null), {
|
|
341
|
+
isAddress: true,
|
|
342
|
+
});
|
|
308
343
|
|
|
309
344
|
return {
|
|
310
345
|
name,
|
package/engine/journals.mjs
CHANGED
|
@@ -30,10 +30,13 @@
|
|
|
30
30
|
* any other: the macro pass reads the same page independently, and withholds
|
|
31
31
|
* nothing from the journal (#1514).
|
|
32
32
|
*
|
|
33
|
-
* Folder placement is identical to the items pack: `sohl.
|
|
34
|
-
* frontmatter is
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* Folder placement is identical to the items pack: `sohl.packFolder` in
|
|
34
|
+
* frontmatter is a folder **note's address**, resolved through the shared
|
|
35
|
+
* address index by the constructor's `folderResolver` (#255, #260). A folder
|
|
36
|
+
* materialises in every pack holding a document that names it, so a journals
|
|
37
|
+
* pack needs to declare nothing (#257) — which is what stopped this pass
|
|
38
|
+
* filing documentation into folders its own pack had never heard of. A
|
|
39
|
+
* documentation entry reuses its document's folder verbatim.
|
|
37
40
|
*
|
|
38
41
|
* Not a standalone script — exports the `Journals` compiler class, imported
|
|
39
42
|
* and driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
|
|
@@ -311,6 +314,20 @@ export class Journals extends BasePackCompiler {
|
|
|
311
314
|
*/
|
|
312
315
|
static requiresId = false;
|
|
313
316
|
|
|
317
|
+
/**
|
|
318
|
+
* **None.** A JournalEntry has no artwork — no `img` property, and no
|
|
319
|
+
* nested place for one — so a note whose whole document is prose has
|
|
320
|
+
* nowhere to put an authored path (#349).
|
|
321
|
+
*
|
|
322
|
+
* The emptiness is the declaration, in the sense `JOURNAL_ONLY_FIELDS` is:
|
|
323
|
+
* it is what separates a pass that emits no art from one that has simply
|
|
324
|
+
* not said, and it is the fact the frontmatter lint reports a `lore` note's
|
|
325
|
+
* inert `img:` from.
|
|
326
|
+
*
|
|
327
|
+
* @type {readonly string[]}
|
|
328
|
+
*/
|
|
329
|
+
static emitsArt = Object.freeze([]);
|
|
330
|
+
|
|
314
331
|
/**
|
|
315
332
|
* How many of the compiled entries were documentation for a document
|
|
316
333
|
* compiled elsewhere, for the summary.
|
|
@@ -381,12 +398,8 @@ export class Journals extends BasePackCompiler {
|
|
|
381
398
|
|
|
382
399
|
// A documentation entry is filed exactly where the document it
|
|
383
400
|
// describes is, so the journals pack mirrors the items pack and a doc
|
|
384
|
-
// sits under the same heading a reader found the item under.
|
|
385
|
-
//
|
|
386
|
-
// against this pack's own folders.yaml — an item folder is declared in
|
|
387
|
-
// the items one, a macro folder in the macros one, and a map's in the
|
|
388
|
-
// scenes one.
|
|
389
|
-
const { value: authoredFolder, isAddress } = folderField(fm);
|
|
401
|
+
// sits under the same heading a reader found the item under.
|
|
402
|
+
//
|
|
390
403
|
// An address is resolved wherever it is written, including here — and
|
|
391
404
|
// resolving it *here* is what cures the defect this comment used to
|
|
392
405
|
// describe. A folder note has one definition and one address, so the
|
|
@@ -395,12 +408,10 @@ export class Journals extends BasePackCompiler {
|
|
|
395
408
|
// with the first, and so no arrangement to assume: the mirroring
|
|
396
409
|
// failure is unrepresentable rather than merely reported.
|
|
397
410
|
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
: ownsDoc ? authoredFolder
|
|
403
|
-
: this.folderResolver(authoredFolder);
|
|
411
|
+
// The id spelling used to cross packs verbatim here, on the assumption
|
|
412
|
+
// both declared it — the arrangement #260 retires with the YAML.
|
|
413
|
+
const { value: authoredFolder } = folderField(fm);
|
|
414
|
+
const folder = this.folderResolver(authoredFolder, { isAddress: true });
|
|
404
415
|
|
|
405
416
|
return buildJournalEntry({
|
|
406
417
|
id,
|
package/engine/macros.mjs
CHANGED
|
@@ -295,6 +295,14 @@ export class Macros extends BasePackCompiler {
|
|
|
295
295
|
*/
|
|
296
296
|
static convertsWikilinks = false;
|
|
297
297
|
|
|
298
|
+
/**
|
|
299
|
+
* A Macro carries an `img` — the tile art Foundry shows on the hotbar —
|
|
300
|
+
* defaulting to {@link DEFAULT_MACRO_IMG} where the note names none.
|
|
301
|
+
*
|
|
302
|
+
* @type {readonly string[]}
|
|
303
|
+
*/
|
|
304
|
+
static emitsArt = Object.freeze(["img"]);
|
|
305
|
+
|
|
298
306
|
/**
|
|
299
307
|
* @param {object} fm - The note's frontmatter.
|
|
300
308
|
* @returns {boolean} True for a `macro` note.
|
package/engine/map-notes.mjs
CHANGED
|
@@ -56,9 +56,9 @@ import { compendiumUuid, makeId, MAP_SUBTYPES, MAP_TYPES } from "./ids.mjs";
|
|
|
56
56
|
// bridge (`SohlRegionTriggerBehavior`), so an event this build accepts is
|
|
57
57
|
// exactly one the bridge forwards.
|
|
58
58
|
import { CURATED_REGION_EVENTS, EXCLUDED_REGION_EVENTS } from "./region-events.mjs";
|
|
59
|
-
// A map's background art is `img`, as every other note type's art is
|
|
60
|
-
//
|
|
61
|
-
import {
|
|
59
|
+
// A map's background art is `img`, as every other note type's art is. `image`,
|
|
60
|
+
// the spelling a map alone once used, is retired and gone (#149).
|
|
61
|
+
import { sohlField } from "./frontmatter.mjs";
|
|
62
62
|
|
|
63
63
|
/* -------------------------------------------------------------------- */
|
|
64
64
|
/* Note types and their canvas profiles */
|
|
@@ -907,7 +907,7 @@ export function buildScene(fm, ctx) {
|
|
|
907
907
|
// Read from the note rather than from its `sohl:` block: art is not
|
|
908
908
|
// system-specific, so `img` is authored at the top level like every other
|
|
909
909
|
// type's, and `sohlField` honours the block for anything already there.
|
|
910
|
-
const img =
|
|
910
|
+
const img = sohlField(fm, "img");
|
|
911
911
|
if (!img) throw new Error("a map note needs an `img`");
|
|
912
912
|
|
|
913
913
|
const warn = (message) => {
|
|
@@ -981,11 +981,11 @@ export function buildScene(fm, ctx) {
|
|
|
981
981
|
* @param {string} sceneId - The owning scene's `_id`.
|
|
982
982
|
* @param {string} [img] - The background art, already resolved from the note.
|
|
983
983
|
* Passed by {@link buildScene}, which reads it from the note rather than from
|
|
984
|
-
* the block; defaults to
|
|
985
|
-
*
|
|
984
|
+
* the block; defaults to the block's own `img`, so a direct two-argument call
|
|
985
|
+
* still works.
|
|
986
986
|
* @returns {object} The Level document, keyed for the pack.
|
|
987
987
|
*/
|
|
988
|
-
export function buildLevel(sohl, sceneId, img =
|
|
988
|
+
export function buildLevel(sohl, sceneId, img = sohlField({ sohl }, "img")) {
|
|
989
989
|
const level = {
|
|
990
990
|
_id: DEFAULT_LEVEL_ID,
|
|
991
991
|
name: sohl.levelName ?? "Ground",
|
package/engine/note-ids.mjs
CHANGED
|
@@ -44,6 +44,11 @@ import { documentId } from "./content-address.mjs";
|
|
|
44
44
|
import { systemOf } from "./document-subtypes.mjs";
|
|
45
45
|
import { contentPackage } from "./content-package.mjs";
|
|
46
46
|
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
47
|
+
// The folder id's derivation, taken from the pass that owns it rather than
|
|
48
|
+
// restated here — see the `folder` branch below. `folder-notes.mjs` reaches
|
|
49
|
+
// only `content-address`, `address-charset`, `ids` and `retired-fields`, none
|
|
50
|
+
// of which reach this module, so the direction closes no cycle.
|
|
51
|
+
import { FOLDER_TYPE, folderDocId } from "./folder-notes.mjs";
|
|
47
52
|
|
|
48
53
|
/**
|
|
49
54
|
* A frontmatter value read as a non-blank string, or `undefined`.
|
|
@@ -64,6 +69,20 @@ function text(value) {
|
|
|
64
69
|
/**
|
|
65
70
|
* The document id a note compiles under: its pin, or its address.
|
|
66
71
|
*
|
|
72
|
+
* **One type hashes its address differently, and that is not an exception to
|
|
73
|
+
* the rule but an application of it.** A `Folder` is a document of its own
|
|
74
|
+
* class, and its id is hashed under the `folder` namespace so that a folder and
|
|
75
|
+
* an item sharing a shortcode cannot derive one id — a collision Foundry would
|
|
76
|
+
* not report, since it keys folders and documents in separate collections
|
|
77
|
+
* (#258). So the answer for a folder comes from
|
|
78
|
+
* {@link module:engine/folder-notes.folderDocId}, the pass that emits those
|
|
79
|
+
* documents, rather than from a second derivation here.
|
|
80
|
+
*
|
|
81
|
+
* That this function ever answered differently was invisible from inside a
|
|
82
|
+
* build — no pass reads a folder's id from here — and surfaced only in the
|
|
83
|
+
* content index, which is read from outside and had no way to be checked
|
|
84
|
+
* against what shipped (#310).
|
|
85
|
+
*
|
|
67
86
|
* Returns `undefined` for a file with **no address** — no `type`, or no
|
|
68
87
|
* `shortcode`. Such a file is not an addressable note, so it has no document
|
|
69
88
|
* and inventing an id for one would file it under nothing. Every caller already
|
|
@@ -88,7 +107,12 @@ export function noteDocId(fm, { pkg, maps = KNOWN_DOCUMENT_SUBTYPE_MAPS } = {})
|
|
|
88
107
|
const type = text(fm.type);
|
|
89
108
|
const shortcode = text(fm.shortcode);
|
|
90
109
|
if (!type || !shortcode) return undefined;
|
|
91
|
-
|
|
110
|
+
const owner = pkg ?? contentPackage();
|
|
111
|
+
// Lowercased because `collectFolderNotes` matches the type that way, and
|
|
112
|
+
// the two must answer alike about the same note or the divergence this
|
|
113
|
+
// branch closes reopens under a capitalised `type: Folder`.
|
|
114
|
+
if (type.toLowerCase() === FOLDER_TYPE) return folderDocId(owner, shortcode);
|
|
115
|
+
return documentId(owner, systemOf(type, maps), type, shortcode);
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
/**
|
|
@@ -422,7 +422,14 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
422
422
|
// values here would put a second, weaker answer beside the real one.
|
|
423
423
|
subTypes: null,
|
|
424
424
|
data: Object.freeze([
|
|
425
|
-
{
|
|
425
|
+
{
|
|
426
|
+
name: "portrait",
|
|
427
|
+
...TEXT,
|
|
428
|
+
describe:
|
|
429
|
+
"Path to the portrait image. Its first segment says which package owns " +
|
|
430
|
+
"the file: `systems/…` and `modules/…` are emitted unchanged, anything " +
|
|
431
|
+
"else is this package's own and is rooted under its assets.",
|
|
432
|
+
},
|
|
426
433
|
TEMPLATE_PRIORITY,
|
|
427
434
|
{ name: "archetypes", ...LIST, describe: "Archetypal behaviours the being fits." },
|
|
428
435
|
{ name: "occupation", ...TEXT, describe: "What the being does for a living." },
|
|
@@ -466,7 +473,14 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
466
473
|
vehicle: Object.freeze({
|
|
467
474
|
subTypes: null,
|
|
468
475
|
data: Object.freeze([
|
|
469
|
-
{
|
|
476
|
+
{
|
|
477
|
+
name: "portrait",
|
|
478
|
+
...TEXT,
|
|
479
|
+
describe:
|
|
480
|
+
"Path to the portrait image. Its first segment says which package owns " +
|
|
481
|
+
"the file: `systems/…` and `modules/…` are emitted unchanged, anything " +
|
|
482
|
+
"else is this package's own and is rooted under its assets.",
|
|
483
|
+
},
|
|
470
484
|
TEMPLATE_PRIORITY,
|
|
471
485
|
]),
|
|
472
486
|
}),
|
|
@@ -577,16 +591,31 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
577
591
|
...TEXT,
|
|
578
592
|
describe: "Roll formula for the delay between contraction and onset.",
|
|
579
593
|
},
|
|
594
|
+
{
|
|
595
|
+
name: "onsetDurationBase",
|
|
596
|
+
...NUM,
|
|
597
|
+
describe: "That delay in seconds, stated outright instead of rolled.",
|
|
598
|
+
},
|
|
580
599
|
{
|
|
581
600
|
name: "healingCheckDurationFormula",
|
|
582
601
|
...TEXT,
|
|
583
602
|
describe: "Roll formula for the interval between healing checks.",
|
|
584
603
|
},
|
|
604
|
+
{
|
|
605
|
+
name: "healingCheckDurationBase",
|
|
606
|
+
...NUM,
|
|
607
|
+
describe: "That interval in seconds, stated outright instead of rolled.",
|
|
608
|
+
},
|
|
585
609
|
{
|
|
586
610
|
name: "resolutionDurationFormula",
|
|
587
611
|
...TEXT,
|
|
588
612
|
describe: "Roll formula for the time from onset to resolution.",
|
|
589
613
|
},
|
|
614
|
+
{
|
|
615
|
+
name: "resolutionDurationBase",
|
|
616
|
+
...NUM,
|
|
617
|
+
describe: "That time in seconds, stated outright instead of rolled.",
|
|
618
|
+
},
|
|
590
619
|
]),
|
|
591
620
|
}),
|
|
592
621
|
|
|
@@ -719,7 +748,39 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
719
748
|
"shock",
|
|
720
749
|
"coma",
|
|
721
750
|
]),
|
|
722
|
-
data: Object.freeze([
|
|
751
|
+
data: Object.freeze([
|
|
752
|
+
TEMPLATE_PRIORITY,
|
|
753
|
+
{
|
|
754
|
+
name: "healingCheckDurationFormula",
|
|
755
|
+
...TEXT,
|
|
756
|
+
describe: "Roll formula for the interval between healing checks.",
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
name: "healingCheckDurationBase",
|
|
760
|
+
...NUM,
|
|
761
|
+
describe: "That interval in seconds, stated outright instead of rolled.",
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
name: "bloodLossAdvanceDurationFormula",
|
|
765
|
+
...TEXT,
|
|
766
|
+
describe: "Roll formula for the interval between blood-loss advances.",
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: "bloodLossAdvanceDurationBase",
|
|
770
|
+
...NUM,
|
|
771
|
+
describe: "That interval in seconds. Setting it is what makes the wound bleed.",
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: "courseDurationFormula",
|
|
775
|
+
...TEXT,
|
|
776
|
+
describe: "Roll formula for the interval between course tests.",
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
name: "courseDurationBase",
|
|
780
|
+
...NUM,
|
|
781
|
+
describe: "That interval in seconds, stated outright instead of rolled.",
|
|
782
|
+
},
|
|
783
|
+
]),
|
|
723
784
|
}),
|
|
724
785
|
|
|
725
786
|
weapongear: Object.freeze({
|
|
@@ -809,6 +870,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
809
870
|
"folk",
|
|
810
871
|
"culture",
|
|
811
872
|
"bestiary",
|
|
873
|
+
"gathering",
|
|
812
874
|
]),
|
|
813
875
|
// Nothing of its own: a lore note is prose, and what it *is* about is
|
|
814
876
|
// its subType. The specification declares an empty table for it, and
|
|
@@ -881,12 +943,17 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
881
943
|
// derived for them, which is precisely what a subType decides (#174).
|
|
882
944
|
subTypes: Object.freeze(["battlemap", "localmap", "regionalmap"]),
|
|
883
945
|
data: Object.freeze([
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
946
|
+
// `img`, as every other note type spells its artwork. A map alone
|
|
947
|
+
// read `image` out of its `sohl:` block; that spelling is retired
|
|
948
|
+
// and gone (#149), so the two names are one again.
|
|
949
|
+
{
|
|
950
|
+
name: "img",
|
|
951
|
+
...TEXT,
|
|
952
|
+
describe:
|
|
953
|
+
"Path to the map art, owned by whichever package its first segment " +
|
|
954
|
+
"names — `systems/…` and `modules/…` unchanged, anything else this " +
|
|
955
|
+
"package's own.",
|
|
956
|
+
},
|
|
890
957
|
{
|
|
891
958
|
name: "dimensions",
|
|
892
959
|
...LIST,
|