@heroiclands/package-build 23.0.0 → 23.1.1
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 +66 -0
- package/README.md +1 -1
- package/bin/content-commands.mjs +1 -1
- package/ci/content-format-reference.mjs +46 -0
- package/docs/api.md +124 -141
- package/docs/authoring/assets.md +1 -1
- package/docs/authoring/dates-and-calendars.md +12 -6
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +145 -29
- package/docs/commands.md +8 -10
- package/docs/configuration.md +13 -11
- package/docs/content-format.md +2 -2
- package/docs/guides/book.md +59 -4
- package/docs/guides/icons.md +4 -5
- package/docs/guides/packs.md +6 -0
- package/docs/guides/reference-pages.md +21 -1
- package/docs/guides/release-and-deploy.md +39 -7
- package/docs/guides/site.md +17 -18
- package/docs/reference/format-details.md +877 -276
- package/docs/reference/note-types.md +278 -91
- package/docs/reference/theme-pages.md +58 -97
- package/engine/address-values.mjs +124 -4
- package/engine/address.mjs +44 -3
- package/engine/anchors.mjs +103 -3
- package/engine/art-slots.mjs +2 -9
- package/engine/base-compiler.mjs +25 -6
- package/engine/calendar-artifacts.mjs +4 -1
- package/engine/calendar-notes.mjs +166 -102
- package/engine/content-format-check.mjs +2 -3
- package/engine/content-format.mjs +44 -1
- package/engine/content-format.yaml +79 -16
- package/engine/content-images.mjs +10 -1
- package/engine/content-index.mjs +37 -9
- package/engine/content-links.mjs +24 -5
- package/engine/content-tables.mjs +24 -3
- package/engine/data-keys.mjs +347 -0
- package/engine/derived-sections.mjs +322 -0
- package/engine/diagnostics.mjs +28 -0
- package/engine/event-fields.mjs +187 -0
- package/engine/event-rows.mjs +126 -0
- package/engine/event-views.mjs +305 -0
- package/engine/exported-scene.mjs +29 -53
- package/engine/frontmatter-lint.mjs +103 -77
- package/engine/frontmatter.mjs +7 -2
- package/engine/generate.mjs +53 -0
- package/engine/generated-sections.mjs +223 -0
- package/engine/heading-attributes.mjs +4 -2
- package/engine/helpers.mjs +12 -1
- package/engine/holdings.mjs +12 -33
- package/engine/ids.mjs +17 -11
- package/engine/index.mjs +4 -4
- package/engine/infobox-render.mjs +21 -6
- package/engine/infobox.mjs +217 -99
- package/engine/journals.mjs +104 -47
- package/engine/literature-notes.mjs +4 -1
- package/engine/literature-works.mjs +21 -13
- package/engine/map-graphviz.mjs +4 -18
- package/engine/markdown-expressions.mjs +116 -11
- package/engine/metadata-index.mjs +26 -0
- package/engine/near-miss.mjs +90 -0
- package/engine/note-addresses.mjs +75 -19
- package/engine/note-dates.mjs +5 -3
- package/engine/note-event-terms.mjs +364 -0
- package/engine/note-events.mjs +823 -0
- package/engine/note-vocabulary.mjs +184 -88
- package/engine/office-holders.mjs +20 -54
- package/engine/packagebuild-index.mjs +16 -1
- package/engine/pdf-build.mjs +97 -60
- package/engine/pdf-index.mjs +263 -0
- package/engine/pdf-render.mjs +47 -5
- package/engine/place-relation-terms.mjs +174 -0
- package/engine/place-relations.mjs +72 -160
- package/engine/rank-ladder.mjs +14 -41
- package/engine/reckoning-markers.mjs +2 -0
- package/engine/region-events.mjs +6 -12
- package/engine/scenes.mjs +73 -468
- package/engine/site-build.mjs +75 -101
- package/engine/site-index.mjs +3 -1
- package/engine/site-maps.mjs +43 -29
- package/engine/site-preview.mjs +40 -31
- package/engine/social-ties.mjs +2 -1
- package/engine/sql-tables.mjs +212 -9
- package/engine/standing-terms.mjs +145 -6
- package/engine/standings.mjs +37 -49
- package/engine/web-wikilinks.mjs +21 -1
- package/engine/wikilink-syntax.mjs +34 -4
- package/engine/wikilinks.mjs +27 -2
- package/engine/yaml-lint.mjs +7 -4
- package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
- package/hugo-theme/layouts/partials/page-body.html +0 -3
- package/hugo-theme/static/css/style.css +25 -55
- package/package.json +1 -2
- package/packagebuild-metadata.jsonl +13 -13
- package/release.mjs +62 -4
- package/sohl/note-schemas.mjs +8 -73
- package/types/engine/address-values.d.mts +50 -3
- package/types/engine/address.d.mts +21 -0
- package/types/engine/anchors.d.mts +49 -2
- package/types/engine/art-slots.d.mts +1 -1
- package/types/engine/base-compiler.d.mts +13 -4
- package/types/engine/calendar-notes.d.mts +7 -0
- package/types/engine/content-format.d.mts +52 -0
- package/types/engine/content-images.d.mts +5 -1
- package/types/engine/content-tables.d.mts +9 -1
- package/types/engine/data-keys.d.mts +145 -0
- package/types/engine/derived-sections.d.mts +73 -0
- package/types/engine/diagnostics.d.mts +15 -0
- package/types/engine/event-fields.d.mts +99 -0
- package/types/engine/event-rows.d.mts +12 -0
- package/types/engine/event-views.d.mts +67 -0
- package/types/engine/exported-scene.d.mts +5 -2
- package/types/engine/frontmatter-lint.d.mts +3 -3
- package/types/engine/generated-sections.d.mts +143 -0
- package/types/engine/holdings.d.mts +6 -20
- package/types/engine/ids.d.mts +15 -12
- package/types/engine/index.d.mts +1 -1
- package/types/engine/infobox.d.mts +13 -3
- package/types/engine/journals.d.mts +24 -11
- package/types/engine/literature-works.d.mts +1 -2
- package/types/engine/map-graphviz.d.mts +2 -10
- package/types/engine/markdown-expressions.d.mts +12 -3
- package/types/engine/near-miss.d.mts +42 -0
- package/types/engine/note-addresses.d.mts +7 -1
- package/types/engine/note-event-terms.d.mts +68 -0
- package/types/engine/note-events.d.mts +71 -0
- package/types/engine/note-vocabulary.d.mts +37 -8
- package/types/engine/office-holders.d.mts +3 -0
- package/types/engine/pdf-index.d.mts +35 -0
- package/types/engine/pdf-render.d.mts +8 -2
- package/types/engine/place-relation-terms.d.mts +83 -0
- package/types/engine/place-relations.d.mts +11 -66
- package/types/engine/rank-ladder.d.mts +2 -0
- package/types/engine/region-events.d.mts +6 -12
- package/types/engine/scenes.d.mts +6 -32
- package/types/engine/site-build.d.mts +8 -22
- package/types/engine/site-maps.d.mts +15 -7
- package/types/engine/sql-tables.d.mts +77 -2
- package/types/engine/standing-terms.d.mts +37 -4
- package/types/engine/standings.d.mts +14 -9
- package/types/engine/wikilink-syntax.d.mts +20 -0
- package/types/engine/wikilinks.d.mts +3 -2
- package/types/engine/yaml-lint.d.mts +5 -4
- package/types/release.d.mts +4 -0
- package/docs/guides/government-migration.md +0 -52
- package/engine/itinerary-scenes.mjs +0 -166
- package/engine/lore-events.mjs +0 -197
- package/engine/map-notes.mjs +0 -1309
- package/engine/map-raster.mjs +0 -51
- package/hugo-theme/layouts/partials/holdings/entry.html +0 -21
- package/hugo-theme/layouts/partials/holdings/kind-groups.html +0 -71
- package/hugo-theme/layouts/partials/holdings.html +0 -61
- package/hugo-theme/layouts/partials/literature-works.html +0 -29
- package/hugo-theme/layouts/partials/place-map.html +0 -9
- package/types/engine/itinerary-scenes.d.mts +0 -29
- package/types/engine/lore-events.d.mts +0 -14
- package/types/engine/map-notes.d.mts +0 -297
- package/types/engine/map-raster.d.mts +0 -8
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The near miss a closed vocabulary names when it refuses a key: the declared
|
|
16
|
+
* key an unknown one was most likely meant to be, and the declared list as an
|
|
17
|
+
* English sentence.
|
|
18
|
+
*
|
|
19
|
+
* Shared by every closed region — the top level, `data:`, a system block, and
|
|
20
|
+
* the inner keys of a `data:` field — so a misspelling is answered the same
|
|
21
|
+
* way at every depth.
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Edit distance, capped — enough to answer "did you mean".
|
|
28
|
+
*
|
|
29
|
+
* A misspelled property is the failure class this check exists for, and a
|
|
30
|
+
* finding that names the key the author *meant* turns a hunt through the
|
|
31
|
+
* reference into a one-character fix.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} a - One string.
|
|
34
|
+
* @param {string} b - The other.
|
|
35
|
+
* @returns {number} The Levenshtein distance.
|
|
36
|
+
*/
|
|
37
|
+
export function distance(a, b) {
|
|
38
|
+
const rows = a.length + 1;
|
|
39
|
+
const cols = b.length + 1;
|
|
40
|
+
let prev = Array.from({ length: cols }, (_, j) => j);
|
|
41
|
+
for (let i = 1; i < rows; i += 1) {
|
|
42
|
+
const row = [i];
|
|
43
|
+
for (let j = 1; j < cols; j += 1) {
|
|
44
|
+
row[j] = Math.min(
|
|
45
|
+
prev[j] + 1,
|
|
46
|
+
row[j - 1] + 1,
|
|
47
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
prev = row;
|
|
51
|
+
}
|
|
52
|
+
return prev[cols - 1];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A closed vocabulary as an English list, for the message that names it.
|
|
57
|
+
*
|
|
58
|
+
* Written from the declaration rather than spelled into the message, so a key
|
|
59
|
+
* the vocabulary gains cannot go unmentioned by the finding that refuses its
|
|
60
|
+
* neighbours.
|
|
61
|
+
*
|
|
62
|
+
* @param {readonly string[]} keys - The declared keys, in declared order.
|
|
63
|
+
* @returns {string} `"a, b, or c"`.
|
|
64
|
+
*/
|
|
65
|
+
export function listed(keys) {
|
|
66
|
+
if (keys.length < 2) return keys.join("");
|
|
67
|
+
return `${keys.slice(0, -1).join(", ")}, or ${keys[keys.length - 1]}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The declared key an unknown one was most likely meant to be.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} key - The unknown key.
|
|
74
|
+
* @param {Iterable<string>} candidates - The declared keys.
|
|
75
|
+
* @returns {string|undefined} The nearest, when it is near enough to suggest.
|
|
76
|
+
*/
|
|
77
|
+
export function nearest(key, candidates) {
|
|
78
|
+
let best;
|
|
79
|
+
let bestAt = Infinity;
|
|
80
|
+
for (const candidate of candidates) {
|
|
81
|
+
const d = distance(key.toLowerCase(), candidate.toLowerCase());
|
|
82
|
+
if (d < bestAt) {
|
|
83
|
+
bestAt = d;
|
|
84
|
+
best = candidate;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// A third of the key's length, so a suggestion is a plausible typo rather
|
|
88
|
+
// than the least-bad of a list of unrelated words.
|
|
89
|
+
return bestAt <= Math.max(1, Math.floor(key.length / 3)) ? best : undefined;
|
|
90
|
+
}
|
|
@@ -12,15 +12,25 @@ import {
|
|
|
12
12
|
parseAddress,
|
|
13
13
|
renderAddress,
|
|
14
14
|
acceptsType,
|
|
15
|
+
hasAnchor,
|
|
15
16
|
readCanonicalKey,
|
|
17
|
+
splitAnchor,
|
|
16
18
|
} from "./address.mjs";
|
|
17
19
|
import { NOTE_VOCABULARY, dataFields } from "./note-vocabulary.mjs";
|
|
18
20
|
import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
|
|
19
21
|
import { ART_SLOTS } from "./art-slots.mjs";
|
|
20
22
|
import { SHIPPED_SYSTEMS } from "./subtype-registry.mjs";
|
|
23
|
+
import { EVENT_ADDRESS_POSITIONS } from "./note-events.mjs";
|
|
21
24
|
|
|
22
|
-
import { AddressEntries, AddressLink } from "./address-values.mjs";
|
|
23
|
-
export {
|
|
25
|
+
import { AddressEntries, AddressLink, readPublishedAddresses } from "./address-values.mjs";
|
|
26
|
+
export {
|
|
27
|
+
AddressEntries,
|
|
28
|
+
encodeAddresses,
|
|
29
|
+
cloneAddressState,
|
|
30
|
+
publishAddresses,
|
|
31
|
+
readPublishedAddresses,
|
|
32
|
+
flattenPublishedAddresses,
|
|
33
|
+
} from "./address-values.mjs";
|
|
24
34
|
|
|
25
35
|
/** Structured positions whose Address is nested inside a declared container. */
|
|
26
36
|
export const STRUCTURED_ADDRESSES = Object.freeze([
|
|
@@ -77,12 +87,24 @@ export function addressPositions(fm, context = {}) {
|
|
|
77
87
|
shape,
|
|
78
88
|
type: field.ref,
|
|
79
89
|
accepts: field.accepts,
|
|
90
|
+
...(field.anchors ? { anchors: field.anchors } : {}),
|
|
80
91
|
...(ASSET_TYPE_NAMES.has(field.ref) || field.ref === "folder" ?
|
|
81
92
|
{ system: "none" }
|
|
82
93
|
: {}),
|
|
83
94
|
});
|
|
84
95
|
}
|
|
85
96
|
out.push(...STRUCTURED_ADDRESSES.filter((p) => !p.noteType || p.noteType === fm.type));
|
|
97
|
+
// An event's Addresses, on every type that carries events. Read leniently:
|
|
98
|
+
// a value that is no accepted Address stays as written, for the event
|
|
99
|
+
// check to refuse at its own key with the message its key earns, rather
|
|
100
|
+
// than taking the whole note out of the index.
|
|
101
|
+
if ((dataFields(fm.type) ?? []).some((field) => field.name === "events"))
|
|
102
|
+
for (const position of EVENT_ADDRESS_POSITIONS)
|
|
103
|
+
out.push({
|
|
104
|
+
...position,
|
|
105
|
+
path: ["data", "events", "*", ...position.path],
|
|
106
|
+
lenient: true,
|
|
107
|
+
});
|
|
86
108
|
for (const system of new Set([
|
|
87
109
|
...SHIPPED_SYSTEMS,
|
|
88
110
|
...Object.keys(context.systemBlocks ?? {}),
|
|
@@ -206,7 +228,27 @@ export function decodeNoteAddresses(fm, context) {
|
|
|
206
228
|
};
|
|
207
229
|
const read = (value, path) => {
|
|
208
230
|
if (value == null || value === "") return value;
|
|
209
|
-
|
|
231
|
+
if (value instanceof AddressLink) {
|
|
232
|
+
if (!position.anchors)
|
|
233
|
+
fail(
|
|
234
|
+
value,
|
|
235
|
+
path,
|
|
236
|
+
"takes no anchor — it names a whole note, so write the Address without `#…`",
|
|
237
|
+
);
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
// An anchor is admitted only where the position declares the kinds
|
|
241
|
+
// it takes; the anchor's existence and kind are a resolution
|
|
242
|
+
// question, asked where the index is.
|
|
243
|
+
const anchored = hasAnchor(value);
|
|
244
|
+
if (anchored && !position.anchors)
|
|
245
|
+
fail(
|
|
246
|
+
value,
|
|
247
|
+
path,
|
|
248
|
+
"takes no anchor — it names a whole note, so write the Address without `#…`",
|
|
249
|
+
);
|
|
250
|
+
const { address: written, anchor } = anchored ? splitAnchor(value) : { address: value };
|
|
251
|
+
const tuple = parseAddress(written, defaults, {
|
|
210
252
|
declared: true,
|
|
211
253
|
legacyShortcodeCase: position.legacyShortcodeCase,
|
|
212
254
|
});
|
|
@@ -217,7 +259,13 @@ export function decodeNoteAddresses(fm, context) {
|
|
|
217
259
|
path,
|
|
218
260
|
`must name a ${position.accepts.join(" or ")} Address, but names ${tuple.type}`,
|
|
219
261
|
);
|
|
220
|
-
return
|
|
262
|
+
return anchored ?
|
|
263
|
+
new AddressLink(
|
|
264
|
+
tuple,
|
|
265
|
+
anchor,
|
|
266
|
+
position.anchors.length === 1 ? position.anchors[0] : null,
|
|
267
|
+
)
|
|
268
|
+
: tuple;
|
|
221
269
|
};
|
|
222
270
|
const convert = (value, path) => {
|
|
223
271
|
if (value == null || value === "") return value;
|
|
@@ -258,12 +306,24 @@ export function decodeNoteAddresses(fm, context) {
|
|
|
258
306
|
}
|
|
259
307
|
return read(value, path);
|
|
260
308
|
};
|
|
309
|
+
// A lenient position keeps a value it cannot read, as written.
|
|
310
|
+
const settle = (value, path) => {
|
|
311
|
+
if (!position.lenient) return convert(value, path);
|
|
312
|
+
try {
|
|
313
|
+
return convert(value, path);
|
|
314
|
+
} catch {
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
261
318
|
// Copy each traversed owner so YAML aliases retain their own read context.
|
|
262
319
|
const visit = (owner, path, offset = 0, actual = []) => {
|
|
263
320
|
if (owner == null || owner === "") return owner;
|
|
264
321
|
const key = path[offset];
|
|
265
322
|
if (key === "*") {
|
|
266
|
-
if (!Array.isArray(owner))
|
|
323
|
+
if (!Array.isArray(owner)) {
|
|
324
|
+
if (position.lenient) return owner;
|
|
325
|
+
fail(owner, actual, "must be a list of entries");
|
|
326
|
+
}
|
|
267
327
|
return owner.map((value, i) => visit(value, path, offset + 1, [...actual, i]));
|
|
268
328
|
}
|
|
269
329
|
if (typeof owner !== "object") return owner;
|
|
@@ -271,7 +331,7 @@ export function decodeNoteAddresses(fm, context) {
|
|
|
271
331
|
const copy = Array.isArray(owner) ? [...owner] : { ...owner };
|
|
272
332
|
copy[key] =
|
|
273
333
|
offset === path.length - 1 ?
|
|
274
|
-
|
|
334
|
+
settle(owner[key], [...actual, key])
|
|
275
335
|
: visit(owner[key], path, offset + 1, [...actual, key]);
|
|
276
336
|
return copy;
|
|
277
337
|
};
|
|
@@ -312,11 +372,18 @@ export function noteAddressContext(config) {
|
|
|
312
372
|
|
|
313
373
|
/**
|
|
314
374
|
* Decode Address properties contributed by an index record.
|
|
375
|
+
*
|
|
376
|
+
* A record read from a published index writes every Address as
|
|
377
|
+
* `{ address, anchor, anchorKind }`; each becomes a tuple, or an
|
|
378
|
+
* {@link AddressLink} where it carries an anchor, before the declared
|
|
379
|
+
* positions are read.
|
|
380
|
+
*
|
|
315
381
|
* @param {object} record - The record.
|
|
316
382
|
* @param {object} context - Builder defaults.
|
|
317
383
|
* @returns {object} The typed record.
|
|
318
384
|
*/
|
|
319
385
|
export function decodeIndexAddresses(record, context) {
|
|
386
|
+
Object.assign(record, readPublishedAddresses(record));
|
|
320
387
|
decodeNoteAddresses(record, context);
|
|
321
388
|
const canonical = (value, keyPath) => {
|
|
322
389
|
const target = readCanonicalKey(value);
|
|
@@ -335,19 +402,8 @@ export function decodeIndexAddresses(record, context) {
|
|
|
335
402
|
if (record.address?.canonical != null)
|
|
336
403
|
record.address.canonical = canonical(record.address.canonical, ["address", "canonical"]);
|
|
337
404
|
for (const anchor of record.anchors ?? []) {
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
const target = parseAddress(
|
|
341
|
-
written,
|
|
342
|
-
{
|
|
343
|
-
...context,
|
|
344
|
-
system: "note",
|
|
345
|
-
types: new Set([...Object.keys(NOTE_VOCABULARY), ...ASSET_TYPE_NAMES]),
|
|
346
|
-
},
|
|
347
|
-
{ declared: true },
|
|
348
|
-
);
|
|
349
|
-
if (target.reason) throw new Error(`Index anchor link is not an Address: ${anchor.link}`);
|
|
350
|
-
anchor.link = new AddressLink(target, section);
|
|
405
|
+
if (anchor.link != null && !(anchor.link instanceof AddressLink))
|
|
406
|
+
throw new Error(`Index anchor link is not an Address: ${JSON.stringify(anchor.link)}`);
|
|
351
407
|
}
|
|
352
408
|
return record;
|
|
353
409
|
}
|
package/engine/note-dates.mjs
CHANGED
|
@@ -955,11 +955,13 @@ function resolveEventEntry(entry, context) {
|
|
|
955
955
|
|
|
956
956
|
/** Normalize a note's declared dates for indexes and generated pages. */
|
|
957
957
|
export function resolvedDateFields(fm, context) {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
958
|
+
// A note carrying `data.events` — a lore, place or affiliation note — is
|
|
959
|
+
// dated by them.
|
|
960
|
+
if (Array.isArray(fm?.data?.events)) {
|
|
961
|
+
const resolved = fm.data.events.map((entry) => resolveEventEntry(entry, context));
|
|
961
962
|
return resolved.length ? { events: resolved } : {};
|
|
962
963
|
}
|
|
964
|
+
if (fm?.type === "lore") return {};
|
|
963
965
|
const fields =
|
|
964
966
|
fm?.type === "being" ?
|
|
965
967
|
[
|
|
@@ -0,0 +1,364 @@
|
|
|
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
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The **terms of an event**: the closed value lists an event entry draws on,
|
|
16
|
+
* and the declaration of every key an entry and each map inside it take.
|
|
17
|
+
*
|
|
18
|
+
* A leaf module, importing nothing. The note vocabulary reads
|
|
19
|
+
* {@link EVENT_ENTRY} as it loads, so the frontmatter lint's inner-key check
|
|
20
|
+
* closes every key of an entry the way it closes every other `data:` field's;
|
|
21
|
+
* {@link module:engine/note-events} walks the same declaration for what the
|
|
22
|
+
* values mean — closed lists, Addresses, identity and order — and reaches the
|
|
23
|
+
* vocabulary through the Address reader.
|
|
24
|
+
*
|
|
25
|
+
* An entry is declared in the inner-key vocabulary of
|
|
26
|
+
* {@link module:engine/data-keys} — `fields`, `entries`, `kind`, `shape`,
|
|
27
|
+
* `required`, `describe` — with four properties of its own that the event
|
|
28
|
+
* check reads:
|
|
29
|
+
*
|
|
30
|
+
* - `oneOf` — `{name, values}`: the value is one of `values`, a closed list the
|
|
31
|
+
* format reference states as the vocabulary table `name`;
|
|
32
|
+
* - `ref`, `accepts`, `anchors`, `single`, `eventOrLore`, `calendar` on an
|
|
33
|
+
* Address: its default type, its accepted types, the anchor kinds it may
|
|
34
|
+
* name, and whether it must name exactly one event, an event or a `lore`
|
|
35
|
+
* note, or a calendar note;
|
|
36
|
+
* - `own` — the key's value is judged by the event check's own code (`id`,
|
|
37
|
+
* `when`, `until`, `recurs`) rather than by the walker.
|
|
38
|
+
*
|
|
39
|
+
* @module
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** What an event is, grouped for reading and closed for validation. */
|
|
43
|
+
export const EVENT_KINDS = Object.freeze([
|
|
44
|
+
// peoples
|
|
45
|
+
"arrival",
|
|
46
|
+
"departure",
|
|
47
|
+
"migration",
|
|
48
|
+
"contact",
|
|
49
|
+
"displacement",
|
|
50
|
+
// polities
|
|
51
|
+
"founding",
|
|
52
|
+
"charter",
|
|
53
|
+
"accession",
|
|
54
|
+
"secession",
|
|
55
|
+
"conquest",
|
|
56
|
+
"treaty",
|
|
57
|
+
"dissolution",
|
|
58
|
+
// conflict
|
|
59
|
+
"battle",
|
|
60
|
+
"war",
|
|
61
|
+
"siege",
|
|
62
|
+
"revolt",
|
|
63
|
+
// ruin
|
|
64
|
+
"fall",
|
|
65
|
+
"catastrophe",
|
|
66
|
+
"plague",
|
|
67
|
+
"famine",
|
|
68
|
+
// works
|
|
69
|
+
"raising",
|
|
70
|
+
"ruin",
|
|
71
|
+
"making",
|
|
72
|
+
"loss",
|
|
73
|
+
"discovery",
|
|
74
|
+
// institutions and belief
|
|
75
|
+
"schism",
|
|
76
|
+
"law",
|
|
77
|
+
"council",
|
|
78
|
+
// persons
|
|
79
|
+
"birth",
|
|
80
|
+
"death",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/** How far an event's history reaches. Set by hand. */
|
|
84
|
+
export const EVENT_DEPTHS = Object.freeze(["world", "region", "local"]);
|
|
85
|
+
|
|
86
|
+
/** What the world's evidence supports about an event. */
|
|
87
|
+
export const EVENT_STANDINGS = Object.freeze([
|
|
88
|
+
"attested",
|
|
89
|
+
"single-source",
|
|
90
|
+
"reconstructed",
|
|
91
|
+
"disputed",
|
|
92
|
+
"legendary",
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
/** Whether a place that felt an event knows what caused it. */
|
|
96
|
+
export const REACH_KNOWLEDGE = Object.freeze(["named", "misattributed", "unlinked"]);
|
|
97
|
+
|
|
98
|
+
/** What a participant was to an event. */
|
|
99
|
+
export const PARTICIPANT_ROLES = Object.freeze([
|
|
100
|
+
"actor",
|
|
101
|
+
"victim",
|
|
102
|
+
"instrument",
|
|
103
|
+
"witness",
|
|
104
|
+
"founder",
|
|
105
|
+
"ruler",
|
|
106
|
+
"author",
|
|
107
|
+
"signatory",
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
/** How a later event follows from an earlier one. */
|
|
111
|
+
export const FOLLOWS_HOW = Object.freeze(["caused", "enabled", "ended", "answered"]);
|
|
112
|
+
|
|
113
|
+
/** How far an account agrees with the event's summary. */
|
|
114
|
+
export const ACCOUNT_AGREES = Object.freeze(["full", "partly", "disputes", "denies", "silent"]);
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Every closed list, by the name of the vocabulary table the format reference
|
|
118
|
+
* and `engine/content-format.yaml` state it under. `eventKind` is not `kind`
|
|
119
|
+
* because the infobox's row kinds already hold that name.
|
|
120
|
+
*/
|
|
121
|
+
export const EVENT_VOCABULARIES = Object.freeze({
|
|
122
|
+
eventKind: EVENT_KINDS,
|
|
123
|
+
depth: EVENT_DEPTHS,
|
|
124
|
+
standing: EVENT_STANDINGS,
|
|
125
|
+
knowledge: REACH_KNOWLEDGE,
|
|
126
|
+
role: PARTICIPANT_ROLES,
|
|
127
|
+
followsHow: FOLLOWS_HOW,
|
|
128
|
+
agrees: ACCOUNT_AGREES,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/** A key holding text, stated with something in it. */
|
|
132
|
+
const text = (name, describe, required = false) =>
|
|
133
|
+
Object.freeze({ name, kind: "string", shape: "a string", required, describe });
|
|
134
|
+
|
|
135
|
+
/** A key holding one value of a closed list. */
|
|
136
|
+
const oneOf = (name, vocabulary, describe, required = false) =>
|
|
137
|
+
Object.freeze({
|
|
138
|
+
name,
|
|
139
|
+
kind: "string",
|
|
140
|
+
shape: `one \`${vocabulary}\` value`,
|
|
141
|
+
oneOf: Object.freeze({ name: vocabulary, values: EVENT_VOCABULARIES[vocabulary] }),
|
|
142
|
+
required,
|
|
143
|
+
describe,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/** A key holding one Address, with what it may name. */
|
|
147
|
+
const address = (name, shape, describe, extra = {}, required = false) =>
|
|
148
|
+
Object.freeze({ name, kind: "address", shape, required, describe, ...extra });
|
|
149
|
+
|
|
150
|
+
/** A key holding a list of maps, each closed to `fields`. */
|
|
151
|
+
const listOf = (name, shape, describe, fields) =>
|
|
152
|
+
Object.freeze({
|
|
153
|
+
name,
|
|
154
|
+
kind: "list",
|
|
155
|
+
shape,
|
|
156
|
+
entries: Object.freeze({
|
|
157
|
+
kind: "map",
|
|
158
|
+
shape: `a map — ${shape.replace(/\[\]$/, "")}`,
|
|
159
|
+
fields,
|
|
160
|
+
}),
|
|
161
|
+
describe,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/** An Address naming a place; a bare shortcode is read as one. */
|
|
165
|
+
const PLACE = Object.freeze({ ref: "place", accepts: Object.freeze(["place"]) });
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* One event entry, as an inner-key declaration: every key it takes, and every
|
|
169
|
+
* key of each map nested in it.
|
|
170
|
+
*
|
|
171
|
+
* @type {Readonly<{kind: "map", shape: string, fields: readonly object[]}>}
|
|
172
|
+
*/
|
|
173
|
+
export const EVENT_ENTRY = Object.freeze({
|
|
174
|
+
kind: "map",
|
|
175
|
+
shape: "an event — a map of the keys below",
|
|
176
|
+
fields: Object.freeze([
|
|
177
|
+
Object.freeze({
|
|
178
|
+
name: "id",
|
|
179
|
+
kind: "shortcode",
|
|
180
|
+
shape: "an address segment",
|
|
181
|
+
own: true,
|
|
182
|
+
describe: "The event's name within its note — lowercase letters and digits.",
|
|
183
|
+
}),
|
|
184
|
+
oneOf("kind", "eventKind", "What sort of thing happened."),
|
|
185
|
+
oneOf("depth", "depth", "How far the event's history reaches."),
|
|
186
|
+
Object.freeze({
|
|
187
|
+
name: "when",
|
|
188
|
+
kind: "date",
|
|
189
|
+
shape: 'a date, or `"0.<day>"`',
|
|
190
|
+
required: true,
|
|
191
|
+
own: true,
|
|
192
|
+
describe: "When it happened, or the anchor and first instance of an event that recurs.",
|
|
193
|
+
}),
|
|
194
|
+
Object.freeze({
|
|
195
|
+
name: "until",
|
|
196
|
+
kind: "date",
|
|
197
|
+
shape: "a date",
|
|
198
|
+
own: true,
|
|
199
|
+
describe: "When a continuous event ended, or where a recurring series stopped.",
|
|
200
|
+
}),
|
|
201
|
+
Object.freeze({
|
|
202
|
+
name: "recurs",
|
|
203
|
+
kind: "map",
|
|
204
|
+
shape: "`{ every }` or `{ on }`",
|
|
205
|
+
own: true,
|
|
206
|
+
fields: Object.freeze([
|
|
207
|
+
Object.freeze({
|
|
208
|
+
name: "every",
|
|
209
|
+
kind: "integer",
|
|
210
|
+
shape: "a whole number of years, 1 or more",
|
|
211
|
+
describe: "A period counted on the canonical axis from `when`.",
|
|
212
|
+
}),
|
|
213
|
+
Object.freeze({
|
|
214
|
+
name: "on",
|
|
215
|
+
kind: "list",
|
|
216
|
+
shape: "a list of dates, strictly increasing, each later than `when`",
|
|
217
|
+
entries: Object.freeze({ kind: "date", shape: "a date" }),
|
|
218
|
+
describe: "Recorded occurrences beyond the first, in place of a period.",
|
|
219
|
+
}),
|
|
220
|
+
]),
|
|
221
|
+
describe: "How further occurrences are found; absent for an event that happened once.",
|
|
222
|
+
}),
|
|
223
|
+
text("summary", "What happened, stated plainly.", true),
|
|
224
|
+
oneOf("standing", "standing", "What the world's evidence supports."),
|
|
225
|
+
listOf(
|
|
226
|
+
"names",
|
|
227
|
+
"`{ name, by, gloss? }[]`",
|
|
228
|
+
"The event's names in the world, each with who uses it.",
|
|
229
|
+
[
|
|
230
|
+
text("name", "One name the event goes by in the world.", true),
|
|
231
|
+
address(
|
|
232
|
+
"by",
|
|
233
|
+
"an Address naming an `affiliation`, `lore`, `place`, `being` or `skill` note",
|
|
234
|
+
"Who uses that name — a people, a polity, a faith, a place, a tongue.",
|
|
235
|
+
{ accepts: Object.freeze(["affiliation", "lore", "place", "being", "skill"]) },
|
|
236
|
+
true,
|
|
237
|
+
),
|
|
238
|
+
text("gloss", "What the name means, or how it is used."),
|
|
239
|
+
],
|
|
240
|
+
),
|
|
241
|
+
Object.freeze({
|
|
242
|
+
name: "where",
|
|
243
|
+
kind: "map",
|
|
244
|
+
shape: "`{ locus?, reach? }`",
|
|
245
|
+
fields: Object.freeze([
|
|
246
|
+
Object.freeze({
|
|
247
|
+
name: "locus",
|
|
248
|
+
kind: "list",
|
|
249
|
+
shape: "a list of Addresses, each defaulting to `place`",
|
|
250
|
+
entries: Object.freeze({ kind: "address", shape: "a place Address", ...PLACE }),
|
|
251
|
+
describe: "Where the event physically happened; each resolves to a place.",
|
|
252
|
+
}),
|
|
253
|
+
listOf(
|
|
254
|
+
"reach",
|
|
255
|
+
"`{ place, how, knowledge, attributedTo? }[]`",
|
|
256
|
+
"Where the event was felt, and whether each place knows why.",
|
|
257
|
+
[
|
|
258
|
+
address(
|
|
259
|
+
"place",
|
|
260
|
+
"an Address, defaulting to `place`",
|
|
261
|
+
"A place where the event was felt.",
|
|
262
|
+
PLACE,
|
|
263
|
+
true,
|
|
264
|
+
),
|
|
265
|
+
text(
|
|
266
|
+
"how",
|
|
267
|
+
"One clause: a consequence someone in that place could notice.",
|
|
268
|
+
true,
|
|
269
|
+
),
|
|
270
|
+
oneOf(
|
|
271
|
+
"knowledge",
|
|
272
|
+
"knowledge",
|
|
273
|
+
"Whether that place connects what it felt to this event.",
|
|
274
|
+
true,
|
|
275
|
+
),
|
|
276
|
+
address(
|
|
277
|
+
"attributedTo",
|
|
278
|
+
"an Address, defaulting to `lore`, naming an event or a `lore` note",
|
|
279
|
+
"Only beside `knowledge: misattributed` — the cause that place names instead.",
|
|
280
|
+
{ ref: "lore", anchors: Object.freeze(["event"]), eventOrLore: true },
|
|
281
|
+
),
|
|
282
|
+
],
|
|
283
|
+
),
|
|
284
|
+
]),
|
|
285
|
+
describe: "Where it happened, and where it was felt.",
|
|
286
|
+
}),
|
|
287
|
+
listOf("who", "`{ ref, role }[]`", "Who took part, and as what.", [
|
|
288
|
+
address(
|
|
289
|
+
"ref",
|
|
290
|
+
"an Address naming a `being`, `affiliation` or `lore` note",
|
|
291
|
+
"A participant — a being, a people, an affiliation.",
|
|
292
|
+
{ accepts: Object.freeze(["being", "affiliation", "lore"]) },
|
|
293
|
+
true,
|
|
294
|
+
),
|
|
295
|
+
oneOf("role", "role", "What the participant was to the event.", true),
|
|
296
|
+
]),
|
|
297
|
+
listOf(
|
|
298
|
+
"follows",
|
|
299
|
+
"`{ event, how, note? }[]`",
|
|
300
|
+
"The earlier events this one follows from.",
|
|
301
|
+
[
|
|
302
|
+
address(
|
|
303
|
+
"event",
|
|
304
|
+
"an Address, defaulting to `lore`, anchored to an `event` where its note holds several",
|
|
305
|
+
"The earlier event; it resolves to exactly one event.",
|
|
306
|
+
{ ref: "lore", anchors: Object.freeze(["event"]), single: true },
|
|
307
|
+
true,
|
|
308
|
+
),
|
|
309
|
+
oneOf("how", "followsHow", "How this event follows from it.", true),
|
|
310
|
+
text("note", "One clause saying what connects the two."),
|
|
311
|
+
],
|
|
312
|
+
),
|
|
313
|
+
listOf(
|
|
314
|
+
"accounts",
|
|
315
|
+
"`{ by, says, agrees, withholds? }[]`",
|
|
316
|
+
"What each people, polity, faith or place says about it.",
|
|
317
|
+
[
|
|
318
|
+
address(
|
|
319
|
+
"by",
|
|
320
|
+
"an Address naming an `affiliation`, `lore`, `place` or `being` note",
|
|
321
|
+
"Who holds the account.",
|
|
322
|
+
{ accepts: Object.freeze(["affiliation", "lore", "place", "being"]) },
|
|
323
|
+
true,
|
|
324
|
+
),
|
|
325
|
+
text("says", "What they say happened, in their terms.", true),
|
|
326
|
+
oneOf("agrees", "agrees", "How far their account agrees with `summary`.", true),
|
|
327
|
+
text("withholds", "What they decline to say."),
|
|
328
|
+
],
|
|
329
|
+
),
|
|
330
|
+
Object.freeze({
|
|
331
|
+
name: "unresolved",
|
|
332
|
+
kind: "list",
|
|
333
|
+
shape: "a list of strings",
|
|
334
|
+
entries: Object.freeze({ kind: "string", shape: "a string" }),
|
|
335
|
+
describe: "What the world itself has not settled.",
|
|
336
|
+
}),
|
|
337
|
+
Object.freeze({
|
|
338
|
+
name: "sources",
|
|
339
|
+
kind: "list",
|
|
340
|
+
shape: "a list of Addresses",
|
|
341
|
+
entries: Object.freeze({ kind: "address", shape: "an Address, written with its type" }),
|
|
342
|
+
describe: "The notes that state or support the event; each resolves.",
|
|
343
|
+
}),
|
|
344
|
+
Object.freeze({
|
|
345
|
+
name: "stated",
|
|
346
|
+
kind: "map",
|
|
347
|
+
shape: "`{ calendar, text }`",
|
|
348
|
+
fields: Object.freeze([
|
|
349
|
+
address(
|
|
350
|
+
"calendar",
|
|
351
|
+
"an Address, or a lore shortcode",
|
|
352
|
+
"The calendar that tradition reckons in — a `lore` note with `subType: calendar`.",
|
|
353
|
+
{ ref: "lore", accepts: Object.freeze(["lore"]), calendar: true },
|
|
354
|
+
true,
|
|
355
|
+
),
|
|
356
|
+
text("text", "The date as that tradition writes it.", true),
|
|
357
|
+
]),
|
|
358
|
+
describe: "The date as one tradition writes it in its own reckoning.",
|
|
359
|
+
}),
|
|
360
|
+
]),
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
/** Every key an event entry admits, in the order the reference documents them. */
|
|
364
|
+
export const EVENT_ENTRY_KEYS = Object.freeze(EVENT_ENTRY.fields.map((field) => field.name));
|