@heroiclands/package-build 9.0.0 → 10.0.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 +694 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +437 -7
- package/content-config.mjs +259 -28
- package/docs/content-format.md +1418 -0
- package/engine/address-charset.mjs +62 -0
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +194 -4
- package/engine/content-address.mjs +4 -4
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +132 -56
- package/engine/content-lint.mjs +8 -1
- package/engine/diagnostics.mjs +33 -0
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-spec.mjs +49 -43
- package/engine/frontmatter-lint.mjs +343 -27
- package/engine/generate.mjs +32 -4
- package/engine/helpers.mjs +41 -29
- package/engine/ids.mjs +19 -1
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -5
- package/engine/kb-manifest.mjs +36 -7
- package/engine/map-notes.mjs +34 -18
- package/engine/note-claims.mjs +383 -0
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +39 -22
- package/engine/pack-router.mjs +17 -6
- package/engine/prose-lint.mjs +55 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scenes.mjs +19 -1
- package/engine/schema-check.mjs +347 -3
- package/engine/site-build.mjs +1 -1
- package/engine/site-index.mjs +38 -21
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +112 -80
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +67 -51
- package/package.json +6 -2
- package/sohl/actors.mjs +249 -36
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +3 -0
- package/sohl/items.mjs +110 -14
- package/sohl/note-schemas.mjs +11 -7
- package/types/content-config.d.mts +48 -4
- package/types/engine/address-charset.d.mts +45 -0
- package/types/engine/alias-index.d.mts +122 -0
- package/types/engine/base-compiler.d.mts +132 -4
- package/types/engine/content-address.d.mts +2 -2
- package/types/engine/content-format-check.d.mts +163 -0
- package/types/engine/content-format.d.mts +101 -0
- package/types/engine/content-links.d.mts +16 -1
- package/types/engine/content-lint.d.mts +6 -0
- package/types/engine/diagnostics.d.mts +29 -0
- package/types/engine/document-subtypes.d.mts +233 -0
- package/types/engine/field-spec.d.mts +76 -23
- package/types/engine/frontmatter-lint.d.mts +47 -2
- package/types/engine/generate.d.mts +14 -1
- package/types/engine/helpers.d.mts +21 -13
- package/types/engine/ids.d.mts +10 -0
- package/types/engine/index.d.mts +5 -0
- package/types/engine/item-registry.d.mts +21 -2
- package/types/engine/kb-manifest.d.mts +35 -8
- package/types/engine/map-notes.d.mts +21 -11
- package/types/engine/note-claims.d.mts +113 -0
- package/types/engine/note-vocabulary.d.mts +251 -0
- package/types/engine/pack-config.d.mts +4 -3
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/prose-lint.d.mts +6 -2
- package/types/engine/retired-fields.d.mts +73 -2
- package/types/engine/schema-check.d.mts +182 -0
- package/types/engine/system-block.d.mts +281 -0
- package/types/engine/web-wikilinks.d.mts +23 -12
- package/types/engine/wikilink-syntax.d.mts +29 -0
- package/types/sohl/actors.d.mts +62 -6
- package/types/sohl/document-subtypes.d.mts +14 -0
- package/types/sohl/index.d.mts +1 -0
- package/types/sohl/items.d.mts +21 -0
package/engine/pack-config.mjs
CHANGED
|
@@ -431,9 +431,10 @@ export function locateConfigError(err, configPath) {
|
|
|
431
431
|
* - **`rootDir`** is the configuration's own directory, always. A data file
|
|
432
432
|
* cannot write `import.meta.dirname`, and any absolute path it wrote instead
|
|
433
433
|
* would be one machine's — so authoring it is rejected rather than honoured.
|
|
434
|
-
* - **`itemBuilders`** is a *name* (`sohl`),
|
|
435
|
-
*
|
|
436
|
-
*
|
|
434
|
+
* - **`itemBuilders`** is a *name* (`sohl`) — or a list of names, for a tree
|
|
435
|
+
* feeding more than one system (#58) — resolved against the built-in
|
|
436
|
+
* registries. A registry's name is the system it belongs to. A registry of a
|
|
437
|
+
* consumer's own is code, and code goes in an `.mjs` configuration.
|
|
437
438
|
* - **`stats.systemVersion`** is derived from the adjacent `package.json` when
|
|
438
439
|
* the configuration does not state it. Stating it is still allowed: a
|
|
439
440
|
* repository shipping content *for* another package (a module declaring
|
|
@@ -482,26 +483,42 @@ export function configFromData(data, configPath) {
|
|
|
482
483
|
input.foundryPackage = foundryPackageId(rootDir);
|
|
483
484
|
|
|
484
485
|
if (input.itemBuilders !== undefined) {
|
|
485
|
-
const
|
|
486
|
+
const declared = input.itemBuilders;
|
|
486
487
|
const known = Object.keys(ITEM_BUILDER_REGISTRIES).join(", ");
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
488
|
+
// One name or several. A repository feeding two systems needs both
|
|
489
|
+
// vocabularies, and one registry can only carry one (#58); the scalar
|
|
490
|
+
// form every existing configuration uses is the one-element case and
|
|
491
|
+
// means exactly what it always did.
|
|
492
|
+
const names = Array.isArray(declared) ? declared : [declared];
|
|
493
|
+
const resolved = names.map((named, index) => {
|
|
494
|
+
const where = Array.isArray(declared) ? `itemBuilders[${index}]` : "itemBuilders";
|
|
495
|
+
if (typeof named !== "string") {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`package-build: ${configPath} must name its \`${where}\` ` +
|
|
498
|
+
`registry as a string — the registry is code, and data ` +
|
|
499
|
+
`cannot carry it. Known registries: ${known}; a registry ` +
|
|
500
|
+
`of your own goes in ${CONFIG_BASENAME}.mjs.`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
const load = ITEM_BUILDER_REGISTRIES[named];
|
|
504
|
+
if (!load) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`package-build: ${configPath} names the \`${where}\` ` +
|
|
507
|
+
`registry "${named}", which this package does not ship. ` +
|
|
508
|
+
`Known registries: ${known}. To supply your own, declare ` +
|
|
509
|
+
`it in ${CONFIG_BASENAME}.mjs.`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
// A shipped registry's *name* is the system it belongs to, which is
|
|
513
|
+
// what lets a data configuration declare a set without carrying the
|
|
514
|
+
// system id a second time.
|
|
515
|
+
return { system: named, builders: load() };
|
|
516
|
+
});
|
|
517
|
+
// The scalar form stays the flat, system-less registry it has always
|
|
518
|
+
// resolved to. Wrapping it as a one-entry set would be tidier and would
|
|
519
|
+
// change what every existing configuration means — `itemBuildersBySystem`
|
|
520
|
+
// would gain an entry the consumer never declared.
|
|
521
|
+
input.itemBuilders = Array.isArray(declared) ? resolved : resolved[0].builders;
|
|
505
522
|
}
|
|
506
523
|
|
|
507
524
|
const stats = input.stats;
|
package/engine/pack-router.mjs
CHANGED
|
@@ -47,12 +47,18 @@
|
|
|
47
47
|
* - A note's **derived** documents are routed by the default of *their* type,
|
|
48
48
|
* not by the note's declaration: an item note's prose compiles into a
|
|
49
49
|
* JournalEntry, and `pack:` names where the *item* goes.
|
|
50
|
+
* - A note feeding **more than one system** declares `pack:` inside the block
|
|
51
|
+
* that differs (#58). `pack` needed no new mechanism for that: it is an
|
|
52
|
+
* ordinary shared property, so `<system>.pack` overrides the top-level one
|
|
53
|
+
* for that system's document and leaves it standing for every other. A note
|
|
54
|
+
* wanting one pack for both says it once at the top.
|
|
50
55
|
*
|
|
51
56
|
* @module
|
|
52
57
|
*/
|
|
53
58
|
|
|
54
59
|
import { packForType } from "./ids.mjs";
|
|
55
60
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
61
|
+
import { blockProperty } from "./system-block.mjs";
|
|
56
62
|
|
|
57
63
|
/**
|
|
58
64
|
* A note that cannot be routed to a pack. Thrown rather than returned so no
|
|
@@ -84,8 +90,8 @@ export const PACK_FIELD = "pack";
|
|
|
84
90
|
*
|
|
85
91
|
* @param {readonly object[]} packs - The resolved `packs` list from
|
|
86
92
|
* `defineConfig`.
|
|
87
|
-
* @returns {{resolve: (fm: object, docType: string) => string,
|
|
88
|
-
* resolveOrNull: (fm: object, docType: string) => string|undefined,
|
|
93
|
+
* @returns {{resolve: (fm: object, docType: string, system?: string) => string,
|
|
94
|
+
* resolveOrNull: (fm: object, docType: string, system?: string) => string|undefined,
|
|
89
95
|
* packsOfType: (docType: string) => string[],
|
|
90
96
|
* defaultOf: (docType: string) => string|undefined}} The router.
|
|
91
97
|
*/
|
|
@@ -124,11 +130,15 @@ export function createPackRouter(packs) {
|
|
|
124
130
|
*
|
|
125
131
|
* @param {object} fm - The note's frontmatter.
|
|
126
132
|
* @param {string} docType - The document type the calling pass writes.
|
|
133
|
+
* @param {string} [system] - The system whose document is being routed. Its
|
|
134
|
+
* block's `pack:` wins over the shared one; without it only the shared
|
|
135
|
+
* declaration is read, which is every single-system build.
|
|
127
136
|
* @returns {string} The pack name.
|
|
128
137
|
* @throws {PackRoutingError} When the note routes nowhere.
|
|
129
138
|
*/
|
|
130
|
-
function resolve(fm, docType) {
|
|
131
|
-
const declared =
|
|
139
|
+
function resolve(fm, docType, system) {
|
|
140
|
+
const declared =
|
|
141
|
+
system === undefined ? fm?.[PACK_FIELD] : blockProperty(fm, system, PACK_FIELD);
|
|
132
142
|
// The declaration names where the note's *own* document goes. A pass
|
|
133
143
|
// writing a document derived from it — an item's prose becoming a
|
|
134
144
|
// JournalEntry — is not what the author was addressing.
|
|
@@ -189,11 +199,12 @@ export function createPackRouter(packs) {
|
|
|
189
199
|
*
|
|
190
200
|
* @param {object} fm - The note's frontmatter.
|
|
191
201
|
* @param {string} docType - The document type being addressed.
|
|
202
|
+
* @param {string} [system] - The system whose document is addressed.
|
|
192
203
|
* @returns {string|undefined} The pack name, or `undefined`.
|
|
193
204
|
*/
|
|
194
|
-
resolveOrNull(fm, docType) {
|
|
205
|
+
resolveOrNull(fm, docType, system) {
|
|
195
206
|
try {
|
|
196
|
-
return resolve(fm, docType);
|
|
207
|
+
return resolve(fm, docType, system);
|
|
197
208
|
} catch {
|
|
198
209
|
return undefined;
|
|
199
210
|
}
|
package/engine/prose-lint.mjs
CHANGED
|
@@ -61,6 +61,23 @@ const NEVER_WALK = Object.freeze(new Set([".git", "node_modules"]));
|
|
|
61
61
|
*/
|
|
62
62
|
const IGNORE_FILES = Object.freeze([".gitignore", ".prettierignore"]);
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* How many times `--write` will format one file looking for a fixpoint.
|
|
66
|
+
*
|
|
67
|
+
* `format` is *assumed* idempotent and is not guaranteed to be: a single pass
|
|
68
|
+
* can leave text the next pass would still change, and a `--write` run that
|
|
69
|
+
* takes one pass then reports success has called such a file formatted while
|
|
70
|
+
* `prettier --check` still rejects it (#125). Formatting to a fixpoint removes
|
|
71
|
+
* the assumption — the file lands on the value repeated formatting converges
|
|
72
|
+
* to, whatever it took to get there.
|
|
73
|
+
*
|
|
74
|
+
* Three, not "until it stops": a file that oscillates would loop forever, and
|
|
75
|
+
* the cap turns that into a report. Three is enough for the case this is for
|
|
76
|
+
* (one pass short) with a pass to spare, and costs nothing on a tree that is
|
|
77
|
+
* already formatted, where the first pass converges immediately.
|
|
78
|
+
*/
|
|
79
|
+
const MAX_FORMAT_PASSES = 3;
|
|
80
|
+
|
|
64
81
|
/**
|
|
65
82
|
* Every file under a root, minus the directories nothing should walk.
|
|
66
83
|
*
|
|
@@ -106,11 +123,15 @@ function walkFiles(root) {
|
|
|
106
123
|
* @param {readonly string[]} [opts.paths] - Files or directories to check
|
|
107
124
|
* instead of the whole root.
|
|
108
125
|
* @param {boolean} [opts.write=false] - Rewrite unformatted files in place
|
|
109
|
-
* rather than reporting them.
|
|
126
|
+
* rather than reporting them. Each file is formatted to a fixpoint (up to
|
|
127
|
+
* {@link MAX_FORMAT_PASSES} passes), so a written tree is one a second run
|
|
128
|
+
* leaves alone; a file that will not converge is reported and left unchanged
|
|
129
|
+
* (#125).
|
|
110
130
|
* @param {object} [opts.prettier] - The Prettier module, for tests.
|
|
111
131
|
* @returns {Promise<{findings: Array<{file: string, severity: string,
|
|
112
132
|
* message: string}>, checked: number, written: string[]}>} The findings, how
|
|
113
|
-
* many files were considered, and what was rewritten.
|
|
133
|
+
* many files were considered, and what was rewritten. `--write` reports
|
|
134
|
+
* findings too — a file it cannot parse, or cannot format to a fixpoint.
|
|
114
135
|
*/
|
|
115
136
|
export async function checkFormatting(root, opts = {}) {
|
|
116
137
|
const { paths, write = false } = opts;
|
|
@@ -155,7 +176,38 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
155
176
|
// every other one.
|
|
156
177
|
try {
|
|
157
178
|
if (write) {
|
|
158
|
-
|
|
179
|
+
// Format to a fixpoint rather than once, so what lands on disk
|
|
180
|
+
// is what a second run would have produced (#125).
|
|
181
|
+
let formatted = source;
|
|
182
|
+
let converged = false;
|
|
183
|
+
for (let pass = 0; pass < MAX_FORMAT_PASSES; pass += 1) {
|
|
184
|
+
const next = await prettier.format(formatted, options);
|
|
185
|
+
if (next === formatted) {
|
|
186
|
+
converged = true;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
formatted = next;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!converged) {
|
|
193
|
+
// Nothing is written. A formatting the command cannot
|
|
194
|
+
// reproduce is not one to commit to disk — writing it
|
|
195
|
+
// would make `--write` churn the file on every run — and a
|
|
196
|
+
// file that never settles is a defect somewhere that has
|
|
197
|
+
// to be named rather than absorbed.
|
|
198
|
+
findings.push({
|
|
199
|
+
file,
|
|
200
|
+
severity: "error",
|
|
201
|
+
// No line or column: the verdict is about the whole
|
|
202
|
+
// file, and #17's rule is to drop a field rather than
|
|
203
|
+
// invent one.
|
|
204
|
+
message:
|
|
205
|
+
`did not converge after ${MAX_FORMAT_PASSES} formatting passes; ` +
|
|
206
|
+
"left unchanged",
|
|
207
|
+
});
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
159
211
|
if (formatted !== source) {
|
|
160
212
|
fs.writeFileSync(file, formatted);
|
|
161
213
|
written.push(file);
|
|
@@ -34,12 +34,24 @@
|
|
|
34
34
|
* *unresolvable*, silently — and it also suppressed real build failures, since
|
|
35
35
|
* a note the compilers never reached could not fail on the defects it carried.
|
|
36
36
|
*
|
|
37
|
+
* **A field retired in favour of another is a third case (#142).** `draft:` and
|
|
38
|
+
* `package:` were retired outright: nothing replaced them, so no value made
|
|
39
|
+
* writing one right and refusal was the only honest answer. A *renamed* field
|
|
40
|
+
* has a replacement, and the two spellings mean the same thing — so the note
|
|
41
|
+
* still compiles, correctly, and refusing it would fail a build over a document
|
|
42
|
+
* that is not wrong. Those retire in the three steps `package:` took (#56), and
|
|
43
|
+
* this module carries the **first**: both spellings are read, the current one
|
|
44
|
+
* wins, and the retired one is *reported* rather than refused. The sweep and
|
|
45
|
+
* the refusal come later, once no tree writes it. See
|
|
46
|
+
* {@link RETIRED_FIELD_ALIASES}.
|
|
47
|
+
*
|
|
37
48
|
* @module
|
|
38
49
|
*/
|
|
39
50
|
|
|
40
51
|
import fs from "node:fs";
|
|
41
52
|
|
|
42
53
|
import { positionInFrontmatter } from "./diagnostics.mjs";
|
|
54
|
+
import { sohlField } from "./frontmatter.mjs";
|
|
43
55
|
|
|
44
56
|
/**
|
|
45
57
|
* What a note declaring `draft:` is told, in one place.
|
|
@@ -105,12 +117,15 @@ export function assertNoDraftField(fm, { file, absPath } = {}) {
|
|
|
105
117
|
* both refusals need it and a second copy is a second thing to keep correct.
|
|
106
118
|
*
|
|
107
119
|
* @param {string|undefined} absPath - The note's file.
|
|
108
|
-
* @param {string} key - The
|
|
120
|
+
* @param {string} key - The frontmatter key.
|
|
121
|
+
* @param {string} [value] - When given, prefer the occurrence whose line also
|
|
122
|
+
* carries this text — so a finding about one entry of a block opens on that
|
|
123
|
+
* entry rather than on the key that introduces it.
|
|
109
124
|
* @returns {{line?: number, column?: number}|undefined} Spreadable position
|
|
110
125
|
* fields, dropped rather than guessed when the file cannot be read or the key
|
|
111
126
|
* cannot be found — as `formatDiagnostic` requires.
|
|
112
127
|
*/
|
|
113
|
-
export function locateFrontmatterKey(absPath, key) {
|
|
128
|
+
export function locateFrontmatterKey(absPath, key, value = undefined) {
|
|
114
129
|
if (!absPath) return undefined;
|
|
115
130
|
let raw;
|
|
116
131
|
try {
|
|
@@ -118,6 +133,105 @@ export function locateFrontmatterKey(absPath, key) {
|
|
|
118
133
|
} catch {
|
|
119
134
|
return undefined;
|
|
120
135
|
}
|
|
121
|
-
const at = positionInFrontmatter(raw, key);
|
|
136
|
+
const at = positionInFrontmatter(raw, key, value);
|
|
122
137
|
return at.line === undefined ? undefined : at;
|
|
123
138
|
}
|
|
139
|
+
|
|
140
|
+
/* -------------------------------------------------------------------- */
|
|
141
|
+
/* Retired *in favour of another field* */
|
|
142
|
+
/* -------------------------------------------------------------------- */
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The current field name a retired spelling was renamed to.
|
|
146
|
+
*
|
|
147
|
+
* Keyed by the **current** name, because that is what a type's schema declares
|
|
148
|
+
* and what every reader asks for; the value is the spelling still honoured.
|
|
149
|
+
* The table is therefore scoped by the schema without saying so twice: an alias
|
|
150
|
+
* applies to a note only where that note's type declares the current field, so
|
|
151
|
+
* `image` is retired on a map — which declares `img` — and remains an unknown
|
|
152
|
+
* key anywhere else.
|
|
153
|
+
*
|
|
154
|
+
* **`img` (#142).** Every note type names its artwork `img`, at the note's top
|
|
155
|
+
* level, and resolves it the same way. A map alone named its background art
|
|
156
|
+
* `image` and read it out of the `sohl:` block — two spellings for one idea,
|
|
157
|
+
* with nothing to reconcile them, and a specification that had to hedge rather
|
|
158
|
+
* than state a rule. Art is not system-specific: a Scene is a core Foundry
|
|
159
|
+
* document and HM3 would want the identical one, so the field belongs beside
|
|
160
|
+
* every other note's `img`, not inside a system block.
|
|
161
|
+
*
|
|
162
|
+
* @type {Readonly<Record<string, string>>}
|
|
163
|
+
*/
|
|
164
|
+
export const RETIRED_FIELD_ALIASES = Object.freeze({ img: "image" });
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* What a note writing a renamed field is told, in one place.
|
|
168
|
+
*
|
|
169
|
+
* Shared by the compile-time report and the frontmatter lint, because an author
|
|
170
|
+
* meets whichever runs first and they should read the same. It names the key to
|
|
171
|
+
* write rather than a value to correct — no value makes the retired spelling
|
|
172
|
+
* right — and it says the note compiles either way, so a reader knows this is a
|
|
173
|
+
* rename to schedule rather than a build to unbreak.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} retired - The spelling the note used.
|
|
176
|
+
* @param {string} current - What to write instead.
|
|
177
|
+
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
178
|
+
* the caller emits through a diagnostic, whose locator already starts the
|
|
179
|
+
* line — repeating it prints the path twice.
|
|
180
|
+
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
181
|
+
*/
|
|
182
|
+
export function retiredAliasMessage(retired, current, file) {
|
|
183
|
+
return (
|
|
184
|
+
`\`${retired}:\` is a retired frontmatter field — write \`${current}:\` ` +
|
|
185
|
+
`instead` +
|
|
186
|
+
(file ? ` — ${file}` : "") +
|
|
187
|
+
`. Both are read and \`${current}\` wins, so the note compiles ` +
|
|
188
|
+
`identically either way; \`${retired}\` is removed in a later release`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Whether a note writes the retired spelling of a field, wherever it put it.
|
|
194
|
+
*
|
|
195
|
+
* Both regions are searched, because {@link sohlField} reads both: a note that
|
|
196
|
+
* moved the key to the top level without renaming it has done half the
|
|
197
|
+
* migration, and should be told so rather than passing in silence.
|
|
198
|
+
*
|
|
199
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
200
|
+
* @param {string} current - The field's current name.
|
|
201
|
+
* @returns {boolean} Whether the retired spelling is declared.
|
|
202
|
+
*/
|
|
203
|
+
export function declaresRetiredAlias(fm, current) {
|
|
204
|
+
const retired = RETIRED_FIELD_ALIASES[current];
|
|
205
|
+
if (!retired || !fm || typeof fm !== "object") return false;
|
|
206
|
+
const block = fm.sohl;
|
|
207
|
+
const inBlock =
|
|
208
|
+
block &&
|
|
209
|
+
typeof block === "object" &&
|
|
210
|
+
!Array.isArray(block) &&
|
|
211
|
+
Object.hasOwn(block, retired);
|
|
212
|
+
return Boolean(inBlock) || Object.hasOwn(fm, retired);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Read a field that has a retired spelling, the current name winning.
|
|
217
|
+
*
|
|
218
|
+
* This is the whole of the retirement window's behaviour, in one function, so
|
|
219
|
+
* the compiler and the linter cannot disagree about which value a note carries.
|
|
220
|
+
* Resolution within each spelling is {@link sohlField}'s — the `sohl:` block
|
|
221
|
+
* first, then the note's top level — so a renamed field keeps working wherever
|
|
222
|
+
* it was already written while the canonical home is the top level.
|
|
223
|
+
*
|
|
224
|
+
* A blank value counts as absent: `img:` cleared in an editor means the note
|
|
225
|
+
* names no art there, and falling through to the retired spelling is what an
|
|
226
|
+
* author part-way through the rename means by it.
|
|
227
|
+
*
|
|
228
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
229
|
+
* @param {string} current - The field's current name.
|
|
230
|
+
* @returns {any} The value, or `undefined` when neither spelling carries one.
|
|
231
|
+
*/
|
|
232
|
+
export function readAliasedField(fm, current) {
|
|
233
|
+
const value = sohlField(fm, current, undefined);
|
|
234
|
+
if (value !== undefined && value !== null && value !== "") return value;
|
|
235
|
+
const retired = RETIRED_FIELD_ALIASES[current];
|
|
236
|
+
return retired ? sohlField(fm, retired, undefined) : undefined;
|
|
237
|
+
}
|
package/engine/scenes.mjs
CHANGED
|
@@ -59,6 +59,13 @@ import { packRouter } from "./pack-router.mjs";
|
|
|
59
59
|
import { foundryPackageId } from "./content-package.mjs";
|
|
60
60
|
import { itemDocEntryId } from "./item-docs.mjs";
|
|
61
61
|
import { behaviorDocId, buildScene, isMapType, regionDocId } from "./map-notes.mjs";
|
|
62
|
+
import {
|
|
63
|
+
RETIRED_FIELD_ALIASES,
|
|
64
|
+
declaresRetiredAlias,
|
|
65
|
+
locateFrontmatterKey,
|
|
66
|
+
readAliasedField,
|
|
67
|
+
retiredAliasMessage,
|
|
68
|
+
} from "./retired-fields.mjs";
|
|
62
69
|
|
|
63
70
|
/**
|
|
64
71
|
* Every SoHL action name this build knows about, for the `action:` warning on a
|
|
@@ -378,6 +385,17 @@ export class Scenes extends BasePackCompiler {
|
|
|
378
385
|
// pass has to read the other's output.
|
|
379
386
|
const entryId = hasBody ? itemDocEntryId(fm.id) : undefined;
|
|
380
387
|
const folder = this.folderResolver(sohlField(fm, "folder", null));
|
|
388
|
+
// The retired spelling of the background art, reported where an author
|
|
389
|
+
// meets it soonest — every consumer runs the compile, and not every
|
|
390
|
+
// one runs the lint (#142). Located by reading the note back, which is
|
|
391
|
+
// what the other retired-field reports do: this is the one path that
|
|
392
|
+
// needs the position, so it is paid for only here.
|
|
393
|
+
if (declaresRetiredAlias(fm, "img")) {
|
|
394
|
+
this.noteWarn(
|
|
395
|
+
retiredAliasMessage(RETIRED_FIELD_ALIASES.img, "img"),
|
|
396
|
+
locateFrontmatterKey(this.currentNote?.absPath, RETIRED_FIELD_ALIASES.img),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
381
399
|
const warnings = [];
|
|
382
400
|
const scene = buildScene(fm, {
|
|
383
401
|
packageId: foundryPackageId(),
|
|
@@ -423,7 +441,7 @@ export class Scenes extends BasePackCompiler {
|
|
|
423
441
|
this.places.set(placeKey, {
|
|
424
442
|
key: placeKey,
|
|
425
443
|
name: sohlField(fm, "placeName", null) || name,
|
|
426
|
-
img:
|
|
444
|
+
img: readAliasedField(fm, "img") ?? null,
|
|
427
445
|
pinned: false,
|
|
428
446
|
scenes: [],
|
|
429
447
|
journal: [],
|