@heroiclands/package-build 20.0.0 → 20.2.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.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Tier 1 — the letters, and the two whitespace characters a file is made of.
3
+ *
4
+ * The Latin-1 range is split around `U+00D7` and `U+00F7` deliberately: `×` and
5
+ * `÷` sit inside the letter block but are operators, and they are admitted
6
+ * below in Tier 3 on their own merits rather than smuggled in as letters.
7
+ *
8
+ * @param {number} cp - A Unicode code point.
9
+ * @returns {boolean} Whether Tier 1 admits it.
10
+ */
11
+ export function isLetterTier(cp: number): boolean;
12
+ /**
13
+ * Whether the charset admits a code point anywhere in a note.
14
+ *
15
+ * @param {number} cp - A Unicode code point.
16
+ * @returns {boolean} Whether it is allowed outside a code fence.
17
+ */
18
+ export function isAllowedCodePoint(cp: number): boolean;
19
+ /**
20
+ * Whether a code point is diagram furniture, admitted inside a fence only.
21
+ *
22
+ * A fenced block is set in the mono face, and the mono face is not the book
23
+ * face — so the question "does the text font have this" is the wrong question
24
+ * to ask about a character in an ASCII-art org chart. All three ranges were
25
+ * confirmed present in DejaVu Sans Mono, the mono face Typst embeds.
26
+ *
27
+ * @param {number} cp - A Unicode code point.
28
+ * @returns {boolean} Whether a fence may carry it.
29
+ */
30
+ export function isDiagramCodePoint(cp: number): boolean;
31
+ /**
32
+ * The reason a code point is refused.
33
+ *
34
+ * @param {number} cp - A Unicode code point.
35
+ * @returns {string} A clause naming what it is and what to do instead.
36
+ */
37
+ export function refusalFor(cp: number): string;
38
+ /**
39
+ * Every non-NFC run in a string, with the composed form it should have been.
40
+ *
41
+ * Reported as **runs** rather than as bare combining marks, because `y` plus
42
+ * `U+0301` is one authoring mistake and pointing at the accent alone would name
43
+ * the half the author did not type.
44
+ *
45
+ * @param {string} text - File contents.
46
+ * @returns {Array<{sequence: string, composed: string, index: number}>} Each
47
+ * offending run, in the order it appears.
48
+ */
49
+ export function decomposedRuns(text: string): Array<{
50
+ sequence: string;
51
+ composed: string;
52
+ index: number;
53
+ }>;
54
+ /**
55
+ * Check one file's text against the charset and the normalization rule.
56
+ *
57
+ * Findings are **deduplicated per character per line**: a 60-cell table of
58
+ * `━` is one mistake made once, and sixty findings would bury the other
59
+ * fifty-nine things wrong with the tree.
60
+ *
61
+ * @param {string} text - The file's contents.
62
+ * @param {string} file - Path to report, relative to the tree.
63
+ * @returns {Array<{file: string, line: number, column: number,
64
+ * severity: "error", message: string}>} What is wrong, in file order.
65
+ */
66
+ export function checkText(text: string, file: string): Array<{
67
+ file: string;
68
+ line: number;
69
+ column: number;
70
+ severity: "error";
71
+ message: string;
72
+ }>;
73
+ /**
74
+ * Walk a content tree and check every authored file in it.
75
+ *
76
+ * Dot-directories are skipped: `.obsidian` carries editor state, and a plugin
77
+ * manifest's CRLF line endings are not this tree's prose. That is not a
78
+ * theoretical exclusion — it was the first thing a run over `sohl-thalorna`
79
+ * reported before the skip existed.
80
+ *
81
+ * @param {string} contentBase - Root of the content tree.
82
+ * @param {object} [opts]
83
+ * @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore
84
+ * in addition to the dot-directories always skipped.
85
+ * @param {readonly string[]} [opts.extensions] - File extensions to read.
86
+ * @returns {{findings: Array<{file: string, line: number, column: number,
87
+ * severity: "error", message: string}>, files: number}} The findings, and how
88
+ * many files produced them.
89
+ */
90
+ export function lintContentCharset(contentBase: string, { skipDirectories, extensions }?: {
91
+ skipDirectories?: readonly string[] | undefined;
92
+ extensions?: readonly string[] | undefined;
93
+ }): {
94
+ findings: Array<{
95
+ file: string;
96
+ line: number;
97
+ column: number;
98
+ severity: "error";
99
+ message: string;
100
+ }>;
101
+ files: number;
102
+ };
103
+ /**
104
+ * Tier 2 — typography, enumerated one codepoint at a time.
105
+ *
106
+ * **Not the General Punctuation block.** `U+2000`–`U+206F` carries `U+200B`
107
+ * ZERO WIDTH SPACE, the `U+200E`/`U+200F` direction marks, the `U+2028`/`U+2029`
108
+ * separators, the `U+202A`–`U+202E` bidi overrides and `U+2060` WORD JOINER —
109
+ * precisely the invisibles this charset exists to refuse. Admitting the block
110
+ * to reach the em dash would admit all of them, so the ten that are wanted are
111
+ * named and the block is not.
112
+ *
113
+ * @type {ReadonlySet<number>}
114
+ */
115
+ export const TYPOGRAPHY: ReadonlySet<number>;
116
+ /**
117
+ * Tier 3 — the notation the rules and price tables are written in.
118
+ *
119
+ * Every one of these is carried by seven or eight of the eight probed faces, so
120
+ * the tier costs nothing in font freedom. It is a separate tier from the
121
+ * typography above only because it is a separate argument: these earn their
122
+ * place by being *needed* — a Shock threshold reads `≥ 10`, a wall is `10′ ×
123
+ * 11′` — where the typography earns it by being unavoidable.
124
+ *
125
+ * @type {ReadonlySet<number>}
126
+ */
127
+ export const NOTATION: ReadonlySet<number>;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Look one name up.
3
+ *
4
+ * @param {string} name - The name written between the colons, without `icon-`.
5
+ * @param {Record<string, object>} [registry] - Defaults to {@link DEFAULT_ICONS}.
6
+ * @returns {{style: string, icon: string, label: string}|null} The entry, or
7
+ * `null` when the registry does not declare it.
8
+ */
9
+ export function resolveIcon(name: string, registry?: Record<string, object>): {
10
+ style: string;
11
+ icon: string;
12
+ label: string;
13
+ } | null;
14
+ /**
15
+ * The HTML the journals and the website emit — what the system already renders.
16
+ *
17
+ * Carries an accessible name rather than `aria-hidden`. The system's own
18
+ * templates hide their icons because a labelled parent element speaks for them;
19
+ * an icon dropped into a sentence has no such parent, and "the ☆ toggles it"
20
+ * read aloud as "the toggles it" is a sentence with a hole in it.
21
+ *
22
+ * @param {{style: string, icon: string, label: string}} entry - A registry entry.
23
+ * @returns {string} An `<i>` element.
24
+ */
25
+ export function iconHtml(entry: {
26
+ style: string;
27
+ icon: string;
28
+ label: string;
29
+ }): string;
30
+ /**
31
+ * Every icon a string names, in the order written.
32
+ *
33
+ * @param {string} text - Markdown source.
34
+ * @returns {Array<{name: string, index: number, raw: string}>} What it names.
35
+ */
36
+ export function iconsIn(text: string): Array<{
37
+ name: string;
38
+ index: number;
39
+ raw: string;
40
+ }>;
41
+ /**
42
+ * Report every icon a tree names that its registry does not declare.
43
+ *
44
+ * The whole point of a registry is that a typo is answerable, so this is the
45
+ * half that makes `:icon-stra:` a finding rather than three words of literal
46
+ * text nobody notices in a rendered page.
47
+ *
48
+ * @param {string} text - The file's contents.
49
+ * @param {string} file - Path to report.
50
+ * @param {Record<string, object>} [registry] - Defaults to {@link DEFAULT_ICONS}.
51
+ * @returns {Array<{file: string, line: number, column: number,
52
+ * severity: "error", message: string}>} The unknown names.
53
+ */
54
+ export function lintIcons(text: string, file: string, registry?: Record<string, object>): Array<{
55
+ file: string;
56
+ line: number;
57
+ column: number;
58
+ severity: "error";
59
+ message: string;
60
+ }>;
61
+ /**
62
+ * Refuse a registry that names a style Font Awesome Free does not ship.
63
+ *
64
+ * @param {Record<string, object>} registry - A package's icon table.
65
+ * @param {string} [where="icons"] - Where to say the fault is.
66
+ * @returns {Array<{severity: "error", message: string}>} What is wrong with it.
67
+ */
68
+ export function checkIconRegistry(registry: Record<string, object>, where?: string): Array<{
69
+ severity: "error";
70
+ message: string;
71
+ }>;
72
+ /**
73
+ * Walk a content tree and report every icon name its registry does not declare.
74
+ *
75
+ * Its own walk rather than the charset check's, so both modules stay leaves
76
+ * with nothing imported between them. The cost is one extra pass over the tree,
77
+ * which is the cheaper half of a lint that already parses every note.
78
+ *
79
+ * @param {string} contentBase - Root of the content tree.
80
+ * @param {object} [opts]
81
+ * @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore.
82
+ * @param {Record<string, object>} [opts.registry] - The package's icon table.
83
+ * @returns {{findings: Array<{file: string, line: number, column: number,
84
+ * severity: "error", message: string}>, files: number}} What it found.
85
+ */
86
+ export function lintContentIcons(contentBase: string, { skipDirectories, registry }?: {
87
+ skipDirectories?: readonly string[] | undefined;
88
+ registry?: Record<string, object> | undefined;
89
+ }): {
90
+ findings: Array<{
91
+ file: string;
92
+ line: number;
93
+ column: number;
94
+ severity: "error";
95
+ message: string;
96
+ }>;
97
+ files: number;
98
+ };
99
+ /**
100
+ * A markdown-it plugin rendering `:icon-name:` inline.
101
+ *
102
+ * An unknown name is left **exactly as written** rather than dropped. The name
103
+ * is reported by {@link lintIcons}, and a rendered page that still shows
104
+ * `:icon-stra:` is how the author finds it without reading a log.
105
+ *
106
+ * @param {Record<string, object>} [registry] - Defaults to {@link DEFAULT_ICONS}.
107
+ * @returns {(md: object) => void} A markdown-it plugin.
108
+ */
109
+ export function iconPlugin(registry?: Record<string, object>): (md: object) => void;
110
+ /**
111
+ * The Font Awesome styles a registry entry may name.
112
+ *
113
+ * Free ships these three and no others, so a `light` or `duotone` entry would
114
+ * name a glyph the shipped font does not contain — refused here rather than
115
+ * discovered as a blank space in a printed book.
116
+ *
117
+ * @type {readonly string[]}
118
+ */
119
+ export const ICON_STYLES: readonly string[];
120
+ /**
121
+ * The icons the user guide already depicts, under the names it should call them.
122
+ *
123
+ * Each entry was read off the interface it describes rather than invented: the
124
+ * `star`/`star-outline` pair is the filled and hollow star the mastery row and
125
+ * the improve flag draw, and `edit` is the pencil the formula editor opens
126
+ * from. The names are what a *writer* would reach for — `delete`, not
127
+ * `trash-can` — because the writer is the one typing them; the Font Awesome
128
+ * spelling is this table's business, not theirs.
129
+ *
130
+ * @type {Readonly<Record<string, {style: string, icon: string, label: string}>>}
131
+ */
132
+ export const DEFAULT_ICONS: Readonly<Record<string, {
133
+ style: string;
134
+ icon: string;
135
+ label: string;
136
+ }>>;
137
+ /**
138
+ * The shape a note writes, and the one this module claims.
139
+ *
140
+ * The `icon-` prefix is what keeps it out of the way of an emoji shortcode: a
141
+ * surface that also renders `:smile:` can tell the two apart without a lookup,
142
+ * and a reader can tell what `:icon-star:` is without knowing this module
143
+ * exists. Names are lowercase, digits and hyphens — the charset an address
144
+ * segment already uses (#59), so nothing new has to be explained.
145
+ *
146
+ * Not `:name[content]`. That is remark-directive syntax, and this toolchain
147
+ * parses with markdown-it; a directive would render as its own literal text.
148
+ *
149
+ * @type {RegExp}
150
+ */
151
+ export const ICON_PATTERN: RegExp;
@@ -32,6 +32,25 @@ export function catalogDir(config: object, id: string, version: string): string;
32
32
  * @returns {string} The path, whether or not it exists.
33
33
  */
34
34
  export function cachedSchemaPath(config: object, id: string, version: string): string;
35
+ /**
36
+ * What to record about the packs being extracted.
37
+ *
38
+ * Exported so the pair is one fact: {@link foreignItemCatalogDirs} reads what
39
+ * this writes, and a test that hand-wrote the file would prove the reader
40
+ * against a transcription of the format rather than against the format.
41
+ *
42
+ * A pack declaring no `system` records `null` — Foundry requires the field on
43
+ * an Item pack, so this is the shape of a manifest that is wrong rather than a
44
+ * case with a meaning, and `null` reads as "neutral", which is the safe way to
45
+ * be wrong: a neutral pack is read by every system rather than by none.
46
+ *
47
+ * @param {readonly object[]} itemPacks - The manifest's Item pack entries.
48
+ * @returns {Array<{name: string, system: string|null}>} What each one is.
49
+ */
50
+ export function itemPackManifest(itemPacks: readonly object[]): Array<{
51
+ name: string;
52
+ system: string | null;
53
+ }>;
35
54
  /**
36
55
  * Write an unzipped archive's entries under `dest`.
37
56
  *
@@ -159,10 +178,27 @@ export function fetchAllCatalogs(config: object): Promise<number>;
159
178
  * Reads the cache only. A cold cache is an error naming the command that fills
160
179
  * it, rather than a download nobody asked for.
161
180
  *
181
+ * **Scoped to one system when the caller compiles for one (#58)**, exactly as
182
+ * {@link module:engine/generate.itemPackJsonDirs} scopes the local half. The
183
+ * two halves answer the same lookup — `loadItemsMap` merges them into one
184
+ * address space keyed by `subType:shortcode` — so scoping only the local one
185
+ * leaves the collision it was meant to remove: `skill:awar` is a real address
186
+ * in both vocabularies, and a `harn-ensemble` actor compiled for `hm3` would
187
+ * resolve three quarters of its references against whichever document the
188
+ * dependency's `sohl` pack happened to supply. A pack that declares no system
189
+ * is neutral and always read; asking for no system reads every pack, which is
190
+ * every single-system build.
191
+ *
162
192
  * @param {object} config - The resolved build configuration.
163
- * @returns {string[]} Every cached dependency's item directories.
193
+ * @param {string|null} [system] - The system the caller is compiling for.
194
+ * Omitted or `null`, every cached pack is read.
195
+ * @returns {Array<{dir: string, package: string}>} Every cached dependency's
196
+ * item directories, each with the package that published it.
164
197
  */
165
- export function foreignItemCatalogDirs(config: object): string[];
198
+ export function foreignItemCatalogDirs(config: object, system?: string | null): Array<{
199
+ dir: string;
200
+ package: string;
201
+ }>;
166
202
  /**
167
203
  * The file a system publishes its `system` field sets as (#60).
168
204
  *
@@ -1,3 +1,97 @@
1
+ /**
2
+ * What one system block accepts beyond the keys every block carries.
3
+ *
4
+ * @typedef {object} SystemBlockSpec
5
+ * @property {readonly string[]} [known] - Keys stated outright.
6
+ * @property {boolean} [fieldVocabulary] - Whether the note type's declared field
7
+ * names, as the caller's `schemas` state them, are keys of this block.
8
+ * @property {Readonly<Record<string, readonly object[]>>} [fields] - Type → this
9
+ * system's own declared fields. A type it does not name is a type this system
10
+ * says nothing about, and its block is left unchecked on such a note rather
11
+ * than reported wholesale.
12
+ */
13
+ /**
14
+ * Every system a configuration says its tree carries (#58).
15
+ *
16
+ * **Which systems a package ships for is already declared**, in three places
17
+ * that answer different questions, so this reads all three rather than asking a
18
+ * new one:
19
+ *
20
+ * - `systems:` (#48) declares them without requiring one, which is how a
21
+ * package ships for several;
22
+ * - a **pack's** `system:` is the same statement made per pack, and it is the
23
+ * one some trees make: `harn-ensemble` declares an `actors-sohl` and an
24
+ * `actors-hm3` and nothing else about either system. It is already
25
+ * authoritative elsewhere — `eligibleFor` fails a note for want of the block
26
+ * a pack's `system:` names — so a lint that did not read it would refuse a
27
+ * note at compile for a block it never checked;
28
+ * - `stats.systemId` is the package-wide answer where there is one, and it has
29
+ * already absorbed every way of spelling that: a system package is its own
30
+ * system, and a module takes `requiresSystem`, its lone `systems:` entry, or
31
+ * its lone system relationship.
32
+ *
33
+ * A package naming a system in none of them is system-agnostic on purpose — its
34
+ * packs are core document types carrying no system data — so it carries no
35
+ * system block and naming one would invent it.
36
+ *
37
+ * @param {object} [config] - A resolved configuration from `defineConfig`.
38
+ * @returns {string[]} The system ids, deduplicated, in declared order.
39
+ */
40
+ export function declaredSystems(config?: object): string[];
41
+ /**
42
+ * The system blocks a configuration says its tree carries, and what each
43
+ * accepts (#58).
44
+ *
45
+ * The lint checks the blocks its caller names, and for as long as there was one
46
+ * system the only caller named none — so every tree took the `sohl:` of
47
+ * {@link DEFAULT_SYSTEM_BLOCKS}, a constant, in a module whose whole discipline
48
+ * is that it states no vocabulary of its own. That is wrong in both directions
49
+ * the moment a second system exists, and the second direction is the worse:
50
+ *
51
+ * - a package shipping for `hm3` had its `hm3:` block **never looked at**, so
52
+ * every key in it was discarded at compile without a word — the silent-drop
53
+ * family this check exists to close;
54
+ * - and the block that *was* checked was named after a system that package does
55
+ * not ship for, so the one finding it could make was about nothing.
56
+ *
57
+ * **A block's vocabulary has two sources, and a system may have both.**
58
+ *
59
+ * - The **note schemas** the caller hands in as `schemas`. Those belong to one
60
+ * system — the CLI imports `sohl/note-schemas.mjs` — and `schemaSystem` is the
61
+ * caller naming which, because only the caller knows. It is the only source
62
+ * that reaches a type no item registry declares, which is to say `being`: the
63
+ * 2,512 notes `harn-ensemble` is made of, and the reason this is not an
64
+ * optional refinement.
65
+ * - The system's **own registry**, `itemFieldsBySystem`, keyed by system and
66
+ * until now read by nothing. This is what a *second* system's block is held
67
+ * to, since the note schemas describe its neighbour.
68
+ *
69
+ * A system with neither is left out: nothing can state what its block may
70
+ * carry, and holding it to an empty vocabulary would report every key in a
71
+ * correct tree. **That is a check that does not run**, which is
72
+ * indistinguishable from one that passed, so the caller says it out loud —
73
+ * {@link declaredSystems} is the other half of that comparison. `harn-ensemble`
74
+ * is the tree it names: two systems, and an `itemBuilders` registry for
75
+ * neither, so its `hm3:` block is unchecked until it declares one.
76
+ *
77
+ * An earlier draft of this took the note schemas for a system's vocabulary only
78
+ * where the package declared **one** system, on the reasoning that with several
79
+ * there is nothing to say which one they describe. There is: the caller, which
80
+ * chose them. The guess cost `harn-ensemble` its whole `sohl:` check — two
81
+ * systems declared, so the fallback never fired — which is the coverage this
82
+ * change exists to widen rather than narrow.
83
+ *
84
+ * @param {object} [config] - A resolved configuration from `defineConfig`.
85
+ * @param {object} [options] - Options.
86
+ * @param {string} [options.schemaSystem] - The system whose vocabulary the
87
+ * caller's `schemas` state. There are two systems, not an open set, so this is
88
+ * one word from the caller rather than a mechanism.
89
+ * @returns {Readonly<Record<string, SystemBlockSpec>>} The blocks to check, in
90
+ * declared order. A system nothing states the vocabulary of is absent.
91
+ */
92
+ export function systemBlocksFor(config?: object, { schemaSystem }?: {
93
+ schemaSystem?: string | undefined;
94
+ }): Readonly<Record<string, SystemBlockSpec>>;
1
95
  /**
2
96
  * Whether a value satisfies a declared {@link FieldSpec.kind}.
3
97
  *
@@ -32,9 +126,11 @@ export function matchesKind(value: unknown, kind: string): boolean;
32
126
  * Supplied by the caller like `schemas`, so this module states no list of
33
127
  * iconless types of its own; absent it, an inert `img:` goes unreported
34
128
  * rather than every note's being (#349).
35
- * @param {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>} [opts.systems]
36
- * The system blocks to check, and what each accepts. See
37
- * {@link DEFAULT_SYSTEM_BLOCKS}.
129
+ * @param {Readonly<Record<string, SystemBlockSpec>>} [opts.systems]
130
+ * The system blocks to check, and what each accepts. Supplied by the caller
131
+ * for the same reason `schemas` is — a build derives them from its
132
+ * configuration through {@link systemBlocksFor}, and this module states no
133
+ * system name of its own. See {@link DEFAULT_SYSTEM_BLOCKS} for the fallback.
38
134
  * @param {readonly string[]} [opts.packs] - The pack names this package
39
135
  * declares, for a `data:` field whose map is keyed by pack. Supplied by the
40
136
  * caller like `schemas` and `vocabulary`, and absent it no claim is made
@@ -49,10 +145,7 @@ export function lintNote(note: object, { schemas, index, vocabulary, packs, emit
49
145
  document: string | null;
50
146
  art: readonly string[];
51
147
  } | null) | undefined;
52
- systems?: Readonly<Record<string, {
53
- known?: readonly string[];
54
- fieldVocabulary?: boolean;
55
- }>> | undefined;
148
+ systems?: Readonly<Record<string, SystemBlockSpec>> | undefined;
56
149
  packs?: readonly string[] | undefined;
57
150
  }): object[];
58
151
  /**
@@ -64,8 +157,9 @@ export function lintNote(note: object, { schemas, index, vocabulary, packs, emit
64
157
  * @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
65
158
  * it declares (#128); see {@link lintNote}.
66
159
  * @param {boolean} [opts.references=true] - Whether to check references.
67
- * @param {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>} [opts.systems]
68
- * The system blocks to check. See {@link DEFAULT_SYSTEM_BLOCKS}.
160
+ * @param {Readonly<Record<string, SystemBlockSpec>>} [opts.systems]
161
+ * The system blocks to check; see {@link lintNote} and
162
+ * {@link systemBlocksFor}.
69
163
  * @param {readonly string[]} [opts.packs] - The declared pack names; see
70
164
  * {@link lintNote}.
71
165
  * @param {(type: string) => {document: string|null, art: readonly string[]}|null} [opts.emittedArt]
@@ -77,10 +171,7 @@ export function lintFrontmatter(index: object, { schemas, vocabulary, packs, emi
77
171
  schemas: Record<string, readonly object[]>;
78
172
  vocabulary?: Record<string, object> | undefined;
79
173
  references?: boolean | undefined;
80
- systems?: Readonly<Record<string, {
81
- known?: readonly string[];
82
- fieldVocabulary?: boolean;
83
- }>> | undefined;
174
+ systems?: Readonly<Record<string, SystemBlockSpec>> | undefined;
84
175
  packs?: readonly string[] | undefined;
85
176
  emittedArt?: ((type: string) => {
86
177
  document: string | null;
@@ -123,21 +214,48 @@ export const UNIVERSAL_KEYS: ReadonlySet<string>;
123
214
  * The system blocks a build checks, and what each accepts beyond the shared
124
215
  * vocabulary.
125
216
  *
126
- * One entry, because one system is what every existing tree declares — and the
127
- * default is a *declaration*, not a hard-coded assumption: a build that ships
128
- * content for two systems passes both, and each block is then checked against
129
- * its own vocabulary rather than against the other's (#58). A block nothing
130
- * declares is not checked, because nothing can say what it may carry, and
131
- * inventing a rule for it would report a correct tree red.
217
+ * One entry, because one system is what every existing tree declares — and it
218
+ * is a *fallback*, not the rule. {@link systemBlocksFor} derives the map from
219
+ * the configuration, which is what makes the block a package actually ships for
220
+ * the block that gets checked; this is what a caller holding no configuration
221
+ * gets, which in practice is a unit test.
132
222
  *
133
- * `fieldVocabulary` says the note type's own declared field names are keys of
134
- * this block. True for `sohl` and untrue in general: those names come from the
135
- * `itemBuilders` registry that this system declares, and a second system's
136
- * notes write a second system's fields.
223
+ * A block nothing declares is not checked, because nothing can say what it may
224
+ * carry, and inventing a rule for it would report a correct tree red.
137
225
  *
138
- * @type {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>}
226
+ * Three ways a block may state its vocabulary, and a spec declares at most one:
227
+ *
228
+ * - `known` — an explicit list of keys, for a caller stating them outright.
229
+ * - `fieldVocabulary` — the note type's own declared field names, as the
230
+ * caller's `schemas` state them, are keys of this block. That holds for the
231
+ * **one** system a single-registry tree ships for, where `schemas` *is* that
232
+ * system's vocabulary, and in general holds for no other.
233
+ * - `fields` — type → that system's own declared fields, from the registry the
234
+ * system declares. What a second system's block is checked against, because a
235
+ * second system's notes write a second system's fields and the note-type
236
+ * schemas describe somebody else's.
237
+ *
238
+ * @type {Readonly<Record<string, SystemBlockSpec>>}
139
239
  */
140
- export const DEFAULT_SYSTEM_BLOCKS: Readonly<Record<string, {
141
- known?: readonly string[];
142
- fieldVocabulary?: boolean;
143
- }>>;
240
+ export const DEFAULT_SYSTEM_BLOCKS: Readonly<Record<string, SystemBlockSpec>>;
241
+ /**
242
+ * What one system block accepts beyond the keys every block carries.
243
+ */
244
+ export type SystemBlockSpec = {
245
+ /**
246
+ * - Keys stated outright.
247
+ */
248
+ known?: readonly string[] | undefined;
249
+ /**
250
+ * - Whether the note type's declared field
251
+ * names, as the caller's `schemas` state them, are keys of this block.
252
+ */
253
+ fieldVocabulary?: boolean | undefined;
254
+ /**
255
+ * - Type → this
256
+ * system's own declared fields. A type it does not name is a type this system
257
+ * says nothing about, and its block is left unchecked on such a note rather
258
+ * than reported wholesale.
259
+ */
260
+ fields?: Readonly<Record<string, readonly object[]>> | undefined;
261
+ };
@@ -462,6 +462,14 @@ export function expandNoteTables(body: string, { docs, name, fm, bodyLine, sqlTa
462
462
  * underscores.
463
463
  */
464
464
  export function folderFilename(name: any, id: any): string;
465
+ /**
466
+ * The markdown renderer every surface shares.
467
+ *
468
+ * `html: true` is long-standing and load-bearing — notes carry raw blocks — and
469
+ * it is also why {@link module:engine/content-icons} exists rather than an
470
+ * instruction to write `<i class="fa-solid …">` by hand: that would render on
471
+ * the two HTML surfaces and be silently dropped by the third (#378).
472
+ */
465
473
  export const md: import("markdown-it").MarkdownIt;
466
474
  export { slugify } from "./content-slug.mjs";
467
475
  export { makeId } from "./ids.mjs";
@@ -25,6 +25,8 @@ export * as foundryEntries from "./foundry-entries.mjs";
25
25
  export * as contentIndex from "./content-index.mjs";
26
26
  export * as siteBuild from "./site-build.mjs";
27
27
  export * as contentLint from "./content-lint.mjs";
28
+ export * as contentCharset from "./content-charset.mjs";
29
+ export * as contentIcons from "./content-icons.mjs";
28
30
  export * as contentLinks from "./content-links.mjs";
29
31
  export * as webWikilinks from "./web-wikilinks.mjs";
30
32
  export * as contentTables from "./content-tables.mjs";
@@ -15,6 +15,59 @@
15
15
  * @returns {ReadonlySet<string>} The note types such a pass would claim.
16
16
  */
17
17
  export function noteTypesClaimedBy(docType: string, sources?: ClaimSources): ReadonlySet<string>;
18
+ /**
19
+ * Every document class a note of one type compiles into (#152).
20
+ *
21
+ * **A note produces more than one document, and that is the ordinary case.** An
22
+ * item note compiles an Item *and* the JournalEntry its prose becomes; a map
23
+ * note a Scene and a JournalEntry; since #337 an actor note an Actor and a
24
+ * JournalEntry too. {@link claimedNoteTypes} unions over the configured packs
25
+ * and so answers "is this note compiled *at all*", which is #146's question and
26
+ * cannot see a note that compiles one of its two documents and loses the other.
27
+ *
28
+ * Asked of the **claim table** rather than of a list of its own, so the set of
29
+ * documents a type produces and the set of passes that claim it are one
30
+ * statement. A pass that starts claiming a type starts producing its document
31
+ * here, with nothing to remember.
32
+ *
33
+ * **Union across systems, never per system.** A type one system maps and
34
+ * another does not appears once, because the `Item` and `Actor` rows already
35
+ * fold the maps together — so this cannot report a document class a system
36
+ * deliberately declines to produce, which is the silence #79 requires.
37
+ *
38
+ * ## The JournalEntry row is the one that is per *note*
39
+ *
40
+ * Every other row is a property of the type: a `macro` note produces a Macro, a
41
+ * map note a Scene, whatever either says. Documentation is not. `Journals`
42
+ * declines a doc-carrying note whose body is empty — *"an item with no prose
43
+ * gets no doc, and the items pass leaves its description empty rather than
44
+ * pointing at nothing"* — so whether an item note produces a JournalEntry is
45
+ * decided by the note, not by its type.
46
+ *
47
+ * That distinction is the whole difference between a useful finding and a
48
+ * useless one. `sohl-kethira-basic` declares no JournalEntry pack and ships 393
49
+ * notes whose descriptions are *deliberately* empty, under the Fan Material
50
+ * Guidelines its configuration explains at length. A type-level answer would
51
+ * report every one of them for losing a document none of them produces. Asking
52
+ * per note, it reports none, and still reports `harn-ensemble`'s 2,517 beings,
53
+ * whose `{#appearance}` and `{#dossier}` prose is real and is lost.
54
+ *
55
+ * `hasProse` is therefore how the caller answers that, and it is a **thunk** so
56
+ * that the file is read only where the answer could change the outcome. Omitted,
57
+ * the answer is the type's full potential — every document such a note *could*
58
+ * produce — which is what a caller asking about a type rather than a note wants.
59
+ *
60
+ * @param {string} type - The note's declared `type`, current spelling.
61
+ * @param {ClaimSources} [sources] - What to answer from.
62
+ * @param {object} [opts] - Options.
63
+ * @param {(() => boolean)|boolean} [opts.hasProse] - Whether *this note* carries
64
+ * a body. Omitted, the type's potential is reported.
65
+ * @returns {string[]} The document classes, in {@link CLAIMABLE_DOCUMENT_TYPES}
66
+ * order. Empty for a type nothing compiles.
67
+ */
68
+ export function documentClassesFor(type: string, sources?: ClaimSources, { hasProse }?: {
69
+ hasProse?: boolean | (() => boolean) | undefined;
70
+ }): string[];
18
71
  /**
19
72
  * Every note type some pack in a configuration would compile.
20
73
  *
@@ -133,6 +186,20 @@ export const UNIMPLEMENTED_TYPES: ReadonlySet<string>;
133
186
  */
134
187
  export const DERIVED_PACKED_TYPES: ReadonlySet<string>;
135
188
  export { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
189
+ /**
190
+ * Every Foundry document class {@link noteTypesClaimedBy} answers for.
191
+ *
192
+ * The switch above, read the other way round. It is written out rather than
193
+ * derived because a `switch` cannot be enumerated — and
194
+ * `tests/unclaimed-note-types.test.ts` holds the two together by checking that
195
+ * no class outside this list claims anything, so a row added there and not here
196
+ * fails rather than going quiet.
197
+ *
198
+ * Order is the order a reader meets them in a message, not a precedence.
199
+ *
200
+ * @type {readonly string[]}
201
+ */
202
+ export const CLAIMABLE_DOCUMENT_TYPES: readonly string[];
136
203
  /**
137
204
  * What a claim question is asked against.
138
205
  *