@heroiclands/package-build 20.7.0 → 21.1.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/CONTENT.md +132 -44
  3. package/bin/content-build.mjs +37 -5
  4. package/bin/package-build.mjs +77 -0
  5. package/content-config.mjs +59 -1
  6. package/docs/api.md +149 -19
  7. package/docs/commands.md +75 -0
  8. package/docs/configuration.md +37 -10
  9. package/docs/content-format.md +450 -49
  10. package/engine/content-format.mjs +52 -3
  11. package/engine/content-images.mjs +699 -0
  12. package/engine/dependency-bump.mjs +218 -0
  13. package/engine/frontmatter-lint.mjs +89 -2
  14. package/engine/helpers.mjs +81 -142
  15. package/engine/index.mjs +15 -0
  16. package/engine/infobox-registry.mjs +81 -0
  17. package/engine/infobox-render.mjs +382 -0
  18. package/engine/infobox.mjs +963 -0
  19. package/engine/item-registry.mjs +5 -5
  20. package/engine/journals.mjs +22 -1
  21. package/engine/map-notes.mjs +11 -5
  22. package/engine/metadata-index.mjs +5 -0
  23. package/engine/note-vocabulary.mjs +57 -2
  24. package/engine/pathnames.mjs +374 -0
  25. package/engine/pdf-build.mjs +206 -9
  26. package/engine/pdf-render.mjs +453 -20
  27. package/engine/pdf-toc.mjs +77 -5
  28. package/engine/scenes.mjs +2 -1
  29. package/engine/site-build.mjs +106 -7
  30. package/engine/site-index.mjs +93 -4
  31. package/engine/wikilinks.mjs +93 -0
  32. package/hm3/default-item-art.mjs +14 -15
  33. package/hm3/index.mjs +3 -0
  34. package/hm3/infobox.mjs +64 -0
  35. package/package.json +1 -1
  36. package/sohl/default-item-art.mjs +18 -16
  37. package/sohl/index.mjs +3 -0
  38. package/sohl/infobox.mjs +499 -0
  39. package/types/content-config.d.mts +7 -0
  40. package/types/engine/content-format.d.mts +36 -0
  41. package/types/engine/content-images.d.mts +281 -0
  42. package/types/engine/dependency-bump.d.mts +89 -0
  43. package/types/engine/frontmatter-lint.d.mts +23 -0
  44. package/types/engine/helpers.d.mts +30 -72
  45. package/types/engine/index.d.mts +5 -0
  46. package/types/engine/infobox-registry.d.mts +36 -0
  47. package/types/engine/infobox-render.d.mts +87 -0
  48. package/types/engine/infobox.d.mts +443 -0
  49. package/types/engine/item-registry.d.mts +5 -5
  50. package/types/engine/journals.d.mts +9 -1
  51. package/types/engine/note-vocabulary.d.mts +51 -0
  52. package/types/engine/pathnames.d.mts +189 -0
  53. package/types/engine/pdf-build.d.mts +46 -0
  54. package/types/engine/pdf-render.d.mts +97 -1
  55. package/types/engine/pdf-toc.d.mts +10 -5
  56. package/types/engine/site-build.d.mts +11 -3
  57. package/types/engine/site-index.d.mts +35 -3
  58. package/types/engine/wikilinks.d.mts +22 -0
  59. package/types/hm3/default-item-art.d.mts +5 -6
  60. package/types/hm3/index.d.mts +1 -0
  61. package/types/hm3/infobox.d.mts +22 -0
  62. package/types/sohl/index.d.mts +1 -0
  63. package/types/sohl/infobox.d.mts +145 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The default HTML for one link value: an anchor when the medium supplied a
3
+ * URL, and the text alone otherwise.
4
+ *
5
+ * An unresolved reference keeps its words. A page that names something the
6
+ * index has not heard of is better than a page silently missing a row.
7
+ *
8
+ * @param {object} value - A `link` value.
9
+ * @returns {string} HTML.
10
+ */
11
+ export function linkToHtml(value: object): string;
12
+ /**
13
+ * One link value as a Foundry document reference.
14
+ *
15
+ * A compendium journal links **inside** Foundry: a website URL on a panel a
16
+ * player reads at the table sends them out of the game, and a UUID resolves to
17
+ * the document whether or not the world has imported it. Foundry's enricher
18
+ * reads the reference out of the page's own HTML, so no markup is needed
19
+ * around it.
20
+ *
21
+ * @param {object} value - A `link` value.
22
+ * @returns {string} A `@UUID` reference, or the text where there is none.
23
+ */
24
+ export function linkToUuid(value: object): string;
25
+ /**
26
+ * Every box as HTML, in the order given.
27
+ *
28
+ * @param {readonly object[]} boxes - From `buildInfoboxes`.
29
+ * @param {object} [options] - Options.
30
+ * @param {(value: object) => string} [options.link] - How this medium draws a
31
+ * link. Defaults to an anchor on the value's `url`.
32
+ * @returns {string} HTML, empty when there is nothing to draw.
33
+ */
34
+ export function infoboxesToHtml(boxes: readonly object[], { link }?: {
35
+ link?: ((value: object) => string) | undefined;
36
+ }): string;
37
+ /**
38
+ * Whether a section holds anything at all.
39
+ *
40
+ * A section with nothing in it is not drawn: rule 4 says an absent field is
41
+ * absent, and a heading over nothing is the em-dash placeholder in another
42
+ * form. The declaration answers it — {@link module:engine/infobox.sectionHolds}
43
+ * — because the same question decides whether a system box carries a statement
44
+ * instead of sections, and two answers to it would come apart.
45
+ *
46
+ * @param {object} section - The section.
47
+ * @returns {boolean} Whether to draw it.
48
+ */
49
+ export function sectionHasContent(section: object): boolean;
50
+ /**
51
+ * One link value as Typst.
52
+ *
53
+ * An internal destination — a note the book also prints — is a label
54
+ * reference, so the link works on paper as a cross-reference and in a PDF
55
+ * viewer as a jump. Everything else is set as its own words.
56
+ *
57
+ * @param {object} value - A `link` value.
58
+ * @param {Map<string, string>} links - Address slug → the book's anchor.
59
+ * @param {(anchor: string) => string} labelFor - The anchor's Typst label.
60
+ * @returns {string} Typst markup.
61
+ */
62
+ export function linkToTypst(value: object, links: Map<string, string>, labelFor: (anchor: string) => string): string;
63
+ /**
64
+ * Every box as one Typst panel each, breaking between sections.
65
+ *
66
+ * The panel is `breakable: true` and each section inside it is
67
+ * `breakable: false`, which is what puts a break between `ATTRIBUTES` and
68
+ * `SKILLS` on a richly-statted character and never inside either.
69
+ *
70
+ * @param {readonly object[]} boxes - From `buildInfoboxes`.
71
+ * @param {object} [options] - Options.
72
+ * @param {(value: object) => string} [options.link] - How the book draws a link.
73
+ * @returns {string} Typst markup, empty when there is nothing to draw.
74
+ */
75
+ export function infoboxesToTypst(boxes: readonly object[], { link }?: {
76
+ link?: ((value: object) => string) | undefined;
77
+ }): string;
78
+ /**
79
+ * The Typst definitions an infobox panel is drawn with.
80
+ *
81
+ * Emitted once at the head of the book rather than inlined per entry: 2,500
82
+ * entries each carrying their own rules is a megabyte of repetition, and the
83
+ * one place a reader changes the panel's appearance should be one place.
84
+ *
85
+ * @returns {string} Typst markup.
86
+ */
87
+ export function infoboxTypstPreamble(): string;
@@ -0,0 +1,443 @@
1
+ /**
2
+ * One field's overlay entry, qualified by type where the declaration qualifies
3
+ * it.
4
+ *
5
+ * `<type>.<field>` wins over the bare name, so a spelling two types use for two
6
+ * quantities can be said differently on each without splitting the overlay.
7
+ *
8
+ * @param {Readonly<Record<string, object>>} presentation - The overlay.
9
+ * @param {string|undefined} type - The note's type.
10
+ * @param {string} name - The field's declared name.
11
+ * @returns {object} The entry, or an empty one.
12
+ */
13
+ export function overlayFor(presentation: Readonly<Record<string, object>>, type: string | undefined, name: string): object;
14
+ /**
15
+ * Whether a value is a word meaning "nothing here" rather than a value.
16
+ *
17
+ * @param {unknown} value - The authored value.
18
+ * @returns {boolean} Whether it is one of {@link UNSET_VALUES}.
19
+ */
20
+ export function isUnsetSentinel(value: unknown): boolean;
21
+ /**
22
+ * Whether a value is worth a row.
23
+ *
24
+ * Rule 4: an absent field is absent. `null`, `""` and `[]` are how the corpus
25
+ * writes "nobody filled this in" — a note that declares every key of its type
26
+ * and leaves most of them empty is the ordinary shape, not the exception — and
27
+ * so is a sentinel, which is the same absence written as a word. Judged here
28
+ * rather than per field, because a sentinel that reaches a page reaches it the
29
+ * same way whichever box was building the row.
30
+ *
31
+ * @param {unknown} value - The authored value.
32
+ * @returns {boolean} Whether to emit it.
33
+ */
34
+ export function hasValue(value: unknown): boolean;
35
+ /**
36
+ * A key as a reader sees it: `healingRate` → `Healing rate`.
37
+ *
38
+ * The last dotted segment only — a nested key's container is already said by
39
+ * the section it sits in, and "Appearance eye color" says it twice.
40
+ *
41
+ * @param {string} name - The declared key.
42
+ * @returns {string} The label.
43
+ */
44
+ export function humanizeFieldName(name: string): string;
45
+ /**
46
+ * An enumerated value as a reader sees it: `graying_brown` → `graying brown`.
47
+ *
48
+ * @param {unknown} value - The authored value.
49
+ * @returns {string} The text.
50
+ */
51
+ export function humanizeValue(value: unknown): string;
52
+ /**
53
+ * A value as a row shows it.
54
+ *
55
+ * A **single lowercase token** is an enumerated value — `male`, `medium`,
56
+ * `city-state` — and a row reading "male" beside a name reads as a fault
57
+ * rather than as a value, so its words are capitalised. Anything else is prose
58
+ * the author wrote and is shown as written: title-casing "a dispossessed
59
+ * noble" would be this build editing the corpus.
60
+ *
61
+ * @param {unknown} value - The authored value.
62
+ * @returns {string} The text.
63
+ */
64
+ export function presentValue(value: unknown): string;
65
+ /**
66
+ * Whether a built value is worth a row.
67
+ *
68
+ * Separate from {@link hasValue}, which judges what an author wrote: a `link`
69
+ * value is a mapping and a `links` value is a list of them, and both are
70
+ * exactly what the authored form is not.
71
+ *
72
+ * @param {string} kind - One of {@link INFOBOX_VALUE_KINDS}.
73
+ * @param {unknown} value - The built value.
74
+ * @returns {boolean} Whether to emit the row.
75
+ */
76
+ export function hasRenderableValue(kind: string, value: unknown): boolean;
77
+ /**
78
+ * The value kind a `data:` declaration implies.
79
+ *
80
+ * Read from the declaration's own shape rather than from the value, so two
81
+ * notes of one type describe the same field the same way — a one-entry list
82
+ * and a two-entry list are both `links`.
83
+ *
84
+ * @param {object} field - A `DataFieldSpec`.
85
+ * @param {unknown} value - The authored value, for the shapes the declaration
86
+ * makes no claim about.
87
+ * @returns {string} One of {@link INFOBOX_VALUE_KINDS}.
88
+ */
89
+ export function valueKindOf(field: object, value: unknown): string;
90
+ /**
91
+ * One row, with its unit on it.
92
+ *
93
+ * **The unit goes on the value, not on the label**, because it belongs to the
94
+ * quantity rather than to the name of the quantity. A price is 160d and a
95
+ * weight is 1.1 lbs; splitting that across two cells — `Price (d)` beside
96
+ * `160` — makes a reader reassemble one fact from two places, and reads worst
97
+ * in the book, whose label column is a narrow small-caps rule.
98
+ *
99
+ * A medium cannot supply it. Appending `d` to a price means knowing which row
100
+ * is the price, which is the one thing a generic renderer must never know — so
101
+ * the unit is declared here and travels as part of the value.
102
+ *
103
+ * The declared string is appended **verbatim**, which is what lets a symbol
104
+ * hug its number (`160d`) and a word stand off it (`1.1 lbs`). The row's kind
105
+ * becomes `text`: a number with a unit on it is no longer a number, and saying
106
+ * otherwise would invite a medium to format it as one.
107
+ *
108
+ * @param {string} kind - The row's kind.
109
+ * @param {unknown} value - The built value.
110
+ * @param {string} [unit] - The declared unit, or nothing.
111
+ * @returns {{kind: string, value: unknown}} The row's kind and value.
112
+ */
113
+ export function applyUnit(kind: string, value: unknown, unit?: string): {
114
+ kind: string;
115
+ value: unknown;
116
+ };
117
+ /**
118
+ * One reference, resolved as far as the medium's index reaches.
119
+ *
120
+ * An unresolved reference keeps its own text rather than being dropped: a page
121
+ * that names something the index has not heard of is better than a page
122
+ * silently missing a row.
123
+ *
124
+ * @param {unknown} ref - The authored reference — a shortcode or an address.
125
+ * @param {(ref: unknown, hint?: object) => object|undefined} resolve - The
126
+ * medium's resolver.
127
+ * @param {object} [hint] - What the reference is expected to name, where the
128
+ * declaration says: `{type}`. A shortcode is unique within a type and not
129
+ * across a tree, so a hint is what stops a region resolving to a polity.
130
+ * @returns {{text: string, url?: string, uuid?: string, address?: string}} The value.
131
+ */
132
+ export function linkValue(ref: unknown, resolve: (ref: unknown, hint?: object) => object | undefined, hint?: object): {
133
+ text: string;
134
+ url?: string;
135
+ uuid?: string;
136
+ address?: string;
137
+ };
138
+ /**
139
+ * The note infobox: the subject from `data:`, plus the name every note has.
140
+ *
141
+ * The rows are {@link NOTE_VOCABULARY}'s declaration for the type, in its
142
+ * order, minus what {@link NOTE_FIELD_PRESENTATION} withholds and minus every
143
+ * field the note left empty. A type that declares no `data:` fields still gets
144
+ * the box, because `Name` is a fact about every note.
145
+ *
146
+ * @param {object} fm - The note's frontmatter.
147
+ * @param {object} [options] - Options.
148
+ * @param {(ref: unknown) => object|undefined} [options.resolve] - Resolves a
149
+ * reference to `{name, url?, uuid?, address?}`.
150
+ * @param {object} [options.vocabulary] - The note vocabulary to read.
151
+ * @param {object} [options.presentation] - The overlay to read.
152
+ * @returns {object} The box.
153
+ */
154
+ export function noteInfobox(fm: object, options?: {
155
+ resolve?: ((ref: unknown) => object | undefined) | undefined;
156
+ vocabulary?: object | undefined;
157
+ presentation?: object | undefined;
158
+ }): object;
159
+ /**
160
+ * Declare one system's half of the infobox.
161
+ *
162
+ * The **mechanism** is here and the **declaration** is the system's, which is
163
+ * the `engine/` ÷ `sohl/` line everywhere else in this package: what a box
164
+ * looks like is note-format knowledge, and which of a system's fields are
165
+ * worth a row is that system's.
166
+ *
167
+ * A declaration is refused at module evaluation rather than when some note
168
+ * reaches it: it is a small piece of authored data loaded once per build, so a
169
+ * mistake in it should stop the build immediately and name the fault.
170
+ *
171
+ * @param {object} declaration - The system's declaration.
172
+ * @param {string} declaration.system - The system id.
173
+ * @param {string} declaration.title - What the box is called.
174
+ * @param {Readonly<Record<string, readonly object[]>>} [declaration.fields] -
175
+ * Note type → that system's declared field list, read for the generic rows
176
+ * section. The same list the compiler obeys, never a copy of it.
177
+ * @param {Readonly<Record<string, {label?: string, withheld?: string}>>} [declaration.presentation] -
178
+ * The presentation overlay: what one of this system's fields is called where
179
+ * humanising its key is wrong, and the fields that carry no row at all.
180
+ * **Not a second field list** — the fields come from `fields`, and a field
181
+ * the overlay does not mention still gets a row, so a field added to the
182
+ * system reaches the box with no edit here.
183
+ * @param {Record<string, (fm: object, ctx: object) => object[]>} [declaration.sections] -
184
+ * Note type → a builder returning that type's sections, for a type whose box
185
+ * is derived rather than read field by field.
186
+ * @returns {object} The frozen declaration.
187
+ * @throws {Error} When it names no system or no title.
188
+ */
189
+ export function defineInfobox({ system, title, fields, presentation, sections }?: {
190
+ system: string;
191
+ title: string;
192
+ fields?: Readonly<Record<string, readonly object[]>> | undefined;
193
+ presentation?: Readonly<Record<string, {
194
+ label?: string;
195
+ withheld?: string;
196
+ }>> | undefined;
197
+ sections?: Record<string, (fm: object, ctx: object) => object[]> | undefined;
198
+ }): object;
199
+ /**
200
+ * Whether a field's value is the one its own declaration would have supplied.
201
+ *
202
+ * _A field answered by its default is a fact about the compiler, not about the
203
+ * note_ — and that is true of the **value**, not of where it was written. A
204
+ * corpus writes its defaults out: `improveFlag: false`, `combatCategory: none`
205
+ * and `initSkillMult: 0` are typed into hundreds of notes that mean nothing by
206
+ * them, and a row for each says the compiler's word back to a reader who came
207
+ * for the note's.
208
+ *
209
+ * A field declaring no default has nothing to be equal to, so every value it
210
+ * holds is the note's.
211
+ *
212
+ * @param {object} field - The declaration.
213
+ * @param {unknown} raw - The resolved value.
214
+ * @returns {boolean} Whether the value is the declaration's own.
215
+ */
216
+ export function isDeclaredDefault(field: object, raw: unknown): boolean;
217
+ /**
218
+ * The generic rows section: what this note authors in one system's block.
219
+ *
220
+ * Read through the system's own field declaration, in its order, and only
221
+ * where the note said something the declaration does not already say — a value
222
+ * equal to the field's own default is the compiler's answer wherever it was
223
+ * typed.
224
+ *
225
+ * A field the **note box** already put on the page is skipped, so the panel
226
+ * does not say one fact twice. It is what the note box *shows* rather than
227
+ * what its vocabulary declares: a gear item's weight is declared under `data:`
228
+ * and authored at `sohl.system.weightBase`, and standing down on the
229
+ * declaration alone left the fact on no surface at all.
230
+ *
231
+ * @param {object} fm - The note's frontmatter.
232
+ * @param {readonly object[]} fields - The system's field declaration.
233
+ * @param {object} ctx - `{ block, resolve, resolveField, taken, presentation }`.
234
+ * @returns {object[]} Zero or one section.
235
+ */
236
+ export function systemRowsSection(fm: object, fields: readonly object[], { block, resolve, resolveField, taken, presentation }: object): object[];
237
+ /**
238
+ * Every box a note carries, in the order every medium renders them.
239
+ *
240
+ * The note box first — it is the subject itself — then one box per system the
241
+ * note's type maps to, in the order the maps are declared.
242
+ *
243
+ * @param {object} fm - The note's frontmatter.
244
+ * @param {object} options - Options.
245
+ * @param {readonly object[]} options.maps - The document-subtype maps this
246
+ * build ships, which decide the box set.
247
+ * @param {readonly object[]} [options.providers] - The systems' infobox
248
+ * declarations, keyed by `system`.
249
+ * @param {(fm: object, block: string) => boolean} options.carriesBlock -
250
+ * Whether the note says anything about a system, which decides
251
+ * {@link NOT_AVAILABLE}.
252
+ * @param {(field: object, fm: object, opts: object) => object} options.resolveField -
253
+ * Resolves one declared field against the note.
254
+ * @param {(ref: unknown) => object|undefined} [options.resolve] - Resolves a
255
+ * reference to `{name, url?, uuid?, address?}`.
256
+ * @param {object} [options.vocabulary] - The note vocabulary to read.
257
+ * @returns {object[]} The boxes.
258
+ */
259
+ export function buildInfoboxes(fm: object, options: {
260
+ maps: readonly object[];
261
+ providers?: readonly object[] | undefined;
262
+ carriesBlock: (fm: object, block: string) => boolean;
263
+ resolveField: (field: object, fm: object, opts: object) => object;
264
+ resolve?: ((ref: unknown) => object | undefined) | undefined;
265
+ vocabulary?: object | undefined;
266
+ }): object[];
267
+ /**
268
+ * Whether a section holds anything a medium would draw.
269
+ *
270
+ * Keyed by {@link INFOBOX_LAYOUTS}, so a layout added there is understood here
271
+ * without a second edit.
272
+ *
273
+ * @param {object} section - The section.
274
+ * @returns {boolean} Whether it holds anything.
275
+ */
276
+ export function sectionHolds(section: object): boolean;
277
+ /**
278
+ * The box ids a note's type requires, in order.
279
+ *
280
+ * Derived from the same two declarations {@link buildInfoboxes} reads, so the
281
+ * assertion below compares an emitter's output against the format rather than
282
+ * against a restatement of it.
283
+ *
284
+ * @param {object} fm - The note's frontmatter.
285
+ * @param {object} options - `{ maps }`.
286
+ * @returns {string[]} The ids.
287
+ */
288
+ export function requiredInfoboxIds(fm: object, { maps }: object): string[];
289
+ /**
290
+ * Refuse a page carrying anything but the boxes its type maps to.
291
+ *
292
+ * _Every page carries exactly the boxes its type maps to_ is the property the
293
+ * format states and the reason the box set is derived rather than declared.
294
+ * Asserted where the boxes are emitted, in every medium, so a missing infobox
295
+ * is a failure rather than something nobody notices — and so is a box for a
296
+ * system that will compile no document, which is a note claiming a system it
297
+ * does not reach.
298
+ *
299
+ * @param {readonly object[]} boxes - What was built.
300
+ * @param {object} fm - The note's frontmatter.
301
+ * @param {object} options - `{ maps, where }`.
302
+ * @returns {readonly object[]} The boxes, unchanged, so a caller may assert
303
+ * inline.
304
+ * @throws {Error} Naming the note, what is missing and what is extra.
305
+ */
306
+ export function assertInfoboxSet(boxes: readonly object[], fm: object, { maps, where }?: object): readonly object[];
307
+ /**
308
+ * How a section arranges what it holds.
309
+ *
310
+ * Closed, and keyed by the property the section carries its content in — a
311
+ * renderer reads one of four keys and needs no other knowledge of the data.
312
+ *
313
+ * @type {Readonly<Record<string, string>>}
314
+ */
315
+ export const INFOBOX_LAYOUTS: Readonly<Record<string, string>>;
316
+ /**
317
+ * What a row's `value` is.
318
+ *
319
+ * Closed. `link` and `links` carry `{text, url?, uuid?, address?}` — the three
320
+ * addresses a medium may need, whichever of them its own index could supply.
321
+ *
322
+ * @type {readonly string[]}
323
+ */
324
+ export const INFOBOX_VALUE_KINDS: readonly string[];
325
+ /**
326
+ * What a mapped system that produced no document for this note says.
327
+ *
328
+ * Stated rather than inferred from an empty block, because an absence is a
329
+ * poor signal: noticing that something is missing requires already knowing it
330
+ * should have been there, and a reader meeting one page has no way to know.
331
+ *
332
+ * @type {string}
333
+ */
334
+ export const NOT_AVAILABLE: string;
335
+ /**
336
+ * What a system that does produce a document, and holds nothing a reader has
337
+ * not already been shown, says.
338
+ *
339
+ * The third state of a system box, and the reason it is stated rather than
340
+ * drawn as an empty panel: a heading over nothing asserts that something
341
+ * should have been there. It is not {@link NOT_AVAILABLE} — the system
342
+ * compiles this note, and a reader told otherwise would go looking for a
343
+ * document that exists. It is that everything this system holds about the note
344
+ * is either the subject's own fact, carried by the note box above, or a value
345
+ * the compiler would have supplied anyway.
346
+ *
347
+ * @type {string}
348
+ */
349
+ export const NOTHING_BEYOND_PROFILE: string;
350
+ /** The note infobox's id, which is not a system id. @type {string} */
351
+ export const NOTE_BOX_ID: string;
352
+ /** The note infobox's title. @type {string} */
353
+ export const NOTE_BOX_TITLE: string;
354
+ /** The note infobox's single section id. @type {string} */
355
+ export const NOTE_SECTION_ID: string;
356
+ /**
357
+ * What a **duration pair** is called.
358
+ *
359
+ * A note states an interval twice over — once as a roll formula and once as a
360
+ * flat number of seconds — and the declaration names the two by what they hold:
361
+ * `courseDurationFormula` and `courseDurationBase`. A page wants the thing
362
+ * being timed and which of the two it is reading.
363
+ *
364
+ * Shared by the note box's overlay and by a system's, because the two
365
+ * declarations spell these fields identically and a reader meeting an
366
+ * affliction written one way and one written the other should meet one word.
367
+ *
368
+ * @type {Readonly<Record<string, {label: string}>>}
369
+ */
370
+ export const DURATION_LABELS: Readonly<Record<string, {
371
+ label: string;
372
+ }>>;
373
+ /**
374
+ * What a gear item's two measured quantities are called, and measured in.
375
+ *
376
+ * Price is in pence and weight in pounds throughout the corpus, and the same
377
+ * fact is authored under `data:` on one note and at its destination path on
378
+ * another — so both overlays read the one declaration and a reader meets one
379
+ * word and one unit whichever box the row landed in.
380
+ *
381
+ * @type {Readonly<Record<string, {label?: string, unit: string}>>}
382
+ */
383
+ export const GEAR_UNITS: Readonly<Record<string, {
384
+ label?: string;
385
+ unit: string;
386
+ }>>;
387
+ /**
388
+ * The presentation overlay: what a `data:` field is called, and whether it
389
+ * belongs in the box at all.
390
+ *
391
+ * **This is not a second field list.** The fields come from
392
+ * {@link NOTE_VOCABULARY}; this says only what the vocabulary cannot — the
393
+ * label where humanising the key is wrong, the group a field composes into,
394
+ * and the handful of keys that are not reader-facing.
395
+ *
396
+ * Three properties, all optional:
397
+ *
398
+ * - `label` — what the row is called. Absent means the key, humanised.
399
+ * - `withheld` — why the field carries no row. Two reasons only: it is
400
+ * machinery — something that steers a build or an interface rather than
401
+ * describing the subject — or it is an image, which rule 2 keeps out of the
402
+ * box.
403
+ * - `unit` — what the quantity is measured in, appended to the value verbatim.
404
+ * See {@link applyUnit}.
405
+ * - `group` / `phrase` — the field composes into one row with its group mates
406
+ * rather than taking a row of its own. `phrase` turns the value into its
407
+ * clause; without one the value stands alone, so a field added to a group
408
+ * still appears.
409
+ *
410
+ * A key is either a field name or `<type>.<field name>`, and the qualified one
411
+ * wins. One spelling is not one quantity across the vocabulary: `data.weight`
412
+ * is a being's body weight, which reads as a clause of its appearance, and a
413
+ * gear item's mass, which is a row. An overlay keyed on the bare word would
414
+ * compose a coin's weight into its appearance.
415
+ *
416
+ * @type {Readonly<Record<string, {label?: string, withheld?: string,
417
+ * unit?: string, group?: string, phrase?: (value: any) => string}>>}
418
+ */
419
+ export const NOTE_FIELD_PRESENTATION: Readonly<Record<string, {
420
+ label?: string;
421
+ withheld?: string;
422
+ unit?: string;
423
+ group?: string;
424
+ phrase?: (value: any) => string;
425
+ }>>;
426
+ /**
427
+ /**
428
+ * The words a note writes when it means "there is nothing here".
429
+ *
430
+ * A corpus states an absence three ways, and only two of them are the absence
431
+ * of a value: the key is omitted, or it holds the field's own default. The
432
+ * third is a **sentinel** — a token standing in for the unset state, spelled
433
+ * however the note happened to spell it. `potency: na` and `category: none`
434
+ * are not a sodium potion and a category called None; they are two authors
435
+ * writing "not applicable" in the space a value would go.
436
+ *
437
+ * Compared after {@link normalizeToken} strips everything but letters, so
438
+ * `n/a`, `N/A` and `not applicable` are one word and `none of the above` is
439
+ * not one of them.
440
+ *
441
+ * @type {readonly string[]}
442
+ */
443
+ export const UNSET_VALUES: readonly string[];
@@ -61,11 +61,11 @@ export function itemFields(type: string, system?: string): readonly object[] | u
61
61
  * `defaultItemArt` was written for. Only the error's *owner* changed: it now
62
62
  * names the registry the consumer declares and can add to.
63
63
  *
64
- * **Resolved by the same rule a note's `img:` is.** The path goes through
65
- * {@link resolveImg}, so `icons/relic.svg` means the consumer's own asset root
66
- * in the registry exactly as it does on a note, and an already-served path
67
- * (`systems/sohl/assets/…`, as every SoHL default is) passes through untouched.
68
- * One spelling, one meaning, wherever it is written.
64
+ * **Resolved by the same rule a note's `img:` is.** The pathname goes through
65
+ * {@link resolveImg}, so `icons/relic.svg` means the consumer's own package in
66
+ * the registry exactly as it does on a note, and `sohl/assets/…` as every
67
+ * SoHL default is written — names the `sohl` package's file wherever it is
68
+ * compiled. One spelling, one meaning, wherever it is written.
69
69
  *
70
70
  * @param {string} type - the item type.
71
71
  * @param {string} [system] - The system compiling it, where a build declares
@@ -136,9 +136,16 @@ export function buildPages(rawPages: Array<object>, entryId: string, noteName: s
136
136
  * entry: a module may ship the same content for two systems, and each pack's
137
137
  * documents record the system version they were built against. A
138
138
  * caller with no pack in hand gets the package-wide block.
139
+ * @param {string} [params.infobox] - The note's infobox, already rendered to
140
+ * HTML. Prepended to the entry's first page, which is where the format puts
141
+ * it: the box is generated content in document order, before the prose, and
142
+ * a Foundry page is narrow enough that inlining it is the only arrangement
143
+ * that reads. It is **not** a page of its own — a page is what a UUID
144
+ * addresses, and a summary a reader has to navigate to is a summary they do
145
+ * not see.
139
146
  * @returns {object} The JournalEntry document, keyed for the pack.
140
147
  */
141
- export function buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats, }: {
148
+ export function buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats, infobox, }: {
142
149
  id: string;
143
150
  name: string;
144
151
  markdown: string;
@@ -146,6 +153,7 @@ export function buildJournalEntry({ id, name, markdown, leadName, folder, flags,
146
153
  folder?: string | null | undefined;
147
154
  flags?: object | undefined;
148
155
  stats?: object | undefined;
156
+ infobox?: string | undefined;
149
157
  }): object;
150
158
  /**
151
159
  * Journals pack compiler.
@@ -6,6 +6,33 @@
6
6
  * @returns {readonly string[]} The tags, in declaration order.
7
7
  */
8
8
  export function declaredTags(type: string, groups?: object): readonly string[];
9
+ /**
10
+ * The declared groups scoped to this type, in declaration order.
11
+ *
12
+ * The one reading of `types` that the flattened list and the slot check share,
13
+ * so the two can never disagree about which groups a `being` is held to.
14
+ *
15
+ * @param {string} type - The note's type.
16
+ * @param {object} [groups] - The grouped declaration.
17
+ * @returns {object[]} The groups that apply.
18
+ */
19
+ export function applicableTagGroups(type: string, groups?: object): object[];
20
+ /**
21
+ * The single-valued slots a note of this type has, in declaration order.
22
+ *
23
+ * A group carrying `exclusive` states alternatives rather than attributes, so a
24
+ * note carrying two of its tags has answered one question twice. Only such a
25
+ * group is returned: the check has nothing to say about a group whose tags
26
+ * genuinely accumulate.
27
+ *
28
+ * @param {string} type - The note's type.
29
+ * @param {object} [groups] - The grouped declaration.
30
+ * @returns {{slot: string, tags: readonly string[]}[]} The slots and their values.
31
+ */
32
+ export function exclusiveTagGroups(type: string, groups?: object): {
33
+ slot: string;
34
+ tags: readonly string[];
35
+ }[];
9
36
  /**
10
37
  * Whether a note carries a given tag, however the author wrote it.
11
38
  *
@@ -143,6 +170,21 @@ export const DRAFT_TAG: "draft";
143
170
  * Kind and character are separate groups because one slot could not hold both: a
144
171
  * fishing village is a `village` that is `fishing`, and the single-valued field
145
172
  * this replaced had to spell it `Fishing Village` as a value of its own.
173
+ *
174
+ * **A group carrying `exclusive` is a single-valued slot**, and that is the one
175
+ * closure a tag vocabulary can make. Its tags are not several things the subject
176
+ * may be at once — they are the alternative answers to one question, so a note
177
+ * naming two of them has named none, and both together are refused as an error.
178
+ * The property is opt-in and changes nothing for a group without it: a place is
179
+ * freely a `port` and a `town`, and `draft` is orthogonal to everything. The
180
+ * value is what the slot is called, for the message a reader gets.
181
+ *
182
+ * **Closure stops at the slot, and deliberately.** A tag outside an exclusive
183
+ * group's list does not fill that group's slot and is not refused for failing
184
+ * to — `tags:` is open and a being tagged `undead` is describing the subject in
185
+ * the author's own words. What is refused is a near miss of a declared value,
186
+ * and two values of one slot; there is no third refusal to make without taking
187
+ * back the openness of the region these tags sit in.
146
188
  */
147
189
  export const DECLARED_TAGS: Readonly<{
148
190
  /** What a place *is*. */
@@ -165,6 +207,15 @@ export const DECLARED_TAGS: Readonly<{
165
207
  types: string[];
166
208
  tags: readonly string[];
167
209
  }>;
210
+ /**
211
+ * What kind of being this is — a person, or one of the beasts and made
212
+ * things. A being is one or the other, so the group is a slot.
213
+ */
214
+ beingKind: Readonly<{
215
+ types: string[];
216
+ exclusive: "kind";
217
+ tags: readonly string[];
218
+ }>;
168
219
  /** A note's working state, which any note may carry. */
169
220
  state: Readonly<{
170
221
  types: null;