@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
|
@@ -19,7 +19,12 @@
|
|
|
19
19
|
* address other packages link to. Stating it twice is how a manifest comes to
|
|
20
20
|
* assert a URL that resolves at build time and 404s for the reader.
|
|
21
21
|
*
|
|
22
|
-
* **A page's URL is its address** — `<package>/<type>-<shortcode>/` (#181)
|
|
22
|
+
* **A page's URL is its address** — `<package>/<type>-<shortcode>/` (#181), and
|
|
23
|
+
* it carries **no `<system>` segment** even though the canonical address does
|
|
24
|
+
* (#59). That is not an omission: a note publishes one page however many
|
|
25
|
+
* systems' documents it compiles into, so there is nothing for the segment to
|
|
26
|
+
* distinguish, and adding it would split one page's URL in two. The canonical
|
|
27
|
+
* address names a *document*; this names a *page*. It
|
|
23
28
|
* used to be derived from `name.full`, which made a display string load-bearing
|
|
24
29
|
* in three separate ways: a rename moved the URL and nothing redirected, two
|
|
25
30
|
* notes in one section could derive the same URL so a uniqueness check had to
|
|
@@ -37,18 +42,42 @@
|
|
|
37
42
|
// so the direction cannot close a cycle (see `engine/pack-config.mjs`).
|
|
38
43
|
import { DEFAULT_ADDRESS_SCHEME } from "../content-config.mjs";
|
|
39
44
|
|
|
45
|
+
// The system vocabulary is the `<system>` segment's own registry, and
|
|
46
|
+
// `engine/systems.mjs` imports nothing but `engine/address-charset.mjs`, so
|
|
47
|
+
// the direction is toward the leaf and cannot close a cycle.
|
|
48
|
+
import { NO_SYSTEM, assertSystemSegment } from "./systems.mjs";
|
|
49
|
+
|
|
50
|
+
// `ids.mjs` is a leaf with no local imports — the module note there says why —
|
|
51
|
+
// so an address may hash itself without any risk of closing a cycle.
|
|
52
|
+
import { makeId } from "./ids.mjs";
|
|
53
|
+
|
|
40
54
|
export { DEFAULT_ADDRESS_SCHEME };
|
|
41
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Re-exported so the address grammar and the system vocabulary are one fact:
|
|
58
|
+
* {@link canonicalKey} writes this segment, and `engine/systems.mjs` decides
|
|
59
|
+
* what may appear in it.
|
|
60
|
+
*/
|
|
61
|
+
export { NO_SYSTEM };
|
|
62
|
+
|
|
42
63
|
/** The knowledgebase's mount within this package's site (#1470). */
|
|
43
64
|
export const KB_PREFIX = "kb/";
|
|
44
65
|
|
|
45
66
|
/**
|
|
46
67
|
* The single path segment a note is addressed by: `type-shortcode`.
|
|
47
68
|
*
|
|
48
|
-
* Lowercased
|
|
49
|
-
* (
|
|
50
|
-
* makes a manifest entry's `path`
|
|
51
|
-
* rather than transported beside it.
|
|
69
|
+
* Lowercased and hyphen-joined by the same rule as the note's canonical key
|
|
70
|
+
* ({@link canonicalKey}, below, lowercases too), so it is that
|
|
71
|
+
* key's **last two segments** — which is what makes a manifest entry's `path`
|
|
72
|
+
* derivable from the key it is filed under rather than transported beside it.
|
|
73
|
+
*
|
|
74
|
+
* It was once the key's whole tail, and #59 ended that: the key gained a
|
|
75
|
+
* `<system>` segment, so its tail is now `system-type-shortcode` and a slug is
|
|
76
|
+
* the tail with that segment dropped. The behaviour here is unchanged, and
|
|
77
|
+
* deliberately — a page has no system to name (see the module note above), so
|
|
78
|
+
* the two forms diverge rather than one having fallen behind the other. A
|
|
79
|
+
* consumer deriving a `path` from a key drops the *package and the system*, not
|
|
80
|
+
* the package alone.
|
|
52
81
|
*
|
|
53
82
|
* The hyphen is a separator and never occurs inside a segment: a shortcode is
|
|
54
83
|
* `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN`, enforced by `content-lint.mjs`)
|
|
@@ -116,3 +145,236 @@ export function addressSlug(fm) {
|
|
|
116
145
|
export function packageAddress(fm) {
|
|
117
146
|
return `${addressSlug(fm)}/`;
|
|
118
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The **canonical** address of a note: fully qualified, one spelling per
|
|
151
|
+
* document, and globally unique.
|
|
152
|
+
*
|
|
153
|
+
* The written form of a link is a **partial** address: it may omit leading
|
|
154
|
+
* segments, and each one it omits is filled in by rule rather than left
|
|
155
|
+
* unconstrained. An omitted package (`[[skill-lang]]`) defaults to the citing
|
|
156
|
+
* note's own, so an unqualified link resolves locally and only locally, and a
|
|
157
|
+
* link into another package must name it. An omitted **system** is a
|
|
158
|
+
* *wildcard*, not a default — most links target items, which belong to a
|
|
159
|
+
* system — and the resolver requires exactly one match: none is a dead link,
|
|
160
|
+
* more than one is an ambiguity reported with every candidate named.
|
|
161
|
+
* Everything internal — index keys, cache keys, every lookup — uses this fully
|
|
162
|
+
* qualified form instead, so no consumer has to know what a short form
|
|
163
|
+
* defaulted to or matched.
|
|
164
|
+
*
|
|
165
|
+
* Global uniqueness is what lets a dependency's index merge straight into a
|
|
166
|
+
* local one: the keys cannot collide by accident, so a key already present on
|
|
167
|
+
* merge is a real conflict rather than an artefact of two packages sharing a
|
|
168
|
+
* namespace. `(type, shortcode)` alone is unique only *within* a package, and
|
|
169
|
+
* two independently authored packages reaching for the same short string is a
|
|
170
|
+
* matter of time (#1499).
|
|
171
|
+
*
|
|
172
|
+
* **The system segment (#59).** A package may ship content for more than one
|
|
173
|
+
* system, and one note then compiles into a document per system — an actor in
|
|
174
|
+
* `actors-sohl` *and* an actor in `actors-hm3`. Without a system segment both
|
|
175
|
+
* land on one key, so the address cannot name either of them. `harn-ensemble`
|
|
176
|
+
* carries 2,497 such notes.
|
|
177
|
+
*
|
|
178
|
+
* The value is a system id, or the literal **`none`** for a document no game
|
|
179
|
+
* system defines: a journal, a macro, a scene, and an item's documentation
|
|
180
|
+
* journal — which is `none` however many systems the item itself declares,
|
|
181
|
+
* because it is one journal.
|
|
182
|
+
*
|
|
183
|
+
* `none` rather than `any`: every segment of an address is an exact literal,
|
|
184
|
+
* and `any` reads as a wildcard — "matches under any system" — which is not
|
|
185
|
+
* what it does. A resolver written to that misreading would fail silently,
|
|
186
|
+
* since a lookup miss already returns nothing rather than erroring. And not
|
|
187
|
+
* `null` or `~`, both of which are YAML nulls that parse to an absent value and
|
|
188
|
+
* drop the segment entirely.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} pkg - The owning **content** package (`sohl`, `thalorna`) —
|
|
191
|
+
* not the Foundry package, which varies per compilation target.
|
|
192
|
+
* @param {string} system - The system whose document this addresses, or `none`.
|
|
193
|
+
* @param {string} type - The note's `type`.
|
|
194
|
+
* @param {string} shortcode - The note's `shortcode`.
|
|
195
|
+
* @returns {string} `package-system-type-shortcode`, lowercased.
|
|
196
|
+
*/
|
|
197
|
+
export function canonicalKey(pkg, system, type, shortcode) {
|
|
198
|
+
// Checked where an address is *written*, not where one is read: a fetched
|
|
199
|
+
// index naming a system this build has never heard of is data to report,
|
|
200
|
+
// while emitting one is a defect in this build. The registry is closed, so
|
|
201
|
+
// an unknown value here can only be a typo or a system nobody declared.
|
|
202
|
+
assertSystemSegment(system, `the address of ${type}-${shortcode}`);
|
|
203
|
+
return `${pkg}-${system}-${type}-${shortcode}`.toLowerCase();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* How many segments a canonical key has, and therefore how many the reader
|
|
208
|
+
* below counts.
|
|
209
|
+
*
|
|
210
|
+
* Named rather than written as a literal because it is the *grammar*, not an
|
|
211
|
+
* implementation detail of one function: it is the number a change to the
|
|
212
|
+
* address form would move, and the thing a reader of that change has to find.
|
|
213
|
+
*
|
|
214
|
+
* @type {number}
|
|
215
|
+
*/
|
|
216
|
+
export const CANONICAL_KEY_SEGMENTS = 4;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Reads a canonical key back into its parts.
|
|
220
|
+
*
|
|
221
|
+
* Parsing is plain positional counting: split on the separator, require
|
|
222
|
+
* {@link CANONICAL_KEY_SEGMENTS} of them, and assign each position its field.
|
|
223
|
+
* **The charset rule is what makes that sound** — every segment is
|
|
224
|
+
* `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN` in `engine/address-charset.mjs`),
|
|
225
|
+
* so the hyphen is purely a separator and the count alone determines every
|
|
226
|
+
* field. That is enforced at each of the three sources rather than assumed of
|
|
227
|
+
* the data: shortcodes by `content-lint.mjs` (#1397), `contentPackage` by
|
|
228
|
+
* `defineConfig` (#59), and types are bare words. Were any of them free to
|
|
229
|
+
* carry a hyphen, no amount of counting would recover the fields and the reader
|
|
230
|
+
* would need a vocabulary to match against instead.
|
|
231
|
+
*
|
|
232
|
+
* **Nothing to read and nothing readable are different answers.** A key that
|
|
233
|
+
* cannot be canonical — `harn-adventures-sohl-skill-melee`, five segments,
|
|
234
|
+
* because the package name carries the separator — yields `null`, while an
|
|
235
|
+
* absent or blank input yields `undefined`. Both are falsy, so
|
|
236
|
+
* every call site (all of which test the result for truthiness) is unaffected;
|
|
237
|
+
* the distinction is there so a caller reporting "this key is unreadable" can
|
|
238
|
+
* tell that it has a key to report about.
|
|
239
|
+
*
|
|
240
|
+
* @param {unknown} key - A canonical key, or nothing.
|
|
241
|
+
* @returns {{package: string, system: string, type: string, shortcode: string}
|
|
242
|
+
* |null|undefined}
|
|
243
|
+
* The parts; `null` when there is a string that is not in canonical form;
|
|
244
|
+
* `undefined` when there is no key at all.
|
|
245
|
+
*/
|
|
246
|
+
export function readCanonicalKey(key) {
|
|
247
|
+
if (key == null || key === "") return undefined;
|
|
248
|
+
const parts = String(key).split("-");
|
|
249
|
+
if (parts.length !== CANONICAL_KEY_SEGMENTS) return null;
|
|
250
|
+
const [pkg, system, type, shortcode] = parts;
|
|
251
|
+
if (!pkg || !system || !type || !shortcode) return null;
|
|
252
|
+
return { package: pkg, system, type, shortcode };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Where this build serves each package, keyed by package name.
|
|
257
|
+
*
|
|
258
|
+
* One line per package, and the only edit a relocation requires: point a
|
|
259
|
+
* package at another path (`"/setting/thalorna/"`) or another origin
|
|
260
|
+
* (`"https://thalorna.example.org/"`) and every inbound link into it follows.
|
|
261
|
+
* A base is a prefix, so it must end in `/`.
|
|
262
|
+
*
|
|
263
|
+
* Only *foreign* packages are consulted — a package this build publishes is
|
|
264
|
+
* authoritative in its own entries and never resolves through a fetched
|
|
265
|
+
* index — but every linkable package is listed, because which are foreign
|
|
266
|
+
* depends on the consuming repository.
|
|
267
|
+
*/
|
|
268
|
+
export const PACKAGE_BASE = Object.freeze({
|
|
269
|
+
sohl: "/sohl/",
|
|
270
|
+
thalorna: "/thalorna/",
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Asserts a base is usable as a prefix and returns it.
|
|
275
|
+
*
|
|
276
|
+
* Exported for the URL helpers' own callers: a build that composes a base
|
|
277
|
+
* before resolving against it should reject a malformed one at the point it
|
|
278
|
+
* is chosen, not at the point a link is emitted.
|
|
279
|
+
*
|
|
280
|
+
* @param {string} base - The package base.
|
|
281
|
+
* @param {string} what - What is being resolved, for the error message.
|
|
282
|
+
* @returns {string} The base.
|
|
283
|
+
*/
|
|
284
|
+
export function checkBase(base, what) {
|
|
285
|
+
if (typeof base !== "string" || !base.endsWith("/")) {
|
|
286
|
+
throw new Error(`${what}: package base ${JSON.stringify(base)} must end in a slash`);
|
|
287
|
+
}
|
|
288
|
+
return base;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The package-relative address a site-absolute URL records as.
|
|
293
|
+
*
|
|
294
|
+
* Strips the emitting package's own base, so what is recorded says *where in
|
|
295
|
+
* the package* a page is and nothing about where the package itself is
|
|
296
|
+
* mounted. A URL outside the base is an error rather than a best effort: it
|
|
297
|
+
* would record an address that silently resolves to the wrong place once a
|
|
298
|
+
* consumer prefixes its own base.
|
|
299
|
+
*
|
|
300
|
+
* @param {string} url - The site-absolute URL the emitting build publishes at.
|
|
301
|
+
* @param {string} base - That build's base for the package, e.g. `"/thalorna/"`.
|
|
302
|
+
* @returns {string} The address relative to `base`, with no leading slash.
|
|
303
|
+
*/
|
|
304
|
+
export function packageRelative(url, base) {
|
|
305
|
+
checkBase(base, "packageRelative");
|
|
306
|
+
if (typeof url !== "string" || !url.startsWith(base)) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`packageRelative: ${JSON.stringify(url)} does not sit under base ` +
|
|
309
|
+
`${JSON.stringify(base)}`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return url.slice(base.length);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The URL a package-relative address resolves to in this build.
|
|
317
|
+
*
|
|
318
|
+
* Plain concatenation, which is what makes an absolute-origin base work: a base
|
|
319
|
+
* of `"https://thalorna.example.org/"` yields an absolute link, and one of
|
|
320
|
+
* `"/thalorna/"` a root-relative one, with no other rule to keep in step.
|
|
321
|
+
*
|
|
322
|
+
* @param {string} rel - The package-relative address from a fetched index.
|
|
323
|
+
* @param {string} base - This build's base for that package.
|
|
324
|
+
* @returns {string} The resolved URL.
|
|
325
|
+
*/
|
|
326
|
+
export function resolvePackageUrl(rel, base) {
|
|
327
|
+
checkBase(base, "resolvePackageUrl");
|
|
328
|
+
if (typeof rel !== "string" || !rel || rel.startsWith("/")) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`resolvePackageUrl: ${JSON.stringify(rel)} is not a package-` + `relative address`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return `${base}${rel}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The namespace {@link documentId} hashes a canonical address under.
|
|
338
|
+
*
|
|
339
|
+
* Named rather than written as a literal at the one call site, because it is
|
|
340
|
+
* part of the published derivation: a consumer holding a content-index entry
|
|
341
|
+
* recomputes the document's id — and therefore its compendium UUID — as
|
|
342
|
+
* `makeId(DOCUMENT_ID_NAMESPACE, entry.canonical)`, so the string is a fact
|
|
343
|
+
* about the format rather than an implementation detail. Changing it moves
|
|
344
|
+
* every id this toolchain has ever emitted.
|
|
345
|
+
*
|
|
346
|
+
* @type {string}
|
|
347
|
+
*/
|
|
348
|
+
export const DOCUMENT_ID_NAMESPACE = "document";
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The Foundry `_id` of the document a note compiles into, derived from its
|
|
352
|
+
* canonical address (#270).
|
|
353
|
+
*
|
|
354
|
+
* A note used to author this — an opaque 16-character string, one per note,
|
|
355
|
+
* that said nothing its address did not and that no check guaranteed. The
|
|
356
|
+
* address is the identity that *is* guaranteed: `content-lint` refuses a
|
|
357
|
+
* duplicate `(type, shortcode)` across every pack of a document type, which is
|
|
358
|
+
* exactly the scope a primary document's id must be unique within. So the
|
|
359
|
+
* derived id inherits a guard that already exists, where the authored one had
|
|
360
|
+
* none.
|
|
361
|
+
*
|
|
362
|
+
* **The coupling this creates, stated plainly.** The address carries the
|
|
363
|
+
* shortcode, so renaming a shortcode moves the document's id — where an
|
|
364
|
+
* authored id survived one. That is a real trade rather than a free win, and
|
|
365
|
+
* two things make it acceptable: a rename already breaks every wikilink to the
|
|
366
|
+
* note, so it is a breaking change either way; and a note that must keep its
|
|
367
|
+
* identity across a rename pins an `id`, which is what the pin is for. One
|
|
368
|
+
* thing genuinely degrades — `engine/address-diff.mjs` tells a rename from a
|
|
369
|
+
* withdrawal by matching document ids, and can no longer do so for a note that
|
|
370
|
+
* authors none. Its module note records that.
|
|
371
|
+
*
|
|
372
|
+
* @param {string} pkg - The owning **content** package.
|
|
373
|
+
* @param {string} system - The system whose document this is, or `none`.
|
|
374
|
+
* @param {string} type - The note's `type`.
|
|
375
|
+
* @param {string} shortcode - The note's `shortcode`.
|
|
376
|
+
* @returns {string} A 16-character Foundry id.
|
|
377
|
+
*/
|
|
378
|
+
export function documentId(pkg, system, type, shortcode) {
|
|
379
|
+
return makeId(DOCUMENT_ID_NAMESPACE, canonicalKey(pkg, system, type, shortcode));
|
|
380
|
+
}
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
* @module
|
|
100
100
|
*/
|
|
101
101
|
|
|
102
|
+
import { currentType } from "./ids.mjs";
|
|
102
103
|
import { SCHEMA_ARTIFACT_VERSION } from "./schema-check.mjs";
|
|
103
104
|
import { authoredFields } from "./field-spec.mjs";
|
|
104
105
|
import { positionInFrontmatter } from "./diagnostics.mjs";
|
|
@@ -135,12 +136,24 @@ export function declaredPaths(artifact) {
|
|
|
135
136
|
* {@link undeclaredMessage} draws, and the difference between "the
|
|
136
137
|
* specification is wrong" and "the schema has not caught up".
|
|
137
138
|
*
|
|
138
|
-
*
|
|
139
|
+
* A shared row names no type, so it is placed rather than typed: "in the shared
|
|
140
|
+
* mappings" instead of "on a `weapongear`". Reading `on a \`the shared
|
|
141
|
+
* mappings\`` would be the alternative, and a diagnostic is prose (#275).
|
|
142
|
+
*
|
|
143
|
+
* @param {object} finding - `{system, systemVersion, noteType, source, target, shared}`.
|
|
139
144
|
* @returns {string} The message.
|
|
140
145
|
*/
|
|
141
|
-
export function undeclaredTargetMessage({
|
|
146
|
+
export function undeclaredTargetMessage({
|
|
147
|
+
system,
|
|
148
|
+
systemVersion,
|
|
149
|
+
noteType,
|
|
150
|
+
source,
|
|
151
|
+
target,
|
|
152
|
+
shared,
|
|
153
|
+
}) {
|
|
154
|
+
const where = shared ? `in ${noteType}` : `on a \`${noteType}\``;
|
|
142
155
|
return (
|
|
143
|
-
`the format maps \`${source}\`
|
|
156
|
+
`the format maps \`${source}\` ${where} to \`${target}\` in ` +
|
|
144
157
|
`${system}, which ${system}@${systemVersion} does not declare on any ` +
|
|
145
158
|
`document subtype — the specification and the system disagree, and one ` +
|
|
146
159
|
`of the two is wrong`
|
|
@@ -327,7 +340,11 @@ export function measureNote(note, format, { severity = "warning" } = {}) {
|
|
|
327
340
|
message,
|
|
328
341
|
});
|
|
329
342
|
|
|
330
|
-
|
|
343
|
+
// The specification's sections are headed by the current spelling of a note
|
|
344
|
+
// type, so a note still on a renamed one is measured against the section it
|
|
345
|
+
// will move to rather than reported as a type the format does not declare
|
|
346
|
+
// (#78). The rename itself is the frontmatter lint's finding.
|
|
347
|
+
const spec = format.types.get(currentType(type));
|
|
331
348
|
if (!spec) {
|
|
332
349
|
add(
|
|
333
350
|
"unknown-type",
|
|
@@ -454,6 +471,27 @@ export function fieldDriftMessage({ noteType, source, target, name, to }) {
|
|
|
454
471
|
);
|
|
455
472
|
}
|
|
456
473
|
|
|
474
|
+
/**
|
|
475
|
+
* A shared source, as the two sides spell it.
|
|
476
|
+
*
|
|
477
|
+
* The specification writes every type-specific row `data.<key>` — the container
|
|
478
|
+
* `data:` (#128) put those facts in — while a declaration writes either: the
|
|
479
|
+
* bare key it has always named, or the same dotted path now that a field can
|
|
480
|
+
* declare its shared source and its legacy in-block key separately (#305).
|
|
481
|
+
* Both are the same source, so both are normalized before they are compared;
|
|
482
|
+
* matching one spelling against the other would report every moved field as
|
|
483
|
+
* unmapped, which is the opposite of what this check is for.
|
|
484
|
+
*
|
|
485
|
+
* A note-level source — the `subType` several tables name — carries no prefix
|
|
486
|
+
* and is returned unchanged.
|
|
487
|
+
*
|
|
488
|
+
* @param {string} path - The source, as either side wrote it.
|
|
489
|
+
* @returns {string} The source without its `data.` prefix.
|
|
490
|
+
*/
|
|
491
|
+
function sharedSource(path) {
|
|
492
|
+
return String(path).replace(/^data\./, "");
|
|
493
|
+
}
|
|
494
|
+
|
|
457
495
|
/**
|
|
458
496
|
* The specification's mapping rows for one type and one system, as field paths.
|
|
459
497
|
*
|
|
@@ -504,20 +542,23 @@ export function checkDeclaredFields({ format, itemFields, system, severity = "er
|
|
|
504
542
|
|
|
505
543
|
const authored = authoredFields(declared[noteType]);
|
|
506
544
|
for (const claim of claimsFor(format, noteType, system)) {
|
|
507
|
-
|
|
508
|
-
// `subType` several tables name — carries no prefix.
|
|
509
|
-
const source = claim.source.replace(/^data\./, "");
|
|
545
|
+
const source = sharedSource(claim.source);
|
|
510
546
|
const target = claim.target.replace(/^system\./, "");
|
|
511
547
|
// The longest declared name that the source sits under: a field
|
|
512
548
|
// declared `impact.die` claims `data.impact.die` ahead of any
|
|
513
|
-
// field declared `impact`.
|
|
549
|
+
// field declared `impact`. Compared on the normalized spelling, so
|
|
550
|
+
// a field that has moved its source under `data:` is the same
|
|
551
|
+
// length it was.
|
|
514
552
|
let match;
|
|
515
553
|
let rest;
|
|
554
|
+
let matched = "";
|
|
516
555
|
for (const field of authored) {
|
|
517
|
-
const
|
|
556
|
+
const name = sharedSource(field.name);
|
|
557
|
+
const remainder = under(source, name);
|
|
518
558
|
if (remainder === undefined) continue;
|
|
519
|
-
if (match &&
|
|
559
|
+
if (match && name.length <= matched.length) continue;
|
|
520
560
|
match = field;
|
|
561
|
+
matched = name;
|
|
521
562
|
rest = remainder;
|
|
522
563
|
}
|
|
523
564
|
// No declaration names it. That is coverage, not a contradiction —
|
|
@@ -547,7 +588,9 @@ export function checkDeclaredFields({ format, itemFields, system, severity = "er
|
|
|
547
588
|
// equal: the document names the *shared* source a field is written as,
|
|
548
589
|
// and a declaration names every key the system's own block accepts, so
|
|
549
590
|
// the sets legitimately differ until #127 has moved the corpus.
|
|
550
|
-
const registryKeys = new Set(
|
|
591
|
+
const registryKeys = new Set(
|
|
592
|
+
authored.map((field) => sharedSource(field.name).split(".")[0]),
|
|
593
|
+
);
|
|
551
594
|
const specKeys = format.types.get(noteType).dataKeys;
|
|
552
595
|
coverage.push({
|
|
553
596
|
type: noteType,
|
|
@@ -44,7 +44,15 @@
|
|
|
44
44
|
* | the per-type mapping | `shared source` | one claim per `system.*` cell |
|
|
45
45
|
*
|
|
46
46
|
* A mapping table's remaining header cells name the systems (`→ sohl`,
|
|
47
|
-
* `→ hm3`), so the system vocabulary comes from the document too.
|
|
47
|
+
* `→ hm3`), so the system vocabulary comes from the document too.
|
|
48
|
+
*
|
|
49
|
+
* **A mapping table before the first `### type:` heading is the shared one.**
|
|
50
|
+
* The document states the rows every type maps identically once, at the top,
|
|
51
|
+
* and omits them from all sixteen per-type tables — so a parser that only ever
|
|
52
|
+
* looked inside a type's section could not see them, and the eight rows they
|
|
53
|
+
* cover were checked by nothing (#275). Position is the whole distinction:
|
|
54
|
+
* there is no marker to read and none is wanted, since the document's own
|
|
55
|
+
* argument for stating them once is that they belong to no type in particular. A cell that
|
|
48
56
|
* names no field — `NA`, `**see above**`, a `flags.*` path — is not a claim,
|
|
49
57
|
* and is skipped rather than reported: the check is about `system.*` targets,
|
|
50
58
|
* and a column reading NA is the document saying this type produces no document
|
|
@@ -90,7 +98,13 @@ export const CONTENT_FORMAT_PATH = path.join(
|
|
|
90
98
|
* One `system.*` target the specification names for one note type.
|
|
91
99
|
*
|
|
92
100
|
* @typedef {object} MappingClaim
|
|
93
|
-
* @property {string} noteType - The type whose section makes the claim
|
|
101
|
+
* @property {string} noteType - The type whose section makes the claim, or
|
|
102
|
+
* `the shared mappings` for a row of the shared tables — see `shared`.
|
|
103
|
+
* @property {boolean} [shared] - Whether the row came from a **shared** mapping
|
|
104
|
+
* table, which stands before the first `### type:` heading and states what
|
|
105
|
+
* every type maps identically (#275). Absent on a per-type row, so the two
|
|
106
|
+
* never mix: only a per-type row has a field declaration to be checked
|
|
107
|
+
* against.
|
|
94
108
|
* @property {string} system - The system column it sits under, from the header.
|
|
95
109
|
* @property {string} source - The shared source cell, stripped of its backticks.
|
|
96
110
|
* @property {string} target - The dotted path, `system.` prefix included.
|
|
@@ -107,6 +121,18 @@ export const CONTENT_FORMAT_PATH = path.join(
|
|
|
107
121
|
* @property {MappingClaim[]} claims - Every `system.*` target, in document order.
|
|
108
122
|
*/
|
|
109
123
|
|
|
124
|
+
/**
|
|
125
|
+
* What a shared row's `noteType` reads, in place of a type name.
|
|
126
|
+
*
|
|
127
|
+
* Phrased to be substituted into a diagnostic sentence — "the format maps `x`
|
|
128
|
+
* in the shared mappings to `y`" — because that is the only place it is ever
|
|
129
|
+
* read. Both shared tables use it: the second states what the actor types add,
|
|
130
|
+
* and a row of it is no more a `being`'s than a row of the first is.
|
|
131
|
+
*
|
|
132
|
+
* @type {string}
|
|
133
|
+
*/
|
|
134
|
+
export const SHARED_SCOPE = "the shared mappings";
|
|
135
|
+
|
|
110
136
|
/** A table row's cells, or `null` when the line is not a table row. */
|
|
111
137
|
function cellsOf(line) {
|
|
112
138
|
const trimmed = line.trim();
|
|
@@ -172,7 +198,7 @@ export function parseContentFormat(text, { file = CONTENT_FORMAT_PATH } = {}) {
|
|
|
172
198
|
const lines = String(text ?? "").split("\n");
|
|
173
199
|
/** @type {TypeSpec|undefined} */
|
|
174
200
|
let current;
|
|
175
|
-
/** @type {{kind: "data"|"mapping", systems: string[]}|undefined} */
|
|
201
|
+
/** @type {{kind: "data"|"mapping", systems: string[], shared?: boolean}|undefined} */
|
|
176
202
|
let table;
|
|
177
203
|
|
|
178
204
|
for (let i = 0; i < lines.length; i += 1) {
|
|
@@ -210,12 +236,15 @@ export function parseContentFormat(text, { file = CONTENT_FORMAT_PATH } = {}) {
|
|
|
210
236
|
table = {
|
|
211
237
|
kind: "mapping",
|
|
212
238
|
systems: cells.slice(1).map((cell) => cell.replace(/^→\s*/, "").trim()),
|
|
239
|
+
// Before any type section, so the rows are every type's (#275).
|
|
240
|
+
...(current ? {} : { shared: true }),
|
|
213
241
|
};
|
|
214
242
|
continue;
|
|
215
243
|
}
|
|
216
|
-
if (!table
|
|
244
|
+
if (!table) continue;
|
|
217
245
|
|
|
218
246
|
if (table.kind === "data") {
|
|
247
|
+
if (!current) continue;
|
|
219
248
|
const declared = code(cells[0]);
|
|
220
249
|
if (!declared) continue;
|
|
221
250
|
current.dataPaths.add(declared);
|
|
@@ -223,13 +252,16 @@ export function parseContentFormat(text, { file = CONTENT_FORMAT_PATH } = {}) {
|
|
|
223
252
|
continue;
|
|
224
253
|
}
|
|
225
254
|
|
|
255
|
+
if (!table.shared && !current) continue;
|
|
256
|
+
|
|
226
257
|
for (let c = 1; c < cells.length; c += 1) {
|
|
227
258
|
const target = code(cells[c]);
|
|
228
259
|
if (!target || !target.startsWith("system.")) continue;
|
|
229
260
|
const system = table.systems[c - 1];
|
|
230
261
|
if (!system) continue;
|
|
231
262
|
claims.push({
|
|
232
|
-
noteType: current.name,
|
|
263
|
+
noteType: table.shared ? SHARED_SCOPE : /** @type {TypeSpec} */ (current).name,
|
|
264
|
+
...(table.shared ? { shared: true } : {}),
|
|
233
265
|
system,
|
|
234
266
|
source: code(cells[0]) ?? cells[0],
|
|
235
267
|
target,
|