@heroiclands/package-build 22.28.0 → 23.1.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 +72 -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 +125 -142
- package/docs/authoring/assets.md +1 -1
- package/docs/authoring/dates-and-calendars.md +12 -6
- package/docs/authoring/first-note.md +1 -1
- 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 +3 -3
- 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 +17 -1
- package/docs/guides/site.md +17 -18
- package/docs/reference/format-details.md +904 -275
- package/docs/reference/note-types.md +282 -91
- package/docs/reference/theme-pages.md +58 -66
- 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 +84 -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 +136 -0
- package/engine/literature-works.mjs +161 -0
- package/engine/map-graphviz.mjs +4 -18
- package/engine/markdown-expressions.mjs +116 -11
- package/engine/metadata-index.mjs +34 -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 +195 -92
- 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 +74 -85
- 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/stub-lint.mjs +6 -0
- package/engine/web-wikilinks.mjs +21 -1
- package/engine/wikilink-syntax.mjs +34 -4
- package/engine/wikilinks.mjs +27 -2
- package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
- package/hugo-theme/layouts/partials/page-body.html +0 -2
- package/hugo-theme/static/css/style.css +25 -55
- package/package.json +1 -2
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/default-item-art.mjs +6 -6
- 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-notes.d.mts +29 -0
- package/types/engine/literature-works.d.mts +67 -0
- 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/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/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
|
@@ -128,32 +128,45 @@ function applyFixups(scene, fixups, resolveAddress) {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Add a compendium key to every embedded document that carries an `_id`.
|
|
133
|
+
*
|
|
134
|
+
* The keys are what the compendium CLI splits embedded documents by. A value
|
|
135
|
+
* that is not a document, or one carrying no `_id`, passes through as exported.
|
|
136
|
+
*
|
|
137
|
+
* @param {unknown} docs - One embedded collection, as exported.
|
|
138
|
+
* @param {string} collection - Its key path, such as `regions.behaviors`.
|
|
139
|
+
* @param {string[]} parentIds - The ids of the documents it is embedded in.
|
|
140
|
+
* @param {string} sceneId - The Scene's derived id.
|
|
141
|
+
* @returns {void}
|
|
142
|
+
*/
|
|
143
|
+
function keyDocuments(docs, collection, parentIds, sceneId) {
|
|
144
|
+
if (!Array.isArray(docs)) return;
|
|
145
|
+
for (const doc of docs) {
|
|
146
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) continue;
|
|
147
|
+
if (typeof doc._id !== "string" || !doc._id) continue;
|
|
148
|
+
doc._key = `!scenes.${collection}!${[sceneId, ...parentIds, doc._id].join(".")}`;
|
|
149
|
+
if (collection === "regions") {
|
|
150
|
+
keyDocuments(doc.behaviors, "regions.behaviors", [doc._id], sceneId);
|
|
151
|
+
}
|
|
138
152
|
}
|
|
139
|
-
doc._key = `!scenes.${collection}!${[sceneId, ...parentIds, doc._id].join(".")}`;
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
/**
|
|
143
156
|
* Compile a Foundry Scene export into a pack document.
|
|
144
157
|
*
|
|
145
|
-
* Only the note-derived Scene ID,
|
|
146
|
-
*
|
|
158
|
+
* The Scene passes through as exported. Only the note-derived Scene ID,
|
|
159
|
+
* compendium metadata, `data.fixup` and pins marked `#anchor` change; nothing
|
|
160
|
+
* else inside the Scene is read or checked.
|
|
147
161
|
*
|
|
148
162
|
* @param {object} fm - The map note's frontmatter.
|
|
149
163
|
* @param {string} markdown - Its converted Markdown body.
|
|
150
164
|
* @param {{journalEntryId?: string, stats?: object, resolveAddress?: Function}} [opts] - Pack metadata, derived JournalEntry ID, and asset resolver.
|
|
151
165
|
* @returns {object} The keyed Scene document.
|
|
166
|
+
* @throws {Error} When `data.scene` is not an object, or a fixup or pin anchor
|
|
167
|
+
* names something the Scene or the note does not have.
|
|
152
168
|
*/
|
|
153
169
|
export function buildExportedScene(fm, markdown, { journalEntryId, stats, resolveAddress } = {}) {
|
|
154
|
-
if (!["battlemap", "localmap"].includes(fm.subType)) {
|
|
155
|
-
throw new Error("`data.scene` is for battlemap and localmap notes");
|
|
156
|
-
}
|
|
157
170
|
const source = fm.data?.scene;
|
|
158
171
|
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
159
172
|
throw new Error("a map note needs a Foundry Scene object at `data.scene`");
|
|
@@ -163,48 +176,10 @@ export function buildExportedScene(fm, markdown, { journalEntryId, stats, resolv
|
|
|
163
176
|
}
|
|
164
177
|
const scene = structuredClone(source);
|
|
165
178
|
applyFixups(scene, fm.data?.fixup, resolveAddress);
|
|
166
|
-
if (
|
|
167
|
-
!(Number.isInteger(scene.width) && scene.width > 0) ||
|
|
168
|
-
!(Number.isInteger(scene.height) && scene.height > 0)
|
|
169
|
-
) {
|
|
170
|
-
throw new Error("`data.scene` needs positive whole-number width and height");
|
|
171
|
-
}
|
|
172
|
-
if (!Array.isArray(scene.levels) || scene.levels.length === 0) {
|
|
173
|
-
throw new Error("`data.scene.levels` needs at least one Level");
|
|
174
|
-
}
|
|
175
179
|
scene._id = fm.id;
|
|
176
180
|
scene._key = `!scenes!${fm.id}`;
|
|
177
181
|
if (stats) scene._stats = stats;
|
|
178
|
-
|
|
179
|
-
for (const collection of EMBEDDED) {
|
|
180
|
-
const docs = scene[collection];
|
|
181
|
-
if (docs === undefined) continue;
|
|
182
|
-
if (!Array.isArray(docs)) {
|
|
183
|
-
throw new Error(`data.scene.${collection} must be an array`);
|
|
184
|
-
}
|
|
185
|
-
const ids = new Set();
|
|
186
|
-
for (const doc of docs) {
|
|
187
|
-
keyDocument(doc, collection, [], fm.id);
|
|
188
|
-
if (ids.has(doc._id))
|
|
189
|
-
throw new Error(`data.scene.${collection} repeats _id ${doc._id}`);
|
|
190
|
-
ids.add(doc._id);
|
|
191
|
-
if (collection !== "regions" || doc.behaviors === undefined) continue;
|
|
192
|
-
if (!Array.isArray(doc.behaviors)) {
|
|
193
|
-
throw new Error("data.scene.regions.behaviors must be an array");
|
|
194
|
-
}
|
|
195
|
-
const behaviorIds = new Set();
|
|
196
|
-
for (const behavior of doc.behaviors) {
|
|
197
|
-
keyDocument(behavior, "regions.behaviors", [doc._id], fm.id);
|
|
198
|
-
if (behaviorIds.has(behavior._id)) {
|
|
199
|
-
throw new Error(`data.scene.regions.behaviors repeats _id ${behavior._id}`);
|
|
200
|
-
}
|
|
201
|
-
behaviorIds.add(behavior._id);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
if (scene.initialLevel && !scene.levels.some((level) => level._id === scene.initialLevel)) {
|
|
206
|
-
throw new Error(`data.scene.initialLevel "${scene.initialLevel}" names no Level`);
|
|
207
|
-
}
|
|
182
|
+
for (const collection of EMBEDDED) keyDocuments(scene[collection], collection, [], fm.id);
|
|
208
183
|
|
|
209
184
|
const pages = new Map();
|
|
210
185
|
for (const page of splitPages(markdown, fm.name?.full ?? scene.name)) {
|
|
@@ -214,7 +189,8 @@ export function buildExportedScene(fm, markdown, { journalEntryId, stats, resolv
|
|
|
214
189
|
}
|
|
215
190
|
pages.set(page.anchorSlug, page);
|
|
216
191
|
}
|
|
217
|
-
for (const pin of scene.notes
|
|
192
|
+
for (const pin of Array.isArray(scene.notes) ? scene.notes : []) {
|
|
193
|
+
if (!pin || typeof pin !== "object") continue;
|
|
218
194
|
const match = /^#([A-Za-z0-9][A-Za-z0-9_-]*)$/.exec(String(pin.text ?? ""));
|
|
219
195
|
if (!match) continue;
|
|
220
196
|
const page = pages.get(match[1]);
|
|
@@ -55,6 +55,8 @@
|
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
57
|
import { isAddressTuple } from "./address.mjs";
|
|
58
|
+
import { listed, nearest } from "./near-miss.mjs";
|
|
59
|
+
import { checkDataKeys } from "./data-keys.mjs";
|
|
58
60
|
import {
|
|
59
61
|
authoredNoteKeys,
|
|
60
62
|
CHARACTER_NAME_KEYS,
|
|
@@ -62,7 +64,8 @@ import {
|
|
|
62
64
|
NOTE_TOP_LEVEL_KEY_SET,
|
|
63
65
|
NOTE_TOP_LEVEL_KEYS,
|
|
64
66
|
} from "./note-frontmatter.mjs";
|
|
65
|
-
import { AddressEntries } from "./address-values.mjs";
|
|
67
|
+
import { AddressEntries, AddressLink } from "./address-values.mjs";
|
|
68
|
+
import { resolveEventReference } from "./note-events.mjs";
|
|
66
69
|
import { addressPositions } from "./note-addresses.mjs";
|
|
67
70
|
import { authoredFields, readsLegacyKey, readsRetiredTopLevel } from "./field-spec.mjs";
|
|
68
71
|
import {
|
|
@@ -74,11 +77,15 @@ import {
|
|
|
74
77
|
SYSTEM_BLOCK_KEYS,
|
|
75
78
|
unknownBlockKeys,
|
|
76
79
|
} from "./system-block.mjs";
|
|
77
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
frontmatterValueAt,
|
|
82
|
+
positionInFrontmatter,
|
|
83
|
+
positionOfFrontmatterPath,
|
|
84
|
+
} from "./diagnostics.mjs";
|
|
78
85
|
import { pathnameProblem } from "./pathnames.mjs";
|
|
79
86
|
import { checkHomepageAddressFields } from "./homepage.mjs";
|
|
80
87
|
import { RETIRED_TYPES, RENAMED_TYPES, currentType, renamedTypeMessage } from "./ids.mjs";
|
|
81
|
-
import { parseAddress, acceptsType, renderAddress } from "./address.mjs";
|
|
88
|
+
import { parseAddress, acceptsType, renderAddress, hasAnchor } from "./address.mjs";
|
|
82
89
|
import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
|
|
83
90
|
import { isAddressSegment } from "./address-charset.mjs";
|
|
84
91
|
// The one place the "every pack not named" key is spelled. Imported rather
|
|
@@ -88,7 +95,6 @@ import { DEFAULT_PARENT } from "./folder-notes.mjs";
|
|
|
88
95
|
import {
|
|
89
96
|
BEING_ARCHETYPES,
|
|
90
97
|
dataFields,
|
|
91
|
-
isLegacyDataField,
|
|
92
98
|
declaredTags,
|
|
93
99
|
subTypeCharsetMessage,
|
|
94
100
|
typeCharsetMessage,
|
|
@@ -280,72 +286,6 @@ export { declaredSystems } from "./system-vocabulary.mjs";
|
|
|
280
286
|
*/
|
|
281
287
|
export { systemBlocksFor } from "./system-vocabulary.mjs";
|
|
282
288
|
|
|
283
|
-
/**
|
|
284
|
-
* Edit distance, capped — enough to answer "did you mean".
|
|
285
|
-
*
|
|
286
|
-
* A misspelled property is the failure class this check exists for, and a
|
|
287
|
-
* finding that names the key the author *meant* turns a hunt through the
|
|
288
|
-
* reference into a one-character fix.
|
|
289
|
-
*
|
|
290
|
-
* @param {string} a - One string.
|
|
291
|
-
* @param {string} b - The other.
|
|
292
|
-
* @returns {number} The Levenshtein distance.
|
|
293
|
-
*/
|
|
294
|
-
function distance(a, b) {
|
|
295
|
-
const rows = a.length + 1;
|
|
296
|
-
const cols = b.length + 1;
|
|
297
|
-
let prev = Array.from({ length: cols }, (_, j) => j);
|
|
298
|
-
for (let i = 1; i < rows; i += 1) {
|
|
299
|
-
const row = [i];
|
|
300
|
-
for (let j = 1; j < cols; j += 1) {
|
|
301
|
-
row[j] = Math.min(
|
|
302
|
-
prev[j] + 1,
|
|
303
|
-
row[j - 1] + 1,
|
|
304
|
-
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
prev = row;
|
|
308
|
-
}
|
|
309
|
-
return prev[cols - 1];
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* A closed vocabulary as an English list, for the message that names it.
|
|
314
|
-
*
|
|
315
|
-
* Written from the declaration rather than spelled into the message, so a key
|
|
316
|
-
* the vocabulary gains cannot go unmentioned by the finding that refuses its
|
|
317
|
-
* neighbours.
|
|
318
|
-
*
|
|
319
|
-
* @param {readonly string[]} keys - The declared keys, in declared order.
|
|
320
|
-
* @returns {string} `"a, b, or c"`.
|
|
321
|
-
*/
|
|
322
|
-
function listed(keys) {
|
|
323
|
-
if (keys.length < 2) return keys.join("");
|
|
324
|
-
return `${keys.slice(0, -1).join(", ")}, or ${keys[keys.length - 1]}`;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* The declared key an unknown one was most likely meant to be.
|
|
329
|
-
*
|
|
330
|
-
* @param {string} key - The unknown key.
|
|
331
|
-
* @param {Iterable<string>} candidates - The declared keys.
|
|
332
|
-
* @returns {string|undefined} The nearest, when it is near enough to suggest.
|
|
333
|
-
*/
|
|
334
|
-
function nearest(key, candidates) {
|
|
335
|
-
let best;
|
|
336
|
-
let bestAt = Infinity;
|
|
337
|
-
for (const candidate of candidates) {
|
|
338
|
-
const d = distance(key.toLowerCase(), candidate.toLowerCase());
|
|
339
|
-
if (d < bestAt) {
|
|
340
|
-
bestAt = d;
|
|
341
|
-
best = candidate;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
// A third of the key's length, so a suggestion is a plausible typo rather
|
|
345
|
-
// than the least-bad of a list of unrelated words.
|
|
346
|
-
return bestAt <= Math.max(1, Math.floor(key.length / 3)) ? best : undefined;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
289
|
/**
|
|
350
290
|
* Whether a value satisfies a declared {@link FieldSpec.kind}.
|
|
351
291
|
*
|
|
@@ -503,7 +443,7 @@ function checkDataContainer(note, { type, fields, packs, addressContext, index }
|
|
|
503
443
|
}
|
|
504
444
|
|
|
505
445
|
for (const key of Object.keys(entries)) {
|
|
506
|
-
if (declared.has(key)
|
|
446
|
+
if (declared.has(key)) continue;
|
|
507
447
|
const current = renamed.get(key);
|
|
508
448
|
if (current) {
|
|
509
449
|
findings.push({
|
|
@@ -546,7 +486,19 @@ function checkDataContainer(note, { type, fields, packs, addressContext, index }
|
|
|
546
486
|
value = value && typeof value === "object" ? value[segment] : undefined;
|
|
547
487
|
}
|
|
548
488
|
if (value === undefined || value === null) continue;
|
|
549
|
-
if (
|
|
489
|
+
if (field.kind === "address" && hasAnchor(value) && !field.anchors) {
|
|
490
|
+
findings.push({
|
|
491
|
+
file: note.file,
|
|
492
|
+
...positionOfFrontmatterPath(raw, ["data", ...segments]),
|
|
493
|
+
severity: "error",
|
|
494
|
+
message: `\`data.${field.name}\` ${NO_ANCHOR}, but reads ${JSON.stringify(value)}`,
|
|
495
|
+
});
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (
|
|
499
|
+
!(field.kind === "address" && field.anchors && anchored(value)) &&
|
|
500
|
+
!matchesKind(value, field.kind, { ...addressContext, type: field.ref })
|
|
501
|
+
) {
|
|
550
502
|
findings.push({
|
|
551
503
|
file: note.file,
|
|
552
504
|
...positionOfFrontmatterPath(raw, ["data", ...segments]),
|
|
@@ -570,9 +522,75 @@ function checkDataContainer(note, { type, fields, packs, addressContext, index }
|
|
|
570
522
|
}
|
|
571
523
|
}
|
|
572
524
|
|
|
525
|
+
// One level down and every level below: each field's value is closed to
|
|
526
|
+
// the inner keys its declaration states, as the container is to its own.
|
|
527
|
+
findings.push(...checkDataKeys(note, fields));
|
|
528
|
+
|
|
573
529
|
return findings;
|
|
574
530
|
}
|
|
575
531
|
|
|
532
|
+
/** What a finding says of an anchor on a field that declares no anchor kind. */
|
|
533
|
+
const NO_ANCHOR = "takes no anchor — it names a whole note, so write the Address without `#…`";
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Whether an Address value names an anchor: written with `#<anchor>`, or read
|
|
537
|
+
* into an {@link AddressLink}.
|
|
538
|
+
*
|
|
539
|
+
* @param {unknown} value - The value as the note holds it.
|
|
540
|
+
* @returns {boolean} Whether it carries an anchor.
|
|
541
|
+
*/
|
|
542
|
+
function anchored(value) {
|
|
543
|
+
return value instanceof AddressLink || hasAnchor(value);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* The value as an author wrote it, for a message: a read Address back to its
|
|
548
|
+
* canonical text, with `#<anchor>` where it has one.
|
|
549
|
+
*
|
|
550
|
+
* @param {unknown} value - The value as the note holds it.
|
|
551
|
+
* @returns {unknown} Its written form.
|
|
552
|
+
*/
|
|
553
|
+
function writtenForm(value) {
|
|
554
|
+
if (value instanceof AddressLink) return `${renderAddress(value.target)}#${value.anchor}`;
|
|
555
|
+
if (isAddressTuple(value)) return renderAddress(value);
|
|
556
|
+
return value;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Why an Address on a field declaring anchor kinds names nothing it accepts, or
|
|
561
|
+
* `undefined` when it does.
|
|
562
|
+
*
|
|
563
|
+
* With an index, the Address is resolved as an event's own `follows` is —
|
|
564
|
+
* the anchor must exist and be of a kind the field declares, and a field
|
|
565
|
+
* declaring `single` must name exactly one event. Without one, only its form is
|
|
566
|
+
* checked.
|
|
567
|
+
*
|
|
568
|
+
* @param {unknown} value - The value as the note holds it.
|
|
569
|
+
* @param {object} field - The field's declaration.
|
|
570
|
+
* @param {object} defaults - The Address context, with the field's default type.
|
|
571
|
+
* @param {object} [index] - The link index.
|
|
572
|
+
* @returns {string|undefined} The reason.
|
|
573
|
+
*/
|
|
574
|
+
function eventReferenceProblem(value, field, defaults, index) {
|
|
575
|
+
if (index?.addressHit)
|
|
576
|
+
return resolveEventReference(
|
|
577
|
+
value,
|
|
578
|
+
{
|
|
579
|
+
...(field.ref ? { ref: field.ref } : {}),
|
|
580
|
+
...(field.accepts ? { accepts: field.accepts } : {}),
|
|
581
|
+
anchors: field.anchors,
|
|
582
|
+
...(field.single ? { single: true } : {}),
|
|
583
|
+
},
|
|
584
|
+
index,
|
|
585
|
+
).problem;
|
|
586
|
+
const written = writtenForm(value);
|
|
587
|
+
if (typeof written !== "string") return "should be an Address";
|
|
588
|
+
const { address, anchor } = splitAnchor(written);
|
|
589
|
+
if (anchor !== undefined && !isAddressSegment(anchor))
|
|
590
|
+
return `names the anchor \`${anchor}\`, which is not an address segment`;
|
|
591
|
+
return matchesKind(address, "address", defaults) ? undefined : "should be an Address";
|
|
592
|
+
}
|
|
593
|
+
|
|
576
594
|
/**
|
|
577
595
|
* Validate typed entries and resolve declared non-art Address targets.
|
|
578
596
|
* @param {object} note - The authored note.
|
|
@@ -631,7 +649,11 @@ function checkDataReferences(note, field, value, segments, context, index) {
|
|
|
631
649
|
)
|
|
632
650
|
continue;
|
|
633
651
|
let reason;
|
|
634
|
-
if (
|
|
652
|
+
if (check.kind === "address" && field.anchors && (field.single || anchored(check.value)))
|
|
653
|
+
reason = eventReferenceProblem(check.value, field, defaults, index);
|
|
654
|
+
else if (check.kind === "address" && hasAnchor(check.value) && !field.anchors)
|
|
655
|
+
reason = NO_ANCHOR;
|
|
656
|
+
else if (!matchesKind(check.value, check.kind, defaults))
|
|
635
657
|
reason = `should be ${check.kind === "address" ? "an Address" : "a Shortcode"}`;
|
|
636
658
|
else if (check.kind === "address") {
|
|
637
659
|
const tuple = parseAddress(check.value, defaults, { declared: true });
|
|
@@ -650,7 +672,11 @@ function checkDataReferences(note, field, value, segments, context, index) {
|
|
|
650
672
|
file: note.file,
|
|
651
673
|
...positionOfFrontmatterPath(note.raw ?? "", path, { key: check.key || check.atKey }),
|
|
652
674
|
severity: "error",
|
|
653
|
-
|
|
675
|
+
// The value as the file states it: the note's own Addresses are
|
|
676
|
+
// already canonical, and a key is quoted as the key it is.
|
|
677
|
+
message: `\`${path.join(".")}\` ${reason}, but reads ${JSON.stringify(
|
|
678
|
+
(!check.key && frontmatterValueAt(note.raw, path)) || writtenForm(check.value),
|
|
679
|
+
)}`,
|
|
654
680
|
});
|
|
655
681
|
}
|
|
656
682
|
return findings;
|
|
@@ -981,10 +1007,10 @@ function checkTags(note, { type }) {
|
|
|
981
1007
|
*
|
|
982
1008
|
* {@link module:engine/art-fields.ART_SLOTS} is the declaration; this is that
|
|
983
1009
|
* list in the shape the checks below read, so the linter states no art key of
|
|
984
|
-
* its own and a slot added there is checked here with no second edit.
|
|
985
|
-
*
|
|
1010
|
+
* its own and a slot added there is checked here with no second edit. Every
|
|
1011
|
+
* slot is authored under `data:`, which is what `inData` records.
|
|
986
1012
|
*
|
|
987
|
-
* `document` separates the
|
|
1013
|
+
* `document` separates the slots that reach a compiled document from `banner`,
|
|
988
1014
|
* which reaches none by design. The inert-art check reads it: a key that is
|
|
989
1015
|
* *meant* to reach no document is not an inert key, and reporting every note
|
|
990
1016
|
* that names a hero image would bury the finding that matters.
|
package/engine/frontmatter.mjs
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
// enum cannot drift apart — a value absent from the list is a build
|
|
32
32
|
// error, never a silent ship.
|
|
33
33
|
import { AddressEntries } from "./address-values.mjs";
|
|
34
|
+
import { renderAddress } from "./address-render.mjs";
|
|
34
35
|
import { AFFILIATION_STANDINGS } from "../sohl/affiliation-standings.mjs";
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -287,9 +288,13 @@ export function resolveRelation(fm, ctx = "item") {
|
|
|
287
288
|
for (const [code, value] of entries) {
|
|
288
289
|
const standing = String(value);
|
|
289
290
|
if (!AFFILIATION_STANDINGS.includes(standing)) {
|
|
290
|
-
|
|
291
|
-
`${ctx}: ${key}["${code}"] must be one of ${AFFILIATION_STANDINGS.join(", ")}, got "${value}"`,
|
|
291
|
+
const error = new Error(
|
|
292
|
+
`${ctx}: ${key}["${typeof code === "string" ? code : renderAddress(code)}"] must be one of ${AFFILIATION_STANDINGS.join(", ")}, got "${value}"`,
|
|
292
293
|
);
|
|
294
|
+
// Where the compiler reports it: the authored key's value where the
|
|
295
|
+
// key is still its written text, else the map itself.
|
|
296
|
+
error.keyPath = ["data", key, ...(typeof code === "string" ? [code] : [])];
|
|
297
|
+
throw error;
|
|
293
298
|
}
|
|
294
299
|
if (typed) typedEntries.push({ target: code, value: standing, sourceKey: "" });
|
|
295
300
|
else out[code] = standing;
|
package/engine/generate.mjs
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { noteAddressContext } from "./note-addresses.mjs";
|
|
43
|
+
import { GENERATED_DRAWING_DIR } from "./generated-sections.mjs";
|
|
43
44
|
import fs from "fs";
|
|
44
45
|
import path from "path";
|
|
45
46
|
import log from "loglevel";
|
|
@@ -678,6 +679,56 @@ export function emptyPassErrors(passes) {
|
|
|
678
679
|
);
|
|
679
680
|
}
|
|
680
681
|
|
|
682
|
+
/**
|
|
683
|
+
* Where the module ships the drawings its generated sections name, below the
|
|
684
|
+
* staged package root: `assets/generated/`, so a section's
|
|
685
|
+
* `generated/from-<shortcode>.svg` resolves inside the install as every other
|
|
686
|
+
* own-package pathname does — `modules/<id>/assets/generated/…`.
|
|
687
|
+
*
|
|
688
|
+
* The staged package root is the directory holding the compiled packs, which
|
|
689
|
+
* the manifest addresses as `packs/<name>`.
|
|
690
|
+
*
|
|
691
|
+
* @param {object} config - The resolved configuration.
|
|
692
|
+
* @returns {string} The directory, absolute.
|
|
693
|
+
*/
|
|
694
|
+
function stagedDrawingDir(config) {
|
|
695
|
+
return path.join(path.dirname(config.paths.stage), "assets", GENERATED_DRAWING_DIR);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Draw the map from each place given a **From here** section and stage it
|
|
700
|
+
* into the module, where the section's image resolves. Drawn under
|
|
701
|
+
* `build/map/foundry/` and copied, so the `.dot` each drawing is made from
|
|
702
|
+
* stays out of the module.
|
|
703
|
+
*
|
|
704
|
+
* @param {object[]} records - The corpus this compile derived.
|
|
705
|
+
* @param {object} config - The resolved configuration.
|
|
706
|
+
* @returns {Promise<object[]>} What drawing found, as diagnostics.
|
|
707
|
+
*/
|
|
708
|
+
async function stageGeneratedDrawings(records, config) {
|
|
709
|
+
// Imported here: the derivation reads the map world, which reaches the
|
|
710
|
+
// content index, and a static import from this module would close a cycle
|
|
711
|
+
// through the compilers.
|
|
712
|
+
const { drawMapsFrom } = await import("./derived-sections.mjs");
|
|
713
|
+
let foreignIndex = new Map();
|
|
714
|
+
try {
|
|
715
|
+
const { loadForeignIndexes } = await import("./metadata-index.mjs");
|
|
716
|
+
foreignIndex = loadForeignIndexes(config, [config.contentPackage]).index;
|
|
717
|
+
} catch {
|
|
718
|
+
foreignIndex = new Map();
|
|
719
|
+
}
|
|
720
|
+
const staged = stagedDrawingDir(config);
|
|
721
|
+
fs.rmSync(staged, { recursive: true, force: true });
|
|
722
|
+
const { maps, findings } = drawMapsFrom(records, {
|
|
723
|
+
outDir: path.join(config.rootDir, "build", "map", "foundry"),
|
|
724
|
+
foreignIndex,
|
|
725
|
+
config,
|
|
726
|
+
});
|
|
727
|
+
if (maps.size) fs.mkdirSync(staged, { recursive: true });
|
|
728
|
+
for (const [name, file] of maps) fs.copyFileSync(file, path.join(staged, name));
|
|
729
|
+
return findings;
|
|
730
|
+
}
|
|
731
|
+
|
|
681
732
|
/**
|
|
682
733
|
* Generate the build-only JSON for every pack (or one, when `only` is given).
|
|
683
734
|
*
|
|
@@ -744,6 +795,8 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
|
|
|
744
795
|
problems: corpusProblems,
|
|
745
796
|
});
|
|
746
797
|
for (const problem of corpusProblems) emitDiagnostic(problem);
|
|
798
|
+
for (const finding of await stageGeneratedDrawings(corpus.records, config))
|
|
799
|
+
emitDiagnostic(finding);
|
|
747
800
|
|
|
748
801
|
// A note whose `type:` no configured pack claims compiles into nothing, and
|
|
749
802
|
// used to say nothing — no pass got far enough to reject it, so the
|