@heroiclands/package-build 8.1.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 +748 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +479 -123
- package/bin/package-build.mjs +27 -69
- package/bin/report.mjs +1 -2
- package/bundle.mjs +2 -10
- package/config.mjs +31 -106
- package/container.mjs +13 -57
- package/content-config.mjs +300 -188
- package/coverage.mjs +14 -55
- package/deploy.mjs +4 -13
- package/docs/content-format.md +1418 -0
- package/e2e.mjs +16 -55
- package/engine/address-charset.mjs +62 -0
- package/engine/address-diff.mjs +1 -4
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +203 -31
- package/engine/code-fences.mjs +4 -13
- package/engine/compendiums.mjs +13 -37
- package/engine/content-address.mjs +6 -10
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +144 -99
- package/engine/content-lint.mjs +12 -16
- package/engine/content-slug.mjs +2 -6
- package/engine/content-tables.mjs +26 -79
- package/engine/diagnostics.mjs +37 -15
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-reference.mjs +6 -20
- package/engine/field-spec.mjs +49 -45
- package/engine/foreign-catalog.mjs +7 -22
- package/engine/foreign-manifests.mjs +1 -4
- package/engine/frontmatter-lint.mjs +347 -43
- package/engine/frontmatter.mjs +3 -8
- package/engine/generate.mjs +36 -20
- package/engine/helpers.mjs +54 -81
- package/engine/homepage.mjs +4 -15
- package/engine/ids.mjs +21 -12
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -9
- package/engine/journals.mjs +4 -14
- package/engine/kb-manifest.mjs +41 -24
- package/engine/macros.mjs +2 -10
- package/engine/manifest-emit.mjs +6 -17
- package/engine/map-notes.mjs +53 -87
- package/engine/note-claims.mjs +383 -0
- package/engine/note-package.mjs +1 -4
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +56 -60
- package/engine/pack-router.mjs +18 -8
- package/engine/prose-config.mjs +20 -4
- package/engine/prose-lint.mjs +61 -17
- package/engine/region-events.mjs +1 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scene-levels.mjs +8 -22
- package/engine/scenes.mjs +31 -47
- package/engine/schema-check.mjs +348 -7
- package/engine/schema-extract.mjs +11 -39
- package/engine/site-build.mjs +13 -38
- package/engine/site-index.mjs +40 -35
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +115 -92
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +147 -183
- package/index.mjs +1 -5
- package/lang.mjs +1 -3
- package/manifest.mjs +10 -37
- package/markdownlint-config.mjs +1 -5
- package/package.json +6 -2
- package/sohl/actors.mjs +251 -68
- package/sohl/being-info.mjs +3 -6
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +4 -6
- package/sohl/item-builders.mjs +1 -3
- package/sohl/item-fields.mjs +16 -34
- package/sohl/items.mjs +111 -17
- package/sohl/kb-passes.mjs +29 -39
- package/sohl/note-schemas.mjs +11 -7
- package/sohl/skill-base.mjs +7 -23
- package/stage.mjs +3 -13
- package/templates.mjs +4 -15
- package/types/bundle.d.mts +1 -1
- package/types/container.d.mts +2 -2
- package/types/content-config.d.mts +48 -4
- package/types/coverage.d.mts +1 -1
- package/types/e2e.d.mts +4 -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 +15 -2
- package/types/engine/helpers.d.mts +22 -14
- 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/schema-extract.d.mts +1 -1
- package/types/engine/site-index.d.mts +1 -1
- 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/manifest.d.mts +1 -1
- 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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse the specification's tables.
|
|
3
|
+
*
|
|
4
|
+
* Pure: text in, model out, so a test states a miniature document rather than
|
|
5
|
+
* asserting against the real one and its 1,100 lines of prose.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} text - The document's contents.
|
|
8
|
+
* @param {object} [opts]
|
|
9
|
+
* @param {string} [opts.file] - Path recorded on the result, for diagnostics.
|
|
10
|
+
* @returns {ContentFormat} What the document declares.
|
|
11
|
+
*/
|
|
12
|
+
export function parseContentFormat(text: string, { file }?: {
|
|
13
|
+
file?: string | undefined;
|
|
14
|
+
}): ContentFormat;
|
|
15
|
+
/**
|
|
16
|
+
* Read and parse the specification from disk.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} [file] - The document. Defaults to {@link CONTENT_FORMAT_PATH}.
|
|
19
|
+
* @returns {ContentFormat} What it declares.
|
|
20
|
+
*/
|
|
21
|
+
export function loadContentFormat(file?: string): ContentFormat;
|
|
22
|
+
/**
|
|
23
|
+
* The specification this package ships.
|
|
24
|
+
*
|
|
25
|
+
* Resolved from this module rather than from the working directory: a consumer
|
|
26
|
+
* runs `content-build content-format` inside its own repository, and the
|
|
27
|
+
* document it should be checked against is the one that came with the toolchain
|
|
28
|
+
* version it resolved — the same rule `--version` follows.
|
|
29
|
+
*
|
|
30
|
+
* @type {string}
|
|
31
|
+
*/
|
|
32
|
+
export const CONTENT_FORMAT_PATH: string;
|
|
33
|
+
/**
|
|
34
|
+
* What one note type's section declares.
|
|
35
|
+
*/
|
|
36
|
+
export type TypeSpec = {
|
|
37
|
+
/**
|
|
38
|
+
* - The note type, as the `### type:` heading spells it.
|
|
39
|
+
*/
|
|
40
|
+
name: string;
|
|
41
|
+
/**
|
|
42
|
+
* - 1-based line of that heading.
|
|
43
|
+
*/
|
|
44
|
+
line: number;
|
|
45
|
+
/**
|
|
46
|
+
* - The head segment of each declared `data`
|
|
47
|
+
* property — what a note actually writes. `appearance.eye_color` is authored
|
|
48
|
+
* as `appearance`, so that is the key recorded.
|
|
49
|
+
*/
|
|
50
|
+
dataKeys: Set<string>;
|
|
51
|
+
/**
|
|
52
|
+
* - The declared paths, whole.
|
|
53
|
+
*/
|
|
54
|
+
dataPaths: Set<string>;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* One `system.*` target the specification names for one note type.
|
|
58
|
+
*/
|
|
59
|
+
export type MappingClaim = {
|
|
60
|
+
/**
|
|
61
|
+
* - The type whose section makes the claim.
|
|
62
|
+
*/
|
|
63
|
+
noteType: string;
|
|
64
|
+
/**
|
|
65
|
+
* - The system column it sits under, from the header.
|
|
66
|
+
*/
|
|
67
|
+
system: string;
|
|
68
|
+
/**
|
|
69
|
+
* - The shared source cell, stripped of its backticks.
|
|
70
|
+
*/
|
|
71
|
+
source: string;
|
|
72
|
+
/**
|
|
73
|
+
* - The dotted path, `system.` prefix included.
|
|
74
|
+
*/
|
|
75
|
+
target: string;
|
|
76
|
+
/**
|
|
77
|
+
* - 1-based line of the row.
|
|
78
|
+
*/
|
|
79
|
+
line: number;
|
|
80
|
+
/**
|
|
81
|
+
* - 1-based column of the cell's first character.
|
|
82
|
+
*/
|
|
83
|
+
column: number;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* The specification, as data.
|
|
87
|
+
*/
|
|
88
|
+
export type ContentFormat = {
|
|
89
|
+
/**
|
|
90
|
+
* - Where it was read from, for diagnostics.
|
|
91
|
+
*/
|
|
92
|
+
file: string;
|
|
93
|
+
/**
|
|
94
|
+
* - Note type → what its section declares.
|
|
95
|
+
*/
|
|
96
|
+
types: Map<string, TypeSpec>;
|
|
97
|
+
/**
|
|
98
|
+
* - Every `system.*` target, in document order.
|
|
99
|
+
*/
|
|
100
|
+
claims: MappingClaim[];
|
|
101
|
+
};
|
|
@@ -86,15 +86,30 @@ export function auditHomepageLinks(index: ReturnType<typeof buildLinkIndex>): Ar
|
|
|
86
86
|
/**
|
|
87
87
|
* Every link in a tree that lands nowhere.
|
|
88
88
|
*
|
|
89
|
+
* **The two failure modes are separate findings, because they are separate
|
|
90
|
+
* problems** (#131). A piped target the author declared to be an address, and
|
|
91
|
+
* which resolves nowhere, is a typo: every package it could name is either
|
|
92
|
+
* built here or vendored, so there is no third possibility. An unpiped target
|
|
93
|
+
* naming no note of the source's type may be exactly that typo — or a
|
|
94
|
+
* worldbuilding placeholder, which is a long-standing convention in the
|
|
95
|
+
* setting trees. So the first is an error and the second a warning, and the
|
|
96
|
+
* caller can tell them apart without parsing a message.
|
|
97
|
+
*
|
|
89
98
|
* @param {ReturnType<typeof buildLinkIndex>} index - The built index.
|
|
90
99
|
* @returns {{deadAnchors: object[], deadAddresses: object[],
|
|
100
|
+
* deadAliases: object[], aliasCollisions: object[],
|
|
91
101
|
* frontmatterLinks: object[], homepageLinks: object[],
|
|
92
102
|
* usedManifest: Set<string>}} The findings, and which addresses a foreign
|
|
93
|
-
* manifest answered.
|
|
103
|
+
* manifest answered. Each `deadAddresses` entry carries a `reason`:
|
|
104
|
+
* `"not-an-address"` when the target does not parse as one at all,
|
|
105
|
+
* `"unknown-type"` when it is qualified but names no known type, and
|
|
106
|
+
* `"unresolved"` when it parses and nothing answers it.
|
|
94
107
|
*/
|
|
95
108
|
export function auditLinks(index: ReturnType<typeof buildLinkIndex>): {
|
|
96
109
|
deadAnchors: object[];
|
|
97
110
|
deadAddresses: object[];
|
|
111
|
+
deadAliases: object[];
|
|
112
|
+
aliasCollisions: object[];
|
|
98
113
|
frontmatterLinks: object[];
|
|
99
114
|
homepageLinks: object[];
|
|
100
115
|
usedManifest: Set<string>;
|
|
@@ -39,6 +39,12 @@ export function lintContentTree(contentBase: string, { skipDirectories, contentP
|
|
|
39
39
|
/**
|
|
40
40
|
* The shape every `shortcode` must match: ASCII letters and digits only.
|
|
41
41
|
*
|
|
42
|
+
* This is {@link ADDRESS_SEGMENT_PATTERN}, not a second copy of it. A shortcode
|
|
43
|
+
* is the last segment of a canonical address, and the rule it is held to is the
|
|
44
|
+
* rule *every* segment is held to — so the two are one constant rather than two
|
|
45
|
+
* free to drift apart (#59). The name survives because this is where the rule
|
|
46
|
+
* is applied to a note.
|
|
47
|
+
*
|
|
42
48
|
* Case is deliberately **not** constrained: hundreds of authored shortcodes are
|
|
43
49
|
* mixed-case and collide with nothing, so tightening that is a separate
|
|
44
50
|
* decision from this one.
|
|
@@ -210,3 +210,32 @@ export function positionOfYamlPath(text: string, keyPath: ReadonlyArray<string |
|
|
|
210
210
|
* @returns {Array<string|number>} Its segments, sequence indices as numbers.
|
|
211
211
|
*/
|
|
212
212
|
export function yamlKeyPath(field: string): Array<string | number>;
|
|
213
|
+
/**
|
|
214
|
+
* Where a key sits **inside a note's frontmatter fence**, addressed by path.
|
|
215
|
+
*
|
|
216
|
+
* {@link positionInFrontmatter} searches the fence for a key by name, which is
|
|
217
|
+
* the right tool while a note's vocabulary is flat: a key appears once and the
|
|
218
|
+
* first line matching it is the one. It stops being the right tool the moment
|
|
219
|
+
* the same name is legal in two regions — `data.weight` and a top-level
|
|
220
|
+
* `weight` are different keys, and a search finds whichever comes first.
|
|
221
|
+
*
|
|
222
|
+
* So this is the located form for the closed regions (#128). It reuses
|
|
223
|
+
* {@link positionOfYamlPath} rather than repeating its parse, which is what
|
|
224
|
+
* keeps a finding about `data.weight` pointing at `data.weight`.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} raw - The note's full contents, frontmatter included.
|
|
227
|
+
* @param {ReadonlyArray<string|number>} keyPath - Path to the node, from the
|
|
228
|
+
* top of the frontmatter.
|
|
229
|
+
* @param {object} [opts]
|
|
230
|
+
* @param {boolean} [opts.key=false] - Report where the last segment is
|
|
231
|
+
* *declared* rather than where its value sits.
|
|
232
|
+
* @returns {{line?: number, column?: number}} Spreadable position fields, empty
|
|
233
|
+
* when there is no fence or the path resolves to nothing — dropped rather
|
|
234
|
+
* than guessed.
|
|
235
|
+
*/
|
|
236
|
+
export function positionOfFrontmatterPath(raw: string, keyPath: ReadonlyArray<string | number>, { key }?: {
|
|
237
|
+
key?: boolean | undefined;
|
|
238
|
+
}): {
|
|
239
|
+
line?: number;
|
|
240
|
+
column?: number;
|
|
241
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One row of a system's map: what a note of this type becomes.
|
|
3
|
+
*
|
|
4
|
+
* @typedef {object} DocumentSubtypeRow
|
|
5
|
+
* @property {string} document - The Foundry document class — `"Item"`,
|
|
6
|
+
* `"Actor"`, and so on. It must agree with what the engine's own
|
|
7
|
+
* {@link packForType} routes the type to; they are two statements about one
|
|
8
|
+
* fact, and a test holds them together.
|
|
9
|
+
* @property {string} [subType] - The document subtype, for a one-to-one row.
|
|
10
|
+
* Mutually exclusive with `discriminator`.
|
|
11
|
+
* @property {string} [discriminator] - For a one-to-many row: the key the note
|
|
12
|
+
* supplies in this system's own frontmatter block to say which subtype it
|
|
13
|
+
* is.
|
|
14
|
+
* @property {readonly string[]} [subTypes] - The subtypes `discriminator` may
|
|
15
|
+
* name. Required with it, and never empty — a row permitting nothing is a
|
|
16
|
+
* row that can never resolve.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* A system's whole declaration: which system it belongs to, which frontmatter
|
|
20
|
+
* block its notes write, and every note type it maps.
|
|
21
|
+
*
|
|
22
|
+
* @typedef {object} DocumentSubtypeMap
|
|
23
|
+
* @property {string} system - The system this map belongs to (`"sohl"`).
|
|
24
|
+
* @property {string} block - The note frontmatter block this system's notes
|
|
25
|
+
* write, and the only place a discriminator is read from. Defaults to
|
|
26
|
+
* `system`.
|
|
27
|
+
* @property {Readonly<Record<string, DocumentSubtypeRow>>} types - Note type →
|
|
28
|
+
* row.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Declare one system's note-type → document-subtype map.
|
|
32
|
+
*
|
|
33
|
+
* Every malformed row is refused **here**, at module evaluation, rather than
|
|
34
|
+
* when some note happens to reach it: a map is a small piece of authored data
|
|
35
|
+
* loaded once per build, so a mistake in it should stop the build immediately
|
|
36
|
+
* and name the row, not surface as a missing document a thousand notes later.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} declaration - The system's declaration.
|
|
39
|
+
* @param {string} declaration.system - The system this map belongs to.
|
|
40
|
+
* @param {string} [declaration.block] - The frontmatter block its notes write.
|
|
41
|
+
* Defaults to `system`.
|
|
42
|
+
* @param {Readonly<Record<string, DocumentSubtypeRow>>} declaration.types -
|
|
43
|
+
* Note type → row.
|
|
44
|
+
* @returns {DocumentSubtypeMap} The frozen map, rows and all.
|
|
45
|
+
* @throws {Error} When the declaration names no system, or any row is neither
|
|
46
|
+
* of the two permitted shapes.
|
|
47
|
+
*/
|
|
48
|
+
export function defineDocumentSubtypes({ system, block, types }?: {
|
|
49
|
+
system: string;
|
|
50
|
+
block?: string | undefined;
|
|
51
|
+
types: Readonly<Record<string, DocumentSubtypeRow>>;
|
|
52
|
+
}): DocumentSubtypeMap;
|
|
53
|
+
/**
|
|
54
|
+
* The row a system declares for a note type, or nothing.
|
|
55
|
+
*
|
|
56
|
+
* @param {DocumentSubtypeMap} map - The system's map.
|
|
57
|
+
* @param {string|undefined} noteType - The note's declared `type`.
|
|
58
|
+
* @returns {Readonly<DocumentSubtypeRow>|undefined} The row, or `undefined`
|
|
59
|
+
* where this system maps the type at all.
|
|
60
|
+
*/
|
|
61
|
+
export function subtypeRow(map: DocumentSubtypeMap, noteType: string | undefined): Readonly<DocumentSubtypeRow> | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Whether a system maps a note type — optionally, onto one document class.
|
|
64
|
+
*
|
|
65
|
+
* This is what a pass asks to decide whether it claims a note, which is the
|
|
66
|
+
* whole of the "no mapping, no document" property: a type the map does not
|
|
67
|
+
* carry is skipped exactly as quietly as the thousands of notes that belong to
|
|
68
|
+
* another pass.
|
|
69
|
+
*
|
|
70
|
+
* @param {DocumentSubtypeMap} map - The system's map.
|
|
71
|
+
* @param {string|undefined} noteType - The note's declared `type`.
|
|
72
|
+
* @param {string} [document] - Restrict the question to one document class.
|
|
73
|
+
* @returns {boolean} True when the map carries a matching row.
|
|
74
|
+
*/
|
|
75
|
+
export function mapsNoteType(map: DocumentSubtypeMap, noteType: string | undefined, document?: string): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Every note type a system maps onto one document class, sorted.
|
|
78
|
+
*
|
|
79
|
+
* A pass reports what it skipped by naming what it would have claimed, and
|
|
80
|
+
* that list is the map's — never a constant restating it.
|
|
81
|
+
*
|
|
82
|
+
* @param {DocumentSubtypeMap} map - The system's map.
|
|
83
|
+
* @param {string} document - The Foundry document class.
|
|
84
|
+
* @returns {string[]} The note types, in sorted order.
|
|
85
|
+
*/
|
|
86
|
+
export function noteTypesFor(map: DocumentSubtypeMap, document: string): string[];
|
|
87
|
+
/**
|
|
88
|
+
* The document subtype a note compiles into for one system.
|
|
89
|
+
*
|
|
90
|
+
* @param {DocumentSubtypeMap} map - The system's map.
|
|
91
|
+
* @param {string|undefined} noteType - The note's declared `type`.
|
|
92
|
+
* @param {object} fm - The note's frontmatter, read only for a one-to-many
|
|
93
|
+
* row's discriminator.
|
|
94
|
+
* @param {object} [options] - Options.
|
|
95
|
+
* @param {string} [options.file] - The note's path, appended to the message.
|
|
96
|
+
* Omit it where the caller emits through a diagnostic, which already puts
|
|
97
|
+
* the locator at the start of the line.
|
|
98
|
+
* @param {string} [options.absPath] - The note's file on disk, read only on
|
|
99
|
+
* the failing path to locate the offending line. The position rides on the
|
|
100
|
+
* thrown error as `position`, for a caller that emits a diagnostic.
|
|
101
|
+
* @returns {string|undefined} The subtype, or `undefined` where this system
|
|
102
|
+
* maps nothing for the type — which means no document, not an error.
|
|
103
|
+
* @throws {Error} When a one-to-many row's discriminator is absent, blank, or
|
|
104
|
+
* names a value the row does not permit.
|
|
105
|
+
*/
|
|
106
|
+
export function documentSubtype(map: DocumentSubtypeMap, noteType: string | undefined, fm: object, { file, absPath }?: {
|
|
107
|
+
file?: string | undefined;
|
|
108
|
+
absPath?: string | undefined;
|
|
109
|
+
}): string | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* The answer {@link referencedSubtype} gives: a subtype, or why there is none.
|
|
112
|
+
*
|
|
113
|
+
* Exactly one of the two fields is set. A `problem` is a sentence a caller
|
|
114
|
+
* prefixes with its own context and emits as a finding — it never throws,
|
|
115
|
+
* because the caller resolving a reference is walking a list and has to report
|
|
116
|
+
* this one and carry on.
|
|
117
|
+
*
|
|
118
|
+
* @typedef {object} ReferencedSubtype
|
|
119
|
+
* @property {string} [subType] - The document subtype the reference addresses.
|
|
120
|
+
* @property {string} [problem] - Why the reference names no document subtype.
|
|
121
|
+
*/
|
|
122
|
+
/**
|
|
123
|
+
* The document subtype a `(type, shortcode)` **reference** addresses (#140).
|
|
124
|
+
*
|
|
125
|
+
* A being's frontmatter names each embedded item by the *note's* type — the
|
|
126
|
+
* vocabulary an author writes — while the predefined items it resolves against
|
|
127
|
+
* are compiled documents, which carry only the *subtype*. One side has to
|
|
128
|
+
* translate, and it is this one: the map is a function from note type to
|
|
129
|
+
* subtype by construction, whereas the reverse is not — two note types may
|
|
130
|
+
* compile into one subtype, and a compiled document records nothing about the
|
|
131
|
+
* note that produced it. So the addresses stay keyed on the **document
|
|
132
|
+
* subtype**, which is the only vocabulary both a local pack and an extracted
|
|
133
|
+
* dependency catalogue actually carry, and a reference is translated forward
|
|
134
|
+
* here before it is looked up.
|
|
135
|
+
*
|
|
136
|
+
* Four answers, and only the first resolves:
|
|
137
|
+
*
|
|
138
|
+
* - _A one-to-one row_ → the subtype it declares. `armor` addresses an
|
|
139
|
+
* `armorgear`.
|
|
140
|
+
* - _No row at all_ → the note type itself. A consumer declares its own item
|
|
141
|
+
* types in its `itemBuilders` table rather than in this system's map, and the
|
|
142
|
+
* Item pass stamps such a document with the note type; the reference has to
|
|
143
|
+
* agree, or a consumer's own items would stop resolving the moment a map
|
|
144
|
+
* existed.
|
|
145
|
+
* - _A row for another document class_ → a problem. A being is not an item,
|
|
146
|
+
* however the address is spelled.
|
|
147
|
+
* - _A one-to-many row_ → a problem naming the candidates. The note that owns
|
|
148
|
+
* such a row resolves it from its own frontmatter block; a reference has no
|
|
149
|
+
* block, so nothing here can choose, and choosing anyway would be right about
|
|
150
|
+
* half the time. No system declares a one-to-many **Item** row today, so this
|
|
151
|
+
* is a guard rather than a behaviour — but it is a loud one, which is the
|
|
152
|
+
* whole point of the issue.
|
|
153
|
+
*
|
|
154
|
+
* A **retired** spelling is refused by name before any of that. Without it a
|
|
155
|
+
* reference left behind by a rename would take the unmapped fallback and
|
|
156
|
+
* address a document of the old name — resolving silently, which is precisely
|
|
157
|
+
* what a retirement exists to stop (#78).
|
|
158
|
+
*
|
|
159
|
+
* @param {DocumentSubtypeMap} map - The system's map.
|
|
160
|
+
* @param {string|undefined} noteType - The type the reference names.
|
|
161
|
+
* @param {string} document - The Foundry document class the reference must
|
|
162
|
+
* address — `"Item"` for a being's embedded items.
|
|
163
|
+
* @returns {ReferencedSubtype} The subtype, or why there is none.
|
|
164
|
+
*/
|
|
165
|
+
export function referencedSubtype(map: DocumentSubtypeMap, noteType: string | undefined, document: string): ReferencedSubtype;
|
|
166
|
+
/**
|
|
167
|
+
* One row of a system's map: what a note of this type becomes.
|
|
168
|
+
*/
|
|
169
|
+
export type DocumentSubtypeRow = {
|
|
170
|
+
/**
|
|
171
|
+
* - The Foundry document class — `"Item"`,
|
|
172
|
+
* `"Actor"`, and so on. It must agree with what the engine's own
|
|
173
|
+
* {@link packForType} routes the type to; they are two statements about one
|
|
174
|
+
* fact, and a test holds them together.
|
|
175
|
+
*/
|
|
176
|
+
document: string;
|
|
177
|
+
/**
|
|
178
|
+
* - The document subtype, for a one-to-one row.
|
|
179
|
+
* Mutually exclusive with `discriminator`.
|
|
180
|
+
*/
|
|
181
|
+
subType?: string | undefined;
|
|
182
|
+
/**
|
|
183
|
+
* - For a one-to-many row: the key the note
|
|
184
|
+
* supplies in this system's own frontmatter block to say which subtype it
|
|
185
|
+
* is.
|
|
186
|
+
*/
|
|
187
|
+
discriminator?: string | undefined;
|
|
188
|
+
/**
|
|
189
|
+
* - The subtypes `discriminator` may
|
|
190
|
+
* name. Required with it, and never empty — a row permitting nothing is a
|
|
191
|
+
* row that can never resolve.
|
|
192
|
+
*/
|
|
193
|
+
subTypes?: readonly string[] | undefined;
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* A system's whole declaration: which system it belongs to, which frontmatter
|
|
197
|
+
* block its notes write, and every note type it maps.
|
|
198
|
+
*/
|
|
199
|
+
export type DocumentSubtypeMap = {
|
|
200
|
+
/**
|
|
201
|
+
* - The system this map belongs to (`"sohl"`).
|
|
202
|
+
*/
|
|
203
|
+
system: string;
|
|
204
|
+
/**
|
|
205
|
+
* - The note frontmatter block this system's notes
|
|
206
|
+
* write, and the only place a discriminator is read from. Defaults to
|
|
207
|
+
* `system`.
|
|
208
|
+
*/
|
|
209
|
+
block: string;
|
|
210
|
+
/**
|
|
211
|
+
* - Note type →
|
|
212
|
+
* row.
|
|
213
|
+
*/
|
|
214
|
+
types: Readonly<Record<string, DocumentSubtypeRow>>;
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* The answer {@link referencedSubtype} gives: a subtype, or why there is none.
|
|
218
|
+
*
|
|
219
|
+
* Exactly one of the two fields is set. A `problem` is a sentence a caller
|
|
220
|
+
* prefixes with its own context and emits as a finding — it never throws,
|
|
221
|
+
* because the caller resolving a reference is walking a list and has to report
|
|
222
|
+
* this one and carry on.
|
|
223
|
+
*/
|
|
224
|
+
export type ReferencedSubtype = {
|
|
225
|
+
/**
|
|
226
|
+
* - The document subtype the reference addresses.
|
|
227
|
+
*/
|
|
228
|
+
subType?: string | undefined;
|
|
229
|
+
/**
|
|
230
|
+
* - Why the reference names no document subtype.
|
|
231
|
+
*/
|
|
232
|
+
problem?: string | undefined;
|
|
233
|
+
};
|
|
@@ -1,31 +1,37 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Write `value` at a dotted path, creating the intermediate objects.
|
|
3
|
-
*
|
|
4
|
-
* Insertion order is the emitted JSON's key order, so a declaration's order is
|
|
5
|
-
* the compiled document's order — which is what lets a field list replace a
|
|
6
|
-
* hand-written object literal without changing a single byte of output.
|
|
7
|
-
*
|
|
8
|
-
* @param {object} target - The object to write into (mutated).
|
|
9
|
-
* @param {string} dotted - Path, e.g. `"locations.flexible"`.
|
|
10
|
-
* @param {any} value - The value to set.
|
|
11
|
-
* @returns {object} `target`, for chaining.
|
|
12
|
-
*/
|
|
13
|
-
export function setPath(target: object, dotted: string, value: any): object;
|
|
14
1
|
/**
|
|
15
2
|
* Read one declared field out of a note's frontmatter.
|
|
16
3
|
*
|
|
4
|
+
* The *position* is resolved by {@link resolveFieldValue} — `<system>.system`
|
|
5
|
+
* first, then the legacy in-block key, then the declared shared source, then
|
|
6
|
+
* the default (#58). The **coercion** is applied here, once, wherever the value
|
|
7
|
+
* came from: a field's `read` is a statement about the field, not about where
|
|
8
|
+
* an author happened to write it, so `weight: "7"` reads as `7` at every one of
|
|
9
|
+
* those positions.
|
|
10
|
+
*
|
|
17
11
|
* @param {FieldSpec} field - The declaration.
|
|
18
12
|
* @param {object} fm - The note's frontmatter.
|
|
13
|
+
* @param {object} [options] - Options.
|
|
14
|
+
* @param {string} [options.block="sohl"] - Which system's block to resolve
|
|
15
|
+
* against. The default is the one block every existing tree authors; a
|
|
16
|
+
* second system passes its own.
|
|
19
17
|
* @returns {any} The value to emit.
|
|
20
18
|
*/
|
|
21
|
-
export function readField(field: FieldSpec, fm: object
|
|
19
|
+
export function readField(field: FieldSpec, fm: object, { block }?: {
|
|
20
|
+
block?: string | undefined;
|
|
21
|
+
}): any;
|
|
22
22
|
/**
|
|
23
23
|
* Turn a field declaration into the builder it declares.
|
|
24
24
|
*
|
|
25
25
|
* @param {readonly FieldSpec[]} fields - The declaration, in emission order.
|
|
26
|
+
* @param {object} [options] - Options.
|
|
27
|
+
* @param {string} [options.block="sohl"] - Which system's block the builder
|
|
28
|
+
* reads. One declaration compiles against any block, which is what lets two
|
|
29
|
+
* systems declare the same shared source and different destinations.
|
|
26
30
|
* @returns {(fm: object) => object} A `system`-block builder.
|
|
27
31
|
*/
|
|
28
|
-
export function buildFromFields(fields: readonly FieldSpec[]
|
|
32
|
+
export function buildFromFields(fields: readonly FieldSpec[], { block }?: {
|
|
33
|
+
block?: string | undefined;
|
|
34
|
+
}): (fm: object) => object;
|
|
29
35
|
/**
|
|
30
36
|
* The fields of a declaration an author actually writes.
|
|
31
37
|
*
|
|
@@ -38,12 +44,25 @@ export function buildFromFields(fields: readonly FieldSpec[]): (fm: object) => o
|
|
|
38
44
|
* @returns {FieldSpec[]} Only the fields with a frontmatter `name`.
|
|
39
45
|
*/
|
|
40
46
|
export function authoredFields(fields: readonly FieldSpec[]): FieldSpec[];
|
|
47
|
+
export { setPath };
|
|
41
48
|
/**
|
|
42
49
|
* @typedef {object} FieldSpec
|
|
43
|
-
* @property {string} to - Dotted path in the emitted `system` block
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
50
|
+
* @property {string} to - Dotted path in the emitted `system` block — and,
|
|
51
|
+
* since #58, the path a note authors the field at directly:
|
|
52
|
+
* `<system>.system.<to>`.
|
|
53
|
+
* @property {string} [name] - The **shared, top-level property this field draws
|
|
54
|
+
* from** when the note authors no value at `<system>.system.<to>`. Dotted for
|
|
55
|
+
* a path into a shared container (`data.portrait`), which is now the ordinary
|
|
56
|
+
* case: `data:` (#128) puts every type-specific fact under one.
|
|
57
|
+
*
|
|
58
|
+
* It used to mean "frontmatter key under `sohl:`", and that reading is the
|
|
59
|
+
* degenerate case where the shared source and the system destination happen
|
|
60
|
+
* to share a name. They constantly do not — one shared `data.portrait` feeds
|
|
61
|
+
* `sohl.system.portrait` *and* `hm3.system.bioImage` — so the source is
|
|
62
|
+
* declared rather than matched by spelling (#58). The in-block position is
|
|
63
|
+
* still read, second, until #126 moves the corpus off it.
|
|
64
|
+
*
|
|
65
|
+
* Absent means the value is not authored at all — see `value`.
|
|
47
66
|
* @property {string} [shape] - Human-readable shape, for documentation. Comes
|
|
48
67
|
* paired with `read` from one of the coercion constants below.
|
|
49
68
|
* @property {(raw: any, ctx: {fm: object, field: FieldSpec}) => any} [read] -
|
|
@@ -51,6 +70,15 @@ export function authoredFields(fields: readonly FieldSpec[]): FieldSpec[];
|
|
|
51
70
|
* @property {any} [default] - Emitted when the note does not carry the field.
|
|
52
71
|
* @property {boolean} [required] - Whether a note must carry it. A required
|
|
53
72
|
* field's `read` is expected to throw when it is missing.
|
|
73
|
+
* @property {boolean} [shared] - Whether the field is authored at the note's
|
|
74
|
+
* **top level** rather than inside a system block, because what it states is
|
|
75
|
+
* not system-specific — a map's background art is the same art whichever
|
|
76
|
+
* system reads the note. It changes no reader: the top level is already the
|
|
77
|
+
* third step of {@link module:engine/system-block.resolveFieldValue}'s order,
|
|
78
|
+
* so the value resolves whichever region carries it. What it tells the
|
|
79
|
+
* author-facing surfaces is which of the two is the field's *home*, so a
|
|
80
|
+
* message about it names `img` rather than sending an author to `sohl.img`
|
|
81
|
+
* (#142).
|
|
54
82
|
* @property {"string"|"number"|"boolean"|"list"|"map"} [kind] - The value's
|
|
55
83
|
* shape, for the frontmatter linter (#19). Distinct from `shape`, which is
|
|
56
84
|
* prose for a reader, and from `read`, which is what the compiler does: a
|
|
@@ -121,13 +149,25 @@ export const BLANK_IS_DEFAULT: Readonly<{
|
|
|
121
149
|
}>;
|
|
122
150
|
export type FieldSpec = {
|
|
123
151
|
/**
|
|
124
|
-
* - Dotted path in the emitted `system` block
|
|
152
|
+
* - Dotted path in the emitted `system` block — and,
|
|
153
|
+
* since #58, the path a note authors the field at directly:
|
|
154
|
+
* `<system>.system.<to>`.
|
|
125
155
|
*/
|
|
126
156
|
to: string;
|
|
127
157
|
/**
|
|
128
|
-
* -
|
|
129
|
-
*
|
|
130
|
-
* `
|
|
158
|
+
* - The **shared, top-level property this field draws
|
|
159
|
+
* from** when the note authors no value at `<system>.system.<to>`. Dotted for
|
|
160
|
+
* a path into a shared container (`data.portrait`), which is now the ordinary
|
|
161
|
+
* case: `data:` (#128) puts every type-specific fact under one.
|
|
162
|
+
*
|
|
163
|
+
* It used to mean "frontmatter key under `sohl:`", and that reading is the
|
|
164
|
+
* degenerate case where the shared source and the system destination happen
|
|
165
|
+
* to share a name. They constantly do not — one shared `data.portrait` feeds
|
|
166
|
+
* `sohl.system.portrait` *and* `hm3.system.bioImage` — so the source is
|
|
167
|
+
* declared rather than matched by spelling (#58). The in-block position is
|
|
168
|
+
* still read, second, until #126 moves the corpus off it.
|
|
169
|
+
*
|
|
170
|
+
* Absent means the value is not authored at all — see `value`.
|
|
131
171
|
*/
|
|
132
172
|
name?: string | undefined;
|
|
133
173
|
/**
|
|
@@ -152,6 +192,18 @@ export type FieldSpec = {
|
|
|
152
192
|
* field's `read` is expected to throw when it is missing.
|
|
153
193
|
*/
|
|
154
194
|
required?: boolean | undefined;
|
|
195
|
+
/**
|
|
196
|
+
* - Whether the field is authored at the note's
|
|
197
|
+
* **top level** rather than inside a system block, because what it states is
|
|
198
|
+
* not system-specific — a map's background art is the same art whichever
|
|
199
|
+
* system reads the note. It changes no reader: the top level is already the
|
|
200
|
+
* third step of {@link module :engine/system-block.resolveFieldValue}'s order,
|
|
201
|
+
* so the value resolves whichever region carries it. What it tells the
|
|
202
|
+
* author-facing surfaces is which of the two is the field's *home*, so a
|
|
203
|
+
* message about it names `img` rather than sending an author to `sohl.img`
|
|
204
|
+
* (#142).
|
|
205
|
+
*/
|
|
206
|
+
shared?: boolean | undefined;
|
|
155
207
|
/**
|
|
156
208
|
* - The value's
|
|
157
209
|
* shape, for the frontmatter linter (#19). Distinct from `shape`, which is
|
|
@@ -179,3 +231,4 @@ export type FieldSpec = {
|
|
|
179
231
|
*/
|
|
180
232
|
describe: string;
|
|
181
233
|
};
|
|
234
|
+
import { setPath } from "./system-block.mjs";
|
|
@@ -20,11 +20,25 @@ export function matchesKind(value: unknown, kind: string): boolean;
|
|
|
20
20
|
* @param {Record<string, readonly object[]>} opts.schemas - Type → declaration.
|
|
21
21
|
* @param {object} [opts.index] - The link index, for the reference check. Its
|
|
22
22
|
* absence skips that check rather than reporting every reference as dead.
|
|
23
|
+
* @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
|
|
24
|
+
* it declares, as `engine/note-vocabulary.mjs` states them (#128). Supplied
|
|
25
|
+
* by the caller for the same reason `schemas` is: this module validates a
|
|
26
|
+
* note against whatever its type declares and knows no type names of its
|
|
27
|
+
* own. Its absence skips the `data:` and `subType` checks rather than
|
|
28
|
+
* reporting every key as unknown.
|
|
29
|
+
* @param {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>} [opts.systems]
|
|
30
|
+
* The system blocks to check, and what each accepts. See
|
|
31
|
+
* {@link DEFAULT_SYSTEM_BLOCKS}.
|
|
23
32
|
* @returns {object[]} Findings, each with a locator where one is obtainable.
|
|
24
33
|
*/
|
|
25
|
-
export function lintNote(note: object, { schemas, index }: {
|
|
34
|
+
export function lintNote(note: object, { schemas, index, vocabulary, systems }: {
|
|
26
35
|
schemas: Record<string, readonly object[]>;
|
|
27
36
|
index?: object | undefined;
|
|
37
|
+
vocabulary?: Record<string, object> | undefined;
|
|
38
|
+
systems?: Readonly<Record<string, {
|
|
39
|
+
known?: readonly string[];
|
|
40
|
+
fieldVocabulary?: boolean;
|
|
41
|
+
}>> | undefined;
|
|
28
42
|
}): object[];
|
|
29
43
|
/**
|
|
30
44
|
* Check every note in a built index against its type's schema.
|
|
@@ -32,13 +46,22 @@ export function lintNote(note: object, { schemas, index }: {
|
|
|
32
46
|
* @param {object} index - From `buildLinkIndex`.
|
|
33
47
|
* @param {object} opts
|
|
34
48
|
* @param {Record<string, readonly object[]>} opts.schemas - Type → declaration.
|
|
49
|
+
* @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
|
|
50
|
+
* it declares (#128); see {@link lintNote}.
|
|
35
51
|
* @param {boolean} [opts.references=true] - Whether to check references.
|
|
52
|
+
* @param {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>} [opts.systems]
|
|
53
|
+
* The system blocks to check. See {@link DEFAULT_SYSTEM_BLOCKS}.
|
|
36
54
|
* @returns {{findings: object[], notes: number}} The findings, and how many
|
|
37
55
|
* notes were inspected.
|
|
38
56
|
*/
|
|
39
|
-
export function lintFrontmatter(index: object, { schemas, references }: {
|
|
57
|
+
export function lintFrontmatter(index: object, { schemas, vocabulary, references, systems }: {
|
|
40
58
|
schemas: Record<string, readonly object[]>;
|
|
59
|
+
vocabulary?: Record<string, object> | undefined;
|
|
41
60
|
references?: boolean | undefined;
|
|
61
|
+
systems?: Readonly<Record<string, {
|
|
62
|
+
known?: readonly string[];
|
|
63
|
+
fieldVocabulary?: boolean;
|
|
64
|
+
}>> | undefined;
|
|
42
65
|
}): {
|
|
43
66
|
findings: object[];
|
|
44
67
|
notes: number;
|
|
@@ -72,3 +95,25 @@ export function lintFrontmatter(index: object, { schemas, references }: {
|
|
|
72
95
|
* @type {ReadonlySet<string>}
|
|
73
96
|
*/
|
|
74
97
|
export const UNIVERSAL_KEYS: ReadonlySet<string>;
|
|
98
|
+
/**
|
|
99
|
+
* The system blocks a build checks, and what each accepts beyond the shared
|
|
100
|
+
* vocabulary.
|
|
101
|
+
*
|
|
102
|
+
* One entry, because one system is what every existing tree declares — and the
|
|
103
|
+
* default is a *declaration*, not a hard-coded assumption: a build that ships
|
|
104
|
+
* content for two systems passes both, and each block is then checked against
|
|
105
|
+
* its own vocabulary rather than against the other's (#58). A block nothing
|
|
106
|
+
* declares is not checked, because nothing can say what it may carry, and
|
|
107
|
+
* inventing a rule for it would report a correct tree red.
|
|
108
|
+
*
|
|
109
|
+
* `fieldVocabulary` says the note type's own declared field names are keys of
|
|
110
|
+
* this block. True for `sohl` and untrue in general: those names come from the
|
|
111
|
+
* `itemBuilders` registry that this system declares, and a second system's
|
|
112
|
+
* notes write a second system's fields.
|
|
113
|
+
*
|
|
114
|
+
* @type {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>}
|
|
115
|
+
*/
|
|
116
|
+
export const DEFAULT_SYSTEM_BLOCKS: Readonly<Record<string, {
|
|
117
|
+
known?: readonly string[];
|
|
118
|
+
fieldVocabulary?: boolean;
|
|
119
|
+
}>>;
|