@heroiclands/package-build 19.0.0 → 20.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 +624 -0
- package/CONTENT.md +79 -8
- package/bin/content-build.mjs +7 -1
- package/content-config.mjs +10 -1
- package/docs/content-format.md +394 -72
- package/engine/actor-compiler.mjs +197 -7
- package/engine/address-charset.mjs +23 -5
- package/engine/base-compiler.mjs +63 -2
- package/engine/bundles.mjs +9 -0
- package/engine/content-address.mjs +92 -1
- package/engine/content-format.mjs +102 -0
- package/engine/content-index.mjs +11 -8
- package/engine/content-links.mjs +37 -21
- package/engine/field-reference.mjs +57 -5
- package/engine/field-spec.mjs +214 -7
- package/engine/folder-notes.mjs +24 -1
- package/engine/foreign-catalog.mjs +4 -1
- package/engine/foundry-entries.mjs +14 -0
- package/engine/frontmatter-lint.mjs +186 -27
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +67 -10
- package/engine/helpers.mjs +86 -9
- package/engine/index.mjs +3 -0
- package/engine/item-compiler.mjs +37 -0
- package/engine/journals.mjs +21 -4
- package/engine/macros.mjs +8 -0
- package/engine/map-notes.mjs +7 -7
- package/engine/note-ids.mjs +25 -1
- package/engine/note-vocabulary.mjs +76 -9
- package/engine/retired-fields.mjs +57 -16
- package/engine/runtime-only-fields.mjs +204 -0
- package/engine/scenes.mjs +12 -19
- package/engine/schema-check.mjs +23 -1
- package/engine/site-index.mjs +17 -0
- package/engine/subtype-registry.mjs +30 -0
- package/engine/system-block.mjs +81 -3
- package/engine/web-wikilinks.mjs +33 -27
- package/engine/wikilink-syntax.mjs +7 -0
- package/engine/wikilinks.mjs +74 -16
- package/hm3/actors.mjs +63 -13
- package/package.json +2 -2
- package/sohl/actors.mjs +106 -7
- package/sohl/item-fields.mjs +203 -0
- package/sohl/note-schemas.mjs +6 -3
- package/types/engine/actor-compiler.d.mts +83 -3
- package/types/engine/address-charset.d.mts +22 -4
- package/types/engine/base-compiler.d.mts +54 -3
- package/types/engine/content-address.d.mts +64 -0
- package/types/engine/content-format.d.mts +9 -0
- package/types/engine/field-spec.d.mts +271 -3
- package/types/engine/folder-notes.d.mts +20 -0
- package/types/engine/foundry-entries.d.mts +6 -0
- package/types/engine/frontmatter-lint.d.mts +18 -2
- package/types/engine/frontmatter.d.mts +11 -11
- package/types/engine/generate.d.mts +27 -0
- package/types/engine/helpers.d.mts +37 -9
- package/types/engine/index.d.mts +1 -0
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-ids.d.mts +14 -0
- package/types/engine/retired-fields.d.mts +29 -13
- package/types/engine/runtime-only-fields.d.mts +102 -0
- package/types/engine/schema-check.d.mts +10 -1
- package/types/engine/subtype-registry.d.mts +21 -0
- package/types/engine/system-block.d.mts +28 -2
- package/types/sohl/actors.d.mts +3 -3
|
@@ -113,6 +113,70 @@ export function packageAddress(fm: object): string;
|
|
|
113
113
|
* @returns {string} `package-system-type-shortcode`, lowercased.
|
|
114
114
|
*/
|
|
115
115
|
export function canonicalKey(pkg: string, system: string, type: string, shortcode: string): string;
|
|
116
|
+
/**
|
|
117
|
+
* Which system a frontmatter key path is written under.
|
|
118
|
+
*
|
|
119
|
+
* The **enclosing system block** decides, at any depth within it, and nothing
|
|
120
|
+
* else does: `sohl.items[3].model` and `sohl.system.body.structure` are both
|
|
121
|
+
* `sohl` because both sit under `sohl:`. Everywhere else is {@link NO_SYSTEM} —
|
|
122
|
+
* top-level frontmatter, the shared `data:` container, and body prose, which has
|
|
123
|
+
* no key path at all and passes `undefined`.
|
|
124
|
+
*
|
|
125
|
+
* It is the block rather than the field, so a `WikiLink` field needs no opinion
|
|
126
|
+
* about systems and no per-field table has to be kept in step with the schema.
|
|
127
|
+
*
|
|
128
|
+
* The first segment must **be** a declared system, not merely look like one:
|
|
129
|
+
* `sohlish.items` is a key called `sohlish`, and `notes.sohl.thing` names no
|
|
130
|
+
* block at all.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} [keyPath] - The dotted frontmatter key path, or `undefined`
|
|
133
|
+
* for body prose.
|
|
134
|
+
* @returns {string} The system id, or `none`.
|
|
135
|
+
*/
|
|
136
|
+
export function blockSystem(keyPath?: string): string;
|
|
137
|
+
/**
|
|
138
|
+
* Expand a written address to the one canonical address it names.
|
|
139
|
+
*
|
|
140
|
+
* **An omitted segment defaults from where the link is written** (#336) — it is
|
|
141
|
+
* not a wildcard, and resolution is not a search. Package omitted means the
|
|
142
|
+
* citing note's own; system omitted means {@link blockSystem} of the key path it
|
|
143
|
+
* was written under. So every short form has exactly one expansion, computed
|
|
144
|
+
* before anything is looked up, and there is no candidate set to disambiguate.
|
|
145
|
+
*
|
|
146
|
+
* **Under `none`, a system-bearing type addresses its documentation journal.**
|
|
147
|
+
* A note's `none` address *is* its `doc<type>` entry — the Item is the one with
|
|
148
|
+
* a system — so a prose `[[affiliation-sirvadar|…]]` names the page, which is
|
|
149
|
+
* almost always what prose means. A link that means the Item states the system
|
|
150
|
+
* and gets it. This is the defaulting rule applied, not an exception carved out
|
|
151
|
+
* of it.
|
|
152
|
+
*
|
|
153
|
+
* **Only a type whose own document carries a system is redirected.** A `macro`
|
|
154
|
+
* and the map types have documentation journals too, but their own documents
|
|
155
|
+
* are core ones and already live at `none` — so `<pkg>-none-macro-x` names the
|
|
156
|
+
* Macro and `<pkg>-none-docmacro-x` its journal, two live addresses that the
|
|
157
|
+
* redirect would collapse into one. The test is the note type's own system,
|
|
158
|
+
* not merely whether it has a doc entry.
|
|
159
|
+
*
|
|
160
|
+
* A `doc<type>` written explicitly is `none` **wherever** it appears, even
|
|
161
|
+
* inside a system block: no game system defines a JournalEntry, so there is no
|
|
162
|
+
* other system for one to belong to.
|
|
163
|
+
*
|
|
164
|
+
* @param {{type: string, shortcode: string, package?: string, system?: string,
|
|
165
|
+
* itemDoc?: boolean}} read - A qualifier, as `readQualifier` returns one.
|
|
166
|
+
* @param {{package: string, system?: string}} where - The citing context: the
|
|
167
|
+
* tree's own content package, and the system of the block the link sits in.
|
|
168
|
+
* @returns {string} The canonical `package-system-type-shortcode`.
|
|
169
|
+
*/
|
|
170
|
+
export function expandAddress(read: {
|
|
171
|
+
type: string;
|
|
172
|
+
shortcode: string;
|
|
173
|
+
package?: string;
|
|
174
|
+
system?: string;
|
|
175
|
+
itemDoc?: boolean;
|
|
176
|
+
}, where: {
|
|
177
|
+
package: string;
|
|
178
|
+
system?: string;
|
|
179
|
+
}): string;
|
|
116
180
|
/**
|
|
117
181
|
* Reads a canonical key back into its parts.
|
|
118
182
|
*
|
|
@@ -40,6 +40,9 @@ export const CONTENT_FORMAT_PATH: string;
|
|
|
40
40
|
* property — what a note actually writes. `appearance.eye_color` is authored
|
|
41
41
|
* as `appearance`, so that is the key recorded.
|
|
42
42
|
* @property {Set<string>} dataPaths - The declared paths, whole.
|
|
43
|
+
* @property {string[]} subTypes - The `subType` values the section enumerates,
|
|
44
|
+
* in document order — empty when it states none, which is the ordinary case
|
|
45
|
+
* for a type that has no `subType` at all.
|
|
43
46
|
*/
|
|
44
47
|
/**
|
|
45
48
|
* One `system.*` target the specification names for one note type.
|
|
@@ -99,6 +102,12 @@ export type TypeSpec = {
|
|
|
99
102
|
* - The declared paths, whole.
|
|
100
103
|
*/
|
|
101
104
|
dataPaths: Set<string>;
|
|
105
|
+
/**
|
|
106
|
+
* - The `subType` values the section enumerates,
|
|
107
|
+
* in document order — empty when it states none, which is the ordinary case
|
|
108
|
+
* for a type that has no `subType` at all.
|
|
109
|
+
*/
|
|
110
|
+
subTypes: string[];
|
|
102
111
|
};
|
|
103
112
|
/**
|
|
104
113
|
* One `system.*` target the specification names for one note type.
|
|
@@ -17,6 +17,26 @@
|
|
|
17
17
|
* @returns {boolean} True when the value came from the retiring position.
|
|
18
18
|
*/
|
|
19
19
|
export function readsLegacyKey(field: FieldSpec, from: import("./system-block.mjs").FieldSource): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Whether a resolution read a field from the top-level key `data:` gathered it
|
|
22
|
+
* off — the shared level's retiring position (#332).
|
|
23
|
+
*
|
|
24
|
+
* {@link readsLegacyKey}'s sibling, and the same signal: a finding here counts
|
|
25
|
+
* one note still on the pre-`data:` spelling, so the sweep has something to
|
|
26
|
+
* count down instead of a corpus nobody has surveyed.
|
|
27
|
+
*
|
|
28
|
+
* The `from` tag already carries the whole answer — step 3b is the only thing
|
|
29
|
+
* that produces it, and it produces it only for a `data.` source — so this is a
|
|
30
|
+
* name for the question rather than a second test of it. Named all the same,
|
|
31
|
+
* because a compile-time report and the frontmatter lint both ask it and must
|
|
32
|
+
* not drift apart about what counts.
|
|
33
|
+
*
|
|
34
|
+
* @param {FieldSpec} field - The declaration.
|
|
35
|
+
* @param {import("./system-block.mjs").FieldSource} from - Where
|
|
36
|
+
* {@link resolveFieldValue} said the value came from.
|
|
37
|
+
* @returns {boolean} True when the value came from the retiring top-level key.
|
|
38
|
+
*/
|
|
39
|
+
export function readsRetiredTopLevel(field: FieldSpec, from: import("./system-block.mjs").FieldSource): boolean;
|
|
20
40
|
/**
|
|
21
41
|
* Read one declared field out of a note's frontmatter.
|
|
22
42
|
*
|
|
@@ -38,12 +58,77 @@ export function readsLegacyKey(field: FieldSpec, from: import("./system-block.mj
|
|
|
38
58
|
* rather than a returned list because the caller is a compiler, which already
|
|
39
59
|
* knows the note and how to locate a key in it; this module knows neither and
|
|
40
60
|
* would have to invent a finding shape to say so.
|
|
61
|
+
* @param {(field: FieldSpec) => void} [options.onRetiredTopLevel] - Called with
|
|
62
|
+
* each field read from the top-level key `data:` gathered it off (#332). The
|
|
63
|
+
* shared level's counterpart to `onLegacyKey`, and a separate callback
|
|
64
|
+
* because it is a separate position: a note may have moved one of the two and
|
|
65
|
+
* not the other, and a caller that conflated them would tell its author to
|
|
66
|
+
* fix the wrong line.
|
|
41
67
|
* @returns {any} The value to emit.
|
|
42
68
|
*/
|
|
43
|
-
export function readField(field: FieldSpec, fm: object,
|
|
69
|
+
export function readField(field: FieldSpec, fm: object, options?: {
|
|
44
70
|
block?: string | undefined;
|
|
45
71
|
onLegacyKey?: ((field: FieldSpec) => void) | undefined;
|
|
72
|
+
onRetiredTopLevel?: ((field: FieldSpec) => void) | undefined;
|
|
46
73
|
}): any;
|
|
74
|
+
/**
|
|
75
|
+
* The same read, reporting **where the value came from** as well.
|
|
76
|
+
*
|
|
77
|
+
* {@link readField} answers "what does this field hold", which is what almost
|
|
78
|
+
* every caller wants. A builder has one further question — *should the key be
|
|
79
|
+
* written at all* — and it cannot be answered from the value: `null` from a
|
|
80
|
+
* note and `null` from a declared default are the same value and opposite
|
|
81
|
+
* facts (#329).
|
|
82
|
+
*
|
|
83
|
+
* So the position rides back beside the value, resolved **once**. The
|
|
84
|
+
* alternative is a builder that calls {@link resolveFieldValue} for the source
|
|
85
|
+
* and {@link readField} for the value, which resolves the position twice and
|
|
86
|
+
* states in two places the rule that a field is authored in exactly one.
|
|
87
|
+
*
|
|
88
|
+
* @param {FieldSpec} field - The declaration.
|
|
89
|
+
* @param {object} fm - The note's frontmatter.
|
|
90
|
+
* @param {object} [options] - Options, as {@link readField} takes them.
|
|
91
|
+
* @param {string} [options.block="sohl"] - Which system's block to resolve
|
|
92
|
+
* against.
|
|
93
|
+
* @param {(field: FieldSpec) => void} [options.onLegacyKey] - See
|
|
94
|
+
* {@link readField}.
|
|
95
|
+
* @param {(field: FieldSpec) => void} [options.onRetiredTopLevel] - See
|
|
96
|
+
* {@link readField}.
|
|
97
|
+
* @returns {{value: any, from: import("./system-block.mjs").FieldSource}} The
|
|
98
|
+
* value to emit, and the position it was read from.
|
|
99
|
+
*/
|
|
100
|
+
export function readFieldEntry(field: FieldSpec, fm: object, { block, onLegacyKey, onRetiredTopLevel }?: {
|
|
101
|
+
block?: string | undefined;
|
|
102
|
+
onLegacyKey?: ((field: FieldSpec) => void) | undefined;
|
|
103
|
+
onRetiredTopLevel?: ((field: FieldSpec) => void) | undefined;
|
|
104
|
+
}): {
|
|
105
|
+
value: any;
|
|
106
|
+
from: import("./system-block.mjs").FieldSource;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Whether a note supplied a value for a field, as opposed to a default doing it.
|
|
110
|
+
*
|
|
111
|
+
* The question {@link FieldSpec.omitWhenAbsent} turns on, asked of the
|
|
112
|
+
* *position* rather than of the value — which cannot answer it, since a
|
|
113
|
+
* declared `default: null` and an authored `null` are indistinguishable once
|
|
114
|
+
* the value is in hand (#329).
|
|
115
|
+
*
|
|
116
|
+
* `undefined` counts as absent whatever position reported it, because writing
|
|
117
|
+
* the key then emits a value `JSON.stringify` drops — the key present in the
|
|
118
|
+
* object and absent from the pack, which is the sort of disagreement this
|
|
119
|
+
* package exists to remove. It arrives from one place: the in-block step
|
|
120
|
+
* answers `value ?? field.default` for a key authored as `null`, so a field
|
|
121
|
+
* declaring no default resolves through "authored" to nothing at all. A field
|
|
122
|
+
* that also declares a `read` never reaches this, since its coercion has by
|
|
123
|
+
* then turned the `undefined` into whatever it makes of an absent value —
|
|
124
|
+
* ordinarily `null`, which is a value the note asked for and is emitted.
|
|
125
|
+
*
|
|
126
|
+
* @param {import("./system-block.mjs").FieldSource} from - Where
|
|
127
|
+
* {@link resolveFieldValue} said the value came from.
|
|
128
|
+
* @param {any} value - The value it gave back.
|
|
129
|
+
* @returns {boolean} True when the note wrote one.
|
|
130
|
+
*/
|
|
131
|
+
export function isAuthored(from: import("./system-block.mjs").FieldSource, value: any): boolean;
|
|
47
132
|
/**
|
|
48
133
|
* Turn a field declaration into the builder it declares.
|
|
49
134
|
*
|
|
@@ -55,11 +140,15 @@ export function readField(field: FieldSpec, fm: object, { block, onLegacyKey }?:
|
|
|
55
140
|
* @param {(field: FieldSpec) => void} [options.onLegacyKey] - Passed through to
|
|
56
141
|
* {@link readField}: called with each field the note authored at the position
|
|
57
142
|
* it is being swept off (#305).
|
|
143
|
+
* @param {(field: FieldSpec) => void} [options.onRetiredTopLevel] - Passed
|
|
144
|
+
* through to {@link readField}: called with each field the note authored at
|
|
145
|
+
* the top-level key `data:` gathered it off (#332).
|
|
58
146
|
* @returns {(fm: object) => object} A `system`-block builder.
|
|
59
147
|
*/
|
|
60
|
-
export function buildFromFields(fields: readonly FieldSpec[], { block, onLegacyKey }?: {
|
|
148
|
+
export function buildFromFields(fields: readonly FieldSpec[], { block, onLegacyKey, onRetiredTopLevel }?: {
|
|
61
149
|
block?: string | undefined;
|
|
62
150
|
onLegacyKey?: ((field: FieldSpec) => void) | undefined;
|
|
151
|
+
onRetiredTopLevel?: ((field: FieldSpec) => void) | undefined;
|
|
63
152
|
}): (fm: object) => object;
|
|
64
153
|
/**
|
|
65
154
|
* The fields of a declaration an author actually writes.
|
|
@@ -73,6 +162,20 @@ export function buildFromFields(fields: readonly FieldSpec[], { block, onLegacyK
|
|
|
73
162
|
* @returns {FieldSpec[]} Only the fields with a frontmatter `name`.
|
|
74
163
|
*/
|
|
75
164
|
export function authoredFields(fields: readonly FieldSpec[]): FieldSpec[];
|
|
165
|
+
/**
|
|
166
|
+
* The fields of a declaration a note may **never** write.
|
|
167
|
+
*
|
|
168
|
+
* The complement of {@link authoredFields} in the direction that matters: those
|
|
169
|
+
* are the fields an author may write, these are the ones authoring is an error
|
|
170
|
+
* (#330). Everything else in a declaration — a constant, a derived value — is
|
|
171
|
+
* simply not authored, which is a statement about the *builder* rather than
|
|
172
|
+
* about the author, and says nothing about what happens if a note writes the
|
|
173
|
+
* path anyway.
|
|
174
|
+
*
|
|
175
|
+
* @param {readonly FieldSpec[]} fields - The declaration.
|
|
176
|
+
* @returns {FieldSpec[]} Only the fields declaring `runtimeOnly`.
|
|
177
|
+
*/
|
|
178
|
+
export function runtimeOnlyFields(fields: readonly FieldSpec[]): FieldSpec[];
|
|
76
179
|
/**
|
|
77
180
|
* @typedef {object} FieldSpec
|
|
78
181
|
* @property {string} to - Dotted path in the emitted `system` block — and,
|
|
@@ -135,6 +238,13 @@ export function authoredFields(fields: readonly FieldSpec[]): FieldSpec[];
|
|
|
135
238
|
* describe the *document* rather than the note: `<system>.system.<to>` and
|
|
136
239
|
* the legacy in-block `<system>.<name>`. Absent means the ordinary case —
|
|
137
240
|
* the top level is read, as the third step.
|
|
241
|
+
*
|
|
242
|
+
* **It is read from the other side too**, because the statement is symmetric:
|
|
243
|
+
* if the two positions hold unrelated quantities then the *in-block* position
|
|
244
|
+
* is not the note-level field either, so a check about the note-level field
|
|
245
|
+
* reads past it. `engine/frontmatter-lint.mjs` resolves that through
|
|
246
|
+
* `collidingBlockKeys`. Read for the emitted field alone until #312, which is
|
|
247
|
+
* how an affiliation's office style came to answer for its page heading.
|
|
138
248
|
* @property {string} [shape] - Human-readable shape, for documentation. Comes
|
|
139
249
|
* paired with `read` from one of the coercion constants below.
|
|
140
250
|
* @property {(raw: any, ctx: {fm: object, field: FieldSpec}) => any} [read] -
|
|
@@ -164,6 +274,78 @@ export function authoredFields(fields: readonly FieldSpec[]): FieldSpec[];
|
|
|
164
274
|
* note, so it declares none.
|
|
165
275
|
* @property {any|((fm: object) => any)} [value] - For a field with no `name`:
|
|
166
276
|
* the constant, or a function deriving it from the frontmatter.
|
|
277
|
+
* @property {boolean} [omitWhenAbsent] - **The key is left out entirely when
|
|
278
|
+
* the note does not carry the field** (#329), rather than written from a
|
|
279
|
+
* declared default.
|
|
280
|
+
*
|
|
281
|
+
* Every other field answers absence with a value: an unauthored `weight` is
|
|
282
|
+
* `0`, an unauthored `seat` is `null`. That is right wherever the type has an
|
|
283
|
+
* opinion about the empty case. It is wrong wherever the **DataModel** is the
|
|
284
|
+
* one holding the answer — an affliction's `onsetDurationFormula` has no
|
|
285
|
+
* compile-time value, and writing `null` over it does not merely fail to
|
|
286
|
+
* help: it makes "the author said none" and "the author said nothing"
|
|
287
|
+
* indistinguishable to every reader downstream, and it overwrites an
|
|
288
|
+
* `initial` the system chose on purpose.
|
|
289
|
+
*
|
|
290
|
+
* It is the other conditional row of the same table
|
|
291
|
+
* {@link FieldSpec.runtimeOnly} completes, and the two differ only in what
|
|
292
|
+
* they do about an *authored* value:
|
|
293
|
+
*
|
|
294
|
+
* | declaration | authored | absent |
|
|
295
|
+
* | --- | --- | --- |
|
|
296
|
+
* | ordinary | emitted | default written |
|
|
297
|
+
* | `omitWhenAbsent` | emitted | key omitted |
|
|
298
|
+
* | `runtimeOnly` | refused | key omitted |
|
|
299
|
+
*
|
|
300
|
+
* **A field declaring it must declare no `default`**, and the two are
|
|
301
|
+
* contradictory rather than merely redundant — a default is a value for the
|
|
302
|
+
* absent case, which is the case this says has none. Nor may it be combined
|
|
303
|
+
* with `required` (which fails the build on absence, so nothing is ever
|
|
304
|
+
* omitted) or with `runtimeOnly` (which is never emitted at all). The shipped
|
|
305
|
+
* declarations are checked for all three in `tests/item-fields.test.ts`.
|
|
306
|
+
*
|
|
307
|
+
* Unlike `runtimeOnly` this is a flag rather than a reason, because there is
|
|
308
|
+
* only ever one reason and no message prints it: the DataModel's `initial`
|
|
309
|
+
* stands. What an author needs to know is *that* the field has no default,
|
|
310
|
+
* which the generated reference states in the field's own row.
|
|
311
|
+
* @property {string} [runtimeOnly] - **What the field holds once play has
|
|
312
|
+
* started** — declared on a field the *document* writes for itself, which no
|
|
313
|
+
* note may author (#330).
|
|
314
|
+
*
|
|
315
|
+
* A schema declares plenty of fields a compiled document has no business
|
|
316
|
+
* carrying: an affliction's `onsetDate` is the world time its onset fired
|
|
317
|
+
* at, crystallized when the phase runs. World time does not exist while
|
|
318
|
+
* content is compiled, so there is no authoring-time value — and `0` is
|
|
319
|
+
* itself a valid world time, which is why such a field is nullable rather
|
|
320
|
+
* than sentinelled and why a default cannot stand in for one.
|
|
321
|
+
*
|
|
322
|
+
* Nothing used to stop a note writing one. The three checks that might have
|
|
323
|
+
* each declined for its own correct reason —
|
|
324
|
+
* {@link module:engine/system-block.unknownBlockKeys} reads the block's top
|
|
325
|
+
* level and never descends into `system:`;
|
|
326
|
+
* {@link module:engine/system-block.mergeSystemData} passes through every
|
|
327
|
+
* authored path no declared field claims; and the schema check's fatal
|
|
328
|
+
* direction is *undeclared*, which a field the schema really does declare
|
|
329
|
+
* satisfies. What was missing was a rule saying "declared by the system,
|
|
330
|
+
* but never authorable", and this is it.
|
|
331
|
+
*
|
|
332
|
+
* Declaring it does two things, which are the two directions of one fact:
|
|
333
|
+
*
|
|
334
|
+
* | declaration | authored | absent |
|
|
335
|
+
* | --- | --- | --- |
|
|
336
|
+
* | ordinary | emitted | default written |
|
|
337
|
+
* | runtime-only | **refused** | key omitted |
|
|
338
|
+
*
|
|
339
|
+
* The refusal is {@link module:engine/runtime-only-fields.assertNoRuntimeOnlyFields}'s;
|
|
340
|
+
* the omission is {@link buildFromFields}'s. A runtime-only entry declares a
|
|
341
|
+
* `to` and **no `name`**, so it stays out of {@link authoredFields} and every
|
|
342
|
+
* author-facing surface built on it, while still giving `mergeSystemData` a
|
|
343
|
+
* claimed path and the refusal something to name.
|
|
344
|
+
*
|
|
345
|
+
* **The value is the reason**, as {@link FieldSpec.topLevelMeans}'s is: a
|
|
346
|
+
* boolean would record the decision and lose the case for it, and the reason
|
|
347
|
+
* is what the refusal's message and the generated reference both print. It
|
|
348
|
+
* completes the sentence "it holds …".
|
|
167
349
|
* @property {string} describe - One line, for the author-facing reference.
|
|
168
350
|
*/
|
|
169
351
|
/** Whatever the author wrote, unconverted. */
|
|
@@ -290,6 +472,13 @@ export type FieldSpec = {
|
|
|
290
472
|
* describe the *document* rather than the note: `<system>.system.<to>` and
|
|
291
473
|
* the legacy in-block `<system>.<name>`. Absent means the ordinary case —
|
|
292
474
|
* the top level is read, as the third step.
|
|
475
|
+
*
|
|
476
|
+
* **It is read from the other side too**, because the statement is symmetric:
|
|
477
|
+
* if the two positions hold unrelated quantities then the *in-block* position
|
|
478
|
+
* is not the note-level field either, so a check about the note-level field
|
|
479
|
+
* reads past it. `engine/frontmatter-lint.mjs` resolves that through
|
|
480
|
+
* `collidingBlockKeys`. Read for the emitted field alone until #312, which is
|
|
481
|
+
* how an affiliation's office style came to answer for its page heading.
|
|
293
482
|
*/
|
|
294
483
|
topLevelMeans?: string | undefined;
|
|
295
484
|
/**
|
|
@@ -348,11 +537,90 @@ export type FieldSpec = {
|
|
|
348
537
|
* the constant, or a function deriving it from the frontmatter.
|
|
349
538
|
*/
|
|
350
539
|
value?: any | ((fm: object) => any);
|
|
540
|
+
/**
|
|
541
|
+
* - **The key is left out entirely when
|
|
542
|
+
* the note does not carry the field** (#329), rather than written from a
|
|
543
|
+
* declared default.
|
|
544
|
+
*
|
|
545
|
+
* Every other field answers absence with a value: an unauthored `weight` is
|
|
546
|
+
* `0`, an unauthored `seat` is `null`. That is right wherever the type has an
|
|
547
|
+
* opinion about the empty case. It is wrong wherever the **DataModel** is the
|
|
548
|
+
* one holding the answer — an affliction's `onsetDurationFormula` has no
|
|
549
|
+
* compile-time value, and writing `null` over it does not merely fail to
|
|
550
|
+
* help: it makes "the author said none" and "the author said nothing"
|
|
551
|
+
* indistinguishable to every reader downstream, and it overwrites an
|
|
552
|
+
* `initial` the system chose on purpose.
|
|
553
|
+
*
|
|
554
|
+
* It is the other conditional row of the same table
|
|
555
|
+
* {@link FieldSpec.runtimeOnly} completes, and the two differ only in what
|
|
556
|
+
* they do about an *authored* value:
|
|
557
|
+
*
|
|
558
|
+
* | declaration | authored | absent |
|
|
559
|
+
* | --- | --- | --- |
|
|
560
|
+
* | ordinary | emitted | default written |
|
|
561
|
+
* | `omitWhenAbsent` | emitted | key omitted |
|
|
562
|
+
* | `runtimeOnly` | refused | key omitted |
|
|
563
|
+
*
|
|
564
|
+
* **A field declaring it must declare no `default`**, and the two are
|
|
565
|
+
* contradictory rather than merely redundant — a default is a value for the
|
|
566
|
+
* absent case, which is the case this says has none. Nor may it be combined
|
|
567
|
+
* with `required` (which fails the build on absence, so nothing is ever
|
|
568
|
+
* omitted) or with `runtimeOnly` (which is never emitted at all). The shipped
|
|
569
|
+
* declarations are checked for all three in `tests/item-fields.test.ts`.
|
|
570
|
+
*
|
|
571
|
+
* Unlike `runtimeOnly` this is a flag rather than a reason, because there is
|
|
572
|
+
* only ever one reason and no message prints it: the DataModel's `initial`
|
|
573
|
+
* stands. What an author needs to know is *that* the field has no default,
|
|
574
|
+
* which the generated reference states in the field's own row.
|
|
575
|
+
*/
|
|
576
|
+
omitWhenAbsent?: boolean | undefined;
|
|
577
|
+
/**
|
|
578
|
+
* - **What the field holds once play has
|
|
579
|
+
* started** — declared on a field the *document* writes for itself, which no
|
|
580
|
+
* note may author (#330).
|
|
581
|
+
*
|
|
582
|
+
* A schema declares plenty of fields a compiled document has no business
|
|
583
|
+
* carrying: an affliction's `onsetDate` is the world time its onset fired
|
|
584
|
+
* at, crystallized when the phase runs. World time does not exist while
|
|
585
|
+
* content is compiled, so there is no authoring-time value — and `0` is
|
|
586
|
+
* itself a valid world time, which is why such a field is nullable rather
|
|
587
|
+
* than sentinelled and why a default cannot stand in for one.
|
|
588
|
+
*
|
|
589
|
+
* Nothing used to stop a note writing one. The three checks that might have
|
|
590
|
+
* each declined for its own correct reason —
|
|
591
|
+
* {@link module :engine/system-block.unknownBlockKeys} reads the block's top
|
|
592
|
+
* level and never descends into `system:`;
|
|
593
|
+
* {@link module :engine/system-block.mergeSystemData} passes through every
|
|
594
|
+
* authored path no declared field claims; and the schema check's fatal
|
|
595
|
+
* direction is *undeclared*, which a field the schema really does declare
|
|
596
|
+
* satisfies. What was missing was a rule saying "declared by the system,
|
|
597
|
+
* but never authorable", and this is it.
|
|
598
|
+
*
|
|
599
|
+
* Declaring it does two things, which are the two directions of one fact:
|
|
600
|
+
*
|
|
601
|
+
* | declaration | authored | absent |
|
|
602
|
+
* | --- | --- | --- |
|
|
603
|
+
* | ordinary | emitted | default written |
|
|
604
|
+
* | runtime-only | **refused** | key omitted |
|
|
605
|
+
*
|
|
606
|
+
* The refusal is {@link module :engine/runtime-only-fields.assertNoRuntimeOnlyFields}'s;
|
|
607
|
+
* the omission is {@link buildFromFields}'s. A runtime-only entry declares a
|
|
608
|
+
* `to` and **no `name`**, so it stays out of {@link authoredFields} and every
|
|
609
|
+
* author-facing surface built on it, while still giving `mergeSystemData` a
|
|
610
|
+
* claimed path and the refusal something to name.
|
|
611
|
+
*
|
|
612
|
+
* **The value is the reason**, as {@link FieldSpec.topLevelMeans}'s is: a
|
|
613
|
+
* boolean would record the decision and lose the case for it, and the reason
|
|
614
|
+
* is what the refusal's message and the generated reference both print. It
|
|
615
|
+
* completes the sentence "it holds …".
|
|
616
|
+
*/
|
|
617
|
+
runtimeOnly?: string | undefined;
|
|
351
618
|
/**
|
|
352
619
|
* - One line, for the author-facing reference.
|
|
353
620
|
*/
|
|
354
621
|
describe: string;
|
|
355
622
|
};
|
|
356
623
|
import { legacyKeyOf } from "./system-block.mjs";
|
|
624
|
+
import { retiredTopLevelKey } from "./system-block.mjs";
|
|
357
625
|
import { setPath } from "./system-block.mjs";
|
|
358
|
-
export { legacyKeyOf, setPath };
|
|
626
|
+
export { legacyKeyOf, retiredTopLevelKey, setPath };
|
|
@@ -18,6 +18,26 @@ export function bareAddress(value: string | null): string | null;
|
|
|
18
18
|
* @returns {string} `<pkg>-none-folder-<shortcode>`.
|
|
19
19
|
*/
|
|
20
20
|
export function folderAddress(pkg: string, shortcode: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* The Foundry `_id` a folder note's documents are filed under.
|
|
23
|
+
*
|
|
24
|
+
* **The one derivation, because two passes need the same answer.** The folder
|
|
25
|
+
* pass hashes it here on its way to emitting the `Folder` documents; the
|
|
26
|
+
* content index publishes it, through `noteDocId`, for a consumer who will
|
|
27
|
+
* never run this build. Deriving it twice is how the index came to publish a
|
|
28
|
+
* plausible-looking id that resolved to nothing — the general id rule hashes a
|
|
29
|
+
* note's address under the `document` namespace, and a folder's is hashed under
|
|
30
|
+
* {@link FOLDER_ID_NAMESPACE} (#310).
|
|
31
|
+
*
|
|
32
|
+
* An **authored `id` still wins**, and is applied by the caller: this is the
|
|
33
|
+
* derivation, not the resolution, so the pin rule stays stated once, where
|
|
34
|
+
* every other note type states it ({@link module:engine/note-ids.noteDocId}).
|
|
35
|
+
*
|
|
36
|
+
* @param {string} pkg - The content package.
|
|
37
|
+
* @param {string} shortcode - The folder's shortcode.
|
|
38
|
+
* @returns {string} The folder's 16-character Foundry id.
|
|
39
|
+
*/
|
|
40
|
+
export function folderDocId(pkg: string, shortcode: string): string;
|
|
21
41
|
/**
|
|
22
42
|
* Collect every folder note in a content tree.
|
|
23
43
|
*
|
|
@@ -29,6 +29,12 @@ export function anchorsOf(entryUuid: string, entryId: string, body: string, name
|
|
|
29
29
|
* @param {string} name - The note's display name.
|
|
30
30
|
* @param {string} address - The note's package-relative address.
|
|
31
31
|
* @param {string} body - The note's markdown body.
|
|
32
|
+
* **Each entry carries the `id` of the document it addresses**, not only its
|
|
33
|
+
* UUID. The two are one fact — a UUID ends in the id — but only the entry knows
|
|
34
|
+
* which derivation produced it: an item's is its note's `fm.id`, and its
|
|
35
|
+
* documentation journal's is {@link itemDocEntryId} of that. Stating it here is
|
|
36
|
+
* what lets the content index publish an identity it did not re-derive (#310).
|
|
37
|
+
*
|
|
32
38
|
* @param {object} ctx - Resolved identities: `{ contentPackage,
|
|
33
39
|
* foundryPackageId, packRouter }`.
|
|
34
40
|
* @returns {Array<object>} One or two entries, in {@link buildManifest}'s shape.
|
|
@@ -26,6 +26,12 @@ export function matchesKind(value: unknown, kind: string): boolean;
|
|
|
26
26
|
* note against whatever its type declares and knows no type names of its
|
|
27
27
|
* own. Its absence skips the `data:` and `subType` checks rather than
|
|
28
28
|
* reporting every key as unknown.
|
|
29
|
+
* @param {(type: string) => {document: string|null, art: readonly string[]}|null} [opts.emittedArt]
|
|
30
|
+
* What art a note of one type reaches its document through — the passes' own
|
|
31
|
+
* declaration, asked through `engine/generate.mjs`'s `emittedArtFor`.
|
|
32
|
+
* Supplied by the caller like `schemas`, so this module states no list of
|
|
33
|
+
* iconless types of its own; absent it, an inert `img:` goes unreported
|
|
34
|
+
* rather than every note's being (#349).
|
|
29
35
|
* @param {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>} [opts.systems]
|
|
30
36
|
* The system blocks to check, and what each accepts. See
|
|
31
37
|
* {@link DEFAULT_SYSTEM_BLOCKS}.
|
|
@@ -35,10 +41,14 @@ export function matchesKind(value: unknown, kind: string): boolean;
|
|
|
35
41
|
* about those keys.
|
|
36
42
|
* @returns {object[]} Findings, each with a locator where one is obtainable.
|
|
37
43
|
*/
|
|
38
|
-
export function lintNote(note: object, { schemas, index, vocabulary, packs, systems }: {
|
|
44
|
+
export function lintNote(note: object, { schemas, index, vocabulary, packs, emittedArt, systems }: {
|
|
39
45
|
schemas: Record<string, readonly object[]>;
|
|
40
46
|
index?: object | undefined;
|
|
41
47
|
vocabulary?: Record<string, object> | undefined;
|
|
48
|
+
emittedArt?: ((type: string) => {
|
|
49
|
+
document: string | null;
|
|
50
|
+
art: readonly string[];
|
|
51
|
+
} | null) | undefined;
|
|
42
52
|
systems?: Readonly<Record<string, {
|
|
43
53
|
known?: readonly string[];
|
|
44
54
|
fieldVocabulary?: boolean;
|
|
@@ -58,10 +68,12 @@ export function lintNote(note: object, { schemas, index, vocabulary, packs, syst
|
|
|
58
68
|
* The system blocks to check. See {@link DEFAULT_SYSTEM_BLOCKS}.
|
|
59
69
|
* @param {readonly string[]} [opts.packs] - The declared pack names; see
|
|
60
70
|
* {@link lintNote}.
|
|
71
|
+
* @param {(type: string) => {document: string|null, art: readonly string[]}|null} [opts.emittedArt]
|
|
72
|
+
* What art a type reaches its document through; see {@link lintNote}.
|
|
61
73
|
* @returns {{findings: object[], notes: number}} The findings, and how many
|
|
62
74
|
* notes were inspected.
|
|
63
75
|
*/
|
|
64
|
-
export function lintFrontmatter(index: object, { schemas, vocabulary, packs, references, systems }: {
|
|
76
|
+
export function lintFrontmatter(index: object, { schemas, vocabulary, packs, emittedArt, references, systems }: {
|
|
65
77
|
schemas: Record<string, readonly object[]>;
|
|
66
78
|
vocabulary?: Record<string, object> | undefined;
|
|
67
79
|
references?: boolean | undefined;
|
|
@@ -70,6 +82,10 @@ export function lintFrontmatter(index: object, { schemas, vocabulary, packs, ref
|
|
|
70
82
|
fieldVocabulary?: boolean;
|
|
71
83
|
}>> | undefined;
|
|
72
84
|
packs?: readonly string[] | undefined;
|
|
85
|
+
emittedArt?: ((type: string) => {
|
|
86
|
+
document: string | null;
|
|
87
|
+
art: readonly string[];
|
|
88
|
+
} | null) | undefined;
|
|
73
89
|
}): {
|
|
74
90
|
findings: object[];
|
|
75
91
|
notes: number;
|
|
@@ -132,13 +132,15 @@ export function parseValueDesc(raw: any): {
|
|
|
132
132
|
maxValue: number;
|
|
133
133
|
}[];
|
|
134
134
|
/**
|
|
135
|
-
* The compendium folder a note names
|
|
135
|
+
* The compendium folder a note names.
|
|
136
136
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
137
|
+
* **There is one spelling.** `packFolder:` is a folder note's **address**
|
|
138
|
+
* (`folder-poisonsandtoxins`), resolved through the address index the whole
|
|
139
|
+
* build shares. The `folder:` Foundry-id spelling this function once read
|
|
140
|
+
* beside it, and the per-pack `*-folders.yaml` that id was resolved against,
|
|
141
|
+
* are retired together (#260) — a note declaring `folder:` is refused by
|
|
142
|
+
* {@link module:engine/folder-notes.assertNoDeclaredFolder} rather than
|
|
143
|
+
* reaching here, so there is no second source for a value to come from.
|
|
142
144
|
*
|
|
143
145
|
* **`packFolder` was a path for one release** (`Possessions/Misc_Gear/Cooking`)
|
|
144
146
|
* and is an address now (#255). A path encoded the hierarchy *in the value*, so
|
|
@@ -149,13 +151,11 @@ export function parseValueDesc(raw: any): {
|
|
|
149
151
|
* authors to migrate, which is the whole reason the change was cheap enough to
|
|
150
152
|
* make.
|
|
151
153
|
*
|
|
152
|
-
* `packFolder` wins where both are present. Nothing about `folder` changes: a
|
|
153
|
-
* note that names one is read, resolved and emitted exactly as before, until
|
|
154
|
-
* #260 retires it.
|
|
155
|
-
*
|
|
156
154
|
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
157
155
|
* @returns {{value: string|null, isAddress: boolean}} The authored value, and
|
|
158
|
-
* whether it is a folder note's address.
|
|
156
|
+
* whether it is a folder note's address. `isAddress` is always `true` and is
|
|
157
|
+
* kept so a caller reads the same shape it always did; it distinguished the
|
|
158
|
+
* two spellings, and there is only one left to be.
|
|
159
159
|
*/
|
|
160
160
|
export function folderField(fm: object | null | undefined): {
|
|
161
161
|
value: string | null;
|
|
@@ -8,6 +8,33 @@
|
|
|
8
8
|
* rather than defaulting past.
|
|
9
9
|
*/
|
|
10
10
|
export function compilerFor(docType: string, system?: string | null): Function | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* The art fields a note of one content type reaches its document through, and
|
|
13
|
+
* the document it reaches (#349).
|
|
14
|
+
*
|
|
15
|
+
* **Derived, never listed.** A note's type routes to a document type
|
|
16
|
+
* ({@link packForType}), a document type routes to the pass that compiles it
|
|
17
|
+
* ({@link compilerFor}), and the pass declares which art it emits
|
|
18
|
+
* ({@link BasePackCompiler.emitsArt}). So the answer is assembled from the same
|
|
19
|
+
* three statements the compile itself follows, and a pass that starts or stops
|
|
20
|
+
* emitting art changes this by changing its own declaration. A second table of
|
|
21
|
+
* "types with no image" would be a table free to drift from what is emitted,
|
|
22
|
+
* which is the defect this exists to report rather than to reproduce.
|
|
23
|
+
*
|
|
24
|
+
* **The union across systems**, because a note is compiled by whichever pack
|
|
25
|
+
* claims it: a tree feeding both SoHL and HM3 has two Actor passes, and a field
|
|
26
|
+
* either of them emits is live for the note. Only a field *no* pass emits is
|
|
27
|
+
* inert, and that is the finding this supports.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} type - The note's content type.
|
|
30
|
+
* @returns {{document: string|null, art: readonly string[]}|null} What the type
|
|
31
|
+
* compiles into and the art it carries there, or `null` where no claim can be
|
|
32
|
+
* made — a retired type, which is reported as retired instead.
|
|
33
|
+
*/
|
|
34
|
+
export function emittedArtFor(type: string): {
|
|
35
|
+
document: string | null;
|
|
36
|
+
art: readonly string[];
|
|
37
|
+
} | null;
|
|
11
38
|
/**
|
|
12
39
|
* The generated JSON of **every** configured Item pack — what the actors pass
|
|
13
40
|
* reads its predefined items from.
|
|
@@ -169,19 +169,47 @@ export function systemTemplatePriority(fm: object, label: string): number | null
|
|
|
169
169
|
* alphanumeric runs replaced by underscores.
|
|
170
170
|
*/
|
|
171
171
|
export function makeFilename(name: any, id: any): string;
|
|
172
|
-
/**
|
|
173
|
-
* Standardize a name into a slug: lowercase, apostrophes removed,
|
|
174
|
-
* non-alphanumerics collapsed to single hyphens.
|
|
175
|
-
*/
|
|
176
172
|
/**
|
|
177
173
|
* Translate a content-relative image path into its Foundry-relative form.
|
|
178
174
|
*
|
|
179
175
|
* Content frontmatter (`img` / `portrait`) authors a single path that has to
|
|
180
|
-
* work for Foundry, the knowledgebase, and the website.
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
176
|
+
* work for Foundry, the knowledgebase, and the website. **Its first segment
|
|
177
|
+
* says which package owns the file** (#331), and there are exactly three
|
|
178
|
+
* answers:
|
|
179
|
+
*
|
|
180
|
+
* | Authored path starts with | Owner | Emitted |
|
|
181
|
+
* | ------------------------- | --------------------- | -------------------- |
|
|
182
|
+
* | `systems/` | a separate **system** | unchanged |
|
|
183
|
+
* | `modules/` | a separate **module** | unchanged |
|
|
184
|
+
* | anything else | **this package** | `<assetRoot>/<path>` |
|
|
185
|
+
*
|
|
186
|
+
* So `icons/relic.svg` compiles to `systems/sohl/assets/icons/relic.svg` here
|
|
187
|
+
* and to `modules/sohl-thalorna/assets/icons/relic.svg` in a module — the asset
|
|
188
|
+
* root is derived from the configuration, and is the one place `systems/sohl`
|
|
189
|
+
* is ever spelled (#1508). An authored
|
|
190
|
+
* `systems/sohl/assets/icons/noun/shield.svg` is left exactly as written,
|
|
191
|
+
* whichever package is compiling it.
|
|
192
|
+
*
|
|
193
|
+
* **This is a rule about ownership, not an allowlist of directories.** It used
|
|
194
|
+
* to prefix `icons/…` and `images/…` and pass everything else through — the
|
|
195
|
+
* same answer for every path any tree authors today, and the wrong one for the
|
|
196
|
+
* next directory a package ships. `sohl-kethira-basic` keeps art under
|
|
197
|
+
* `assets/artwork/`, so an authored `artwork/deity.webp` would have shipped
|
|
198
|
+
* unprefixed: a 404 in Foundry, reported by nothing. That a package owns its
|
|
199
|
+
* own tree is the fact; the directory names inside it are that package's
|
|
200
|
+
* business (#331).
|
|
201
|
+
*
|
|
202
|
+
* **Off-install addresses pass through too**, which is the same rule rather
|
|
203
|
+
* than a fourth: a URL, a `data:` URI, or a `/`-rooted path names something no
|
|
204
|
+
* package owns. See {@link addressesAnotherPackage}.
|
|
205
|
+
*
|
|
206
|
+
* **`banner:` does not follow this rule, deliberately (#331).** It is not an
|
|
207
|
+
* asset path inside a Foundry install at all: it reaches no compiled document,
|
|
208
|
+
* and its only consumer is the Hugo theme, which prefixes a relative value with
|
|
209
|
+
* `images/` and joins it onto `params.cdnBaseURL`. The two fields look alike
|
|
210
|
+
* and address different places — `img:` a file Foundry serves, `banner:` a file
|
|
211
|
+
* the CDN serves — so they are documented apart rather than reconciled into one
|
|
212
|
+
* rule that would be true of neither.
|
|
185
213
|
*
|
|
186
214
|
* **Two empties, and they mean opposite things (#218).** `null` — or an absent
|
|
187
215
|
* key, which reaches here as `undefined` — means _unset_: the note names no art
|