@heroiclands/package-build 23.1.1 → 23.2.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 +63 -0
- package/bin/content-commands.mjs +68 -149
- package/bin/package-build.mjs +56 -10
- package/ci/check-banners.mjs +42 -6
- package/ci/confirm-npm-version.mjs +135 -0
- package/ci/content-format-reference.mjs +14 -5
- package/config.mjs +1 -0
- package/container.mjs +185 -4
- package/content-config.mjs +333 -210
- package/deploy.mjs +116 -10
- package/docs/api.md +238 -246
- package/docs/authoring/assets.md +11 -5
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +87 -0
- package/docs/commands.md +138 -99
- package/docs/configuration.md +277 -125
- package/docs/content-format.md +2 -0
- package/docs/diagnostics.md +48 -13
- package/docs/guides/book.md +341 -21
- package/docs/guides/e2e.md +13 -2
- package/docs/guides/icons.md +14 -4
- package/docs/guides/module-project.md +18 -8
- package/docs/guides/release-and-deploy.md +32 -0
- package/docs/guides/site.md +5 -2
- package/docs/project-setup.md +30 -10
- package/docs/reference/format-details.md +373 -170
- package/docs/reference/note-types.md +128 -109
- package/docs/reference/theme-pages.md +31 -6
- package/e2e.mjs +140 -28
- package/engine/actor-compiler.mjs +22 -31
- package/engine/address-charset.mjs +4 -17
- package/engine/address-fields.mjs +1 -1
- package/engine/address-render.mjs +12 -1
- package/engine/address-values.mjs +6 -0
- package/engine/address.mjs +46 -33
- package/engine/art-fields.mjs +30 -5
- package/engine/art-slots.mjs +10 -1
- package/engine/asset-bindings.mjs +12 -1
- package/engine/base-compiler.mjs +34 -65
- package/engine/being-age.mjs +2 -7
- package/engine/being-measurements.mjs +12 -1
- package/engine/calendar-artifacts.mjs +6 -0
- package/engine/calendar-choice.mjs +12 -1
- package/engine/calendar-format.mjs +12 -1
- package/engine/calendar-human.mjs +12 -1
- package/engine/compile-corpus.mjs +3 -1
- package/engine/content-address.mjs +9 -12
- package/engine/content-alerts.mjs +13 -1
- package/engine/content-blocks.mjs +14 -3
- package/engine/content-embeds.mjs +11 -3
- package/engine/content-figures.mjs +9 -0
- package/engine/content-footnotes.mjs +12 -1
- package/engine/content-format-check.mjs +11 -262
- package/engine/content-format.yaml +5 -2
- package/engine/content-images.mjs +3 -0
- package/engine/content-index.mjs +443 -20
- package/engine/content-links.mjs +29 -35
- package/engine/content-lint.mjs +2 -3
- package/engine/content-poetry.mjs +12 -1
- package/engine/content-slug.mjs +2 -14
- package/engine/content-spans.mjs +13 -1
- package/engine/content-tables.mjs +12 -1
- package/engine/culture-choice.mjs +210 -13
- package/engine/data-keys.mjs +19 -16
- package/engine/date-conversion.mjs +12 -1
- package/engine/declaration-check.mjs +9 -0
- package/engine/derived-fields.mjs +6 -7
- package/engine/diagnostics.mjs +34 -4
- package/engine/docs-checks.mjs +9 -0
- package/engine/document-subtypes.mjs +47 -34
- package/engine/event-views.mjs +139 -14
- package/engine/exported-scene.mjs +92 -28
- package/engine/extension-attributes.mjs +12 -1
- package/engine/field-spec.mjs +11 -102
- package/engine/folder-notes.mjs +0 -64
- package/engine/foreign-catalog.mjs +2 -1
- package/engine/format-generated.mjs +9 -0
- package/engine/foundry-entries.mjs +10 -28
- package/engine/frontmatter-lint.mjs +83 -392
- package/engine/frontmatter.mjs +20 -70
- package/engine/generate.mjs +10 -15
- package/engine/heading-attributes.mjs +12 -1
- package/engine/helpers.mjs +41 -102
- package/engine/holdings.mjs +1 -1
- package/engine/homepage.mjs +3 -10
- package/engine/ids.mjs +1 -167
- package/engine/index-records.mjs +6 -4
- package/engine/index.mjs +0 -3
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +13 -14
- package/engine/item-compiler.mjs +4 -10
- package/engine/item-docs.mjs +3 -5
- package/engine/item-registry.mjs +2 -10
- package/engine/journals.mjs +1 -5
- package/engine/literature-notes.mjs +4 -12
- package/engine/macros.mjs +1 -2
- package/engine/map-graphviz.mjs +12 -1
- package/engine/markdown-expressions.mjs +12 -1
- package/engine/markdownlint-run.mjs +567 -0
- package/engine/metadata-index.mjs +42 -12
- package/engine/note-addresses.mjs +143 -35
- package/engine/note-claims.mjs +14 -28
- package/engine/note-dates.mjs +3 -3
- package/engine/note-frontmatter.mjs +6 -0
- package/engine/note-ids.mjs +8 -14
- package/engine/note-package.mjs +8 -25
- package/engine/note-state.mjs +1 -3
- package/engine/note-vocabulary.mjs +148 -84
- package/engine/number-words.mjs +12 -1
- package/engine/pack-config.mjs +15 -25
- package/engine/pack-router.mjs +10 -16
- package/engine/pathnames.mjs +8 -22
- package/engine/pdf-build.mjs +773 -121
- package/engine/pdf-coverage.mjs +188 -0
- package/engine/pdf-index.mjs +54 -7
- package/engine/pdf-pages.mjs +247 -0
- package/engine/pdf-render.mjs +114 -4
- package/engine/pdf-toc.mjs +29 -32
- package/engine/project-init.mjs +46 -6
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +52 -88
- package/engine/readability-lint.mjs +9 -0
- package/engine/reckoning-markers.mjs +12 -1
- package/engine/reference-targets.mjs +7 -0
- package/engine/runtime-only-fields.mjs +5 -8
- package/engine/scene-levels.mjs +2 -2
- package/engine/scenes.mjs +1 -1
- package/engine/shortcode-references.mjs +7 -0
- package/engine/site-build.mjs +30 -16
- package/engine/site-index.mjs +10 -16
- package/engine/site-preview.mjs +6 -0
- package/engine/site-root.mjs +8 -1
- package/engine/social-tie-terms.mjs +12 -1
- package/engine/social-ties.mjs +3 -0
- package/engine/sql-tables.mjs +56 -20
- package/engine/standing-terms.mjs +11 -1
- package/engine/standings.mjs +11 -27
- package/engine/system-block.mjs +39 -141
- package/engine/system-vocabulary.mjs +6 -0
- package/engine/walk-files.mjs +69 -0
- package/engine/web-wikilinks.mjs +23 -3
- package/engine/wikilink-syntax.mjs +32 -21
- package/engine/wikilinks.mjs +27 -16
- package/githooks/attribution.mjs +8 -0
- package/githooks/commit-message.mjs +9 -0
- package/hm3/actors.mjs +9 -48
- package/hm3/default-item-art.mjs +9 -9
- package/hm3/item-fields.mjs +2 -9
- package/hm3/template-priority.mjs +6 -7
- package/hugo-theme/data/banner-kinds.yaml +78 -0
- package/hugo-theme/data/banners.yaml +2 -3
- package/hugo-theme/layouts/404.html +3 -3
- package/hugo-theme/layouts/partials/banner-url.html +31 -7
- package/hugo-theme/layouts/partials/hero-banner.html +13 -14
- package/hugo-theme/layouts/partials/related.html +1 -1
- package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
- package/hugo-theme/static/css/style.css +1 -1
- package/manifest.mjs +11 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +6 -3
- package/release.mjs +21 -14
- package/sohl/actors.mjs +16 -27
- package/sohl/default-item-art.mjs +3 -3
- package/sohl/document-subtypes.mjs +1 -16
- package/sohl/infobox.mjs +2 -3
- package/sohl/item-builders.mjs +5 -8
- package/sohl/item-fields.mjs +5 -35
- package/sohl/items.mjs +0 -4
- package/sohl/kb-passes.mjs +3 -3
- package/types/container.d.mts +116 -5
- package/types/content-config.d.mts +83 -35
- package/types/deploy.d.mts +42 -6
- package/types/e2e.d.mts +35 -3
- package/types/engine/address-charset.d.mts +4 -17
- package/types/engine/address.d.mts +17 -8
- package/types/engine/art-fields.d.mts +7 -4
- package/types/engine/art-slots.d.mts +6 -0
- package/types/engine/base-compiler.d.mts +15 -17
- package/types/engine/content-address.d.mts +9 -12
- package/types/engine/content-embeds.d.mts +4 -2
- package/types/engine/content-format-check.d.mts +0 -64
- package/types/engine/content-index.d.mts +82 -3
- package/types/engine/content-links.d.mts +2 -4
- package/types/engine/culture-choice.d.mts +51 -8
- package/types/engine/data-keys.d.mts +2 -2
- package/types/engine/derived-fields.d.mts +1 -2
- package/types/engine/diagnostics.d.mts +27 -4
- package/types/engine/document-subtypes.d.mts +12 -31
- package/types/engine/event-views.d.mts +14 -2
- package/types/engine/exported-scene.d.mts +3 -2
- package/types/engine/field-spec.d.mts +13 -122
- package/types/engine/folder-notes.d.mts +0 -39
- package/types/engine/foreign-catalog.d.mts +2 -1
- package/types/engine/foundry-entries.d.mts +4 -5
- package/types/engine/frontmatter-lint.d.mts +2 -2
- package/types/engine/frontmatter.d.mts +19 -45
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +25 -42
- package/types/engine/homepage.d.mts +3 -10
- package/types/engine/ids.d.mts +0 -114
- package/types/engine/index-records.d.mts +6 -4
- package/types/engine/index.d.mts +0 -1
- package/types/engine/infobox-render.d.mts +7 -1
- package/types/engine/infobox.d.mts +3 -2
- package/types/engine/item-docs.d.mts +1 -1
- package/types/engine/item-registry.d.mts +2 -4
- package/types/engine/literature-notes.d.mts +1 -1
- package/types/engine/markdownlint-run.d.mts +40 -0
- package/types/engine/metadata-index.d.mts +9 -1
- package/types/engine/note-addresses.d.mts +27 -2
- package/types/engine/note-claims.d.mts +3 -5
- package/types/engine/note-ids.d.mts +2 -4
- package/types/engine/note-package.d.mts +1 -1
- package/types/engine/note-state.d.mts +1 -3
- package/types/engine/note-vocabulary.d.mts +28 -21
- package/types/engine/pack-config.d.mts +2 -6
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/pathnames.d.mts +3 -3
- package/types/engine/pdf-build.d.mts +98 -12
- package/types/engine/pdf-coverage.d.mts +60 -0
- package/types/engine/pdf-index.d.mts +34 -5
- package/types/engine/pdf-pages.d.mts +143 -0
- package/types/engine/pdf-render.d.mts +45 -1
- package/types/engine/pdf-toc.d.mts +12 -14
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +11 -10
- package/types/engine/runtime-only-fields.d.mts +2 -2
- package/types/engine/site-build.d.mts +9 -5
- package/types/engine/site-index.d.mts +4 -5
- package/types/engine/site-root.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -2
- package/types/engine/standing-terms.d.mts +2 -1
- package/types/engine/standings.d.mts +7 -9
- package/types/engine/system-block.d.mts +10 -44
- package/types/engine/walk-files.d.mts +15 -0
- package/types/engine/wikilink-syntax.d.mts +18 -19
- package/types/hm3/actors.d.mts +1 -2
- package/types/hm3/item-fields.d.mts +2 -4
- package/types/hm3/template-priority.d.mts +6 -7
- package/types/manifest.d.mts +3 -2
- package/types/release.d.mts +8 -7
- package/types/sohl/actors.d.mts +2 -1
- package/types/sohl/document-subtypes.d.mts +1 -2
- package/types/sohl/item-fields.d.mts +3 -7
- package/engine/retired-fields.mjs +0 -631
- package/types/engine/retired-fields.d.mts +0 -354
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
3
|
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
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
|
+
*
|
|
5
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
import { declaredSystems, systemBlocksFor } from "./system-vocabulary.mjs";
|
|
9
|
-
import {
|
|
15
|
+
import { inBlockKey } from "./system-block.mjs";
|
|
10
16
|
import {
|
|
11
17
|
isAddressTuple,
|
|
12
18
|
parseAddress,
|
|
@@ -23,6 +29,7 @@ import { SHIPPED_SYSTEMS } from "./subtype-registry.mjs";
|
|
|
23
29
|
import { EVENT_ADDRESS_POSITIONS } from "./note-events.mjs";
|
|
24
30
|
|
|
25
31
|
import { AddressEntries, AddressLink, readPublishedAddresses } from "./address-values.mjs";
|
|
32
|
+
import { formatDiagnostic } from "./diagnostics.mjs";
|
|
26
33
|
export {
|
|
27
34
|
AddressEntries,
|
|
28
35
|
encodeAddresses,
|
|
@@ -72,12 +79,8 @@ export const STRUCTURED_ADDRESSES = Object.freeze([
|
|
|
72
79
|
export function addressPositions(fm, context = {}) {
|
|
73
80
|
const out = [];
|
|
74
81
|
for (const field of dataFields(fm.type) ?? []) {
|
|
75
|
-
// A field that admits both a list and a map declares both halves, and
|
|
76
|
-
// which one a note wrote is not knowable from the declaration — so the
|
|
77
|
-
// position says "either" and the conversion reads the value's shape.
|
|
78
82
|
const shape =
|
|
79
83
|
field.kind === "address" ? "value"
|
|
80
|
-
: field.kind === "list-or-map" ? "keys-or-list"
|
|
81
84
|
: field.keyKind === "address" ? "keys"
|
|
82
85
|
: field.entryKind === "address" ? field.kind
|
|
83
86
|
: undefined;
|
|
@@ -87,6 +90,7 @@ export function addressPositions(fm, context = {}) {
|
|
|
87
90
|
shape,
|
|
88
91
|
type: field.ref,
|
|
89
92
|
accepts: field.accepts,
|
|
93
|
+
...(field.declines === undefined ? {} : { declines: field.declines }),
|
|
90
94
|
...(field.anchors ? { anchors: field.anchors } : {}),
|
|
91
95
|
...(ASSET_TYPE_NAMES.has(field.ref) || field.ref === "folder" ?
|
|
92
96
|
{ system: "none" }
|
|
@@ -120,7 +124,6 @@ export function addressPositions(fm, context = {}) {
|
|
|
120
124
|
path: [system, "items", "*", "model"],
|
|
121
125
|
shape: "value",
|
|
122
126
|
system,
|
|
123
|
-
legacyShortcodeCase: true,
|
|
124
127
|
});
|
|
125
128
|
for (const slot of ART_SLOTS)
|
|
126
129
|
out.push({
|
|
@@ -145,7 +148,7 @@ export function addressPositions(fm, context = {}) {
|
|
|
145
148
|
system,
|
|
146
149
|
types: new Set([...Object.keys(NOTE_VOCABULARY), ...ASSET_TYPE_NAMES]),
|
|
147
150
|
},
|
|
148
|
-
{ declared: true
|
|
151
|
+
{ declared: true },
|
|
149
152
|
);
|
|
150
153
|
const type = model && !model.reason ? model.type : entry?.type;
|
|
151
154
|
for (const field of spec?.fields?.[type] ?? []) {
|
|
@@ -182,30 +185,90 @@ export function addressPositions(fm, context = {}) {
|
|
|
182
185
|
: system,
|
|
183
186
|
};
|
|
184
187
|
out.push({ ...position, path: [system, "system", ...field.to.split(".")] });
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
path: [system, ...
|
|
188
|
-
}
|
|
188
|
+
const inBlock = inBlockKey(field);
|
|
189
|
+
if (inBlock !== undefined) {
|
|
190
|
+
out.push({ ...position, path: [system, ...inBlock.split(".")] });
|
|
191
|
+
}
|
|
189
192
|
if (field.name?.startsWith("data.") && field.topLevelMeans === undefined)
|
|
190
193
|
out.push({ ...position, path: field.name.slice(5).split(".") });
|
|
191
194
|
}
|
|
192
195
|
}
|
|
193
|
-
for (const position of [...out]) {
|
|
194
|
-
const last = position.path.at(-1);
|
|
195
|
-
const retired = RETIRED_FIELD_ALIASES[last];
|
|
196
|
-
if (retired) out.push({ ...position, path: [...position.path.slice(0, -1), retired] });
|
|
197
|
-
}
|
|
198
196
|
return out;
|
|
199
197
|
}
|
|
200
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The finding for a declared Address whose shortcode carries a capital.
|
|
201
|
+
*
|
|
202
|
+
* A shortcode is lowercase, and a declared Address is read as written, so the
|
|
203
|
+
* value is refused rather than folded. Where the lowercase spelling names
|
|
204
|
+
* something the context declares, the message names that shortcode.
|
|
205
|
+
*
|
|
206
|
+
* @param {import("./address.mjs").AddressProblem} problem - The parser's verdict.
|
|
207
|
+
* @param {Array<string|number>} path - The value's key path.
|
|
208
|
+
* @param {{shape?: string}} position - The Address position it sits in.
|
|
209
|
+
* @param {{declaresAddress?: (tuple: object) => boolean}} context - The
|
|
210
|
+
* decode context; `declaresAddress` answers whether a complete Address names
|
|
211
|
+
* a declared note or item.
|
|
212
|
+
* @returns {Error & {keyPath: Array<string|number>, addressKey: boolean}} The
|
|
213
|
+
* located error.
|
|
214
|
+
*/
|
|
215
|
+
function shortcodeCaseError(problem, path, position, context) {
|
|
216
|
+
const declared =
|
|
217
|
+
context.declaresAddress?.(problem.lowercase) ?
|
|
218
|
+
`; the declared shortcode is \`${problem.lowercase.shortcode}\``
|
|
219
|
+
: "";
|
|
220
|
+
const error = new Error(
|
|
221
|
+
`\`${path.join(".")}\` shortcode \`${problem.shortcode}\` must be lowercase${declared}`,
|
|
222
|
+
);
|
|
223
|
+
error.keyPath = path;
|
|
224
|
+
error.addressKey = position.shape === "keys";
|
|
225
|
+
return /** @type {any} */ (error);
|
|
226
|
+
}
|
|
227
|
+
|
|
201
228
|
/**
|
|
202
229
|
* Decode declared properties at the note boundary. Raw mappings cannot impersonate tuples.
|
|
230
|
+
*
|
|
231
|
+
* A declared Address is read as written. A shortcode spelled with a capital is
|
|
232
|
+
* refused, and every such value in the note is collected before the decode
|
|
233
|
+
* throws, so each reference is reported at its own position: the thrown error
|
|
234
|
+
* describes the first, and its `findings` carry all of them.
|
|
235
|
+
*
|
|
203
236
|
* @param {object} fm - Parsed YAML or an index record.
|
|
204
|
-
* @param {object} context - The builder's package and known types
|
|
237
|
+
* @param {object} context - The builder's package and known types, and
|
|
238
|
+
* optionally `declaresAddress`, which names the lowercase spelling of a
|
|
239
|
+
* refused shortcode when it is declared.
|
|
205
240
|
* @returns {object} The same note carrying typed Address properties.
|
|
241
|
+
* @throws {Error} When a declared Address is refused; `keyPath` locates it, and
|
|
242
|
+
* `findings`, where present, lists every refused shortcode.
|
|
206
243
|
*/
|
|
207
244
|
export function decodeNoteAddresses(fm, context) {
|
|
208
245
|
if (!fm || typeof fm !== "object") return fm;
|
|
246
|
+
/** @type {Array<Error & {keyPath: Array<string|number>}>} */
|
|
247
|
+
const refused = [];
|
|
248
|
+
try {
|
|
249
|
+
decodePositions(fm, context, refused);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (refused.length && error?.keyPath) error.findings = [...refused, error];
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
if (refused.length) {
|
|
255
|
+
const [first] = refused;
|
|
256
|
+
/** @type {any} */ (first).findings = refused;
|
|
257
|
+
throw first;
|
|
258
|
+
}
|
|
259
|
+
return fm;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The body of {@link decodeNoteAddresses}: every Address position, decoded in
|
|
264
|
+
* place.
|
|
265
|
+
*
|
|
266
|
+
* @param {object} fm - The note.
|
|
267
|
+
* @param {object} context - The decode context.
|
|
268
|
+
* @param {Error[]} refused - Collects each refused shortcode.
|
|
269
|
+
* @returns {void}
|
|
270
|
+
*/
|
|
271
|
+
function decodePositions(fm, context, refused) {
|
|
209
272
|
const types = new Set([
|
|
210
273
|
...Object.keys(NOTE_VOCABULARY),
|
|
211
274
|
...ASSET_TYPE_NAMES,
|
|
@@ -223,11 +286,14 @@ export function decodeNoteAddresses(fm, context) {
|
|
|
223
286
|
`\`${path.join(".")}\` ${description}: ${JSON.stringify(value)}`,
|
|
224
287
|
);
|
|
225
288
|
error.keyPath = path;
|
|
226
|
-
error.addressKey = position.shape === "keys"
|
|
289
|
+
error.addressKey = position.shape === "keys";
|
|
227
290
|
throw error;
|
|
228
291
|
};
|
|
229
292
|
const read = (value, path) => {
|
|
230
293
|
if (value == null || value === "") return value;
|
|
294
|
+
// The word a position declines with is not an Address; it stays as
|
|
295
|
+
// written so every later stage can tell it from an image.
|
|
296
|
+
if (position.declines !== undefined && value === position.declines) return value;
|
|
231
297
|
if (value instanceof AddressLink) {
|
|
232
298
|
if (!position.anchors)
|
|
233
299
|
fail(
|
|
@@ -248,10 +314,11 @@ export function decodeNoteAddresses(fm, context) {
|
|
|
248
314
|
"takes no anchor — it names a whole note, so write the Address without `#…`",
|
|
249
315
|
);
|
|
250
316
|
const { address: written, anchor } = anchored ? splitAnchor(value) : { address: value };
|
|
251
|
-
const tuple = parseAddress(written, defaults, {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
317
|
+
const tuple = parseAddress(written, defaults, { declared: true });
|
|
318
|
+
if (tuple.reason === "shortcode-not-lowercase" && !position.lenient) {
|
|
319
|
+
refused.push(shortcodeCaseError(tuple, path, position, context));
|
|
320
|
+
return value;
|
|
321
|
+
}
|
|
255
322
|
if (tuple.reason) fail(value, path, "is not an accepted Address");
|
|
256
323
|
if (position.accepts && !acceptsType(tuple, position.accepts))
|
|
257
324
|
fail(
|
|
@@ -269,15 +336,7 @@ export function decodeNoteAddresses(fm, context) {
|
|
|
269
336
|
};
|
|
270
337
|
const convert = (value, path) => {
|
|
271
338
|
if (value == null || value === "") return value;
|
|
272
|
-
|
|
273
|
-
// empty list is the property editor's emptied map and converts to
|
|
274
|
-
// nothing either way.
|
|
275
|
-
const shape =
|
|
276
|
-
position.shape === "keys-or-list" ?
|
|
277
|
-
Array.isArray(value) && value.length ?
|
|
278
|
-
"list"
|
|
279
|
-
: "keys"
|
|
280
|
-
: position.shape;
|
|
339
|
+
const shape = position.shape;
|
|
281
340
|
if (shape === "keys") {
|
|
282
341
|
if (value instanceof AddressEntries) return value;
|
|
283
342
|
if (Array.isArray(value) && value.length === 0) return value;
|
|
@@ -338,7 +397,6 @@ export function decodeNoteAddresses(fm, context) {
|
|
|
338
397
|
const decoded = visit(fm, position.path);
|
|
339
398
|
Object.assign(fm, decoded);
|
|
340
399
|
}
|
|
341
|
-
return fm;
|
|
342
400
|
}
|
|
343
401
|
|
|
344
402
|
/**
|
|
@@ -378,11 +436,56 @@ export function noteAddressContext(config) {
|
|
|
378
436
|
* {@link AddressLink} where it carries an anchor, before the declared
|
|
379
437
|
* positions are read.
|
|
380
438
|
*
|
|
439
|
+
* With a `source`, the record is a dependency's, read from its cached index.
|
|
440
|
+
* Every decode failure then leaves as a located finding that names the
|
|
441
|
+
* dependency, the cached file, the record, the field and the remedy, because
|
|
442
|
+
* the shape of a published index is the dependency's to correct and no note in
|
|
443
|
+
* the reading tree is at fault.
|
|
444
|
+
*
|
|
381
445
|
* @param {object} record - The record.
|
|
382
446
|
* @param {object} context - Builder defaults.
|
|
447
|
+
* @param {{package: string, file: string, line?: number}} [source] - The
|
|
448
|
+
* dependency and the cached index file the record was read from, with the
|
|
449
|
+
* record's 1-based line when known.
|
|
383
450
|
* @returns {object} The typed record.
|
|
451
|
+
* @throws {Error} A located finding (`located: true`) when `source` is given
|
|
452
|
+
* and the record cannot be decoded; otherwise the decode error itself.
|
|
384
453
|
*/
|
|
385
|
-
export function decodeIndexAddresses(record, context) {
|
|
454
|
+
export function decodeIndexAddresses(record, context, source) {
|
|
455
|
+
if (!source) return decodeIndexRecord(record, context);
|
|
456
|
+
try {
|
|
457
|
+
return decodeIndexRecord(record, context);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
const field = error.keyPath?.join(".") ?? error.indexField;
|
|
460
|
+
const named = [record?.type, record?.shortcode].filter(Boolean).join(" ");
|
|
461
|
+
const detail = error.message.replace(/^Index /, "");
|
|
462
|
+
const error_ = new Error(
|
|
463
|
+
formatDiagnostic({
|
|
464
|
+
file: source.file,
|
|
465
|
+
...(Number.isInteger(source.line) ? { line: source.line } : {}),
|
|
466
|
+
severity: "error",
|
|
467
|
+
message:
|
|
468
|
+
`${source.package} published an index this package-build does not read` +
|
|
469
|
+
(named ? ` (record ${named}` : " (record") +
|
|
470
|
+
(field ? `, field ${field}` : "") +
|
|
471
|
+
`): ${detail}. Take a release of ${source.package} built with this ` +
|
|
472
|
+
`package-build, then run \`package-build deps fetch\` again.`,
|
|
473
|
+
}),
|
|
474
|
+
{ cause: error },
|
|
475
|
+
);
|
|
476
|
+
error_.located = true;
|
|
477
|
+
throw error_;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Decode one index record's Address properties.
|
|
483
|
+
*
|
|
484
|
+
* @param {object} record - The record.
|
|
485
|
+
* @param {object} context - Builder defaults.
|
|
486
|
+
* @returns {object} The typed record.
|
|
487
|
+
*/
|
|
488
|
+
function decodeIndexRecord(record, context) {
|
|
386
489
|
Object.assign(record, readPublishedAddresses(record));
|
|
387
490
|
decodeNoteAddresses(record, context);
|
|
388
491
|
const canonical = (value, keyPath) => {
|
|
@@ -402,8 +505,13 @@ export function decodeIndexAddresses(record, context) {
|
|
|
402
505
|
if (record.address?.canonical != null)
|
|
403
506
|
record.address.canonical = canonical(record.address.canonical, ["address", "canonical"]);
|
|
404
507
|
for (const anchor of record.anchors ?? []) {
|
|
405
|
-
if (anchor.link != null && !(anchor.link instanceof AddressLink))
|
|
406
|
-
|
|
508
|
+
if (anchor.link != null && !(anchor.link instanceof AddressLink)) {
|
|
509
|
+
const error = new Error(
|
|
510
|
+
`Index anchor link is not an Address: ${JSON.stringify(anchor.link)}`,
|
|
511
|
+
);
|
|
512
|
+
error.indexField = "anchors[].link";
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
407
515
|
}
|
|
408
516
|
return record;
|
|
409
517
|
}
|
package/engine/note-claims.mjs
CHANGED
|
@@ -87,12 +87,12 @@ import { assertSuppliedCorpus, parseMarkdownFile } from "./helpers.mjs";
|
|
|
87
87
|
// The record accessors only: this module is imported by the content index, so
|
|
88
88
|
// importing the index back would close a cycle.
|
|
89
89
|
import { authoredFrontmatter, isNoteRecord, noteFile } from "./index-records.mjs";
|
|
90
|
-
import { JOURNAL_TYPES, MAP_TYPES, PACK_BY_TYPE
|
|
90
|
+
import { JOURNAL_TYPES, MAP_TYPES, PACK_BY_TYPE } from "./ids.mjs";
|
|
91
91
|
import { itemTypes } from "./item-registry.mjs";
|
|
92
92
|
import { docEntryTypes } from "./item-docs.mjs";
|
|
93
93
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
94
94
|
import { declaresNoPack } from "./pack-router.mjs";
|
|
95
|
-
import { locateFrontmatterKey } from "./
|
|
95
|
+
import { locateFrontmatterKey } from "./diagnostics.mjs";
|
|
96
96
|
import { noteTypesFor, subtypeRow } from "./document-subtypes.mjs";
|
|
97
97
|
import {
|
|
98
98
|
DEFAULT_DOCUMENT_SUBTYPES,
|
|
@@ -359,7 +359,7 @@ export const CLAIMABLE_DOCUMENT_TYPES = Object.freeze([
|
|
|
359
359
|
* the answer is the type's full potential — every document such a note *could*
|
|
360
360
|
* produce — which is what a caller asking about a type rather than a note wants.
|
|
361
361
|
*
|
|
362
|
-
* @param {string} type - The note's declared `type
|
|
362
|
+
* @param {string} type - The note's declared `type`.
|
|
363
363
|
* @param {ClaimSources} [sources] - What to answer from.
|
|
364
364
|
* @param {object} [opts] - Options.
|
|
365
365
|
* @param {(() => boolean)|boolean} [opts.hasProse] - Whether *this note* carries
|
|
@@ -497,18 +497,14 @@ function article(word) {
|
|
|
497
497
|
* @returns {string} The message.
|
|
498
498
|
*/
|
|
499
499
|
function configurationMessage(type, config, sources) {
|
|
500
|
-
// Every table below is keyed by the current spelling of a note type; the
|
|
501
|
-
// message quotes the authored one, which is what the reader has in front of
|
|
502
|
-
// them.
|
|
503
|
-
const current = currentType(type);
|
|
504
500
|
const documents = mappedDocuments(sources.maps, type);
|
|
505
|
-
if (!documents.length && sources.itemTypes.has(
|
|
506
|
-
if (!documents.length && PACK_BY_TYPE[
|
|
501
|
+
if (!documents.length && sources.itemTypes.has(type)) documents.push("Item");
|
|
502
|
+
if (!documents.length && PACK_BY_TYPE[type]) documents.push(PACK_BY_TYPE[type].docType);
|
|
507
503
|
|
|
508
504
|
const systems = mappingSystems(sources.maps, type);
|
|
509
505
|
const configured = new Set((config.packs ?? []).map((pack) => pack.type));
|
|
510
506
|
const packless = documents.filter((document) => !configured.has(document));
|
|
511
|
-
const needsBuilder = documents.includes("Item") && !sources.itemTypes.has(
|
|
507
|
+
const needsBuilder = documents.includes("Item") && !sources.itemTypes.has(type);
|
|
512
508
|
|
|
513
509
|
const into = documents.map((document) => `${article(document)} ${document}`).join(" or ");
|
|
514
510
|
const becomes =
|
|
@@ -668,7 +664,7 @@ function packsServing(config, docType, system) {
|
|
|
668
664
|
*
|
|
669
665
|
* @param {object} fm - The note's frontmatter.
|
|
670
666
|
* @param {string} absPath - The note's path, for the finding's position.
|
|
671
|
-
* @param {string} type - The note's
|
|
667
|
+
* @param {string} type - The note's type.
|
|
672
668
|
* @param {object} config - The resolved build configuration.
|
|
673
669
|
* @returns {Array<{file: string, line?: number, column?: number,
|
|
674
670
|
* severity: "error", message: string, type: string}>} One finding per
|
|
@@ -708,12 +704,10 @@ function discardedSystemBlockFindings(fm, absPath, type, config) {
|
|
|
708
704
|
* note's type and of a note's systems, and a note can fail either while passing
|
|
709
705
|
* the other — see {@link discardedSystemBlockFindings}.
|
|
710
706
|
*
|
|
711
|
-
* Read-only: it walks the tree and reports, and writes nothing.
|
|
707
|
+
* Read-only: it walks the tree and reports, and writes nothing. Two kinds of
|
|
712
708
|
* note are passed over, each for a stated reason rather than by omission — a
|
|
713
709
|
* file with no frontmatter is not a note; a note with no `type:` is the
|
|
714
|
-
* frontmatter linter's finding, which can say what a type is *for
|
|
715
|
-
* retired type is answered by `assertTypeNotRetired` in `ids.mjs`, which names the
|
|
716
|
-
* replacement.
|
|
710
|
+
* frontmatter linter's finding, which can say what a type is *for*.
|
|
717
711
|
*
|
|
718
712
|
* @param {object} [config] - The resolved build configuration. Defaults to this
|
|
719
713
|
* repository's.
|
|
@@ -755,24 +749,16 @@ export function unclaimedNoteFindings(config = loadPackConfig(), sources, { reco
|
|
|
755
749
|
if (!type) continue;
|
|
756
750
|
if (NEVER_PACKED_TYPES.has(type)) continue;
|
|
757
751
|
if (DERIVED_PACKED_TYPES.has(type)) continue;
|
|
758
|
-
if (RETIRED_TYPES[type]) continue;
|
|
759
|
-
// A **renamed** spelling is a live type, not an unknown one: it resolves
|
|
760
|
-
// to the same row, the same registry entry and the same pack. So the
|
|
761
|
-
// claim is asked of the current spelling while the finding quotes the
|
|
762
|
-
// authored one. The rename itself is reported by the frontmatter
|
|
763
|
-
// lint, which can say what to write instead.
|
|
764
|
-
const current = currentType(type);
|
|
765
|
-
|
|
766
752
|
// Asked first, and asked whatever the type's claim turns out to be: a
|
|
767
753
|
// note whose documents all have packs may still carry a block none of
|
|
768
754
|
// them reads, and that is invisible to every question below.
|
|
769
|
-
findings.push(...discardedSystemBlockFindings(fm, absPath,
|
|
755
|
+
findings.push(...discardedSystemBlockFindings(fm, absPath, type, config));
|
|
770
756
|
|
|
771
757
|
// Every document this note produces, against the classes this
|
|
772
758
|
// configuration has a pack for. Three outcomes, and the middle one is
|
|
773
759
|
// the half-claimed one — invisible while the question is only "is anything
|
|
774
760
|
// claiming this note", because the answer there is yes.
|
|
775
|
-
const produces = documentClassesFor(
|
|
761
|
+
const produces = documentClassesFor(type, resolved, {
|
|
776
762
|
// Lazy: only a doc-carrying type whose JournalEntry has nowhere to
|
|
777
763
|
// go asks, so a tree with a JournalEntry pack — which is most of
|
|
778
764
|
// them — reads no bodies at all.
|
|
@@ -824,9 +810,9 @@ export function unclaimedNoteFindings(config = loadPackConfig(), sources, { reco
|
|
|
824
810
|
// nothing routes it, so the `vocabulary.has` branch would never
|
|
825
811
|
// reach it — and reading its absence as the reason is the
|
|
826
812
|
// inference {@link UNIMPLEMENTED_TYPES} exists to replace.
|
|
827
|
-
UNIMPLEMENTED_TYPES.has(
|
|
828
|
-
: vocabulary.has(
|
|
829
|
-
: Object.hasOwn(NOTE_VOCABULARY,
|
|
813
|
+
UNIMPLEMENTED_TYPES.has(type) ? specifiedMessage(type)
|
|
814
|
+
: vocabulary.has(type) ? configurationMessage(type, config, resolved)
|
|
815
|
+
: Object.hasOwn(NOTE_VOCABULARY, type) ? specifiedMessage(type)
|
|
830
816
|
: authoringMessage(type),
|
|
831
817
|
});
|
|
832
818
|
}
|
package/engine/note-dates.mjs
CHANGED
|
@@ -430,9 +430,9 @@ export function parseNoteDate(value, options) {
|
|
|
430
430
|
);
|
|
431
431
|
}
|
|
432
432
|
if (DOTLESS_TOKEN_PATTERN.test(trimmed)) {
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
433
|
+
// A token with no dot names no era. Nothing says which direction
|
|
434
|
+
// a bare token would count, so the message states the rule rather
|
|
435
|
+
// than guessing a concrete replacement.
|
|
436
436
|
return refuse(
|
|
437
437
|
`${subject} names no era — a reckoning is written ` +
|
|
438
438
|
`\`<calendar shortcode>.<era shortcode>\`: a year before ` +
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
3
|
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
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
|
+
*
|
|
5
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
12
|
*/
|
|
7
13
|
|
package/engine/note-ids.mjs
CHANGED
|
@@ -14,13 +14,10 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* The id a note's document is filed under, and the one rule that decides it.
|
|
16
16
|
*
|
|
17
|
-
* A note
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* already guards: a duplicate address is a build error, while a duplicate `id`
|
|
22
|
-
* was checked nowhere. So the derived id inherits a guarantee, where the
|
|
23
|
-
* authored one had none.
|
|
17
|
+
* A note's canonical address (`sohl-none-miscgear-bowlcer`) is the identity
|
|
18
|
+
* `content-lint` guards: a duplicate address is a build error. The id derived
|
|
19
|
+
* from it inherits that guarantee, and an authored `id` would be a second
|
|
20
|
+
* identity for a thing that already has one.
|
|
24
21
|
*
|
|
25
22
|
* **An authored `id` always wins.** That is the pattern the map compiler
|
|
26
23
|
* already uses and documents — `regionDocId(sceneId, key, pinned)` returns
|
|
@@ -30,8 +27,7 @@
|
|
|
30
27
|
* **This is one function because every pass must agree.** Half a dozen corpus
|
|
31
28
|
* readers ask what a note's document id is — the pack passes, the wikilink
|
|
32
29
|
* index, the content index, the Foundry-address pass, the address diff — and
|
|
33
|
-
* they never see each other's answer. They
|
|
34
|
-
* one authored field; they agree now because they all call this.
|
|
30
|
+
* they never see each other's answer. They agree because they all call this.
|
|
35
31
|
*
|
|
36
32
|
* `maps` and `pkg` are parameters with defaults rather than values resolved
|
|
37
33
|
* inside, so a test can pose a package and a registry this toolchain does not
|
|
@@ -49,7 +45,7 @@ import { NOTE_SYSTEM } from "./systems.mjs";
|
|
|
49
45
|
import { HOMEPAGE_TYPE } from "./homepage.mjs";
|
|
50
46
|
// The folder id's derivation, taken from the pass that owns it rather than
|
|
51
47
|
// restated here — see the `folder` branch below. `folder-notes.mjs` reaches
|
|
52
|
-
// only `content-address`, `address-charset
|
|
48
|
+
// only `content-address`, `address-charset` and `ids`, none
|
|
53
49
|
// of which reach this module, so the direction closes no cycle.
|
|
54
50
|
import { FOLDER_TYPE, folderDocId } from "./folder-notes.mjs";
|
|
55
51
|
|
|
@@ -88,10 +84,8 @@ function text(value) {
|
|
|
88
84
|
*
|
|
89
85
|
* Returns `undefined` for a file with **no address** — no `type`, or no
|
|
90
86
|
* `shortcode`. Such a file is not an addressable note, so it has no document
|
|
91
|
-
* and inventing an id for one would file it under nothing.
|
|
92
|
-
*
|
|
93
|
-
* so this reports the same thing rather than throwing where a walk used to
|
|
94
|
-
* skip; whether an id is *required* stays each pass's own decision
|
|
87
|
+
* and inventing an id for one would file it under nothing. This reports the
|
|
88
|
+
* absence rather than throwing, so a walk skips such a file; whether an id is *required* stays each pass's own decision
|
|
95
89
|
* (`BasePackCompiler.requiresId`).
|
|
96
90
|
*
|
|
97
91
|
* @param {object|null|undefined} fm - Parsed frontmatter.
|
package/engine/note-package.mjs
CHANGED
|
@@ -19,28 +19,11 @@
|
|
|
19
19
|
* single-sourced in the repository that ships it — so the package is a property
|
|
20
20
|
* of the *repository*, not of the note, and no note declares it.
|
|
21
21
|
*
|
|
22
|
-
* It is not a property of the note, and not a **selector**:
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* **zero notes and exited 0** — which is exactly the state the un-migrated
|
|
28
|
-
* `hm-loc-*` / `hm-adv-*` repositories are in today.
|
|
29
|
-
*
|
|
30
|
-
* The field was retired in three steps, and all three have landed:
|
|
31
|
-
*
|
|
32
|
-
* 1. **Optional** (3.3.0). An absent `package:` was normal and a present one
|
|
33
|
-
* was accepted while it agreed. Non-breaking, so a consumer adopted it
|
|
34
|
-
* before changing a note.
|
|
35
|
-
* 2. **Swept** out of every content tree on the org — 6,235 notes across
|
|
36
|
-
* `sohl`, `thalorna`, `kethira` and `harnensemble`.
|
|
37
|
-
* 3. **Rejected outright**, as this major. A note declaring the field fails the
|
|
38
|
-
* build naming the file, whatever the value says.
|
|
39
|
-
*
|
|
40
|
-
* Step 3 is deliberately a rejection rather than continued tolerance: a field
|
|
41
|
-
* accepted while it agrees is a field that grows back, one note at a time, and
|
|
42
|
-
* every one of them is a line restating a constant the configuration already
|
|
43
|
-
* carries. There is no value that makes writing it correct, so the diagnostic
|
|
22
|
+
* It is not a property of the note, and not a **selector**: a compile loop that
|
|
23
|
+
* skipped notes naming another package would compile zero notes and exit 0 on a
|
|
24
|
+
* tree whose notes named a package no configuration answered to. A note
|
|
25
|
+
* declaring the field fails the build naming the file, whatever the value
|
|
26
|
+
* says: there is no value that makes writing it correct, so the diagnostic
|
|
44
27
|
* says what to write instead rather than which value to change.
|
|
45
28
|
*
|
|
46
29
|
* SQL queries can refer to the configured package. Search readers receive that
|
|
@@ -50,7 +33,7 @@
|
|
|
50
33
|
*/
|
|
51
34
|
|
|
52
35
|
import { contentPackage } from "./content-package.mjs";
|
|
53
|
-
import { locateFrontmatterKey } from "./
|
|
36
|
+
import { locateFrontmatterKey } from "./diagnostics.mjs";
|
|
54
37
|
|
|
55
38
|
/**
|
|
56
39
|
* A copy of frontmatter carrying the configured package for query evaluation.
|
|
@@ -67,7 +50,7 @@ export function searchableFrontmatter(fm, configured) {
|
|
|
67
50
|
* Refuse a note that declares `package:` at all.
|
|
68
51
|
*
|
|
69
52
|
* Presence is the whole test — a declaration that *agrees* with the
|
|
70
|
-
* configuration is
|
|
53
|
+
* configuration is refused like one that disagrees, and an empty one
|
|
71
54
|
* (`package:`, which parses as `null`) is still the field.
|
|
72
55
|
*
|
|
73
56
|
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
@@ -95,7 +78,7 @@ export function assertNoDeclaredPackage(fm, { file, absPath, configured } = {})
|
|
|
95
78
|
: `\`package: ${declared}\``;
|
|
96
79
|
|
|
97
80
|
const err = new Error(
|
|
98
|
-
`${wrote} is a
|
|
81
|
+
`${wrote} is not a frontmatter field — delete it` +
|
|
99
82
|
(file ? ` — ${file}` : "") +
|
|
100
83
|
`. A note's package is this repository's configured ` +
|
|
101
84
|
`\`contentPackage\` ("${target}", in package-build.config.yaml), ` +
|
package/engine/note-state.mjs
CHANGED
|
@@ -62,9 +62,7 @@ export const NOTE_STATES = Object.freeze(["stub", "draft", "full"]);
|
|
|
62
62
|
* either state.
|
|
63
63
|
*
|
|
64
64
|
* An HTML comment is deliberately not empty. An invisible marker deciding
|
|
65
|
-
* whether a page exists
|
|
66
|
-
* for, and reintroducing it in comment form would be worse, because nothing
|
|
67
|
-
* would report it.
|
|
65
|
+
* whether a page exists would be a failure nothing reports.
|
|
68
66
|
*
|
|
69
67
|
* @param {string|null|undefined} body - The note body, frontmatter stripped.
|
|
70
68
|
* @returns {boolean} Whether the body is empty.
|