@heroiclands/package-build 17.1.0 → 18.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 +1773 -0
- package/CONTENT.md +288 -72
- package/README.md +1 -1
- package/bin/content-build.mjs +218 -137
- package/bin/package-build.mjs +43 -0
- package/content-config.mjs +62 -67
- package/docs/content-format.md +768 -58
- package/engine/actor-compiler.mjs +586 -0
- package/engine/address-charset.mjs +11 -8
- package/engine/address-diff.mjs +266 -22
- package/engine/anchored-sections.mjs +83 -0
- package/engine/anchors.mjs +83 -0
- package/engine/base-compiler.mjs +148 -18
- package/engine/bundle-notes.mjs +276 -0
- package/engine/bundles.mjs +307 -0
- package/engine/code-fences.mjs +103 -0
- package/engine/compile-corpus.mjs +89 -0
- package/engine/content-address.mjs +267 -5
- package/engine/content-format-check.mjs +54 -11
- package/engine/content-format.mjs +37 -5
- package/engine/content-index.mjs +394 -111
- package/engine/content-links.mjs +185 -77
- package/engine/content-lint.mjs +232 -14
- package/engine/content-package.mjs +2 -1
- package/engine/content-tables.mjs +99 -3
- package/engine/document-subtypes.mjs +49 -6
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +56 -6
- package/engine/folder-notes.mjs +470 -0
- package/engine/foreign-catalog.mjs +195 -4
- package/engine/{manifest-emit.mjs → foundry-entries.mjs} +78 -81
- package/engine/frontmatter-lint.mjs +245 -19
- package/engine/frontmatter.mjs +88 -8
- package/engine/generate.mjs +304 -13
- package/engine/helpers.mjs +293 -64
- package/engine/ids.mjs +137 -2
- package/engine/index-records.mjs +126 -0
- package/engine/index.mjs +23 -5
- package/engine/item-compiler.mjs +349 -0
- package/engine/item-docs.mjs +7 -1
- package/engine/item-registry.mjs +6 -0
- package/engine/journals.mjs +93 -27
- package/engine/macros.mjs +4 -2
- package/engine/metadata-index.mjs +495 -0
- package/engine/note-claims.mjs +146 -25
- package/engine/note-ids.mjs +112 -0
- package/engine/note-renames.mjs +134 -0
- package/engine/note-schemas.mjs +25 -0
- package/engine/note-vocabulary.mjs +110 -27
- package/engine/pack-config.mjs +4 -0
- package/engine/pack-router.mjs +23 -0
- package/engine/retired-fields.mjs +137 -3
- package/engine/scenes.mjs +66 -34
- package/engine/schema-check.mjs +58 -17
- package/engine/site-build.mjs +71 -12
- package/engine/site-index.mjs +20 -7
- package/engine/sql-tables.mjs +485 -0
- package/engine/subtype-registry.mjs +102 -0
- package/engine/system-block.mjs +96 -12
- package/engine/systems.mjs +201 -0
- package/engine/web-wikilinks.mjs +50 -20
- package/engine/wikilink-syntax.mjs +17 -8
- package/engine/wikilinks.mjs +124 -51
- package/engine/yaml-lint.mjs +282 -0
- package/hm3/actors.mjs +352 -0
- package/hm3/default-item-art.mjs +75 -0
- package/hm3/document-subtypes.mjs +134 -0
- package/hm3/index.mjs +56 -0
- package/hm3/item-builders.mjs +84 -0
- package/hm3/item-fields.mjs +180 -0
- package/hm3/items.mjs +96 -0
- package/hm3/template-priority.mjs +85 -0
- package/labels.mjs +191 -0
- package/manifest.mjs +43 -2
- package/package.json +19 -4
- package/release.mjs +62 -7
- package/sohl/actors.mjs +33 -487
- package/sohl/being-info.mjs +16 -7
- package/sohl/default-item-art.mjs +14 -3
- package/sohl/document-subtypes.mjs +16 -10
- package/sohl/item-builders.mjs +14 -5
- package/sohl/item-fields.mjs +68 -7
- package/sohl/items.mjs +44 -258
- package/sohl/note-schemas.mjs +2 -2
- package/types/content-config.d.mts +21 -42
- package/types/engine/actor-compiler.d.mts +204 -0
- package/types/engine/address-charset.d.mts +11 -8
- package/types/engine/address-diff.d.mts +53 -5
- package/types/engine/anchored-sections.d.mts +21 -0
- package/types/engine/anchors.d.mts +20 -0
- package/types/engine/base-compiler.d.mts +17 -17
- package/types/engine/bundle-notes.d.mts +173 -0
- package/types/engine/bundles.d.mts +60 -0
- package/types/engine/code-fences.d.mts +43 -0
- package/types/engine/compile-corpus.d.mts +32 -0
- package/types/engine/content-address.d.mts +205 -5
- package/types/engine/content-format-check.d.mts +6 -2
- package/types/engine/content-format.d.mts +57 -1
- package/types/engine/content-index.d.mts +64 -80
- package/types/engine/content-links.d.mts +52 -7
- package/types/engine/content-lint.d.mts +10 -1
- package/types/engine/content-package.d.mts +2 -1
- package/types/engine/content-tables.d.mts +21 -39
- package/types/engine/document-subtypes.d.mts +37 -3
- package/types/engine/field-spec.d.mts +76 -5
- package/types/engine/folder-notes.d.mts +159 -0
- package/types/engine/foreign-catalog.d.mts +53 -0
- package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
- package/types/engine/frontmatter-lint.d.mts +10 -2
- package/types/engine/frontmatter.d.mts +64 -0
- package/types/engine/generate.d.mts +38 -0
- package/types/engine/helpers.d.mts +94 -30
- package/types/engine/ids.d.mts +96 -0
- package/types/engine/index-records.d.mts +68 -0
- package/types/engine/index.d.mts +9 -3
- package/types/engine/item-compiler.d.mts +131 -0
- package/types/engine/journals.d.mts +47 -9
- package/types/engine/metadata-index.d.mts +226 -0
- package/types/engine/note-claims.d.mts +57 -11
- package/types/engine/note-ids.d.mts +38 -0
- package/types/engine/note-renames.d.mts +102 -0
- package/types/engine/note-vocabulary.d.mts +44 -8
- package/types/engine/retired-fields.d.mts +75 -0
- package/types/engine/scenes.d.mts +3 -2
- package/types/engine/schema-check.d.mts +25 -4
- package/types/engine/site-build.d.mts +4 -4
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +185 -0
- package/types/engine/subtype-registry.d.mts +49 -0
- package/types/engine/system-block.d.mts +40 -1
- package/types/engine/systems.d.mts +106 -0
- package/types/engine/web-wikilinks.d.mts +4 -2
- package/types/engine/wikilink-syntax.d.mts +10 -3
- package/types/engine/wikilinks.d.mts +41 -13
- package/types/engine/yaml-lint.d.mts +107 -0
- package/types/hm3/actors.d.mts +48 -0
- package/types/hm3/default-item-art.d.mts +42 -0
- package/types/hm3/document-subtypes.d.mts +24 -0
- package/types/hm3/index.d.mts +7 -0
- package/types/hm3/item-builders.d.mts +11 -0
- package/types/hm3/item-fields.d.mts +12 -0
- package/types/hm3/items.d.mts +23 -0
- package/types/hm3/template-priority.d.mts +21 -0
- package/types/manifest.d.mts +22 -1
- package/types/release.d.mts +9 -4
- package/types/sohl/actors.d.mts +7 -74
- package/types/sohl/being-info.d.mts +8 -27
- package/types/sohl/default-item-art.d.mts +5 -3
- package/types/sohl/items.d.mts +17 -32
- package/engine/foreign-manifests.mjs +0 -126
- package/engine/kb-manifest.mjs +0 -490
- package/types/engine/foreign-manifests.d.mts +0 -43
- package/types/engine/kb-manifest.d.mts +0 -241
package/engine/helpers.mjs
CHANGED
|
@@ -37,7 +37,13 @@ import { loadPackConfig } from "./pack-config.mjs";
|
|
|
37
37
|
import { packRouter } from "./pack-router.mjs";
|
|
38
38
|
import { contentPackage, foundryPackageId } from "./content-package.mjs";
|
|
39
39
|
import { searchableFrontmatter } from "./note-package.mjs";
|
|
40
|
-
import {
|
|
40
|
+
import { PACKAGE_BASE } from "./content-address.mjs";
|
|
41
|
+
import { resolveNoteId } from "./note-ids.mjs";
|
|
42
|
+
import { loadForeignIndexes } from "./metadata-index.mjs";
|
|
43
|
+
// The record accessors only — deriving records reaches the pack router and the
|
|
44
|
+
// manifest emitter, which reach the compilers, which load this module. Reading
|
|
45
|
+
// a record needs none of that (#243).
|
|
46
|
+
import { authoredFrontmatter, isNoteRecord, noteFile } from "./index-records.mjs";
|
|
41
47
|
import { buildWikilinkIndex, convertWikilinks } from "./wikilinks.mjs";
|
|
42
48
|
// One vocabulary of link findings, and one message per class, so the three
|
|
43
49
|
// resolvers cannot word the same defect differently (#184).
|
|
@@ -54,6 +60,7 @@ import { getFrontmatter } from "./frontmatter.mjs";
|
|
|
54
60
|
export {
|
|
55
61
|
getFrontmatter,
|
|
56
62
|
sohlField,
|
|
63
|
+
folderField,
|
|
57
64
|
resolveCharges,
|
|
58
65
|
resolveSkillAptitudes,
|
|
59
66
|
resolveRelation,
|
|
@@ -120,10 +127,74 @@ export function parseMarkdownFile(filePath) {
|
|
|
120
127
|
* @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore.
|
|
121
128
|
* Defaults to the configured list.
|
|
122
129
|
*/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Refuse a corpus read whose scope its caller did not state.
|
|
132
|
+
*
|
|
133
|
+
* The rule of #243 in one place, so every reader of the tree refuses the same
|
|
134
|
+
* way and says so in the same words. It is shared rather than repeated because
|
|
135
|
+
* the corpus is no longer read only by {@link walkMarkdownTree}: a pass that
|
|
136
|
+
* reads the content index instead is making the identical claim about which
|
|
137
|
+
* files it is looking at, and must be held to the identical requirement — a
|
|
138
|
+
* scope that quietly defaulted there would reintroduce exactly the second
|
|
139
|
+
* answer the walk's requirement removed.
|
|
140
|
+
*
|
|
141
|
+
* @param {readonly string[]|undefined} skipDirectories - The stated scope.
|
|
142
|
+
* @param {string} who - The reader, named in the message.
|
|
143
|
+
* @throws {Error} When no scope was stated.
|
|
144
|
+
* @returns {void}
|
|
145
|
+
*/
|
|
146
|
+
export function assertStatedScope(skipDirectories, who) {
|
|
147
|
+
if (skipDirectories === undefined) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`${who} requires \`skipDirectories\`: the scope is the ` +
|
|
150
|
+
"caller's to state, so two passes cannot disagree about which " +
|
|
151
|
+
"files are the corpus",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Refuse a corpus read whose records its caller did not supply.
|
|
158
|
+
*
|
|
159
|
+
* The sibling of {@link assertStatedScope}, and required for the same reason
|
|
160
|
+
* one step further on. These two readers cannot derive the corpus themselves:
|
|
161
|
+
* deriving it reaches the pack router and the manifest emitter, which reach the
|
|
162
|
+
* compilers, which load this module — so importing the index here closes a
|
|
163
|
+
* cycle. They take the records their caller already holds.
|
|
164
|
+
*
|
|
165
|
+
* That is not a workaround dressed up as a rule. A compile runs several passes
|
|
166
|
+
* over one tree, and the whole of #243 is that they must not each answer "which
|
|
167
|
+
* files are the corpus?" for themselves. Requiring the answer to be handed in
|
|
168
|
+
* makes the sharing structural rather than remembered.
|
|
169
|
+
*
|
|
170
|
+
* @param {readonly object[]|undefined} records - The supplied corpus.
|
|
171
|
+
* @param {string} who - The reader, named in the message.
|
|
172
|
+
* @throws {Error} When no corpus was supplied.
|
|
173
|
+
* @returns {void}
|
|
174
|
+
*/
|
|
175
|
+
export function assertSuppliedCorpus(records, who) {
|
|
176
|
+
if (!records) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`${who} requires \`records\`: the corpus is derived once per ` +
|
|
179
|
+
"compile and handed to every pass, so no two passes can " +
|
|
180
|
+
"disagree about which files it holds",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function* walkMarkdownTree(rootDir, { skipDirectories } = {}) {
|
|
186
|
+
// Stated by the caller, never resolved here. The default this used to carry
|
|
187
|
+
// — `loadPackConfig().skipDirectories` — read whichever configuration
|
|
188
|
+
// resolved from the working directory rather than the one the caller was
|
|
189
|
+
// working under. In an ordinary build those are the same object and nothing
|
|
190
|
+
// shows; they are not the same when a test injects a configuration, when
|
|
191
|
+
// `PACKAGE_BUILD_CONFIG` names one, or when the command runs from a
|
|
192
|
+
// worktree. Six of this function's twelve callers were on that default, so
|
|
193
|
+
// "which files are the corpus?" had two answers depending on who asked
|
|
194
|
+
// (#243) — the same defect class as `entriesForNote` reading
|
|
195
|
+
// `docEntryTypes` from the ambient config, fixed in #240 after a fixture
|
|
196
|
+
// had been passing on the leak for as long as it existed.
|
|
197
|
+
assertStatedScope(skipDirectories, "walkMarkdownTree");
|
|
127
198
|
if (!fs.existsSync(rootDir)) return;
|
|
128
199
|
const stack = [rootDir];
|
|
129
200
|
while (stack.length > 0) {
|
|
@@ -152,65 +223,168 @@ export function* walkMarkdownTree(
|
|
|
152
223
|
}
|
|
153
224
|
|
|
154
225
|
/**
|
|
155
|
-
*
|
|
226
|
+
* Every position a note may state its template priority at, for one system
|
|
227
|
+
* block, in the order they answer — and whichever of them the note actually
|
|
228
|
+
* wrote (#266).
|
|
229
|
+
*
|
|
230
|
+
* Three places, in the order the migration runs. The specification calls this
|
|
231
|
+
* `data.templatePriority`; `sohl-thalorna` already writes it there on 941
|
|
232
|
+
* notes, beside the `archetype` the build reads — so a tree that has authored
|
|
233
|
+
* forward is read from the key it authored, and only then does the retiring
|
|
234
|
+
* spelling answer.
|
|
235
|
+
*
|
|
236
|
+
* **The block is a parameter because the value is shared, not per-system.** One
|
|
237
|
+
* `data.templatePriority` is the note's statement that it is a template; SoHL
|
|
238
|
+
* records it as `system.templatePriority` and HM3 as `flags.hm3.templatePriority`.
|
|
239
|
+
* The legacy in-block position is therefore read from *the block being compiled*
|
|
240
|
+
* rather than always from `sohl:`, so an HM3-only note is not asked to author a
|
|
241
|
+
* SoHL block to be read.
|
|
242
|
+
*
|
|
243
|
+
* @param {object} fm Parsed frontmatter.
|
|
244
|
+
* @param {string} block The system block being compiled.
|
|
245
|
+
* @returns {{found: [object, string]|undefined, retiring: [object, string]|undefined,
|
|
246
|
+
* NEW: string, OLD: string}} The answering position, the retiring spelling's
|
|
247
|
+
* position if the note also carries it, and the two key names.
|
|
248
|
+
*/
|
|
249
|
+
function findTemplatePriority(fm, block) {
|
|
250
|
+
const inBlock =
|
|
251
|
+
fm != null && typeof fm[block] === "object" && fm[block] !== null ? fm[block] : null;
|
|
252
|
+
const data = fm != null && typeof fm.data === "object" && fm.data !== null ? fm.data : null;
|
|
253
|
+
|
|
254
|
+
const NEW = "templatePriority";
|
|
255
|
+
const OLD = "archetype";
|
|
256
|
+
const sources = /** @type {[object|null, string][]} */ ([
|
|
257
|
+
[data, NEW],
|
|
258
|
+
[inBlock, NEW],
|
|
259
|
+
[fm, NEW],
|
|
260
|
+
[inBlock, OLD],
|
|
261
|
+
[fm, OLD],
|
|
262
|
+
]);
|
|
263
|
+
const found = sources.find(([where, key]) => where != null && key in where);
|
|
264
|
+
// A note part-way through the rename may carry both spellings, and they may
|
|
265
|
+
// *disagree*: 145 of `sohl-thalorna`'s 941 dual-spelled notes say
|
|
266
|
+
// `templatePriority: null` where `archetype: 0` says the opposite — "not a
|
|
267
|
+
// template" against "a template at priority 0". Preferring the new key
|
|
268
|
+
// silently would flip those documents, and preferring the old would ignore
|
|
269
|
+
// what an author wrote most recently. Neither is a decision this function
|
|
270
|
+
// gets to make quietly, so a contradiction is refused and named.
|
|
271
|
+
const retiring = sources
|
|
272
|
+
.slice(sources.findIndex(([, key]) => key === OLD))
|
|
273
|
+
.find(([where, key]) => where != null && key in where);
|
|
274
|
+
return {
|
|
275
|
+
found: /** @type {[object, string]|undefined} */ (found),
|
|
276
|
+
retiring: /** @type {[object, string]|undefined} */ (retiring),
|
|
277
|
+
NEW,
|
|
278
|
+
OLD,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The template priority a note states, for a system that treats an unstated one
|
|
284
|
+
* as "not a template" rather than as an authoring error (#266).
|
|
285
|
+
*
|
|
286
|
+
* Reads exactly the positions {@link resolveTemplatePriority} reads, including
|
|
287
|
+
* the retiring `archetype` spelling, and refuses the same contradiction — so
|
|
288
|
+
* the two systems cannot disagree about what a note said. It differs only in
|
|
289
|
+
* what silence means: SoHL requires the statement, while HM3 keeps the value in
|
|
290
|
+
* a flag it simply omits, so there is no tri-state for an absent value to
|
|
291
|
+
* corrupt and nothing to demand.
|
|
292
|
+
*
|
|
293
|
+
* @param {object} fm Parsed frontmatter.
|
|
294
|
+
* @param {string} label Human-readable context for error messages.
|
|
295
|
+
* @param {object} [options] Options.
|
|
296
|
+
* @param {string} [options.block="sohl"] The system block being compiled.
|
|
297
|
+
* @returns {number|null} The priority, or `null` when the note is not a
|
|
298
|
+
* template or states nothing.
|
|
299
|
+
* @throws {Error} When both spellings are present and disagree.
|
|
300
|
+
*/
|
|
301
|
+
export function statedTemplatePriority(fm, label, { block = "sohl" } = {}) {
|
|
302
|
+
const { found, retiring, NEW, OLD } = findTemplatePriority(fm, block);
|
|
303
|
+
if (found && retiring && found[1] !== OLD && found[0][found[1]] !== retiring[0][OLD]) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Conflicting ${NEW} for ${label}: ` +
|
|
306
|
+
`${NEW} is ${JSON.stringify(found[0][found[1]])} and the retiring ` +
|
|
307
|
+
`${OLD} is ${JSON.stringify(retiring[0][OLD])}. Both are read and ` +
|
|
308
|
+
`${NEW} wins, so they must agree — delete ${OLD}, or correct it`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (!found) return null;
|
|
312
|
+
const raw = found[0][found[1]];
|
|
313
|
+
return raw === null || raw === "" || raw === undefined ? null : raw;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Resolve the required `templatePriority` frontmatter for an Item/Actor entry
|
|
156
318
|
* (the archetype contract, #604). The property is a nullable number that
|
|
157
319
|
* authors must state explicitly:
|
|
158
|
-
* - a number → the document is
|
|
159
|
-
* - `null` → the document is not
|
|
160
|
-
* - absent → an authoring error (throws), so "not
|
|
320
|
+
* - a number → the document is a template of that priority.
|
|
321
|
+
* - `null` → the document is not a template.
|
|
322
|
+
* - absent → an authoring error (throws), so "not a template" is never
|
|
161
323
|
* silently assumed.
|
|
162
324
|
*
|
|
163
|
-
* Reads
|
|
164
|
-
*
|
|
325
|
+
* Reads the positions {@link findTemplatePriority} lists: `data.templatePriority`
|
|
326
|
+
* first — the specified home — then the system block and the top level, and
|
|
327
|
+
* finally the retiring `archetype` spelling in the same two places (#266).
|
|
165
328
|
*
|
|
166
329
|
* @param {object} fm Parsed frontmatter.
|
|
167
330
|
* @param {string} label Human-readable context for error messages.
|
|
168
|
-
* @
|
|
169
|
-
* @
|
|
331
|
+
* @param {object} [options] Options.
|
|
332
|
+
* @param {string} [options.block="sohl"] The system block being compiled.
|
|
333
|
+
* @returns {number|undefined} The template priority, or `undefined` when null.
|
|
334
|
+
* @throws {Error} When the property is absent, is not a number/null, or both
|
|
335
|
+
* spellings are present and disagree.
|
|
170
336
|
*/
|
|
171
|
-
export function
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
337
|
+
export function resolveTemplatePriority(fm, label, { block = "sohl" } = {}) {
|
|
338
|
+
const { found, retiring, NEW, OLD } = findTemplatePriority(fm, block);
|
|
339
|
+
|
|
340
|
+
if (found && retiring && found[1] !== OLD && found[0][found[1]] !== retiring[0][OLD]) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`Conflicting ${NEW} for ${label}: ` +
|
|
343
|
+
`${NEW} is ${JSON.stringify(found[0][found[1]])} and the retiring ` +
|
|
344
|
+
`${OLD} is ${JSON.stringify(retiring[0][OLD])}. Both are read and ` +
|
|
345
|
+
`${NEW} wins, so they must agree — delete ${OLD}, or correct it`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
if (!found) {
|
|
176
349
|
throw new Error(
|
|
177
|
-
`Missing required
|
|
350
|
+
`Missing required ${NEW} for ${label} — set a number (this is a template, at that priority) or null (it is not)`,
|
|
178
351
|
);
|
|
179
352
|
}
|
|
180
|
-
const raw =
|
|
353
|
+
const raw = found[0][found[1]];
|
|
181
354
|
if (raw === null) return undefined;
|
|
182
355
|
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
183
356
|
throw new Error(
|
|
184
|
-
`Invalid
|
|
357
|
+
`Invalid ${found[1]} for ${label}: expected a number or null, got ${JSON.stringify(raw)}`,
|
|
185
358
|
);
|
|
186
359
|
}
|
|
187
360
|
return raw;
|
|
188
361
|
}
|
|
189
362
|
|
|
190
363
|
/**
|
|
191
|
-
* The value a document's `system.
|
|
192
|
-
* `
|
|
364
|
+
* The value a document's `system.templatePriority` carries, from the required
|
|
365
|
+
* `templatePriority` frontmatter (#126, sohl#1780, renamed off `archetype` by
|
|
366
|
+
* #266 / sohl#1836).
|
|
193
367
|
*
|
|
194
368
|
* A **schema field**, so the tri-state is written out in full rather than
|
|
195
|
-
* expressed by a key's presence: a number is
|
|
196
|
-
*
|
|
369
|
+
* expressed by a key's presence: a number is a template at that priority, and
|
|
370
|
+
* `null` is not a template. This is where {@link resolveTemplatePriority}'s
|
|
197
371
|
* `undefined` becomes the field's `null` — an emitted `undefined` would be
|
|
198
372
|
* dropped by `JSON.stringify`, leaving the compiled document with no
|
|
199
|
-
* `
|
|
373
|
+
* `templatePriority` at all and the tri-state readable as two.
|
|
200
374
|
*
|
|
201
|
-
* **`0` is
|
|
202
|
-
*
|
|
375
|
+
* **`0` is a template.** It is the priority SoHL's own templates ship at, and
|
|
376
|
+
* it is falsy, so this returns it unchanged and every caller must ask
|
|
203
377
|
* `typeof v === "number"` rather than testing truthiness.
|
|
204
378
|
*
|
|
205
379
|
* @param {object} fm Parsed frontmatter.
|
|
206
380
|
* @param {string} label Human-readable context for error messages.
|
|
207
|
-
* @returns {number|null} The
|
|
208
|
-
*
|
|
209
|
-
* @throws {Error} When
|
|
381
|
+
* @returns {number|null} The template priority, or `null` for a document that
|
|
382
|
+
* is not a template.
|
|
383
|
+
* @throws {Error} When the property is absent or invalid.
|
|
210
384
|
*/
|
|
211
|
-
export function
|
|
212
|
-
const
|
|
213
|
-
return
|
|
385
|
+
export function systemTemplatePriority(fm, label) {
|
|
386
|
+
const priority = resolveTemplatePriority(fm, label);
|
|
387
|
+
return priority === undefined ? null : priority;
|
|
214
388
|
}
|
|
215
389
|
|
|
216
390
|
/**
|
|
@@ -434,6 +608,7 @@ export { makeId } from "./ids.mjs";
|
|
|
434
608
|
// The content-type → document-type map, which decides *which* pack list a
|
|
435
609
|
// note's own document is routed against.
|
|
436
610
|
import { assertTypeNotRetired, packForType } from "./ids.mjs";
|
|
611
|
+
import { collectAnchors } from "./anchors.mjs";
|
|
437
612
|
|
|
438
613
|
/* ------------------------------------------------------------------------ */
|
|
439
614
|
/* Wikilink resolution: the content-wide link index */
|
|
@@ -461,16 +636,39 @@ import { assertTypeNotRetired, packForType } from "./ids.mjs";
|
|
|
461
636
|
* this repository's own.
|
|
462
637
|
* @returns {{byShortcode: Map, types: Set}} From `buildWikilinkIndex`.
|
|
463
638
|
*/
|
|
464
|
-
export function buildContentLinkIndex(
|
|
639
|
+
export function buildContentLinkIndex(
|
|
640
|
+
contentBase,
|
|
641
|
+
router = packRouter(),
|
|
642
|
+
{ skipDirectories, config, records, problems } = {},
|
|
643
|
+
) {
|
|
465
644
|
const docs = [];
|
|
466
|
-
|
|
645
|
+
const resolved = config ?? loadPackConfig();
|
|
646
|
+
assertSuppliedCorpus(records, "buildContentLinkIndex");
|
|
647
|
+
for (const record of records) {
|
|
648
|
+
// A documentation journal is a document this tree emits, not a note in
|
|
649
|
+
// it; the note it documents is indexed here and carries its address.
|
|
650
|
+
if (!isNoteRecord(record)) continue;
|
|
651
|
+
// The note as its author wrote it — the router, the draft tag and the
|
|
652
|
+
// retired-type check all read authored fields, and none of them may be
|
|
653
|
+
// handed the keys the index derived.
|
|
654
|
+
const fm = authoredFrontmatter(record);
|
|
655
|
+
const absPath = noteFile(contentBase, record);
|
|
656
|
+
// The id a note's document is filed under: its authored pin, or the
|
|
657
|
+
// one derived from its canonical address (#270). Derived by the index
|
|
658
|
+
// against the configuration this build resolved — it used to be
|
|
659
|
+
// derived here through `resolveNoteId(fm)` with no package, which falls
|
|
660
|
+
// back to the ambient `contentPackage()` and so to whichever
|
|
661
|
+
// configuration the working directory answers with (#243).
|
|
662
|
+
// What is left after that is a file with **no address** — no type, or
|
|
663
|
+
// no shortcode — which is not an addressable note and has no document
|
|
664
|
+
// to link to.
|
|
467
665
|
if (!fm?.id) continue;
|
|
468
666
|
// The first walk of every note in the tree, and the only one holding
|
|
469
667
|
// both the declared type and the file that declares it — so a note
|
|
470
668
|
// left on a retired type is reported here, by name, rather than
|
|
471
669
|
// several frames deeper with nothing to go on (SoHL#1580).
|
|
472
670
|
assertTypeNotRetired(fm.type, absPath);
|
|
473
|
-
const base =
|
|
671
|
+
const base = String(record.file.name).replace(/_/g, " ");
|
|
474
672
|
docs.push({
|
|
475
673
|
type: fm.type,
|
|
476
674
|
id: fm.id,
|
|
@@ -485,32 +683,40 @@ export function buildContentLinkIndex(contentBase, router = packRouter()) {
|
|
|
485
683
|
// *into* this note renders marked. It takes no part in resolution,
|
|
486
684
|
// so the note is indexed, compiled and published as any other.
|
|
487
685
|
draft: isDraftNote(fm),
|
|
686
|
+
// The anchors this note declares, carried so the *builds* can check
|
|
687
|
+
// a `#section` link and not only the checker (#193). A foreign
|
|
688
|
+
// anchor has always been checked, because a fetched index
|
|
689
|
+
// publishes the map; a local one was not, because the set was
|
|
690
|
+
// discarded here — the walk yields the body and nothing read it.
|
|
691
|
+
// Read from the record rather than from a second reading of the
|
|
692
|
+
// note's headings — the one-anchor-reader rule (#243).
|
|
693
|
+
anchors: new Set((record.anchors ?? []).map((anchor) => anchor.slug)),
|
|
488
694
|
});
|
|
489
695
|
}
|
|
490
|
-
// Packages this build links *into* but does not publish.
|
|
491
|
-
//
|
|
492
|
-
// checked out resolves the same
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
const { index: foreign, stale } =
|
|
496
|
-
|
|
497
|
-
[contentPackage
|
|
696
|
+
// Packages this build links *into* but does not publish. Each publishes
|
|
697
|
+
// its own content index and this build fetched the ones it depends on, so
|
|
698
|
+
// a contributor without every repository checked out resolves the same
|
|
699
|
+
// links CI does — from an artifact the producer shipped rather than a copy
|
|
700
|
+
// this repository committed (#239).
|
|
701
|
+
const { index: foreign, stale } = loadForeignIndexes(
|
|
702
|
+
resolved,
|
|
703
|
+
[resolved.contentPackage],
|
|
498
704
|
PACKAGE_BASE,
|
|
499
705
|
);
|
|
500
706
|
if (stale.length) {
|
|
501
707
|
for (const st of stale) {
|
|
502
|
-
log.error(`Unusable
|
|
708
|
+
log.error(`Unusable content index for "${st.package}": ${st.reason}`);
|
|
503
709
|
}
|
|
504
710
|
throw new Error(
|
|
505
|
-
"Cross-package links cannot be resolved from
|
|
506
|
-
"re-
|
|
711
|
+
"Cross-package links cannot be resolved from an unusable index; " +
|
|
712
|
+
"re-run `content-build deps fetch`.",
|
|
507
713
|
);
|
|
508
714
|
}
|
|
509
715
|
log.debug(
|
|
510
716
|
`Wikilink index: ${docs.length} local document(s), ` +
|
|
511
717
|
`${foreign.size} foreign address(es)`,
|
|
512
718
|
);
|
|
513
|
-
return buildWikilinkIndex(docs,
|
|
719
|
+
return buildWikilinkIndex(docs, resolved.foundryPackage, foreign, resolved.contentPackage);
|
|
514
720
|
}
|
|
515
721
|
|
|
516
722
|
/**
|
|
@@ -609,16 +815,25 @@ export function convertNoteWikilinks(
|
|
|
609
815
|
* @returns {Array<{fm: object, path: string, tld: string, folder: string,
|
|
610
816
|
* absPath: string}>}
|
|
611
817
|
*/
|
|
612
|
-
export function collectContentDocs(
|
|
818
|
+
export function collectContentDocs(
|
|
819
|
+
contentBase,
|
|
820
|
+
{ skipDirectories, config, records, problems } = {},
|
|
821
|
+
) {
|
|
613
822
|
const docs = [];
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
823
|
+
const resolved = config ?? loadPackConfig();
|
|
824
|
+
assertSuppliedCorpus(records, "collectContentDocs");
|
|
825
|
+
for (const record of records) {
|
|
826
|
+
if (!isNoteRecord(record)) continue;
|
|
827
|
+
const fm = authoredFrontmatter(record);
|
|
828
|
+
const absPath = noteFile(contentBase, record);
|
|
829
|
+
const segments = String(record.file.path).split("/");
|
|
617
830
|
docs.push({
|
|
618
831
|
// With its package supplied for a `WHERE … package = "…"` query —
|
|
619
|
-
// synthesised from the configuration, since no
|
|
620
|
-
// (#56)
|
|
621
|
-
|
|
832
|
+
// synthesised from the configuration this build resolved, since no
|
|
833
|
+
// note declares it (#56) and the ambient one is a different
|
|
834
|
+
// configuration in a worktree or under `PACKAGE_BUILD_CONFIG`
|
|
835
|
+
// (#243).
|
|
836
|
+
fm: searchableFrontmatter(fm, resolved.contentPackage),
|
|
622
837
|
// POSIX-separated and relative to the content root — what a
|
|
623
838
|
// `path:` search term globs, on every platform.
|
|
624
839
|
path: segments.join("/"),
|
|
@@ -670,7 +885,7 @@ const packLinkable = (doc) => Boolean(doc.fm?.shortcode) && Boolean(doc.fm?.type
|
|
|
670
885
|
* compile rather than shipping a table-shaped hole. The error carries
|
|
671
886
|
* `position`, the directive's own line.
|
|
672
887
|
*/
|
|
673
|
-
export function expandNoteTables(body, { docs, name, fm, bodyLine }) {
|
|
888
|
+
export function expandNoteTables(body, { docs, name, fm, bodyLine, sqlTables }) {
|
|
674
889
|
const self =
|
|
675
890
|
fm ?
|
|
676
891
|
(docs.find((d) => d.fm?.id && d.fm.id === fm.id) ?? {
|
|
@@ -682,6 +897,7 @@ export function expandNoteTables(body, { docs, name, fm, bodyLine }) {
|
|
|
682
897
|
linkable: packLinkable,
|
|
683
898
|
source: name,
|
|
684
899
|
self,
|
|
900
|
+
sqlTables,
|
|
685
901
|
});
|
|
686
902
|
if (errors.length) {
|
|
687
903
|
const err = new Error(errors.map((e) => `content table — ${e.reason}`).join("; "));
|
|
@@ -767,14 +983,27 @@ export function buildFolderResolver(folders) {
|
|
|
767
983
|
siblings.add(f.name);
|
|
768
984
|
}
|
|
769
985
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
986
|
+
/**
|
|
987
|
+
* The folder id a note names, by id.
|
|
988
|
+
*
|
|
989
|
+
* **Only by id.** This resolver answers for `folder:` alone; `packFolder:`
|
|
990
|
+
* names a folder *note* and is resolved through the address index instead
|
|
991
|
+
* (#255). The path lookup that briefly lived here is gone with the path
|
|
992
|
+
* spelling it served — it was never released, so there is nothing to
|
|
993
|
+
* deprecate.
|
|
994
|
+
*
|
|
995
|
+
* @param {string|null|undefined} value - As authored.
|
|
996
|
+
* @returns {string|null} The id, or `null` for an absent value.
|
|
997
|
+
* @throws {Error} When the id is not one this pack declares.
|
|
998
|
+
*/
|
|
999
|
+
function resolver(value) {
|
|
1000
|
+
if (value == null || value === "") return null;
|
|
1001
|
+
const authored = String(value).trim();
|
|
1002
|
+
if (!authored) return null;
|
|
1003
|
+
if (!byId.has(authored)) {
|
|
1004
|
+
throw new Error(`Unknown folder id "${authored}"`);
|
|
776
1005
|
}
|
|
777
|
-
return
|
|
1006
|
+
return authored;
|
|
778
1007
|
}
|
|
779
1008
|
|
|
780
1009
|
return { resolver, folders };
|
package/engine/ids.mjs
CHANGED
|
@@ -64,6 +64,28 @@ export const MAP_TYPES = Object.freeze(new Set(["map"]));
|
|
|
64
64
|
*/
|
|
65
65
|
export const MAP_SUBTYPES = Object.freeze(["battlemap", "localmap", "regionalmap"]);
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Content types whose whole document **is** a JournalEntry.
|
|
69
|
+
*
|
|
70
|
+
* Prose, and nothing else: each compiles into one journal entry of its own,
|
|
71
|
+
* with no second document to point at. That is what separates them from the
|
|
72
|
+
* doc-carrying types in `item-docs.mjs`, whose prose becomes a journal *beside*
|
|
73
|
+
* an item, a macro or a scene — those are two documents, and the pair is
|
|
74
|
+
* addressed as `<type>` and `doc<type>`. These are one, so there is no
|
|
75
|
+
* `docplace` and nothing synthesizes one.
|
|
76
|
+
*
|
|
77
|
+
* `doc` was the only member until #241. `place`, `lore` and `scenario` are in
|
|
78
|
+
* the published content format and were declared for validation in #233, but
|
|
79
|
+
* nothing routed them: a note of one lint-ed clean and then compiled into
|
|
80
|
+
* nothing, because {@link PACK_BY_TYPE} did not name it and the open-set
|
|
81
|
+
* default sent it to the items pack. `sohl-thalorna` could not compile a single
|
|
82
|
+
* pack for exactly this reason — 450 notes, and the same 450 the linter had
|
|
83
|
+
* reported before it learned the types.
|
|
84
|
+
*
|
|
85
|
+
* @type {ReadonlySet<string>}
|
|
86
|
+
*/
|
|
87
|
+
export const JOURNAL_TYPES = Object.freeze(new Set(["doc", "place", "lore", "scenario"]));
|
|
88
|
+
|
|
67
89
|
/**
|
|
68
90
|
* Content type → the pack its documents compile into, and the document type
|
|
69
91
|
* that pack holds.
|
|
@@ -78,9 +100,17 @@ export const MAP_SUBTYPES = Object.freeze(["battlemap", "localmap", "regionalmap
|
|
|
78
100
|
* @type {Readonly<Record<string, {pack: string, docType: string}>>}
|
|
79
101
|
*/
|
|
80
102
|
export const PACK_BY_TYPE = Object.freeze({
|
|
81
|
-
|
|
103
|
+
...Object.fromEntries(
|
|
104
|
+
[...JOURNAL_TYPES].map((type) => [type, { pack: "journals", docType: "JournalEntry" }]),
|
|
105
|
+
),
|
|
82
106
|
macro: { pack: "macros", docType: "Macro" },
|
|
83
107
|
being: { pack: "actors", docType: "Actor" },
|
|
108
|
+
// A bundle is Foundry's `Adventure` — a set of document *copies* packaged
|
|
109
|
+
// for one-shot import (#259). The conventional name is `adventures`, which
|
|
110
|
+
// is also what the scenes pass calls its **companion**; the two are
|
|
111
|
+
// different packs and a repository shipping both names them apart, since
|
|
112
|
+
// the router refuses a note that addresses a companion.
|
|
113
|
+
bundle: { pack: "adventures", docType: "Adventure" },
|
|
84
114
|
...Object.fromEntries(
|
|
85
115
|
[...MAP_TYPES].map((type) => [type, { pack: "scenes", docType: "Scene" }]),
|
|
86
116
|
),
|
|
@@ -134,6 +164,106 @@ export function assertTypeNotRetired(type, where) {
|
|
|
134
164
|
);
|
|
135
165
|
}
|
|
136
166
|
|
|
167
|
+
/* -------------------------------------------------------------------- */
|
|
168
|
+
/* Renamed in favour of another type — the retirement window */
|
|
169
|
+
/* -------------------------------------------------------------------- */
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Content types that were **renamed**, and what each is called now.
|
|
173
|
+
*
|
|
174
|
+
* A retired type and a renamed one are different cases, and only the first can
|
|
175
|
+
* be refused. `character` above was retired *outright* — nothing a note wrote
|
|
176
|
+
* made it right, so {@link assertTypeNotRetired} throws. A renamed type has a
|
|
177
|
+
* replacement, the two spellings mean the same thing, and a note carrying the
|
|
178
|
+
* old one compiles into exactly the document it always did: refusing it would
|
|
179
|
+
* fail a build over a note that is not wrong.
|
|
180
|
+
*
|
|
181
|
+
* So these retire in the three steps `package:` took (#56), and this table is
|
|
182
|
+
* the **first**: both spellings resolve, the current one is canonical, and the
|
|
183
|
+
* retired one is *reported* — never refused. The sweep of the content trees and
|
|
184
|
+
* the refusal come after, once no tree writes the old name. That is the same
|
|
185
|
+
* rule `RETIRED_FIELD_ALIASES` states for a renamed *field*, and for the same
|
|
186
|
+
* reason: a consumer must be able to adopt the new toolchain before its content
|
|
187
|
+
* moves, and there are some 31,000 references to move.
|
|
188
|
+
*
|
|
189
|
+
* **Why these three (#78).** `armorgear`, `concoctiongear` and `projectilegear`
|
|
190
|
+
* named the *SoHL document subtype* a note happened to compile into rather than
|
|
191
|
+
* the thing the note is about. A note's `type` sits outside the `sohl:` and
|
|
192
|
+
* `hm3:` blocks precisely because it belongs to no system, and HM3 already
|
|
193
|
+
* compiles a `projectile` into a `missilegear` — so the suffix was never a fact
|
|
194
|
+
* about the note. `weapongear` keeps its name: SoHL and HM3 both call the
|
|
195
|
+
* document that, and #78's table has no row for it.
|
|
196
|
+
*
|
|
197
|
+
* **Keyed by the retired spelling**, which is the opposite of
|
|
198
|
+
* `RETIRED_FIELD_ALIASES`. The direction follows the operation: a field alias
|
|
199
|
+
* is scoped by the schema that declares the *current* name, so that is what a
|
|
200
|
+
* reader asks with; a type alias is applied to a value read off a note, so what
|
|
201
|
+
* a reader has in hand is the retired spelling and what it needs is the
|
|
202
|
+
* replacement.
|
|
203
|
+
*
|
|
204
|
+
* **The address is not normalised.** A note's canonical address — and therefore
|
|
205
|
+
* its document id — carries its `type` as authored, so both spellings keep the
|
|
206
|
+
* addresses they already publish and a tree that has not swept compiles
|
|
207
|
+
* byte-identically. Renaming the type in a note *is* an address move, which is
|
|
208
|
+
* the sweep's business and not this table's.
|
|
209
|
+
*
|
|
210
|
+
* @type {Readonly<Record<string, string>>}
|
|
211
|
+
*/
|
|
212
|
+
export const RENAMED_TYPES = Object.freeze({
|
|
213
|
+
armorgear: "armor",
|
|
214
|
+
concoctiongear: "concoction",
|
|
215
|
+
projectilegear: "projectile",
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The current spelling of a content type: itself, or what it was renamed to.
|
|
220
|
+
*
|
|
221
|
+
* Every table keyed by note type is keyed by the **current** name, and every
|
|
222
|
+
* lookup goes through this — which is the whole of the window's behaviour, in
|
|
223
|
+
* one function, so no two readers can disagree about which vocabulary a note
|
|
224
|
+
* is held to.
|
|
225
|
+
*
|
|
226
|
+
* A non-string passes through untouched: callers hand this whatever the
|
|
227
|
+
* frontmatter carried, and inventing a type for a number would hide the
|
|
228
|
+
* missing-`type:` finding that belongs to the linter.
|
|
229
|
+
*
|
|
230
|
+
* @param {any} type - The type as authored, or as a link spells it.
|
|
231
|
+
* @returns {any} The current spelling.
|
|
232
|
+
*/
|
|
233
|
+
export function currentType(type) {
|
|
234
|
+
if (typeof type !== "string") return type;
|
|
235
|
+
return RENAMED_TYPES[type] ?? type;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* What a note declaring a renamed type is told, in one place.
|
|
240
|
+
*
|
|
241
|
+
* Shared by every reporter, because an author meets whichever runs first and
|
|
242
|
+
* they should read the same. It names the type to write rather than a value to
|
|
243
|
+
* correct — no value makes the retired spelling right — and it says the note
|
|
244
|
+
* compiles either way, so a reader knows this is a rename to schedule rather
|
|
245
|
+
* than a build to unbreak.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} retired - The spelling the note used.
|
|
248
|
+
* @param {string} current - What to write instead.
|
|
249
|
+
* @param {string} [where] - What carries it — a file path, a link target —
|
|
250
|
+
* appended to the message. Omit it where the caller emits through a
|
|
251
|
+
* diagnostic, whose locator already starts the line.
|
|
252
|
+
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
253
|
+
*/
|
|
254
|
+
export function renamedTypeMessage(retired, current, where) {
|
|
255
|
+
return (
|
|
256
|
+
`content type "${retired}" was renamed to "${current}" — the suffix ` +
|
|
257
|
+
`named a SoHL document subtype rather than the thing the note is ` +
|
|
258
|
+
`about` +
|
|
259
|
+
(where ? ` — ${where}` : "") +
|
|
260
|
+
`. Both spellings compile to the same document, so the note is not ` +
|
|
261
|
+
`wrong; write "${current}", and move every \`(type, shortcode)\` ` +
|
|
262
|
+
`reference and wikilink to it in the same change. "${retired}" is ` +
|
|
263
|
+
`refused in a later release`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
137
267
|
/** Where every other content type compiles: the items pack. */
|
|
138
268
|
export const ITEM_PACK = Object.freeze({ pack: "items", docType: "Item" });
|
|
139
269
|
|
|
@@ -162,7 +292,12 @@ export const ITEM_PACK = Object.freeze({ pack: "items", docType: "Item" });
|
|
|
162
292
|
*/
|
|
163
293
|
export function packForType(type) {
|
|
164
294
|
assertTypeNotRetired(type);
|
|
165
|
-
|
|
295
|
+
// Through {@link currentType} like every other type-keyed lookup, so the
|
|
296
|
+
// rule holds without exception. None of today's renamed types is named in
|
|
297
|
+
// `PACK_BY_TYPE` — all three are items and take the default — but a router
|
|
298
|
+
// that read the authored spelling would be the one table left to remember
|
|
299
|
+
// when the next rename lands.
|
|
300
|
+
return PACK_BY_TYPE[currentType(type)] ?? ITEM_PACK;
|
|
166
301
|
}
|
|
167
302
|
|
|
168
303
|
/**
|