@heroiclands/package-build 23.1.1 → 23.2.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 +69 -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/labels.mjs +2 -2
- 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
package/engine/frontmatter.mjs
CHANGED
|
@@ -83,63 +83,39 @@ export function sohlField(fm, key, defaultValue = undefined) {
|
|
|
83
83
|
* `relations` and a projectile's impact die — because each validates a *shape
|
|
84
84
|
* spread over several keys* rather than coercing one scalar.
|
|
85
85
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* `subType` is a thrown build error, and the other four ship empty in silence,
|
|
91
|
-
* which is the failure class the passthrough exists to prevent.
|
|
86
|
+
* {@link sohlField} sees `sohl.<key>` and the top level, never inside
|
|
87
|
+
* `sohl.system`, so this reads the destination first: a note may author at
|
|
88
|
+
* `sohl.system.<to>`, and a reader that could not see it would ship the
|
|
89
|
+
* field's empty value in silence.
|
|
92
90
|
*
|
|
93
91
|
* The **destination wins**, matching every other field's resolution order.
|
|
94
92
|
*
|
|
95
|
-
* `
|
|
96
|
-
*
|
|
97
|
-
* has to be told both.
|
|
98
|
-
* for the same reason — one name cannot key two positions.
|
|
93
|
+
* `name` is for the one pair whose two positions are spelled differently: a
|
|
94
|
+
* projectile authors `impact.die` and stores `impactBase.die`, so the reader
|
|
95
|
+
* has to be told both.
|
|
99
96
|
*
|
|
100
|
-
* `shared` is the field's declared shared source
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* reader that knew the destination, the block and the top level alone skipped
|
|
104
|
-
* straight past the container: every authored value read as unset, and the
|
|
105
|
-
* document shipped the field's default with nothing said. It sits **between**
|
|
106
|
-
* the block and the top level, exactly where
|
|
107
|
-
* {@link module:engine/system-block.resolveFieldValue} puts it — the block is the
|
|
108
|
-
* position a tree still writes and wins while it does, and the bare top-level
|
|
109
|
-
* key is the spelling `data:` gathered the fact off.
|
|
97
|
+
* `shared` is the field's declared shared source when it is a path into
|
|
98
|
+
* `data:`. Such a field is authored in that container and nowhere else, so the
|
|
99
|
+
* block and the top level are not consulted.
|
|
110
100
|
*
|
|
111
101
|
* @param {object} fm - The note's frontmatter.
|
|
112
102
|
* @param {string} to - The key at the destination, dotted for a nested one.
|
|
113
103
|
* @param {any} [defaultValue] - What an unauthored field reads as.
|
|
114
104
|
* @param {object} [options] - Options.
|
|
115
|
-
* @param {string} [options.
|
|
116
|
-
*
|
|
105
|
+
* @param {string} [options.name] - The key the block carries, when it is not
|
|
106
|
+
* spelled `to`. Defaults to `to`.
|
|
117
107
|
* @param {string} [options.shared] - The field's declared shared source, when it
|
|
118
|
-
* is a path into a container (`data.relations`).
|
|
119
|
-
* shared position is the note's top level, which `sohlField` already reads.
|
|
108
|
+
* is a path into a container (`data.relations`).
|
|
120
109
|
* @returns {any} The value.
|
|
121
110
|
*/
|
|
122
|
-
export function sohlSystemField(fm, to, defaultValue = undefined, {
|
|
111
|
+
export function sohlSystemField(fm, to, defaultValue = undefined, { name = to, shared } = {}) {
|
|
123
112
|
const system = fm?.sohl?.system;
|
|
124
113
|
if (system && typeof system === "object" && !Array.isArray(system)) {
|
|
125
114
|
const found = getFrontmatter(system, to, undefined);
|
|
126
115
|
if (found !== undefined) return found;
|
|
127
116
|
}
|
|
128
|
-
if (shared !== undefined)
|
|
129
|
-
|
|
130
|
-
const inBlock =
|
|
131
|
-
block && typeof block === "object" ?
|
|
132
|
-
getFrontmatter(block, legacyKey, undefined)
|
|
133
|
-
: undefined;
|
|
134
|
-
// Consulted only where the block does not carry the key, so step 2 keeps
|
|
135
|
-
// winning; `sohlField` below then reads the block and the retiring
|
|
136
|
-
// top-level spelling exactly as it always has.
|
|
137
|
-
if (inBlock === undefined) {
|
|
138
|
-
const container = getFrontmatter(fm, shared, undefined);
|
|
139
|
-
if (container !== undefined) return container;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return sohlField(fm, legacyKey, defaultValue);
|
|
117
|
+
if (shared !== undefined) return getFrontmatter(fm, shared, defaultValue);
|
|
118
|
+
return sohlField(fm, name, defaultValue);
|
|
143
119
|
}
|
|
144
120
|
|
|
145
121
|
/**
|
|
@@ -184,9 +160,6 @@ function readMapEntries(fm, key, { shared } = {}) {
|
|
|
184
160
|
* so absent frontmatter must resolve to `null` — coercing it to `0` would ship
|
|
185
161
|
* every item as an uncapped charge-user.
|
|
186
162
|
*
|
|
187
|
-
* A legacy `usesCharges` flag in authored frontmatter is ignored: it was inert
|
|
188
|
-
* and has been dropped from the schema.
|
|
189
|
-
*
|
|
190
163
|
* @param {object} fm - The item frontmatter.
|
|
191
164
|
* @returns {{value: number|null, max: number|null}} The persisted charges block.
|
|
192
165
|
*/
|
|
@@ -249,7 +222,7 @@ export function resolveSkillAptitudes(fm, ctx = "item") {
|
|
|
249
222
|
}
|
|
250
223
|
|
|
251
224
|
/**
|
|
252
|
-
* Resolve an affiliation's `
|
|
225
|
+
* Resolve an affiliation's `relations` map — the shortcode of another
|
|
253
226
|
* affiliation → this one's standing toward it.
|
|
254
227
|
*
|
|
255
228
|
* An unrecognized standing would fail the schema's `choices` validation at load
|
|
@@ -266,18 +239,7 @@ export function resolveSkillAptitudes(fm, ctx = "item") {
|
|
|
266
239
|
* @throws {Error} When the map is malformed or names an unknown standing.
|
|
267
240
|
*/
|
|
268
241
|
export function resolveRelation(fm, ctx = "item") {
|
|
269
|
-
|
|
270
|
-
// underneath it so a tree converts on its own schedule. The
|
|
271
|
-
// current name wins wherever a note writes both, and the lint reports the
|
|
272
|
-
// old one through {@link RETIRED_FIELD_ALIASES}.
|
|
273
|
-
// Probed at the destination too: a note that has moved to
|
|
274
|
-
// `sohl.system.relations` carries the current spelling, and a probe that
|
|
275
|
-
// could not see it would fall through to the retired one and read `{}`.
|
|
276
|
-
// Each spelling is probed at all four of its positions, the `data:` source
|
|
277
|
-
// the specification names included, so a retirement and a container move are
|
|
278
|
-
// independent of each other.
|
|
279
|
-
const current = sohlSystemField(fm, "relations", undefined, { shared: "data.relations" });
|
|
280
|
-
const key = current == null ? "relation" : "relations";
|
|
242
|
+
const key = "relations";
|
|
281
243
|
const entries = readMapEntries(fm, key, { shared: `data.${key}` });
|
|
282
244
|
if (entries === null) {
|
|
283
245
|
throw new Error(`${ctx}: ${key} must be a map of shortcode → standing`);
|
|
@@ -362,20 +324,8 @@ export function parseValueDesc(raw) {
|
|
|
362
324
|
*
|
|
363
325
|
* `data.packFolder` is a folder note's **address**
|
|
364
326
|
* (`folder-poisonsandtoxins`), resolved through the address index the whole
|
|
365
|
-
* build shares.
|
|
366
|
-
*
|
|
367
|
-
* are retired together — a note declaring `folder:` is refused by
|
|
368
|
-
* {@link module:engine/folder-notes.assertNoDeclaredFolder} rather than
|
|
369
|
-
* reaching here, so there is no second source for a value to come from.
|
|
370
|
-
*
|
|
371
|
-
* **`packFolder` was a path for one release** (`Possessions/Misc_Gear/Cooking`)
|
|
372
|
-
* and is an address now. A path encoded the hierarchy *in the value*, so
|
|
373
|
-
* reparenting a folder made every note naming it wrong — a structural edit
|
|
374
|
-
* became a corpus-wide rewrite. An address is stable under reparenting, which
|
|
375
|
-
* is exactly why a note is addressed by `(type, shortcode)` and never by
|
|
376
|
-
* `file.path`. The path form is **removed rather than deprecated**: it had no
|
|
377
|
-
* authors to migrate, which is the whole reason the change was cheap enough to
|
|
378
|
-
* make.
|
|
327
|
+
* build shares. An address is stable under reparenting, which is exactly why a
|
|
328
|
+
* note is addressed by `(type, shortcode)` and never by `file.path`.
|
|
379
329
|
*
|
|
380
330
|
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
381
331
|
* @returns {{value: string|null, isAddress: boolean}} The authored value, and
|
package/engine/generate.mjs
CHANGED
|
@@ -28,15 +28,14 @@
|
|
|
28
28
|
* anywhere**. A folder is a note like any other, named by `packFolder`
|
|
29
29
|
* — its address — and it materialises in every pack that holds a document
|
|
30
30
|
* naming it, its ancestors with it. So no pack loads a folder list, and
|
|
31
|
-
* two packs
|
|
31
|
+
* two packs cannot disagree about a folder they both hold.
|
|
32
32
|
*
|
|
33
33
|
* **The order the passes run in is derived, not declared** — see
|
|
34
34
|
* {@link orderPassesByDependency}. The declared list is the manifest's `packs`
|
|
35
35
|
* array as well, so it is ordered for a reader; a pass that reads another's
|
|
36
36
|
* output states that on its compiler and is scheduled after it.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
* HeroicLands vault is no longer a build input for SoHL content.
|
|
38
|
+
* The HeroicLands vault is not a build input for SoHL content.
|
|
40
39
|
*/
|
|
41
40
|
|
|
42
41
|
import { noteAddressContext } from "./note-addresses.mjs";
|
|
@@ -73,7 +72,7 @@ import { routerFor } from "./pack-router.mjs";
|
|
|
73
72
|
import { NEVER_PACKED_TYPES, unclaimedNoteFindings } from "./note-claims.mjs";
|
|
74
73
|
// Which document a content type compiles into, so the art declaration below is
|
|
75
74
|
// answered from the same routing the compile uses.
|
|
76
|
-
import {
|
|
75
|
+
import { packForType } from "./ids.mjs";
|
|
77
76
|
import { contentPackage } from "./content-package.mjs";
|
|
78
77
|
|
|
79
78
|
/**
|
|
@@ -230,22 +229,22 @@ export function compilerFor(docType, system = null) {
|
|
|
230
229
|
* @param {string} type - The note's content type.
|
|
231
230
|
* @returns {{document: string|null, art: readonly string[]}|null} What the type
|
|
232
231
|
* compiles into and the art it carries there, or `null` where no claim can be
|
|
233
|
-
* made
|
|
232
|
+
* made.
|
|
234
233
|
*/
|
|
235
234
|
export function emittedArtFor(type) {
|
|
236
235
|
const name = String(type ?? "");
|
|
237
|
-
if (!name
|
|
236
|
+
if (!name) return null;
|
|
238
237
|
|
|
239
238
|
// A homepage compiles into a *page*, not a compendium document, so nothing
|
|
240
239
|
// it authors reaches one. It is the one type whose absence from every pack
|
|
241
240
|
// is the intended state (`NEVER_PACKED_TYPES`).
|
|
242
|
-
if (NEVER_PACKED_TYPES.has(
|
|
241
|
+
if (NEVER_PACKED_TYPES.has(name)) return { document: null, art: [] };
|
|
243
242
|
|
|
244
243
|
// A folder reaches a pack by a route of its own — it materialises in every
|
|
245
244
|
// pack holding a document that references it — so `packForType` has no
|
|
246
245
|
// answer for it and no compiler class writes it. `folderDocument` does, and
|
|
247
246
|
// a Foundry `Folder` has no artwork at all.
|
|
248
|
-
if (
|
|
247
|
+
if (name === FOLDER_TYPE) return { document: "Folder", art: [] };
|
|
249
248
|
|
|
250
249
|
const { docType } = packForType(name);
|
|
251
250
|
const passes = [
|
|
@@ -516,11 +515,8 @@ async function generatePack(
|
|
|
516
515
|
/**
|
|
517
516
|
* The Foundry folder id a note names, by its folder note's address.
|
|
518
517
|
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
* Foundry-id spelling and the per-pack `*-folders.yaml` it resolved
|
|
522
|
-
* against are retired together, so there is no second source left
|
|
523
|
-
* for a value to come from.
|
|
518
|
+
* `packFolder` names a folder **note**, resolved through the address index
|
|
519
|
+
* shared by the whole build.
|
|
524
520
|
*
|
|
525
521
|
* @param {string|null|undefined} value - As authored.
|
|
526
522
|
* @returns {string|null} The folder id, or `null` for an absent value.
|
|
@@ -799,8 +795,7 @@ export async function generatePacksJson({ only, config = loadPackConfig() } = {}
|
|
|
799
795
|
emitDiagnostic(finding);
|
|
800
796
|
|
|
801
797
|
// A note whose `type:` no configured pack claims compiles into nothing, and
|
|
802
|
-
//
|
|
803
|
-
// silence had no owner. Asked once, of the whole configuration, because
|
|
798
|
+
// no pass gets far enough to reject it, so the silence has no owner. Asked once, of the whole configuration, because
|
|
804
799
|
// that is the only place it can be answered: a per-pass check would report
|
|
805
800
|
// every type a system deliberately does not map, which is exactly the
|
|
806
801
|
// silence the rule requires. Independent of `only`, since it is a fact about the
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
/*
|
|
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
|
+
*/
|
|
2
13
|
|
|
3
14
|
/**
|
|
4
15
|
* The braced attribute block a heading may end with, read once.
|
package/engine/helpers.mjs
CHANGED
|
@@ -303,7 +303,7 @@ export function parseMarkdownFile(filePath, { addressContext } = {}) {
|
|
|
303
303
|
*
|
|
304
304
|
* The rule in one place, so every reader of the tree refuses the same
|
|
305
305
|
* way and says so in the same words. It is shared rather than repeated because
|
|
306
|
-
* the corpus is
|
|
306
|
+
* the corpus is read by more than {@link walkMarkdownTree}: a pass that
|
|
307
307
|
* reads the content index instead is making the identical claim about which
|
|
308
308
|
* files it is looking at, and must be held to the identical requirement — a
|
|
309
309
|
* scope that quietly defaulted there would reintroduce exactly the second
|
|
@@ -423,68 +423,45 @@ export function* walkMarkdownTree(rootDir, { skipDirectories, addressContext } =
|
|
|
423
423
|
* block, in the order they answer — and whichever of them the note actually
|
|
424
424
|
* wrote.
|
|
425
425
|
*
|
|
426
|
-
* Three places
|
|
427
|
-
*
|
|
428
|
-
* notes, beside the `archetype` the build reads — so a tree that has authored
|
|
429
|
-
* forward is read from the key it authored, and only then does the retiring
|
|
430
|
-
* spelling answer.
|
|
426
|
+
* Three places: the specification calls this `data.templatePriority`, and the
|
|
427
|
+
* system block and the note's top level answer after it.
|
|
431
428
|
*
|
|
432
429
|
* **The block is a parameter because the value is shared, not per-system.** One
|
|
433
430
|
* `data.templatePriority` is the note's statement that it is a template; SoHL
|
|
434
431
|
* records it as `system.templatePriority` and HM3 as `flags.hm3.templatePriority`.
|
|
435
|
-
* The
|
|
432
|
+
* The in-block position is therefore read from *the block being compiled*
|
|
436
433
|
* rather than always from `sohl:`, so an HM3-only note is not asked to author a
|
|
437
434
|
* SoHL block to be read.
|
|
438
435
|
*
|
|
439
436
|
* @param {object} fm Parsed frontmatter.
|
|
440
437
|
* @param {string} block The system block being compiled.
|
|
441
|
-
* @returns {{found: [object, string]|undefined,
|
|
442
|
-
*
|
|
443
|
-
* position if the note also carries it, and the two key names.
|
|
438
|
+
* @returns {{found: [object, string]|undefined, KEY: string}} The answering
|
|
439
|
+
* position and the key name.
|
|
444
440
|
*/
|
|
445
441
|
function findTemplatePriority(fm, block) {
|
|
446
442
|
const inBlock =
|
|
447
443
|
fm != null && typeof fm[block] === "object" && fm[block] !== null ? fm[block] : null;
|
|
448
444
|
const data = fm != null && typeof fm.data === "object" && fm.data !== null ? fm.data : null;
|
|
449
445
|
|
|
450
|
-
const
|
|
451
|
-
const OLD = "archetype";
|
|
446
|
+
const KEY = "templatePriority";
|
|
452
447
|
const sources = /** @type {[object|null, string][]} */ ([
|
|
453
|
-
[data,
|
|
454
|
-
[inBlock,
|
|
455
|
-
[fm,
|
|
456
|
-
[inBlock, OLD],
|
|
457
|
-
[fm, OLD],
|
|
448
|
+
[data, KEY],
|
|
449
|
+
[inBlock, KEY],
|
|
450
|
+
[fm, KEY],
|
|
458
451
|
]);
|
|
459
452
|
const found = sources.find(([where, key]) => where != null && key in where);
|
|
460
|
-
|
|
461
|
-
// *disagree*: 145 of `sohl-thalorna`'s 941 dual-spelled notes say
|
|
462
|
-
// `templatePriority: null` where `archetype: 0` says the opposite — "not a
|
|
463
|
-
// template" against "a template at priority 0". Preferring the new key
|
|
464
|
-
// silently would flip those documents, and preferring the old would ignore
|
|
465
|
-
// what an author wrote most recently. Neither is a decision this function
|
|
466
|
-
// gets to make quietly, so a contradiction is refused and named.
|
|
467
|
-
const retiring = sources
|
|
468
|
-
.slice(sources.findIndex(([, key]) => key === OLD))
|
|
469
|
-
.find(([where, key]) => where != null && key in where);
|
|
470
|
-
return {
|
|
471
|
-
found: /** @type {[object, string]|undefined} */ (found),
|
|
472
|
-
retiring: /** @type {[object, string]|undefined} */ (retiring),
|
|
473
|
-
NEW,
|
|
474
|
-
OLD,
|
|
475
|
-
};
|
|
453
|
+
return { found: /** @type {[object, string]|undefined} */ (found), KEY };
|
|
476
454
|
}
|
|
477
455
|
|
|
478
456
|
/**
|
|
479
457
|
* The template priority a note states, for a system that treats an unstated one
|
|
480
458
|
* as "not a template" rather than as an authoring error.
|
|
481
459
|
*
|
|
482
|
-
* Reads exactly the positions {@link resolveTemplatePriority} reads,
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
* corrupt and nothing to demand.
|
|
460
|
+
* Reads exactly the positions {@link resolveTemplatePriority} reads, so the two
|
|
461
|
+
* systems cannot disagree about what a note said. It differs only in what
|
|
462
|
+
* silence means: SoHL requires the statement, while HM3 keeps the value in a
|
|
463
|
+
* flag it simply omits, so there is no tri-state for an absent value to corrupt
|
|
464
|
+
* and nothing to demand.
|
|
488
465
|
*
|
|
489
466
|
* @param {object} fm Parsed frontmatter.
|
|
490
467
|
* @param {string} label Human-readable context for error messages.
|
|
@@ -492,18 +469,9 @@ function findTemplatePriority(fm, block) {
|
|
|
492
469
|
* @param {string} [options.block="sohl"] The system block being compiled.
|
|
493
470
|
* @returns {number|null} The priority, or `null` when the note is not a
|
|
494
471
|
* template or states nothing.
|
|
495
|
-
* @throws {Error} When both spellings are present and disagree.
|
|
496
472
|
*/
|
|
497
473
|
export function statedTemplatePriority(fm, label, { block = "sohl" } = {}) {
|
|
498
|
-
const { found
|
|
499
|
-
if (found && retiring && found[1] !== OLD && found[0][found[1]] !== retiring[0][OLD]) {
|
|
500
|
-
throw new Error(
|
|
501
|
-
`Conflicting ${NEW} for ${label}: ` +
|
|
502
|
-
`${NEW} is ${JSON.stringify(found[0][found[1]])} and the retiring ` +
|
|
503
|
-
`${OLD} is ${JSON.stringify(retiring[0][OLD])}. Both are read and ` +
|
|
504
|
-
`${NEW} wins, so they must agree — delete ${OLD}, or correct it`,
|
|
505
|
-
);
|
|
506
|
-
}
|
|
474
|
+
const { found } = findTemplatePriority(fm, block);
|
|
507
475
|
if (!found) return null;
|
|
508
476
|
const raw = found[0][found[1]];
|
|
509
477
|
return raw === null || raw === "" || raw === undefined ? null : raw;
|
|
@@ -519,31 +487,21 @@ export function statedTemplatePriority(fm, label, { block = "sohl" } = {}) {
|
|
|
519
487
|
* silently assumed.
|
|
520
488
|
*
|
|
521
489
|
* Reads the positions {@link findTemplatePriority} lists: `data.templatePriority`
|
|
522
|
-
* first — the specified home — then the system block and the top level
|
|
523
|
-
* finally the retiring `archetype` spelling in the same two places.
|
|
490
|
+
* first — the specified home — then the system block and the top level.
|
|
524
491
|
*
|
|
525
492
|
* @param {object} fm Parsed frontmatter.
|
|
526
493
|
* @param {string} label Human-readable context for error messages.
|
|
527
494
|
* @param {object} [options] Options.
|
|
528
495
|
* @param {string} [options.block="sohl"] The system block being compiled.
|
|
529
496
|
* @returns {number|undefined} The template priority, or `undefined` when null.
|
|
530
|
-
* @throws {Error} When the property is absent
|
|
531
|
-
* spellings are present and disagree.
|
|
497
|
+
* @throws {Error} When the property is absent or is not a number/null.
|
|
532
498
|
*/
|
|
533
499
|
export function resolveTemplatePriority(fm, label, { block = "sohl" } = {}) {
|
|
534
|
-
const { found,
|
|
500
|
+
const { found, KEY } = findTemplatePriority(fm, block);
|
|
535
501
|
|
|
536
|
-
if (found && retiring && found[1] !== OLD && found[0][found[1]] !== retiring[0][OLD]) {
|
|
537
|
-
throw new Error(
|
|
538
|
-
`Conflicting ${NEW} for ${label}: ` +
|
|
539
|
-
`${NEW} is ${JSON.stringify(found[0][found[1]])} and the retiring ` +
|
|
540
|
-
`${OLD} is ${JSON.stringify(retiring[0][OLD])}. Both are read and ` +
|
|
541
|
-
`${NEW} wins, so they must agree — delete ${OLD}, or correct it`,
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
502
|
if (!found) {
|
|
545
503
|
throw new Error(
|
|
546
|
-
`Missing required ${
|
|
504
|
+
`Missing required ${KEY} for ${label} — set a number (this is a template, at that priority) or null (it is not)`,
|
|
547
505
|
);
|
|
548
506
|
}
|
|
549
507
|
const raw = found[0][found[1]];
|
|
@@ -558,7 +516,7 @@ export function resolveTemplatePriority(fm, label, { block = "sohl" } = {}) {
|
|
|
558
516
|
|
|
559
517
|
/**
|
|
560
518
|
* The value a document's `system.templatePriority` carries, from the required
|
|
561
|
-
* `templatePriority` frontmatter
|
|
519
|
+
* `templatePriority` frontmatter.
|
|
562
520
|
*
|
|
563
521
|
* A **schema field**, so the tri-state is written out in full rather than
|
|
564
522
|
* expressed by a key's presence: a number is a template at that priority, and
|
|
@@ -602,15 +560,11 @@ export function makeFilename(name, id) {
|
|
|
602
560
|
* and nothing else, and because the default a caller applies to an unset one is
|
|
603
561
|
* domain-specific — actors default differently from items, and gear differently
|
|
604
562
|
* again — so each compiler owns its default and applies it with **nullish**
|
|
605
|
-
* coalescing: `resolveImg(fm.img) ?? <default>`.
|
|
606
|
-
* a deliberate blank back into the default.
|
|
563
|
+
* coalescing: `resolveImg(fm.img) ?? <default>`.
|
|
607
564
|
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
* no art **and wants none**, so no default may replace it. Both come back
|
|
612
|
-
* distinguishable, `null` and `""` respectively, and neither is invented from
|
|
613
|
-
* the other.
|
|
565
|
+
* `null` — or an absent key, which reaches here as `undefined` — means _unset_:
|
|
566
|
+
* the note names no art and the caller's default applies. An empty string is
|
|
567
|
+
* not a pathname and is refused like any other value that names no file.
|
|
614
568
|
*
|
|
615
569
|
* **`title` does not follow this rule**, and must not be made to. On a
|
|
616
570
|
* `type: affiliation` note `title` is a declared item field with its own
|
|
@@ -633,11 +587,11 @@ export function makeFilename(name, id) {
|
|
|
633
587
|
* @param {string | null | undefined} raw - The pathname from frontmatter.
|
|
634
588
|
* @param {{assetRoot: string|null}} [config] - The resolved build configuration.
|
|
635
589
|
* Defaults to this repository's.
|
|
636
|
-
* @returns {string | null} the Foundry-relative path
|
|
637
|
-
*
|
|
638
|
-
* @throws {Error} When
|
|
639
|
-
* package, which Foundry installs nothing of,
|
|
640
|
-
* this build declares no relationship with.
|
|
590
|
+
* @returns {string | null} the Foundry-relative path, and `null` when the
|
|
591
|
+
* note names no art at all.
|
|
592
|
+
* @throws {Error} When the pathname is empty, or when no Foundry address can
|
|
593
|
+
* be derived — a `documentation` package, which Foundry installs nothing of,
|
|
594
|
+
* or a pathname naming a package this build declares no relationship with.
|
|
641
595
|
*/
|
|
642
596
|
export function resolveImg(raw, config = loadPackConfig()) {
|
|
643
597
|
const forms = resolvePathname(raw, config);
|
|
@@ -672,10 +626,8 @@ export function resolveName(fm, defaultValue = "Unnamed") {
|
|
|
672
626
|
* every v14 build and so left all shipped content permanently eligible for
|
|
673
627
|
* every v14 migration.
|
|
674
628
|
*
|
|
675
|
-
* **Read from the configuration, not from the shipped manifest.**
|
|
676
|
-
*
|
|
677
|
-
* because the configuration had no way to state the fact. Now that it does, the
|
|
678
|
-
* manifest is generated *from* the configuration — so reading it back would be
|
|
629
|
+
* **Read from the configuration, not from the shipped manifest.** The
|
|
630
|
+
* manifest is generated *from* the configuration, so reading it back would be
|
|
679
631
|
* a round trip through an artifact that need not exist yet: `build:db` can run
|
|
680
632
|
* before the manifest is written.
|
|
681
633
|
*
|
|
@@ -704,9 +656,7 @@ export function supportedCoreVersion(config = loadPackConfig()) {
|
|
|
704
656
|
/**
|
|
705
657
|
* Default `_stats` block for compiled compendium entries.
|
|
706
658
|
*
|
|
707
|
-
* Every stamped identity is configuration
|
|
708
|
-
* the same frozen `"0.6.0"` literal, and `systemId` / `lastModifiedBy` were
|
|
709
|
-
* written into this function. `coreVersion` alone is *not* configuration — it
|
|
659
|
+
* Every stamped identity is configuration. `coreVersion` alone is *not* configuration — it
|
|
710
660
|
* comes from {@link supportedCoreVersion}, the configured Foundry floor,
|
|
711
661
|
* so a document never claims to predate the migrations that would rewrite it.
|
|
712
662
|
*
|
|
@@ -771,11 +721,9 @@ let cachedDefaultStats;
|
|
|
771
721
|
/**
|
|
772
722
|
* The `_stats` block every compiler stamps on an entry it emits, built once.
|
|
773
723
|
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
* going to be compiled. Deferred to first use and memoised here, the
|
|
778
|
-
* cost and the identity are what they always were; only the moment moved.
|
|
724
|
+
* Built on first use and memoised, not at module scope: building it reads the
|
|
725
|
+
* shipped package manifest, and importing a compiler must not require a
|
|
726
|
+
* manifest to exist when nothing is going to be compiled.
|
|
779
727
|
*
|
|
780
728
|
* @returns {object} The default `_stats` block, shared by every compiler.
|
|
781
729
|
*/
|
|
@@ -802,7 +750,7 @@ export { makeId } from "./ids.mjs";
|
|
|
802
750
|
|
|
803
751
|
// The content-type → document-type map, which decides *which* pack list a
|
|
804
752
|
// note's own document is routed against.
|
|
805
|
-
import {
|
|
753
|
+
import { packForType } from "./ids.mjs";
|
|
806
754
|
import { collectAnchors } from "./anchors.mjs";
|
|
807
755
|
|
|
808
756
|
/* ------------------------------------------------------------------------ */
|
|
@@ -820,10 +768,7 @@ import { collectAnchors } from "./anchors.mjs";
|
|
|
820
768
|
* would otherwise address every one of them as the first. A note whose
|
|
821
769
|
* declaration is unroutable is indexed against the conventional name and left
|
|
822
770
|
* for the compile pass to report — the index has no business failing a build,
|
|
823
|
-
* and the pass fails it with a far better message.
|
|
824
|
-
* **retired** content type: this walk is the first to see every
|
|
825
|
-
* note together with its path, and unlike an unroutable declaration there is
|
|
826
|
-
* no pass that would ever claim such a note and report it.
|
|
771
|
+
* and the pass fails it with a far better message.
|
|
827
772
|
*
|
|
828
773
|
* @param {string} contentBase - Root of the content tree.
|
|
829
774
|
* @param {object} [router] - The pack router. Supplied by the calling pass so
|
|
@@ -861,9 +806,8 @@ export function buildContentLinkIndex(
|
|
|
861
806
|
// A documentation journal is a document this tree emits, not a note in
|
|
862
807
|
// it; the note it documents is indexed here and carries its address.
|
|
863
808
|
if (!isNoteRecord(record)) continue;
|
|
864
|
-
// The note as its author wrote it — the router
|
|
865
|
-
//
|
|
866
|
-
// handed the keys the index derived.
|
|
809
|
+
// The note as its author wrote it — the router and the draft tag read
|
|
810
|
+
// authored fields, and neither may be handed the keys the index derived.
|
|
867
811
|
const fm = authoredFrontmatter(record);
|
|
868
812
|
const absPath = noteFile(contentBase, record);
|
|
869
813
|
// The id a note's document is filed under: its authored pin, or the
|
|
@@ -876,11 +820,6 @@ export function buildContentLinkIndex(
|
|
|
876
820
|
// no shortcode — which is not an addressable note and has no document
|
|
877
821
|
// to link to.
|
|
878
822
|
if (!fm?.id) continue;
|
|
879
|
-
// The first walk of every note in the tree, and the only one holding
|
|
880
|
-
// both the declared type and the file that declares it — so a note
|
|
881
|
-
// left on a retired type is reported here, by name, rather than
|
|
882
|
-
// several frames deeper with nothing to go on.
|
|
883
|
-
assertTypeNotRetired(fm.type, absPath);
|
|
884
823
|
const base = String(record.file.name).replace(/_/g, " ");
|
|
885
824
|
const documentation = recordsByAddress.get(encodeAddresses(record.documentation));
|
|
886
825
|
const noOwnDocument =
|
package/engine/holdings.mjs
CHANGED
|
@@ -84,7 +84,7 @@ function referenceKey(value, node, type) {
|
|
|
84
84
|
type,
|
|
85
85
|
types: new Set([type]),
|
|
86
86
|
},
|
|
87
|
-
{ declared: true
|
|
87
|
+
{ declared: true },
|
|
88
88
|
);
|
|
89
89
|
if (tuple.reason || tuple.type !== type) return "";
|
|
90
90
|
return renderAddress({ ...tuple, system: "note" }).toLowerCase();
|
package/engine/homepage.mjs
CHANGED
|
@@ -138,10 +138,6 @@ export function isHomepage(fm) {
|
|
|
138
138
|
* closed vocabulary, so repeating that judgement per type would be a second
|
|
139
139
|
* answer to one question.
|
|
140
140
|
*
|
|
141
|
-
* `aliases` is deliberately not in the class: it is a **retired** field, refused
|
|
142
|
-
* on every note whatever its type, so it is answered there rather than
|
|
143
|
-
* here.
|
|
144
|
-
*
|
|
145
141
|
* @type {ReadonlyMap<string, string>}
|
|
146
142
|
*/
|
|
147
143
|
export const HOMEPAGE_REFUSED_FIELDS = Object.freeze(
|
|
@@ -184,8 +180,7 @@ export const HOMEPAGE_REFUSED_FIELDS = Object.freeze(
|
|
|
184
180
|
*
|
|
185
181
|
* Each finding carries the `locator` key to position it at, because the two
|
|
186
182
|
* things that would resolve one — the raw note text and the position helper —
|
|
187
|
-
* belong to the caller.
|
|
188
|
-
* whose retired-field messages are likewise positioned by whoever reports them.
|
|
183
|
+
* belong to the caller.
|
|
189
184
|
*
|
|
190
185
|
* **A refused field must be one the note *wrote*.** `resolveNoteId` fills
|
|
191
186
|
* `fm.id` **in place** so every downstream reader sees one derived value —
|
|
@@ -364,8 +359,7 @@ export function homepageTitle(fm, config) {
|
|
|
364
359
|
*
|
|
365
360
|
* The note's own, plus the two derived values every emitted page carries: the
|
|
366
361
|
* resolved `title`, and the package the build derived — no note declares one
|
|
367
|
-
*
|
|
368
|
-
* `.Params.package`.
|
|
362
|
+
* and the theme's breadcrumb partial reads `.Params.package`.
|
|
369
363
|
*
|
|
370
364
|
* **No `url` and no `slug`.** Hugo publishes the `home` kind at `baseURL`,
|
|
371
365
|
* whose path is already where the package is served, so the page has no
|
|
@@ -374,8 +368,7 @@ export function homepageTitle(fm, config) {
|
|
|
374
368
|
*
|
|
375
369
|
* An authored `aliases` is dropped for the same reason it is on every other
|
|
376
370
|
* page: Hugo reads it as URL redirects, so passing it through would publish a
|
|
377
|
-
* redirect stub at each one.
|
|
378
|
-
* build reaches here, which makes this a guard rather than a working path.
|
|
371
|
+
* redirect stub at each one.
|
|
379
372
|
*
|
|
380
373
|
* `related` is derived like every other page's — what links to the homepage
|
|
381
374
|
* is a fact about the rest of the tree — so an authored value is dropped and
|