@heroiclands/package-build 22.9.1 → 22.10.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 +54 -0
- package/CONTENT.md +33 -0
- package/bin/content-build.mjs +3 -1
- package/docs/api.md +10 -6
- package/docs/content-format.md +384 -200
- package/docs/project-setup.md +23 -0
- package/engine/actor-compiler.mjs +20 -7
- package/engine/address-fields.mjs +404 -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 +59 -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 +20 -4
- package/engine/folder-notes.mjs +44 -15
- package/engine/foundry-entries.mjs +3 -0
- package/engine/frontmatter-lint.mjs +144 -43
- 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 +310 -0
- package/engine/note-vocabulary.mjs +145 -26
- 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/githooks/attribution.mjs +82 -0
- package/githooks/commit-message.mjs +26 -0
- package/githooks/commit-msg +3 -35
- package/hm3/actors.mjs +8 -24
- package/hm3/item-fields.mjs +15 -5
- package/package.json +1 -1
- package/sohl/infobox.mjs +80 -17
- package/sohl/item-fields.mjs +114 -41
- package/types/engine/address-fields.d.mts +90 -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 +44 -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 +4 -5
- package/types/sohl/item-fields.d.mts +8 -0
|
@@ -0,0 +1,310 @@
|
|
|
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 { 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
|
+
return {
|
|
264
|
+
package: config.contentPackage,
|
|
265
|
+
system: "none",
|
|
266
|
+
systemBlocks: systemBlocksFor(config),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Decode Address properties contributed by an index record.
|
|
272
|
+
* @param {object} record - The record.
|
|
273
|
+
* @param {object} context - Builder defaults.
|
|
274
|
+
* @returns {object} The typed record.
|
|
275
|
+
*/
|
|
276
|
+
export function decodeIndexAddresses(record, context) {
|
|
277
|
+
decodeNoteAddresses(record, context);
|
|
278
|
+
const canonical = (value, keyPath) => {
|
|
279
|
+
const target = readCanonicalKey(value);
|
|
280
|
+
if (!isAddressTuple(target)) {
|
|
281
|
+
const error = new Error(
|
|
282
|
+
`Index ${keyPath.join(".")} is not a complete Address: ${JSON.stringify(value)}`,
|
|
283
|
+
);
|
|
284
|
+
error.keyPath = keyPath;
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
return target;
|
|
288
|
+
};
|
|
289
|
+
for (const key of ["documentation", "documents"]) {
|
|
290
|
+
if (record[key] != null) record[key] = canonical(record[key], [key]);
|
|
291
|
+
}
|
|
292
|
+
if (record.address?.canonical != null)
|
|
293
|
+
record.address.canonical = canonical(record.address.canonical, ["address", "canonical"]);
|
|
294
|
+
for (const anchor of record.anchors ?? []) {
|
|
295
|
+
if (!anchor.link || anchor.link instanceof AddressLink) continue;
|
|
296
|
+
const [written, section] = anchor.link.split("#");
|
|
297
|
+
const target = parseAddress(
|
|
298
|
+
written,
|
|
299
|
+
{
|
|
300
|
+
...context,
|
|
301
|
+
system: "none",
|
|
302
|
+
types: new Set([...Object.keys(NOTE_VOCABULARY), ...ASSET_TYPE_NAMES]),
|
|
303
|
+
},
|
|
304
|
+
{ declared: true },
|
|
305
|
+
);
|
|
306
|
+
if (target.reason) throw new Error(`Index anchor link is not an Address: ${anchor.link}`);
|
|
307
|
+
anchor.link = new AddressLink(target, section);
|
|
308
|
+
}
|
|
309
|
+
return record;
|
|
310
|
+
}
|
|
@@ -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,
|
|
@@ -732,27 +764,54 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
732
764
|
{
|
|
733
765
|
name: "commonSkills",
|
|
734
766
|
...LINKS,
|
|
767
|
+
ref: "skill",
|
|
768
|
+
accepts: ["skill"],
|
|
735
769
|
describe: "Skills common among members — languages first among them.",
|
|
736
770
|
},
|
|
737
|
-
{
|
|
738
|
-
|
|
771
|
+
{
|
|
772
|
+
name: "seat",
|
|
773
|
+
...LINK,
|
|
774
|
+
ref: "place",
|
|
775
|
+
accepts: ["place"],
|
|
776
|
+
describe: "Where the affiliation's authority sits.",
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
name: "domains",
|
|
780
|
+
...LINKS,
|
|
781
|
+
ref: "place",
|
|
782
|
+
accepts: ["place"],
|
|
783
|
+
describe: "Places over which it holds sway.",
|
|
784
|
+
},
|
|
739
785
|
{ name: "population", ...NUM, describe: "How many people it counts." },
|
|
740
786
|
{
|
|
741
787
|
name: "economy",
|
|
742
788
|
...LINKS,
|
|
789
|
+
accepts: ["affiliation", "lore"],
|
|
743
790
|
describe: "What its economic life runs on — currencies, banking bodies, goods.",
|
|
744
791
|
},
|
|
745
792
|
{
|
|
746
793
|
name: "lore",
|
|
747
794
|
...LINKS,
|
|
795
|
+
ref: "lore",
|
|
796
|
+
accepts: ["lore"],
|
|
748
797
|
describe:
|
|
749
798
|
"Lore concerning it — the peoples it draws on, the god a faith " +
|
|
750
799
|
"venerates, its law, its calendar.",
|
|
751
800
|
},
|
|
752
|
-
{
|
|
801
|
+
{
|
|
802
|
+
name: "parents",
|
|
803
|
+
...LINKS,
|
|
804
|
+
ref: "affiliation",
|
|
805
|
+
accepts: ["affiliation"],
|
|
806
|
+
describe: "Affiliations it is subordinate to.",
|
|
807
|
+
},
|
|
753
808
|
{
|
|
754
809
|
name: "relations",
|
|
755
|
-
|
|
810
|
+
kind: "map",
|
|
811
|
+
keyKind: "address",
|
|
812
|
+
ref: "affiliation",
|
|
813
|
+
accepts: ["affiliation"],
|
|
814
|
+
shape: "a map keyed by Address",
|
|
756
815
|
describe: "Standing with other affiliations — aligned, unaligned, rival, nemesis.",
|
|
757
816
|
},
|
|
758
817
|
]),
|
|
@@ -862,15 +921,26 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
862
921
|
subTypes: Object.freeze(["boon", "boost", "fate", "grace", "birthsign", "other", "piety"]),
|
|
863
922
|
data: Object.freeze([
|
|
864
923
|
TEMPLATE_PRIORITY,
|
|
865
|
-
{
|
|
924
|
+
{
|
|
925
|
+
name: "assocSkill",
|
|
926
|
+
...LINK,
|
|
927
|
+
ref: "skill",
|
|
928
|
+
accepts: ["skill"],
|
|
929
|
+
describe: "The skill it is associated with.",
|
|
930
|
+
},
|
|
866
931
|
{
|
|
867
932
|
name: "assocAffiliation",
|
|
868
933
|
...LINK,
|
|
934
|
+
ref: "affiliation",
|
|
935
|
+
accepts: ["affiliation"],
|
|
869
936
|
describe: "The affiliation it is associated with.",
|
|
870
937
|
},
|
|
871
938
|
{
|
|
872
939
|
name: "skillAptitudes",
|
|
873
|
-
|
|
940
|
+
kind: "map",
|
|
941
|
+
keyKind: "shortcode",
|
|
942
|
+
keySelector: "subType",
|
|
943
|
+
shape: "a map keyed by Shortcode or subType selector",
|
|
874
944
|
describe:
|
|
875
945
|
"Bonuses and penalties, each naming a skill or a `subType:<skill-subtype>`.",
|
|
876
946
|
},
|
|
@@ -895,10 +965,18 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
895
965
|
]),
|
|
896
966
|
data: Object.freeze([
|
|
897
967
|
TEMPLATE_PRIORITY,
|
|
898
|
-
{
|
|
968
|
+
{
|
|
969
|
+
name: "assocSkill",
|
|
970
|
+
...LINK,
|
|
971
|
+
ref: "skill",
|
|
972
|
+
accepts: ["skill"],
|
|
973
|
+
describe: "The skill it is associated with.",
|
|
974
|
+
},
|
|
899
975
|
{
|
|
900
976
|
name: "assocAffiliation",
|
|
901
977
|
...LINK,
|
|
978
|
+
ref: "affiliation",
|
|
979
|
+
accepts: ["affiliation"],
|
|
902
980
|
describe: "The affiliation it is associated with.",
|
|
903
981
|
},
|
|
904
982
|
{ name: "masteryLevel", ...NUM, describe: "Mastery before any modifier." },
|
|
@@ -930,7 +1008,13 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
930
1008
|
data: Object.freeze([
|
|
931
1009
|
TEMPLATE_PRIORITY,
|
|
932
1010
|
{ name: "masteryLevel", ...NUM, describe: "Mastery before any modifier." },
|
|
933
|
-
{
|
|
1011
|
+
{
|
|
1012
|
+
name: "parentSkill",
|
|
1013
|
+
...LINK,
|
|
1014
|
+
ref: "skill",
|
|
1015
|
+
accepts: ["skill"],
|
|
1016
|
+
describe: "The skill this one specialises.",
|
|
1017
|
+
},
|
|
934
1018
|
]),
|
|
935
1019
|
}),
|
|
936
1020
|
|
|
@@ -1024,6 +1108,8 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1024
1108
|
{
|
|
1025
1109
|
name: "parent",
|
|
1026
1110
|
...LINK_BY_PACK,
|
|
1111
|
+
ref: "folder",
|
|
1112
|
+
accepts: ["folder"],
|
|
1027
1113
|
describe:
|
|
1028
1114
|
"The folder this one sits in, as an address — or one " +
|
|
1029
1115
|
"address per pack, keyed by pack name with `default` for " +
|
|
@@ -1110,6 +1196,8 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1110
1196
|
{
|
|
1111
1197
|
name: "lore",
|
|
1112
1198
|
...LINKS,
|
|
1199
|
+
ref: "lore",
|
|
1200
|
+
accepts: ["lore"],
|
|
1113
1201
|
describe:
|
|
1114
1202
|
"Lore concerning this place — its peoples, its law, its calendar, " +
|
|
1115
1203
|
"its history.",
|
|
@@ -1117,6 +1205,8 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1117
1205
|
{
|
|
1118
1206
|
name: "parents",
|
|
1119
1207
|
...LINKS,
|
|
1208
|
+
ref: "place",
|
|
1209
|
+
accepts: ["place"],
|
|
1120
1210
|
describe: "Enclosing places this one sits within.",
|
|
1121
1211
|
},
|
|
1122
1212
|
{
|
|
@@ -1162,13 +1252,39 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1162
1252
|
stubbable: true,
|
|
1163
1253
|
subTypes: Object.freeze(["campaign", "adventure", "encounter"]),
|
|
1164
1254
|
data: Object.freeze([
|
|
1165
|
-
{
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1255
|
+
{
|
|
1256
|
+
name: "parents",
|
|
1257
|
+
...LINKS,
|
|
1258
|
+
ref: "scenario",
|
|
1259
|
+
accepts: ["scenario"],
|
|
1260
|
+
describe: "Scenarios this one sits within.",
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
name: "locations",
|
|
1264
|
+
...LINKS,
|
|
1265
|
+
ref: "place",
|
|
1266
|
+
accepts: ["place"],
|
|
1267
|
+
describe: "Places the scenario takes place in.",
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
name: "cast",
|
|
1271
|
+
...LINKS,
|
|
1272
|
+
ref: "being",
|
|
1273
|
+
accepts: ["being"],
|
|
1274
|
+
describe: "Beings who appear in it.",
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
name: "factions",
|
|
1278
|
+
...LINKS,
|
|
1279
|
+
ref: "affiliation",
|
|
1280
|
+
accepts: ["affiliation"],
|
|
1281
|
+
describe: "Affiliations with a stake in it.",
|
|
1282
|
+
},
|
|
1169
1283
|
{
|
|
1170
1284
|
name: "follows",
|
|
1171
1285
|
...LINKS,
|
|
1286
|
+
ref: "scenario",
|
|
1287
|
+
accepts: ["scenario"],
|
|
1172
1288
|
describe: "Scenarios that should be played before this one.",
|
|
1173
1289
|
},
|
|
1174
1290
|
{
|
|
@@ -1203,6 +1319,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1203
1319
|
name: "bgImage",
|
|
1204
1320
|
...LINK,
|
|
1205
1321
|
ref: "image",
|
|
1322
|
+
accepts: ART_SLOTS.find((slot) => slot.key === "bgImage").accepts,
|
|
1206
1323
|
describe: "The map's background art — an `image` address.",
|
|
1207
1324
|
},
|
|
1208
1325
|
{
|
|
@@ -1240,7 +1357,9 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1240
1357
|
},
|
|
1241
1358
|
{
|
|
1242
1359
|
name: "place",
|
|
1243
|
-
...
|
|
1360
|
+
...LINK,
|
|
1361
|
+
ref: "place",
|
|
1362
|
+
accepts: ["place"],
|
|
1244
1363
|
describe:
|
|
1245
1364
|
"The place this map depicts. Named here and not on the place, " +
|
|
1246
1365
|
"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;
|