@heroiclands/package-build 22.9.2 → 22.11.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 +56 -0
- package/CONTENT.md +54 -0
- package/bin/content-build.mjs +3 -1
- package/docs/api.md +11 -6
- package/docs/commands.md +1 -1
- package/docs/content-format.md +411 -201
- package/engine/actor-compiler.mjs +20 -7
- package/engine/address-fields.mjs +426 -0
- package/engine/address-values.mjs +80 -0
- package/engine/address.mjs +769 -0
- package/engine/art-fields.mjs +112 -97
- package/engine/art-slots.mjs +81 -0
- package/engine/base-compiler.mjs +61 -7
- package/engine/bundle-notes.mjs +5 -2
- package/engine/bundles.mjs +17 -10
- package/engine/content-address.mjs +23 -186
- package/engine/content-embeds.mjs +18 -4
- package/engine/content-format-check.mjs +50 -22
- package/engine/content-index.mjs +85 -43
- package/engine/content-links.mjs +105 -104
- package/engine/content-lint.mjs +11 -1
- package/engine/content-tables.mjs +9 -1
- package/engine/field-spec.mjs +21 -4
- package/engine/folder-notes.mjs +44 -15
- package/engine/foundry-entries.mjs +3 -0
- package/engine/frontmatter-lint.mjs +150 -47
- package/engine/frontmatter.mjs +55 -10
- package/engine/generate.mjs +7 -1
- package/engine/helpers.mjs +37 -9
- package/engine/holdings.mjs +41 -12
- package/engine/index-records.mjs +2 -0
- package/engine/infobox-render.mjs +2 -1
- package/engine/infobox.mjs +26 -6
- package/engine/item-compiler.mjs +5 -0
- package/engine/item-docs.mjs +7 -44
- package/engine/map-dot.mjs +2 -0
- package/engine/map-notes.mjs +7 -2
- package/engine/map-places.mjs +42 -10
- package/engine/metadata-index.mjs +60 -5
- package/engine/note-addresses.mjs +325 -0
- package/engine/note-vocabulary.mjs +140 -28
- package/engine/pdf-build.mjs +1 -1
- package/engine/pdf-toc.mjs +17 -2
- package/engine/place-relations.mjs +160 -50
- package/engine/populations.mjs +17 -9
- package/engine/reference-targets.mjs +54 -0
- package/engine/scenes.mjs +5 -2
- package/engine/shortcode-references.mjs +68 -0
- package/engine/site-build.mjs +39 -15
- package/engine/site-index.mjs +57 -75
- package/engine/sql-tables.mjs +43 -9
- package/engine/system-block.mjs +21 -0
- package/engine/system-vocabulary.mjs +46 -0
- package/engine/web-wikilinks.mjs +11 -9
- package/engine/wikilink-syntax.mjs +21 -0
- package/engine/wikilinks.mjs +92 -306
- package/hm3/actors.mjs +8 -24
- package/hm3/item-fields.mjs +15 -5
- package/package.json +1 -1
- package/sohl/infobox.mjs +111 -17
- package/sohl/item-fields.mjs +131 -41
- package/types/engine/address-fields.d.mts +95 -0
- package/types/engine/address-values.d.mts +34 -0
- package/types/engine/address.d.mts +452 -0
- package/types/engine/art-fields.d.mts +59 -53
- package/types/engine/art-slots.d.mts +40 -0
- package/types/engine/base-compiler.d.mts +24 -2
- package/types/engine/bundle-notes.d.mts +2 -2
- package/types/engine/content-address.d.mts +3 -139
- package/types/engine/content-embeds.d.mts +1 -1
- package/types/engine/content-format-check.d.mts +25 -0
- package/types/engine/content-index.d.mts +2 -2
- package/types/engine/content-links.d.mts +6 -5
- package/types/engine/field-spec.d.mts +46 -9
- package/types/engine/folder-notes.d.mts +11 -10
- package/types/engine/frontmatter-lint.d.mts +7 -97
- package/types/engine/frontmatter.d.mts +16 -1
- package/types/engine/helpers.d.mts +2 -2
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/item-docs.d.mts +1 -35
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-addresses.d.mts +41 -0
- package/types/engine/note-vocabulary.d.mts +20 -3
- package/types/engine/place-relations.d.mts +12 -1
- package/types/engine/populations.d.mts +17 -2
- package/types/engine/reference-targets.d.mts +9 -0
- package/types/engine/shortcode-references.d.mts +14 -0
- package/types/engine/site-build.d.mts +3 -3
- package/types/engine/site-index.d.mts +9 -30
- package/types/engine/sql-tables.d.mts +2 -2
- package/types/engine/system-block.d.mts +17 -0
- package/types/engine/system-vocabulary.d.mts +16 -0
- package/types/engine/wikilink-syntax.d.mts +7 -0
- package/types/engine/wikilinks.d.mts +6 -109
- package/types/hm3/item-fields.d.mts +5 -0
- package/types/sohl/infobox.d.mts +6 -5
- package/types/sohl/item-fields.d.mts +8 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { declaredSystems, systemBlocksFor } from "./system-vocabulary.mjs";
|
|
9
|
+
import { RETIRED_FIELD_ALIASES } from "./retired-fields.mjs";
|
|
10
|
+
import {
|
|
11
|
+
isAddressTuple,
|
|
12
|
+
parseAddress,
|
|
13
|
+
renderAddress,
|
|
14
|
+
acceptsType,
|
|
15
|
+
readCanonicalKey,
|
|
16
|
+
} from "./address.mjs";
|
|
17
|
+
import { NOTE_VOCABULARY, dataFields } from "./note-vocabulary.mjs";
|
|
18
|
+
import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
|
|
19
|
+
import { ART_SLOTS } from "./art-slots.mjs";
|
|
20
|
+
import { SHIPPED_SYSTEMS } from "./subtype-registry.mjs";
|
|
21
|
+
|
|
22
|
+
import { AddressEntries, AddressLink } from "./address-values.mjs";
|
|
23
|
+
export { AddressEntries, encodeAddresses, cloneAddressState } from "./address-values.mjs";
|
|
24
|
+
|
|
25
|
+
/** Structured positions whose Address is nested inside a declared container. */
|
|
26
|
+
export const STRUCTURED_ADDRESSES = Object.freeze([
|
|
27
|
+
{ path: ["packFolder"], shape: "scalar-or-map", type: "folder", accepts: ["folder"] },
|
|
28
|
+
{
|
|
29
|
+
noteType: "affiliation",
|
|
30
|
+
path: ["data", "governance", "ranks", "*", "lore"],
|
|
31
|
+
shape: "value",
|
|
32
|
+
type: "lore",
|
|
33
|
+
accepts: ["lore"],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
noteType: "place",
|
|
37
|
+
path: ["data", "borders", "*", "to"],
|
|
38
|
+
shape: "value",
|
|
39
|
+
type: "place",
|
|
40
|
+
accepts: ["place"],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
noteType: "place",
|
|
44
|
+
path: ["data", "routes", "*", "to"],
|
|
45
|
+
shape: "value",
|
|
46
|
+
type: "place",
|
|
47
|
+
accepts: ["place"],
|
|
48
|
+
},
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* All declared Address positions of a note, including embedded notes.
|
|
53
|
+
* @param {object} fm - The note.
|
|
54
|
+
* @returns {object[]} Position descriptors with container shape and defaults.
|
|
55
|
+
*/
|
|
56
|
+
export function addressPositions(fm, context = {}) {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const field of dataFields(fm.type) ?? []) {
|
|
59
|
+
const shape =
|
|
60
|
+
field.kind === "address" ? "value"
|
|
61
|
+
: field.keyKind === "address" ? "keys"
|
|
62
|
+
: field.entryKind === "address" ? field.kind
|
|
63
|
+
: undefined;
|
|
64
|
+
if (shape)
|
|
65
|
+
out.push({
|
|
66
|
+
path: ["data", ...field.name.split(".")],
|
|
67
|
+
shape,
|
|
68
|
+
type: field.ref,
|
|
69
|
+
accepts: field.accepts,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
out.push(...STRUCTURED_ADDRESSES.filter((p) => !p.noteType || p.noteType === fm.type));
|
|
73
|
+
for (const system of new Set([
|
|
74
|
+
...SHIPPED_SYSTEMS,
|
|
75
|
+
...Object.keys(context.systemBlocks ?? {}),
|
|
76
|
+
])) {
|
|
77
|
+
out.push({
|
|
78
|
+
path: [system, "packFolder"],
|
|
79
|
+
shape: "scalar-or-map",
|
|
80
|
+
type: "folder",
|
|
81
|
+
accepts: ["folder"],
|
|
82
|
+
system,
|
|
83
|
+
});
|
|
84
|
+
out.push({
|
|
85
|
+
path: [system, "items", "*", "model"],
|
|
86
|
+
shape: "value",
|
|
87
|
+
system,
|
|
88
|
+
legacyShortcodeCase: true,
|
|
89
|
+
});
|
|
90
|
+
for (const slot of ART_SLOTS)
|
|
91
|
+
out.push({
|
|
92
|
+
path: [system, "items", "*", "data", slot.key],
|
|
93
|
+
shape: "value",
|
|
94
|
+
type: slot.type,
|
|
95
|
+
accepts: slot.accepts,
|
|
96
|
+
system,
|
|
97
|
+
});
|
|
98
|
+
const spec = context.systemBlocks?.[system];
|
|
99
|
+
for (const [index, entry] of (Array.isArray(fm[system]?.items) ?
|
|
100
|
+
fm[system].items
|
|
101
|
+
: []
|
|
102
|
+
).entries()) {
|
|
103
|
+
const model =
|
|
104
|
+
entry?.model == null ?
|
|
105
|
+
null
|
|
106
|
+
: parseAddress(
|
|
107
|
+
entry.model,
|
|
108
|
+
{
|
|
109
|
+
...context,
|
|
110
|
+
system,
|
|
111
|
+
types: new Set([...Object.keys(NOTE_VOCABULARY), ...ASSET_TYPE_NAMES]),
|
|
112
|
+
},
|
|
113
|
+
{ declared: true, legacyShortcodeCase: true },
|
|
114
|
+
);
|
|
115
|
+
const type = model && !model.reason ? model.type : entry?.type;
|
|
116
|
+
for (const field of spec?.fields?.[type] ?? []) {
|
|
117
|
+
if (!field.address) continue;
|
|
118
|
+
out.push({
|
|
119
|
+
path: [system, "items", index, "system", ...field.to.split(".")],
|
|
120
|
+
shape:
|
|
121
|
+
field.address.holds === "items" ? "list" : (field.address.holds ?? "value"),
|
|
122
|
+
type: field.address.type,
|
|
123
|
+
accepts: field.address.accepts,
|
|
124
|
+
system,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const fields = [
|
|
129
|
+
...(spec?.fields?.[fm.type] ?? []),
|
|
130
|
+
...(spec?.fieldVocabulary ? (context.schemas?.[fm.type]?.fields ?? []) : []),
|
|
131
|
+
];
|
|
132
|
+
for (const field of fields) {
|
|
133
|
+
if (!field.address) continue;
|
|
134
|
+
const position = {
|
|
135
|
+
shape: field.address.holds === "items" ? "list" : (field.address.holds ?? "value"),
|
|
136
|
+
type: field.address.type,
|
|
137
|
+
accepts: field.address.accepts,
|
|
138
|
+
system,
|
|
139
|
+
};
|
|
140
|
+
out.push({ ...position, path: [system, "system", ...field.to.split(".")] });
|
|
141
|
+
out.push({
|
|
142
|
+
...position,
|
|
143
|
+
path: [system, ...(field.legacyKey ?? field.name).split(".")],
|
|
144
|
+
});
|
|
145
|
+
if (field.name?.startsWith("data.") && field.topLevelMeans === undefined)
|
|
146
|
+
out.push({ ...position, system: "none", path: field.name.slice(5).split(".") });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const position of [...out]) {
|
|
150
|
+
const last = position.path.at(-1);
|
|
151
|
+
const retired = RETIRED_FIELD_ALIASES[last];
|
|
152
|
+
if (retired) out.push({ ...position, path: [...position.path.slice(0, -1), retired] });
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Decode declared properties at the note boundary. Raw mappings cannot impersonate tuples.
|
|
159
|
+
* @param {object} fm - Parsed YAML or an index record.
|
|
160
|
+
* @param {object} context - The builder's package and known types.
|
|
161
|
+
* @returns {object} The same note carrying typed Address properties.
|
|
162
|
+
*/
|
|
163
|
+
export function decodeNoteAddresses(fm, context) {
|
|
164
|
+
if (!fm || typeof fm !== "object") return fm;
|
|
165
|
+
const types = new Set([
|
|
166
|
+
...Object.keys(NOTE_VOCABULARY),
|
|
167
|
+
...ASSET_TYPE_NAMES,
|
|
168
|
+
...(context.types ?? []),
|
|
169
|
+
]);
|
|
170
|
+
for (const position of addressPositions(fm, context)) {
|
|
171
|
+
const defaults = {
|
|
172
|
+
...context,
|
|
173
|
+
types,
|
|
174
|
+
system: position.system ?? context.system ?? "none",
|
|
175
|
+
type: position.type,
|
|
176
|
+
};
|
|
177
|
+
const fail = (value, path, description) => {
|
|
178
|
+
const error = new Error(
|
|
179
|
+
`\`${path.join(".")}\` ${description}: ${JSON.stringify(value)}`,
|
|
180
|
+
);
|
|
181
|
+
error.keyPath = path;
|
|
182
|
+
error.addressKey = position.shape === "keys";
|
|
183
|
+
throw error;
|
|
184
|
+
};
|
|
185
|
+
const read = (value, path) => {
|
|
186
|
+
if (value == null || value === "") return value;
|
|
187
|
+
const tuple = parseAddress(value, defaults, {
|
|
188
|
+
declared: true,
|
|
189
|
+
legacyShortcodeCase: position.legacyShortcodeCase,
|
|
190
|
+
});
|
|
191
|
+
if (tuple.reason) fail(value, path, "is not an accepted Address");
|
|
192
|
+
if (position.accepts && !acceptsType(tuple, position.accepts))
|
|
193
|
+
fail(
|
|
194
|
+
value,
|
|
195
|
+
path,
|
|
196
|
+
`must name a ${position.accepts.join(" or ")} Address, but names ${tuple.type}`,
|
|
197
|
+
);
|
|
198
|
+
return tuple;
|
|
199
|
+
};
|
|
200
|
+
const convert = (value, path) => {
|
|
201
|
+
if (value == null || value === "") return value;
|
|
202
|
+
if (position.shape === "keys") {
|
|
203
|
+
if (value instanceof AddressEntries) return value;
|
|
204
|
+
if (Array.isArray(value) && value.length === 0) return value;
|
|
205
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
206
|
+
fail(value, path, "must be an Address-keyed map");
|
|
207
|
+
return new AddressEntries(
|
|
208
|
+
Object.entries(value).map(([key, v]) => ({
|
|
209
|
+
target: read(key, [...path, key]),
|
|
210
|
+
value: v,
|
|
211
|
+
sourceKey: key,
|
|
212
|
+
})),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (position.shape === "list") {
|
|
216
|
+
if (!Array.isArray(value)) fail(value, path, "must be a list of Addresses");
|
|
217
|
+
return value.map((v, i) => read(v, [...path, i]));
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
position.shape === "scalar-or-map" &&
|
|
221
|
+
typeof value === "object" &&
|
|
222
|
+
!isAddressTuple(value)
|
|
223
|
+
) {
|
|
224
|
+
if (Array.isArray(value)) {
|
|
225
|
+
if (!value.length) return value;
|
|
226
|
+
fail(value, path, "must be an Address or pack map");
|
|
227
|
+
}
|
|
228
|
+
return Object.fromEntries(
|
|
229
|
+
Object.entries(value).map(([key, v]) => [key, read(v, [...path, key])]),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return read(value, path);
|
|
233
|
+
};
|
|
234
|
+
// Copy each traversed owner so YAML aliases retain their own read context.
|
|
235
|
+
const visit = (owner, path, offset = 0, actual = []) => {
|
|
236
|
+
if (owner == null || owner === "") return owner;
|
|
237
|
+
const key = path[offset];
|
|
238
|
+
if (key === "*") {
|
|
239
|
+
if (!Array.isArray(owner)) fail(owner, actual, "must be a list of entries");
|
|
240
|
+
return owner.map((value, i) => visit(value, path, offset + 1, [...actual, i]));
|
|
241
|
+
}
|
|
242
|
+
if (typeof owner !== "object") return owner;
|
|
243
|
+
if (!Object.hasOwn(owner, key)) return owner;
|
|
244
|
+
const copy = Array.isArray(owner) ? [...owner] : { ...owner };
|
|
245
|
+
copy[key] =
|
|
246
|
+
offset === path.length - 1 ?
|
|
247
|
+
convert(owner[key], [...actual, key])
|
|
248
|
+
: visit(owner[key], path, offset + 1, [...actual, key]);
|
|
249
|
+
return copy;
|
|
250
|
+
};
|
|
251
|
+
const decoded = visit(fm, position.path);
|
|
252
|
+
Object.assign(fm, decoded);
|
|
253
|
+
}
|
|
254
|
+
return fm;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Read context supplied by the package configuration.
|
|
259
|
+
* @param {object} config - Resolved package configuration.
|
|
260
|
+
* @returns {object} Builder defaults and system declarations.
|
|
261
|
+
*/
|
|
262
|
+
export function noteAddressContext(config) {
|
|
263
|
+
const systemBlocks = { ...systemBlocksFor(config) };
|
|
264
|
+
// A module with one declared system uses its flat Item registry for that
|
|
265
|
+
// system's Address positions, including fields stored in its content index.
|
|
266
|
+
const systems = new Set([
|
|
267
|
+
...declaredSystems(config),
|
|
268
|
+
...(config.relationships?.systems ?? []).map((relationship) => relationship.id),
|
|
269
|
+
]);
|
|
270
|
+
if (
|
|
271
|
+
systems.size === 1 &&
|
|
272
|
+
config.itemFields &&
|
|
273
|
+
Object.keys(config.itemFieldsBySystem ?? {}).length === 0
|
|
274
|
+
) {
|
|
275
|
+
const [system] = systems;
|
|
276
|
+
systemBlocks[system] = { ...systemBlocks[system], fields: config.itemFields };
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
package: config.contentPackage,
|
|
280
|
+
system: "none",
|
|
281
|
+
systemBlocks,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Decode Address properties contributed by an index record.
|
|
287
|
+
* @param {object} record - The record.
|
|
288
|
+
* @param {object} context - Builder defaults.
|
|
289
|
+
* @returns {object} The typed record.
|
|
290
|
+
*/
|
|
291
|
+
export function decodeIndexAddresses(record, context) {
|
|
292
|
+
decodeNoteAddresses(record, context);
|
|
293
|
+
const canonical = (value, keyPath) => {
|
|
294
|
+
const target = readCanonicalKey(value);
|
|
295
|
+
if (!isAddressTuple(target)) {
|
|
296
|
+
const error = new Error(
|
|
297
|
+
`Index ${keyPath.join(".")} is not a complete Address: ${JSON.stringify(value)}`,
|
|
298
|
+
);
|
|
299
|
+
error.keyPath = keyPath;
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
return target;
|
|
303
|
+
};
|
|
304
|
+
for (const key of ["documentation", "documents"]) {
|
|
305
|
+
if (record[key] != null) record[key] = canonical(record[key], [key]);
|
|
306
|
+
}
|
|
307
|
+
if (record.address?.canonical != null)
|
|
308
|
+
record.address.canonical = canonical(record.address.canonical, ["address", "canonical"]);
|
|
309
|
+
for (const anchor of record.anchors ?? []) {
|
|
310
|
+
if (!anchor.link || anchor.link instanceof AddressLink) continue;
|
|
311
|
+
const [written, section] = anchor.link.split("#");
|
|
312
|
+
const target = parseAddress(
|
|
313
|
+
written,
|
|
314
|
+
{
|
|
315
|
+
...context,
|
|
316
|
+
system: "none",
|
|
317
|
+
types: new Set([...Object.keys(NOTE_VOCABULARY), ...ASSET_TYPE_NAMES]),
|
|
318
|
+
},
|
|
319
|
+
{ declared: true },
|
|
320
|
+
);
|
|
321
|
+
if (target.reason) throw new Error(`Index anchor link is not an Address: ${anchor.link}`);
|
|
322
|
+
anchor.link = new AddressLink(target, section);
|
|
323
|
+
}
|
|
324
|
+
return record;
|
|
325
|
+
}
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
|
|
78
78
|
// The one charset, read rather than restated. A second spelling of the pattern
|
|
79
79
|
// is how a disagreement between the three arises.
|
|
80
|
+
import { ART_SLOTS } from "./art-slots.mjs";
|
|
80
81
|
import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./address-charset.mjs";
|
|
81
82
|
// The retirement window for a renamed type, read rather than restated: a
|
|
82
83
|
// vocabulary that answered only to the current spelling would report every key
|
|
@@ -114,20 +115,25 @@ import { MARKET_CLASSES, checkMarket } from "./market-class.mjs";
|
|
|
114
115
|
* @typedef {object} DataFieldSpec
|
|
115
116
|
* @property {string} name - The key under `data:`, dotted for a nested one
|
|
116
117
|
* (`charges.value`).
|
|
117
|
-
* @property {"string"|"number"|"boolean"|"list"|"map"|"scalar-or-map"} [kind] -
|
|
118
|
+
* @property {"string"|"number"|"boolean"|"list"|"map"|"scalar-or-map"|"address"|"shortcode"} [kind] -
|
|
118
119
|
* The value's shape, for the lint. Absent means no claim is made about the
|
|
119
120
|
* value — which is the honest answer wherever the specification's stated
|
|
120
121
|
* shape and the shape notes are authored in today disagree.
|
|
122
|
+
* @property {"address"|"shortcode"} [entryKind] - Type of each list or pack-map value.
|
|
123
|
+
* @property {"address"|"shortcode"} [keyKind] - Type of each map key.
|
|
124
|
+
* @property {"subType"} [keySelector] - An alternative `subType:<skill-subtype>` key.
|
|
125
|
+
* @property {readonly string[]} [accepts] - Allowed Address types, separate from `ref`.
|
|
121
126
|
* @property {string} [shape] - Human-readable shape, for a finding and for
|
|
122
127
|
* documentation.
|
|
123
128
|
* @property {string} [entryShape] - For a `scalar-or-map` field, what one
|
|
124
129
|
* entry of the map is. A finding names the entry at fault rather than
|
|
125
130
|
* quoting the whole map back, so the string an author has to correct is the
|
|
126
131
|
* one the message holds.
|
|
127
|
-
* @property {string} [ref] - The
|
|
132
|
+
* @property {string} [ref] - The Address's default type, an art slot declares
|
|
128
133
|
* one — `icon` for `icon` and `tokenIcon`, `image` for `bgImage` and
|
|
129
134
|
* `banner` — so `icon: anvil` names `icon-anvil` while a value carrying the
|
|
130
|
-
* separator states its own address.
|
|
135
|
+
* separator states its own address. Distinct from a `FieldSpec`'s `code`
|
|
136
|
+
* ({@link module:engine/field-spec}), which names a **Shortcode** instead.
|
|
131
137
|
* @property {"pack"} [keys] - For a `scalar-or-map` field, what its keys name.
|
|
132
138
|
* `"pack"` means each is a pack this package declares, so a key naming none
|
|
133
139
|
* is a finding of its own: it addresses a hierarchy nothing will ever read.
|
|
@@ -189,14 +195,14 @@ const TEXT = Object.freeze({ shape: "string", kind: "string" });
|
|
|
189
195
|
/** A list. */
|
|
190
196
|
const LIST = Object.freeze({ shape: "list", kind: "list" });
|
|
191
197
|
|
|
192
|
-
/** A single
|
|
193
|
-
const LINK = Object.freeze({ shape: "
|
|
198
|
+
/** A single Address. */
|
|
199
|
+
const LINK = Object.freeze({ shape: "an Address", kind: "address" });
|
|
194
200
|
|
|
195
|
-
/** A list of
|
|
196
|
-
const LINKS = Object.freeze({ shape: "list of
|
|
201
|
+
/** A list of Addresses. */
|
|
202
|
+
const LINKS = Object.freeze({ shape: "list of Addresses", kind: "list", entryKind: "address" });
|
|
197
203
|
|
|
198
204
|
/**
|
|
199
|
-
* A single
|
|
205
|
+
* A single Address, or one per pack.
|
|
200
206
|
*
|
|
201
207
|
* The map form is not a convenience spelling of the scalar: it says something
|
|
202
208
|
* the scalar cannot, that the answer *differs by pack*. A folder's `parent` is
|
|
@@ -209,9 +215,10 @@ const LINKS = Object.freeze({ shape: "list of wikilinks", kind: "list" });
|
|
|
209
215
|
* specification prescribes was a finding and no note using it was not.
|
|
210
216
|
*/
|
|
211
217
|
const LINK_BY_PACK = Object.freeze({
|
|
212
|
-
shape: "
|
|
218
|
+
shape: "an Address, or a map of Addresses keyed by pack",
|
|
219
|
+
entryKind: "address",
|
|
213
220
|
kind: "scalar-or-map",
|
|
214
|
-
entryShape: "
|
|
221
|
+
entryShape: "an Address",
|
|
215
222
|
keys: "pack",
|
|
216
223
|
});
|
|
217
224
|
|
|
@@ -246,6 +253,7 @@ const TOKEN_ICON = Object.freeze({
|
|
|
246
253
|
name: "tokenIcon",
|
|
247
254
|
...LINK,
|
|
248
255
|
ref: "icon",
|
|
256
|
+
accepts: ART_SLOTS.find((slot) => slot.key === "tokenIcon").accepts,
|
|
249
257
|
describe: "What a token on the canvas wears — an `icon` address; unset, it follows `icon`.",
|
|
250
258
|
});
|
|
251
259
|
|
|
@@ -273,12 +281,14 @@ export const SHARED_DATA_FIELDS = Object.freeze([
|
|
|
273
281
|
name: "icon",
|
|
274
282
|
...LINK,
|
|
275
283
|
ref: "icon",
|
|
284
|
+
accepts: ART_SLOTS.find((slot) => slot.key === "icon").accepts,
|
|
276
285
|
describe: "The document's profile art — an `icon` address, resolved into `img`.",
|
|
277
286
|
}),
|
|
278
287
|
Object.freeze({
|
|
279
288
|
name: "banner",
|
|
280
289
|
...LINK,
|
|
281
290
|
ref: "image",
|
|
291
|
+
accepts: ART_SLOTS.find((slot) => slot.key === "banner").accepts,
|
|
282
292
|
describe: "The page's hero image — an `image` address. Reaches no compiled document.",
|
|
283
293
|
}),
|
|
284
294
|
]);
|
|
@@ -599,22 +609,44 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
599
609
|
TEMPLATE_PRIORITY,
|
|
600
610
|
{ name: "archetypes", ...LIST, describe: "Archetypal behaviours the being fits." },
|
|
601
611
|
{ name: "occupation", ...TEXT, describe: "What the being does for a living." },
|
|
602
|
-
{
|
|
612
|
+
{
|
|
613
|
+
name: "stations",
|
|
614
|
+
...LINKS,
|
|
615
|
+
ref: "lore",
|
|
616
|
+
accepts: ["lore"],
|
|
617
|
+
describe: "Stations the being holds.",
|
|
618
|
+
},
|
|
603
619
|
{
|
|
604
620
|
name: "lore",
|
|
605
621
|
...LINKS,
|
|
622
|
+
ref: "lore",
|
|
623
|
+
accepts: ["lore"],
|
|
606
624
|
describe:
|
|
607
625
|
"Lore concerning this being — the people it is of, the standing it " +
|
|
608
626
|
"holds, the law it lives under.",
|
|
609
627
|
},
|
|
610
|
-
{
|
|
628
|
+
{
|
|
629
|
+
name: "homes",
|
|
630
|
+
...LINKS,
|
|
631
|
+
ref: "place",
|
|
632
|
+
accepts: ["place"],
|
|
633
|
+
describe: "Places the being calls home.",
|
|
634
|
+
},
|
|
611
635
|
{
|
|
612
636
|
name: "affiliations",
|
|
613
637
|
...LINKS,
|
|
638
|
+
ref: "affiliation",
|
|
639
|
+
accepts: ["affiliation"],
|
|
614
640
|
describe: "Affiliations the being belongs to — traditions, polities, and the rest.",
|
|
615
641
|
},
|
|
616
642
|
{ name: "gender", ...TEXT, describe: "`male`, `female` or `other`." },
|
|
617
|
-
{
|
|
643
|
+
{
|
|
644
|
+
name: "species",
|
|
645
|
+
...LINK,
|
|
646
|
+
ref: "lore",
|
|
647
|
+
accepts: ["lore"],
|
|
648
|
+
describe: "The being's species, as a lore note.",
|
|
649
|
+
},
|
|
618
650
|
{
|
|
619
651
|
name: "born",
|
|
620
652
|
...TEXT,
|
|
@@ -730,29 +762,49 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
730
762
|
describe: "The named offices it appoints, each with what the office does.",
|
|
731
763
|
},
|
|
732
764
|
{
|
|
733
|
-
name: "
|
|
765
|
+
name: "seat",
|
|
766
|
+
...LINK,
|
|
767
|
+
ref: "place",
|
|
768
|
+
accepts: ["place"],
|
|
769
|
+
describe: "Where the affiliation's authority sits.",
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
name: "domains",
|
|
734
773
|
...LINKS,
|
|
735
|
-
|
|
774
|
+
ref: "place",
|
|
775
|
+
accepts: ["place"],
|
|
776
|
+
describe: "Places over which it holds sway.",
|
|
736
777
|
},
|
|
737
|
-
{ name: "seat", ...LINK, describe: "Where the affiliation's authority sits." },
|
|
738
|
-
{ name: "domains", ...LINKS, describe: "Places over which it holds sway." },
|
|
739
778
|
{ name: "population", ...NUM, describe: "How many people it counts." },
|
|
740
779
|
{
|
|
741
780
|
name: "economy",
|
|
742
781
|
...LINKS,
|
|
782
|
+
accepts: ["affiliation", "lore"],
|
|
743
783
|
describe: "What its economic life runs on — currencies, banking bodies, goods.",
|
|
744
784
|
},
|
|
745
785
|
{
|
|
746
786
|
name: "lore",
|
|
747
787
|
...LINKS,
|
|
788
|
+
ref: "lore",
|
|
789
|
+
accepts: ["lore"],
|
|
748
790
|
describe:
|
|
749
791
|
"Lore concerning it — the peoples it draws on, the god a faith " +
|
|
750
792
|
"venerates, its law, its calendar.",
|
|
751
793
|
},
|
|
752
|
-
{
|
|
794
|
+
{
|
|
795
|
+
name: "parents",
|
|
796
|
+
...LINKS,
|
|
797
|
+
ref: "affiliation",
|
|
798
|
+
accepts: ["affiliation"],
|
|
799
|
+
describe: "Affiliations it is subordinate to.",
|
|
800
|
+
},
|
|
753
801
|
{
|
|
754
802
|
name: "relations",
|
|
755
|
-
|
|
803
|
+
kind: "map",
|
|
804
|
+
keyKind: "address",
|
|
805
|
+
ref: "affiliation",
|
|
806
|
+
accepts: ["affiliation"],
|
|
807
|
+
shape: "a map keyed by Address",
|
|
756
808
|
describe: "Standing with other affiliations — aligned, unaligned, rival, nemesis.",
|
|
757
809
|
},
|
|
758
810
|
]),
|
|
@@ -862,15 +914,26 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
862
914
|
subTypes: Object.freeze(["boon", "boost", "fate", "grace", "birthsign", "other", "piety"]),
|
|
863
915
|
data: Object.freeze([
|
|
864
916
|
TEMPLATE_PRIORITY,
|
|
865
|
-
{
|
|
917
|
+
{
|
|
918
|
+
name: "assocSkill",
|
|
919
|
+
...LINK,
|
|
920
|
+
ref: "skill",
|
|
921
|
+
accepts: ["skill"],
|
|
922
|
+
describe: "The skill it is associated with.",
|
|
923
|
+
},
|
|
866
924
|
{
|
|
867
925
|
name: "assocAffiliation",
|
|
868
926
|
...LINK,
|
|
927
|
+
ref: "affiliation",
|
|
928
|
+
accepts: ["affiliation"],
|
|
869
929
|
describe: "The affiliation it is associated with.",
|
|
870
930
|
},
|
|
871
931
|
{
|
|
872
932
|
name: "skillAptitudes",
|
|
873
|
-
|
|
933
|
+
kind: "map",
|
|
934
|
+
keyKind: "shortcode",
|
|
935
|
+
keySelector: "subType",
|
|
936
|
+
shape: "a map keyed by Shortcode or subType selector",
|
|
874
937
|
describe:
|
|
875
938
|
"Bonuses and penalties, each naming a skill or a `subType:<skill-subtype>`.",
|
|
876
939
|
},
|
|
@@ -895,10 +958,18 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
895
958
|
]),
|
|
896
959
|
data: Object.freeze([
|
|
897
960
|
TEMPLATE_PRIORITY,
|
|
898
|
-
{
|
|
961
|
+
{
|
|
962
|
+
name: "assocSkill",
|
|
963
|
+
...LINK,
|
|
964
|
+
ref: "skill",
|
|
965
|
+
accepts: ["skill"],
|
|
966
|
+
describe: "The skill it is associated with.",
|
|
967
|
+
},
|
|
899
968
|
{
|
|
900
969
|
name: "assocAffiliation",
|
|
901
970
|
...LINK,
|
|
971
|
+
ref: "affiliation",
|
|
972
|
+
accepts: ["affiliation"],
|
|
902
973
|
describe: "The affiliation it is associated with.",
|
|
903
974
|
},
|
|
904
975
|
{ name: "masteryLevel", ...NUM, describe: "Mastery before any modifier." },
|
|
@@ -930,7 +1001,13 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
930
1001
|
data: Object.freeze([
|
|
931
1002
|
TEMPLATE_PRIORITY,
|
|
932
1003
|
{ name: "masteryLevel", ...NUM, describe: "Mastery before any modifier." },
|
|
933
|
-
{
|
|
1004
|
+
{
|
|
1005
|
+
name: "parentSkill",
|
|
1006
|
+
...LINK,
|
|
1007
|
+
ref: "skill",
|
|
1008
|
+
accepts: ["skill"],
|
|
1009
|
+
describe: "The skill this one specialises.",
|
|
1010
|
+
},
|
|
934
1011
|
]),
|
|
935
1012
|
}),
|
|
936
1013
|
|
|
@@ -1024,6 +1101,8 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1024
1101
|
{
|
|
1025
1102
|
name: "parent",
|
|
1026
1103
|
...LINK_BY_PACK,
|
|
1104
|
+
ref: "folder",
|
|
1105
|
+
accepts: ["folder"],
|
|
1027
1106
|
describe:
|
|
1028
1107
|
"The folder this one sits in, as an address — or one " +
|
|
1029
1108
|
"address per pack, keyed by pack name with `default` for " +
|
|
@@ -1110,6 +1189,8 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1110
1189
|
{
|
|
1111
1190
|
name: "lore",
|
|
1112
1191
|
...LINKS,
|
|
1192
|
+
ref: "lore",
|
|
1193
|
+
accepts: ["lore"],
|
|
1113
1194
|
describe:
|
|
1114
1195
|
"Lore concerning this place — its peoples, its law, its calendar, " +
|
|
1115
1196
|
"its history.",
|
|
@@ -1117,6 +1198,8 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1117
1198
|
{
|
|
1118
1199
|
name: "parents",
|
|
1119
1200
|
...LINKS,
|
|
1201
|
+
ref: "place",
|
|
1202
|
+
accepts: ["place"],
|
|
1120
1203
|
describe: "Enclosing places this one sits within.",
|
|
1121
1204
|
},
|
|
1122
1205
|
{
|
|
@@ -1162,13 +1245,39 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1162
1245
|
stubbable: true,
|
|
1163
1246
|
subTypes: Object.freeze(["campaign", "adventure", "encounter"]),
|
|
1164
1247
|
data: Object.freeze([
|
|
1165
|
-
{
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1248
|
+
{
|
|
1249
|
+
name: "parents",
|
|
1250
|
+
...LINKS,
|
|
1251
|
+
ref: "scenario",
|
|
1252
|
+
accepts: ["scenario"],
|
|
1253
|
+
describe: "Scenarios this one sits within.",
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
name: "locations",
|
|
1257
|
+
...LINKS,
|
|
1258
|
+
ref: "place",
|
|
1259
|
+
accepts: ["place"],
|
|
1260
|
+
describe: "Places the scenario takes place in.",
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
name: "cast",
|
|
1264
|
+
...LINKS,
|
|
1265
|
+
ref: "being",
|
|
1266
|
+
accepts: ["being"],
|
|
1267
|
+
describe: "Beings who appear in it.",
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
name: "factions",
|
|
1271
|
+
...LINKS,
|
|
1272
|
+
ref: "affiliation",
|
|
1273
|
+
accepts: ["affiliation"],
|
|
1274
|
+
describe: "Affiliations with a stake in it.",
|
|
1275
|
+
},
|
|
1169
1276
|
{
|
|
1170
1277
|
name: "follows",
|
|
1171
1278
|
...LINKS,
|
|
1279
|
+
ref: "scenario",
|
|
1280
|
+
accepts: ["scenario"],
|
|
1172
1281
|
describe: "Scenarios that should be played before this one.",
|
|
1173
1282
|
},
|
|
1174
1283
|
{
|
|
@@ -1203,6 +1312,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1203
1312
|
name: "bgImage",
|
|
1204
1313
|
...LINK,
|
|
1205
1314
|
ref: "image",
|
|
1315
|
+
accepts: ART_SLOTS.find((slot) => slot.key === "bgImage").accepts,
|
|
1206
1316
|
describe: "The map's background art — an `image` address.",
|
|
1207
1317
|
},
|
|
1208
1318
|
{
|
|
@@ -1240,7 +1350,9 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1240
1350
|
},
|
|
1241
1351
|
{
|
|
1242
1352
|
name: "place",
|
|
1243
|
-
...
|
|
1353
|
+
...LINK,
|
|
1354
|
+
ref: "place",
|
|
1355
|
+
accepts: ["place"],
|
|
1244
1356
|
describe:
|
|
1245
1357
|
"The place this map depicts. Named here and not on the place, " +
|
|
1246
1358
|
"because a place has several maps and a map depicts one place.",
|
package/engine/pdf-build.mjs
CHANGED
|
@@ -379,7 +379,7 @@ export async function buildPdf({ config, out, version = "", compile = true } = {
|
|
|
379
379
|
scheme,
|
|
380
380
|
};
|
|
381
381
|
const collected = collectContentPages(contentBase, ctx);
|
|
382
|
-
const gates = siteGates(collected.pages, collected, { config: resolved });
|
|
382
|
+
const gates = siteGates(collected.pages, collected, { config: resolved, records });
|
|
383
383
|
if (gatesFailed(gates)) {
|
|
384
384
|
for (const [name, list] of Object.entries(gates)) {
|
|
385
385
|
if (!Array.isArray(list)) continue;
|