@heroiclands/package-build 9.0.0 → 10.0.1
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 +721 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +437 -7
- package/content-config.mjs +259 -28
- package/docs/content-format.md +1418 -0
- package/engine/address-charset.mjs +62 -0
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +194 -4
- package/engine/content-address.mjs +4 -4
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +132 -56
- package/engine/content-lint.mjs +8 -1
- package/engine/diagnostics.mjs +33 -0
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-spec.mjs +49 -43
- package/engine/frontmatter-lint.mjs +351 -28
- package/engine/generate.mjs +32 -4
- package/engine/helpers.mjs +41 -29
- package/engine/ids.mjs +19 -1
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -5
- package/engine/kb-manifest.mjs +36 -7
- package/engine/map-notes.mjs +34 -18
- package/engine/note-claims.mjs +383 -0
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +39 -22
- package/engine/pack-router.mjs +17 -6
- package/engine/prose-lint.mjs +55 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scenes.mjs +19 -1
- package/engine/schema-check.mjs +347 -3
- package/engine/site-build.mjs +1 -1
- package/engine/site-index.mjs +38 -21
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +112 -80
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +67 -51
- package/package.json +6 -2
- package/sohl/actors.mjs +249 -36
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +3 -0
- package/sohl/items.mjs +110 -14
- package/sohl/note-schemas.mjs +11 -7
- package/types/content-config.d.mts +48 -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 +14 -1
- package/types/engine/helpers.d.mts +21 -13
- 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/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/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
package/sohl/items.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
resolveName,
|
|
42
42
|
resolveImg,
|
|
43
43
|
defaultStats,
|
|
44
|
-
|
|
44
|
+
systemArchetype,
|
|
45
45
|
} from "../engine/helpers.mjs";
|
|
46
46
|
import { BasePackCompiler } from "../engine/base-compiler.mjs";
|
|
47
47
|
// Per-type default art lives in one framework-free module shared with the
|
|
@@ -56,6 +56,15 @@ import { itemDocEntryId, itemDocPointer } from "../engine/item-docs.mjs";
|
|
|
56
56
|
// them with are one table — the consuming repository's, not this package's
|
|
57
57
|
// (#1504/#1563).
|
|
58
58
|
import { itemTypes, itemBuilder, itemArt } from "../engine/item-registry.mjs";
|
|
59
|
+
// Which Foundry Item subtype a note's `type` compiles into. Looked up in the
|
|
60
|
+
// system's declared map, never inferred from the type itself (#79).
|
|
61
|
+
import { documentSubtype, subtypeRow } from "../engine/document-subtypes.mjs";
|
|
62
|
+
import { SOHL_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
|
|
63
|
+
// The note-level `sohl:` block: `sohl.system` onto the document's `system`
|
|
64
|
+
// verbatim, and `sohl.img` / `sohl.effects` / `sohl.flags` overriding their
|
|
65
|
+
// shared top-level forms for this system alone (#58).
|
|
66
|
+
import { blockProperty, claimedPaths, mergeSystemData } from "../engine/system-block.mjs";
|
|
67
|
+
import { itemFields } from "../engine/item-registry.mjs";
|
|
59
68
|
|
|
60
69
|
/**
|
|
61
70
|
* The description an item carries: a pointer to its **item doc**, the
|
|
@@ -86,11 +95,19 @@ function itemDescription(markdown, fm, name) {
|
|
|
86
95
|
|
|
87
96
|
/**
|
|
88
97
|
* Build the `system.*` fields shared by every item type:
|
|
89
|
-
* shortcode, actionDefs, notes, docHtml.
|
|
98
|
+
* shortcode, archetype, actionDefs, notes, docHtml.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} fm - The note's frontmatter.
|
|
101
|
+
* @param {string} description - The item's documentation pointer.
|
|
102
|
+
* @param {string} label - Human-readable context for error messages.
|
|
103
|
+
* @returns {object} The shared `system` fields.
|
|
90
104
|
*/
|
|
91
|
-
function commonSystem(fm, description) {
|
|
105
|
+
function commonSystem(fm, description, label) {
|
|
92
106
|
return {
|
|
93
107
|
shortcode: fm.shortcode,
|
|
108
|
+
// Required nullable number: a priority, or `null` for a document that
|
|
109
|
+
// is not an archetype (#126 / archetype contract #604).
|
|
110
|
+
archetype: systemArchetype(fm, label),
|
|
94
111
|
actionDefs: Array.isArray(fm.actionDefs) ? fm.actionDefs : [],
|
|
95
112
|
notes: "",
|
|
96
113
|
docHtml: description || "",
|
|
@@ -105,10 +122,27 @@ function commonSystem(fm, description) {
|
|
|
105
122
|
/* Compiler */
|
|
106
123
|
/* -------------------------------------------------------------------- */
|
|
107
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The system this pass compiles for — the block its notes write, and the
|
|
127
|
+
* registry its builders come from.
|
|
128
|
+
*
|
|
129
|
+
* Read from the map rather than spelled here, so the block name, the subtype
|
|
130
|
+
* map and the registry key are one statement (#58/#79).
|
|
131
|
+
*
|
|
132
|
+
* @type {string}
|
|
133
|
+
*/
|
|
134
|
+
const SYSTEM = SOHL_DOCUMENT_SUBTYPES.block;
|
|
135
|
+
|
|
108
136
|
export class Items extends BasePackCompiler {
|
|
109
137
|
static id = "items";
|
|
110
138
|
static label = "item";
|
|
111
139
|
|
|
140
|
+
/**
|
|
141
|
+
* An Item **is** a system's data, so this pack takes only notes carrying
|
|
142
|
+
* this system's block (#58).
|
|
143
|
+
*/
|
|
144
|
+
static requiresSystemBlock = true;
|
|
145
|
+
|
|
112
146
|
/**
|
|
113
147
|
* How many of each item type this pass wrote, for the summary. Every type
|
|
114
148
|
* is present from the start so the tally reads as a census of the
|
|
@@ -121,11 +155,47 @@ export class Items extends BasePackCompiler {
|
|
|
121
155
|
/**
|
|
122
156
|
* Every content type that compiles into an item.
|
|
123
157
|
*
|
|
158
|
+
* The whitelist is the consuming repository's `itemBuilders` keys (#1504),
|
|
159
|
+
* and the system's own map is a second filter on top of it: a type SoHL
|
|
160
|
+
* maps onto some *other* document class is not an item however a registry
|
|
161
|
+
* spells it, which is the "no wrongly-typed document" half of #79. A type
|
|
162
|
+
* the map does not name at all is left to the registry — see
|
|
163
|
+
* {@link Items#itemSubtype}.
|
|
164
|
+
*
|
|
124
165
|
* @param {object} fm - The note's frontmatter.
|
|
125
166
|
* @returns {boolean} True for a whitelisted item type.
|
|
126
167
|
*/
|
|
127
168
|
selects(fm) {
|
|
128
|
-
|
|
169
|
+
if (!fm.type || !itemTypes().has(fm.type)) return false;
|
|
170
|
+
const row = subtypeRow(SOHL_DOCUMENT_SUBTYPES, fm.type);
|
|
171
|
+
return !row || row.document === "Item";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The Foundry Item subtype a note compiles into.
|
|
176
|
+
*
|
|
177
|
+
* **Looked up, not inferred.** For every type this system declares, the
|
|
178
|
+
* emitted subtype is the map's, so the note vocabulary and the document
|
|
179
|
+
* vocabulary are two separately-stated things rather than one string
|
|
180
|
+
* written twice (#79).
|
|
181
|
+
*
|
|
182
|
+
* **A type the map does not name belongs to the consumer**, and its
|
|
183
|
+
* registry entry is the declaration: a repository shipping an item type of
|
|
184
|
+
* its own writes it once, in the `itemBuilders` table of its
|
|
185
|
+
* `package-build.config.yaml`, and that key is what the document is a
|
|
186
|
+
* subtype of. That is an authored statement in the consumer's own
|
|
187
|
+
* configuration, not a coincidence inside this package's source — and
|
|
188
|
+
* refusing it here would silently drop every document of a type SoHL has
|
|
189
|
+
* no opinion about (#7/#1563).
|
|
190
|
+
*
|
|
191
|
+
* @param {object} fm - The note's frontmatter.
|
|
192
|
+
* @returns {string} The document's `type`.
|
|
193
|
+
*/
|
|
194
|
+
itemSubtype(fm) {
|
|
195
|
+
const declared = documentSubtype(SOHL_DOCUMENT_SUBTYPES, fm.type, fm, {
|
|
196
|
+
absPath: this.currentNote?.absPath,
|
|
197
|
+
});
|
|
198
|
+
return declared ?? fm.type;
|
|
129
199
|
}
|
|
130
200
|
|
|
131
201
|
/** An item is named by its own type in the log, not by "item". */
|
|
@@ -147,26 +217,52 @@ export class Items extends BasePackCompiler {
|
|
|
147
217
|
const name = resolveName(fm);
|
|
148
218
|
const description = itemDescription(markdown, fm, name);
|
|
149
219
|
const id = fm.id;
|
|
220
|
+
const subType = this.itemSubtype(fm);
|
|
150
221
|
const system = {
|
|
151
|
-
...commonSystem(fm, description),
|
|
152
|
-
...itemBuilder(type)(fm),
|
|
222
|
+
...commonSystem(fm, description, `item "${name}"`),
|
|
223
|
+
...itemBuilder(type, SYSTEM)(fm),
|
|
153
224
|
};
|
|
225
|
+
// Whatever the note authors under `sohl.system`, at the DataModel's own
|
|
226
|
+
// paths. A path a declared field already writes is left to that field:
|
|
227
|
+
// its value came from the same authored place and went through the
|
|
228
|
+
// field's own coercion (#58).
|
|
229
|
+
mergeSystemData(system, fm, {
|
|
230
|
+
block: SYSTEM,
|
|
231
|
+
claimed: claimedPaths(itemFields(type, SYSTEM)),
|
|
232
|
+
});
|
|
233
|
+
this.reportUndeclaredSystemData(fm, SYSTEM, "Item", subType);
|
|
234
|
+
// And what *this* pass wrote on its own initiative — `shortcode`,
|
|
235
|
+
// `archetype`, `actionDefs`, `notes`, `docHtml` — which no field
|
|
236
|
+
// declaration states and so no other check can see (#155). Read off the
|
|
237
|
+
// assembled block, so a key added to `commonSystem` is checked without
|
|
238
|
+
// anyone remembering to list it.
|
|
239
|
+
this.reportEmittedSystemData(system, {
|
|
240
|
+
fm,
|
|
241
|
+
block: SYSTEM,
|
|
242
|
+
documentType: "Item",
|
|
243
|
+
subType,
|
|
244
|
+
type,
|
|
245
|
+
fields: itemFields(type, SYSTEM),
|
|
246
|
+
});
|
|
154
247
|
|
|
155
|
-
const effects =
|
|
156
|
-
|
|
248
|
+
const effects = blockProperty(fm, SYSTEM, "effects");
|
|
157
249
|
const folderId = sohlField(fm, "folder", null);
|
|
158
250
|
const folder = this.folderResolver(folderId);
|
|
159
251
|
|
|
160
252
|
return {
|
|
161
253
|
name,
|
|
162
|
-
type
|
|
163
|
-
|
|
254
|
+
// The note's `type` addresses the builder and the default art —
|
|
255
|
+
// both registries are keyed by content type — while the document's
|
|
256
|
+
// own subtype comes from the system's map (#79).
|
|
257
|
+
type: subType,
|
|
258
|
+
img: resolveImg(blockProperty(fm, SYSTEM, "img")) || itemArt(type, SYSTEM),
|
|
164
259
|
_id: id,
|
|
165
260
|
system,
|
|
166
|
-
effects,
|
|
167
|
-
// `
|
|
168
|
-
// `flags.sohl.docArchetype
|
|
169
|
-
|
|
261
|
+
effects: Array.isArray(effects) ? [...effects] : [],
|
|
262
|
+
// Whatever the note authors, and nothing else. `archetype` used to
|
|
263
|
+
// be spliced in here as `flags.sohl.docArchetype`; it is a schema
|
|
264
|
+
// field now and sits in `system` (#126).
|
|
265
|
+
flags: blockProperty(fm, SYSTEM, "flags", {}),
|
|
170
266
|
_stats: this.stats,
|
|
171
267
|
ownership: { default: 0 },
|
|
172
268
|
folder,
|
package/sohl/note-schemas.mjs
CHANGED
|
@@ -122,23 +122,29 @@ const BEING_FIELDS = Object.freeze([
|
|
|
122
122
|
]);
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* A map note — `battlemap
|
|
125
|
+
* A map note — one type whose `battlemap` / `localmap` / `regionalmap`
|
|
126
|
+
* subType decides the derived canvas (#174). Compiled into a
|
|
126
127
|
* Foundry Scene.
|
|
127
128
|
*
|
|
128
129
|
* The three differ only in derived canvas defaults, which is the map compiler's
|
|
129
130
|
* business; their authored vocabulary is the same, so they share one
|
|
130
131
|
* declaration rather than three copies that could drift.
|
|
131
132
|
*
|
|
132
|
-
* `
|
|
133
|
-
*
|
|
133
|
+
* `img` is the one required field — the compiler refuses a map note without it,
|
|
134
|
+
* since a scene with no background is not a map. It was spelled `image` and
|
|
135
|
+
* read from the `sohl:` block until #142; both spellings still compile, and the
|
|
136
|
+
* retired one is reported rather than refused.
|
|
134
137
|
*
|
|
135
138
|
* @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
|
|
136
139
|
*/
|
|
137
140
|
const MAP_FIELDS = Object.freeze([
|
|
138
141
|
{
|
|
139
|
-
name: "
|
|
142
|
+
name: "img",
|
|
140
143
|
...STRING,
|
|
141
144
|
required: true,
|
|
145
|
+
// Art is not system-specific — a Scene is a core Foundry document, and
|
|
146
|
+
// every other note type carries its `img` at the note's top level.
|
|
147
|
+
shared: true,
|
|
142
148
|
describe: "The scene's background image.",
|
|
143
149
|
},
|
|
144
150
|
{
|
|
@@ -324,7 +330,5 @@ export const NOTE_SCHEMAS = Object.freeze({
|
|
|
324
330
|
doc: DOC_FIELDS,
|
|
325
331
|
macro: MACRO_FIELDS,
|
|
326
332
|
being: Object.freeze([...BEING_FIELDS, ...PRESENTATION_FIELDS.being]),
|
|
327
|
-
|
|
328
|
-
localmap: MAP_FIELDS,
|
|
329
|
-
regionalmap: MAP_FIELDS,
|
|
333
|
+
map: MAP_FIELDS,
|
|
330
334
|
});
|
|
@@ -527,6 +527,23 @@ export type ItemBuilderEntry = ((fm: object) => object) | {
|
|
|
527
527
|
img?: string;
|
|
528
528
|
fields?: readonly object[];
|
|
529
529
|
};
|
|
530
|
+
/**
|
|
531
|
+
* One **registry** of a declared set, and the system it belongs to (#58).
|
|
532
|
+
*
|
|
533
|
+
* A repository shipping content for two systems declares one of these per
|
|
534
|
+
* system: the accepted type vocabulary is their union, and a type both declare
|
|
535
|
+
* keeps a builder on each side rather than one of them winning in silence.
|
|
536
|
+
*/
|
|
537
|
+
export type ItemRegistrySpec = {
|
|
538
|
+
/**
|
|
539
|
+
* The system id whose vocabulary this registry is.
|
|
540
|
+
*/
|
|
541
|
+
system: string;
|
|
542
|
+
/**
|
|
543
|
+
* The registry itself.
|
|
544
|
+
*/
|
|
545
|
+
builders: Record<string, ItemBuilderEntry>;
|
|
546
|
+
};
|
|
530
547
|
/**
|
|
531
548
|
* The configuration a consumer writes.
|
|
532
549
|
*/
|
|
@@ -566,9 +583,13 @@ export type ContentBuildConfigInput = {
|
|
|
566
583
|
* note of that type gets when it sets no
|
|
567
584
|
* `img:` of its own. Default `{}` — a
|
|
568
585
|
* content module that ships no items
|
|
569
|
-
* declares none.
|
|
586
|
+
* declares none. A repository feeding
|
|
587
|
+
* two systems declares a **list** of
|
|
588
|
+
* `{ system, builders }` registries
|
|
589
|
+
* instead, and the accepted type
|
|
590
|
+
* vocabulary is their union (#58).
|
|
570
591
|
*/
|
|
571
|
-
itemBuilders?: Record<string, ItemBuilderEntry> | undefined;
|
|
592
|
+
itemBuilders?: Record<string, ItemBuilderEntry> | readonly ItemRegistrySpec[] | undefined;
|
|
572
593
|
/**
|
|
573
594
|
* Packs to compile. More than one entry
|
|
574
595
|
* may share a `type`: a note then names
|
|
@@ -657,11 +678,34 @@ export type ContentBuildConfig = {
|
|
|
657
678
|
* simply undocumented (#22).
|
|
658
679
|
*/
|
|
659
680
|
itemFields: Readonly<Record<string, readonly object[]>>;
|
|
681
|
+
/**
|
|
682
|
+
* Derived: the same builders, kept per
|
|
683
|
+
* declaring system. `{}` for the single
|
|
684
|
+
* registry form, which names no system
|
|
685
|
+
* (#58).
|
|
686
|
+
*/
|
|
687
|
+
itemBuildersBySystem: Readonly<Record<string, Readonly<Record<string, Function>>>>;
|
|
688
|
+
/**
|
|
689
|
+
* Derived: the default art, per system.
|
|
690
|
+
*/
|
|
691
|
+
itemArtBySystem: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
692
|
+
/**
|
|
693
|
+
* Derived: the declared fields, per system.
|
|
694
|
+
*/
|
|
695
|
+
itemFieldsBySystem: Readonly<Record<string, Readonly<Record<string, readonly object[]>>>>;
|
|
696
|
+
/**
|
|
697
|
+
* Derived: the types
|
|
698
|
+
* more than one registry declares — the
|
|
699
|
+
* ones the flat tables cannot answer for
|
|
700
|
+
* without choosing a system for the caller.
|
|
701
|
+
*/
|
|
702
|
+
itemTypesBySeveralSystems: ReadonlySet<string>;
|
|
660
703
|
/**
|
|
661
704
|
* Derived: the keys of
|
|
662
705
|
* {@link ContentBuildConfigInput.itemBuilders},
|
|
663
|
-
*
|
|
664
|
-
*
|
|
706
|
+
* unioned across every declared registry, so
|
|
707
|
+
* the accepted item types and the builder
|
|
708
|
+
* tables are one list (#1504).
|
|
665
709
|
*/
|
|
666
710
|
itemTypes: ReadonlySet<string>;
|
|
667
711
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a value is a well-formed address segment.
|
|
3
|
+
*
|
|
4
|
+
* A blank value is **not** valid: this answers "is this an acceptable segment",
|
|
5
|
+
* never "is a segment present". Presence is a separate question, asked
|
|
6
|
+
* wherever the value is required, and conflating the two would report a missing
|
|
7
|
+
* key as a charset violation.
|
|
8
|
+
*
|
|
9
|
+
* @param {unknown} value - The candidate segment.
|
|
10
|
+
* @returns {boolean} `true` when it matches {@link ADDRESS_SEGMENT_PATTERN}.
|
|
11
|
+
*/
|
|
12
|
+
export function isAddressSegment(value: unknown): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The one charset every segment of a canonical address is held to (#59).
|
|
15
|
+
*
|
|
16
|
+
* An address is a hyphen-joined tuple — `sohl-skill-clmb` — and it is read back
|
|
17
|
+
* by **counting segments**, with a fixed meaning per position. That is sound
|
|
18
|
+
* for exactly one reason: the hyphen is *purely* a separator, because no
|
|
19
|
+
* segment may contain one. Take that away and reading an address needs a
|
|
20
|
+
* vocabulary to match against, a longest-match rule, and an answer for every
|
|
21
|
+
* name that is a prefix of another — none of which exist.
|
|
22
|
+
*
|
|
23
|
+
* So the charset is not a tidiness rule. It is the premise the address grammar
|
|
24
|
+
* rests on, and the issue's word for how it should be held is **enforced rather
|
|
25
|
+
* than assumed**: a value that breaks it is refused where it is written, not
|
|
26
|
+
* discovered later as addresses that fail to parse and report nothing about
|
|
27
|
+
* why. `harn-adventures` was that case — its keys read as four segments and
|
|
28
|
+
* failed as a `null` return.
|
|
29
|
+
*
|
|
30
|
+
* This module is a **leaf with no local imports**, so the validator a
|
|
31
|
+
* consumer's `package-build.config.mjs` reaches (`content-config.mjs`) can name
|
|
32
|
+
* it without closing a cycle around that file.
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* The shape every address segment must match: ASCII letters and digits only.
|
|
38
|
+
*
|
|
39
|
+
* Case is deliberately **not** constrained. Hundreds of authored shortcodes are
|
|
40
|
+
* mixed-case and collide with nothing, and case has no bearing on the
|
|
41
|
+
* separator, which is the whole of what this pattern is protecting.
|
|
42
|
+
*
|
|
43
|
+
* @type {RegExp}
|
|
44
|
+
*/
|
|
45
|
+
export const ADDRESS_SEGMENT_PATTERN: RegExp;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **alias** namespace: what a note can be called, and who may claim a name.
|
|
3
|
+
*
|
|
4
|
+
* A wikilink resolves through one of two namespaces, and the pipe chooses
|
|
5
|
+
* which (#131): `[[x|…]]` is an *address*, parsed by the address grammar;
|
|
6
|
+
* `[[x]]` is an *alias*, looked up here. This module owns the second half —
|
|
7
|
+
* what goes into the index, how a key is spelled, and what happens when two
|
|
8
|
+
* notes claim one name.
|
|
9
|
+
*
|
|
10
|
+
* **An alias is scoped to the claiming note's own type.** The key is
|
|
11
|
+
* `(type, alias)`, so `Shock` may be a `skill` in one place and a `trauma` in
|
|
12
|
+
* another without the two ever meeting. A link resolves against the *source*
|
|
13
|
+
* note's type, which is why a bare name reaches a sibling and never a
|
|
14
|
+
* cross-type target — that one is written as an address.
|
|
15
|
+
*
|
|
16
|
+
* **Three sources, all authored.** `aliases`, `name.aliases`, and `name.full`.
|
|
17
|
+
* Each is something a person wrote down as a name for the note, which is
|
|
18
|
+
* exactly what a bare `[[…]]` cites.
|
|
19
|
+
*
|
|
20
|
+
* **The filename is deliberately not one of them**, and it used to be — every
|
|
21
|
+
* one of the three copies of this index added `basename(file, ".md")` with
|
|
22
|
+
* underscores turned to spaces. That admitted keys no author could ever cite
|
|
23
|
+
* and no author had ever written:
|
|
24
|
+
*
|
|
25
|
+
* - `_Introduction.md` yields the alias `" introduction"`, *with a leading
|
|
26
|
+
* space*. A wikilink target is trimmed, so nothing can ever match it. In one
|
|
27
|
+
* repository thirteen notes — one per documentation section — claimed that
|
|
28
|
+
* key, making it the largest alias collision in the corpus and every one of
|
|
29
|
+
* its claimants blameless.
|
|
30
|
+
* - `README.md` yields `readme`, claimed once per section for the same reason.
|
|
31
|
+
*
|
|
32
|
+
* Since a collision is now a build failure rather than a silent deletion, an
|
|
33
|
+
* index entry that cannot be cited can only ever *cause* one. Removing the
|
|
34
|
+
* source was measured first, across all five content trees: not one link that
|
|
35
|
+
* resolves today resolves through the filename alone, so nothing loses a
|
|
36
|
+
* target — while the collision count falls without a note being edited.
|
|
37
|
+
*
|
|
38
|
+
* @module
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Every alias a note claims, in the order the sources are consulted.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} fm - Parsed frontmatter.
|
|
44
|
+
* @returns {string[]} The claimed aliases, each a non-empty string.
|
|
45
|
+
*/
|
|
46
|
+
export function aliasesOf(fm: object): string[];
|
|
47
|
+
/**
|
|
48
|
+
* The index key one note's claim on one alias is filed under.
|
|
49
|
+
*
|
|
50
|
+
* Stated here so the three indexes — the pack build's, the site build's and
|
|
51
|
+
* the link checker's — cannot spell it differently. All three already used
|
|
52
|
+
* `type|alias`, lowercased; the risk was never that they disagreed today.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} type - The claiming note's content type.
|
|
55
|
+
* @param {string} alias - The alias, as authored.
|
|
56
|
+
* @returns {string} The key.
|
|
57
|
+
*/
|
|
58
|
+
export function aliasKey(type: string, alias: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* One alias claimed by more than one note of a single type.
|
|
61
|
+
*
|
|
62
|
+
* @typedef {object} AliasCollision
|
|
63
|
+
* @property {string} key - The index key, `type|alias`.
|
|
64
|
+
* @property {string} type - The type both claimants share.
|
|
65
|
+
* @property {string} alias - The alias, as the first claimant wrote it.
|
|
66
|
+
* @property {unknown[]} claimants - Every note claiming it, in walk order.
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Build the type-scoped alias index, and report every collision in it.
|
|
70
|
+
*
|
|
71
|
+
* **A collision resolves to nothing, and is reported naming every claimant.**
|
|
72
|
+
* Both halves matter. Resolving to whichever note happened to be walked first
|
|
73
|
+
* makes a link silently point at the wrong document, and which one it is
|
|
74
|
+
* depends on directory order. Reporting it at the *citing* note blames a file
|
|
75
|
+
* whose author did nothing wrong — whoever added the second claimant broke
|
|
76
|
+
* every existing citation (#13) — so the claimants are kept rather than
|
|
77
|
+
* discarded along with the entry.
|
|
78
|
+
*
|
|
79
|
+
* @template T
|
|
80
|
+
* @param {Iterable<{type: string, aliases: Iterable<string>, value: T}>} entries
|
|
81
|
+
* One per note: the type that scopes its claims, the aliases it claims, and
|
|
82
|
+
* whatever the caller wants an alias to resolve to.
|
|
83
|
+
* @param {object} [opts]
|
|
84
|
+
* @param {(a: T, b: T) => boolean} [opts.same] - Whether two values are the
|
|
85
|
+
* same note. Defaults to identity; a caller whose values are freshly built
|
|
86
|
+
* records supplies its own.
|
|
87
|
+
* @returns {{byKey: Map<string, T>, claims: Map<string, T[]>,
|
|
88
|
+
* collisions: AliasCollision[]}} `byKey` omits every colliding key, so a
|
|
89
|
+
* lookup in it can never resolve an ambiguous alias.
|
|
90
|
+
*/
|
|
91
|
+
export function indexAliases<T>(entries: Iterable<{
|
|
92
|
+
type: string;
|
|
93
|
+
aliases: Iterable<string>;
|
|
94
|
+
value: T;
|
|
95
|
+
}>, { same }?: {
|
|
96
|
+
same?: ((a: T, b: T) => boolean) | undefined;
|
|
97
|
+
}): {
|
|
98
|
+
byKey: Map<string, T>;
|
|
99
|
+
claims: Map<string, T[]>;
|
|
100
|
+
collisions: AliasCollision[];
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* One alias claimed by more than one note of a single type.
|
|
104
|
+
*/
|
|
105
|
+
export type AliasCollision = {
|
|
106
|
+
/**
|
|
107
|
+
* - The index key, `type|alias`.
|
|
108
|
+
*/
|
|
109
|
+
key: string;
|
|
110
|
+
/**
|
|
111
|
+
* - The type both claimants share.
|
|
112
|
+
*/
|
|
113
|
+
type: string;
|
|
114
|
+
/**
|
|
115
|
+
* - The alias, as the first claimant wrote it.
|
|
116
|
+
*/
|
|
117
|
+
alias: string;
|
|
118
|
+
/**
|
|
119
|
+
* - Every note claiming it, in walk order.
|
|
120
|
+
*/
|
|
121
|
+
claimants: unknown[];
|
|
122
|
+
};
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* @property {number} compiled - Notes that became a document.
|
|
12
12
|
* @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
|
|
13
13
|
* @property {number} skippedOther - Notes this pass does not claim.
|
|
14
|
-
* @property {number} declined - Notes refused
|
|
15
|
-
* frontmatter field
|
|
14
|
+
* @property {number} declined - Notes this pack **refused** — one declaring a
|
|
15
|
+
* retired frontmatter field, or one routed to a system pack whose system it
|
|
16
|
+
* says nothing about (#58). Counted as errors, never as skips.
|
|
16
17
|
*/
|
|
17
18
|
/**
|
|
18
19
|
* The shared walk → filter → expand → convert → build → write → count loop.
|
|
@@ -79,6 +80,25 @@ export class BasePackCompiler {
|
|
|
79
80
|
* @type {readonly string[]}
|
|
80
81
|
*/
|
|
81
82
|
static readsPackOutputOf: readonly string[];
|
|
83
|
+
/**
|
|
84
|
+
* Whether this pass's document **is** a system's data, and therefore takes
|
|
85
|
+
* only notes that carry that system's block (#58).
|
|
86
|
+
*
|
|
87
|
+
* A pack may declare a `system:` — `harn-ensemble` ships an `actors-hm3`
|
|
88
|
+
* and an `actors-sohl` from one tree — and the note-side half of that is
|
|
89
|
+
* the block named after the system. A note carrying no such block has
|
|
90
|
+
* nothing to say about it, so compiling it there would emit a **hollow
|
|
91
|
+
* document**: a subtype, and none of the fields the subtype exists for.
|
|
92
|
+
*
|
|
93
|
+
* False by default, because most passes write documents that are not
|
|
94
|
+
* system data at all. A JournalEntry of prose is the same document under
|
|
95
|
+
* either system, and a journals pack that declared one must not turn every
|
|
96
|
+
* doc note in the tree into a finding. The Item and Actor passes say so;
|
|
97
|
+
* anything else that genuinely writes a system's data says so too.
|
|
98
|
+
*
|
|
99
|
+
* @type {boolean}
|
|
100
|
+
*/
|
|
101
|
+
static requiresSystemBlock: boolean;
|
|
82
102
|
/**
|
|
83
103
|
* @param {object} options
|
|
84
104
|
* @param {string} options.contentBase - Root of the content tree.
|
|
@@ -111,6 +131,21 @@ export class BasePackCompiler {
|
|
|
111
131
|
folderResolver: (path: string | null) => string | null;
|
|
112
132
|
/** @type {number} */
|
|
113
133
|
errorCount: number;
|
|
134
|
+
/**
|
|
135
|
+
* Emitted-`system` findings, one per `documentType|subtype|field` (#155).
|
|
136
|
+
*
|
|
137
|
+
* A key the compiler writes is on **every** document of a subtype, so
|
|
138
|
+
* reporting it where it is found would print the same sentence 3,126 times
|
|
139
|
+
* and bury the one that is not systemic. Collected here instead and flushed
|
|
140
|
+
* once at the end of the pass, keyed so the class of defect is reported
|
|
141
|
+
* once and the first document carrying it names a file a reader can open.
|
|
142
|
+
*
|
|
143
|
+
* @type {Map<string, {message: string, file: string|undefined}>}
|
|
144
|
+
*/
|
|
145
|
+
emittedFindings: Map<string, {
|
|
146
|
+
message: string;
|
|
147
|
+
file: string | undefined;
|
|
148
|
+
}>;
|
|
114
149
|
/**
|
|
115
150
|
* The pack this pass writes, and the Foundry document type it holds.
|
|
116
151
|
*
|
|
@@ -178,6 +213,22 @@ export class BasePackCompiler {
|
|
|
178
213
|
* routes to no pack at all — a build failure, never a silent drop.
|
|
179
214
|
*/
|
|
180
215
|
routesHere(fm: object): boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Whether a claimed, routed note may become this pack's document at all.
|
|
218
|
+
*
|
|
219
|
+
* The pack-eligibility gate, and it fails rather than skipping: a note that
|
|
220
|
+
* routed *here* and carries nothing for this pack's system is an authoring
|
|
221
|
+
* mistake with a hollow document at the end of it, not a note that belongs
|
|
222
|
+
* to another pass. Skipping it quietly is how a whole tree compiles to
|
|
223
|
+
* documents nobody can use — the failure mode #1502 and #56 are both
|
|
224
|
+
* instances of.
|
|
225
|
+
*
|
|
226
|
+
* @param {object} fm - The note's frontmatter.
|
|
227
|
+
* @returns {boolean} True when the note may be compiled here.
|
|
228
|
+
* @throws {Error} When this pack's system is absent from the note. The
|
|
229
|
+
* error carries a `position` where the note's own file can be read.
|
|
230
|
+
*/
|
|
231
|
+
eligibleFor(fm: object): boolean;
|
|
181
232
|
/**
|
|
182
233
|
* Whether this pass claims a note. **Required.**
|
|
183
234
|
*
|
|
@@ -265,6 +316,82 @@ export class BasePackCompiler {
|
|
|
265
316
|
line?: number;
|
|
266
317
|
column?: number;
|
|
267
318
|
}): void;
|
|
319
|
+
/**
|
|
320
|
+
* Report every `<system>.system` key the receiving subtype does not declare
|
|
321
|
+
* (#58).
|
|
322
|
+
*
|
|
323
|
+
* An **error**, not a warning: Foundry drops an unknown `system` key at
|
|
324
|
+
* construction without a word, so the alternative is a document shipped
|
|
325
|
+
* with a field the author wrote and nobody will ever see. Each finding is
|
|
326
|
+
* located at the offending key where the file can be read, so it points at
|
|
327
|
+
* a line rather than at a note.
|
|
328
|
+
*
|
|
329
|
+
* Silent where nothing can answer — no published schema, or a subtype the
|
|
330
|
+
* artifact does not name. `content-build lint` says that out loud once for
|
|
331
|
+
* the whole build rather than once per note.
|
|
332
|
+
*
|
|
333
|
+
* @param {object} fm - The note's frontmatter.
|
|
334
|
+
* @param {string} block - The system block to read.
|
|
335
|
+
* @param {string} documentType - `Item`, `Actor`, …
|
|
336
|
+
* @param {string} subType - The subtype this note compiles into.
|
|
337
|
+
* @returns {number} How many findings were reported.
|
|
338
|
+
*/
|
|
339
|
+
reportUndeclaredSystemData(fm: object, block: string, documentType: string, subType: string): number;
|
|
340
|
+
/**
|
|
341
|
+
* Record every `system` key the *compiled document* carries that the
|
|
342
|
+
* receiving subtype does not declare (#155).
|
|
343
|
+
*
|
|
344
|
+
* The sibling of {@link BasePackCompiler#reportUndeclaredSystemData}, and
|
|
345
|
+
* the half that sees what no declaration states. A compiler writes keys of
|
|
346
|
+
* its own alongside the declared fields — `shortcode`, `actionDefs`,
|
|
347
|
+
* `notes`, `docHtml`, `archetype` — and neither the field-declaration check
|
|
348
|
+
* nor the authored-`system` check can see them, so until this nothing
|
|
349
|
+
* compared them at all. Foundry's discard is the same silent one either
|
|
350
|
+
* way.
|
|
351
|
+
*
|
|
352
|
+
* Called with the block **after** the builder, the authored merge and any
|
|
353
|
+
* conditional fields have all written into it, so what is checked is what
|
|
354
|
+
* the pack file receives.
|
|
355
|
+
*
|
|
356
|
+
* Recorded rather than reported: see {@link BasePackCompiler#emittedFindings}
|
|
357
|
+
* for why, and {@link BasePackCompiler#reportEmittedFindings} for where they
|
|
358
|
+
* come out.
|
|
359
|
+
*
|
|
360
|
+
* @param {object} system - The `system` block just assembled.
|
|
361
|
+
* @param {object} opts
|
|
362
|
+
* @param {object} opts.fm - The note's frontmatter.
|
|
363
|
+
* @param {string} opts.block - The system block the note writes.
|
|
364
|
+
* @param {string} opts.documentType - `Item`, `Actor`, …
|
|
365
|
+
* @param {string} opts.subType - The subtype this note compiles into.
|
|
366
|
+
* @param {string} opts.type - The note's content type.
|
|
367
|
+
* @param {readonly {to?: string}[]} [opts.fields] - The type's field
|
|
368
|
+
* declaration, which tells a builder emission from a compiler one.
|
|
369
|
+
* @param {object} [opts.config] - The resolved build configuration.
|
|
370
|
+
* @returns {number} How many findings were new to this pass.
|
|
371
|
+
*/
|
|
372
|
+
reportEmittedSystemData(system: object, { fm, block, documentType, subType, type, fields, config }: {
|
|
373
|
+
fm: object;
|
|
374
|
+
block: string;
|
|
375
|
+
documentType: string;
|
|
376
|
+
subType: string;
|
|
377
|
+
type: string;
|
|
378
|
+
fields?: readonly {
|
|
379
|
+
to?: string;
|
|
380
|
+
}[] | undefined;
|
|
381
|
+
config?: object | undefined;
|
|
382
|
+
}): number;
|
|
383
|
+
/**
|
|
384
|
+
* Emit the collected emitted-`system` findings, once each.
|
|
385
|
+
*
|
|
386
|
+
* An **error**, for the reason #60 made its sibling one: the value is gone
|
|
387
|
+
* at load and the build says nothing, and severity that varied by *which
|
|
388
|
+
* part of the build wrote the key* would make the less fixable half the
|
|
389
|
+
* quieter one. What varies is the message, which says whose fix it is —
|
|
390
|
+
* see {@link module:engine/schema-check.emittedUndeclaredMessage}.
|
|
391
|
+
*
|
|
392
|
+
* @returns {number} How many were reported.
|
|
393
|
+
*/
|
|
394
|
+
reportEmittedFindings(): number;
|
|
268
395
|
/**
|
|
269
396
|
* One note → one document. **Required.**
|
|
270
397
|
*
|
|
@@ -367,8 +494,9 @@ export type PassStats = {
|
|
|
367
494
|
*/
|
|
368
495
|
skippedOther: number;
|
|
369
496
|
/**
|
|
370
|
-
* - Notes refused
|
|
371
|
-
* frontmatter field
|
|
497
|
+
* - Notes this pack **refused** — one declaring a
|
|
498
|
+
* retired frontmatter field, or one routed to a system pack whose system it
|
|
499
|
+
* says nothing about (#58). Counted as errors, never as skips.
|
|
372
500
|
*/
|
|
373
501
|
declined: number;
|
|
374
502
|
};
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* The URL section a note routes to.
|
|
3
3
|
*
|
|
4
4
|
* A `doc` is narrative content whose only identity is its subtype label, so it
|
|
5
|
-
* routes by `
|
|
5
|
+
* routes by `subType`; every other type names its own section.
|
|
6
6
|
*
|
|
7
7
|
* @param {object} fm - Parsed frontmatter.
|
|
8
8
|
* @returns {string|undefined} The section, or `undefined` when the note has
|
|
9
|
-
* none — a `doc` with no
|
|
9
|
+
* none — a `doc` with no subtype has no address and is not published.
|
|
10
10
|
*/
|
|
11
11
|
export function sectionOf(fm: object): string | undefined;
|
|
12
12
|
/**
|