@heroiclands/package-build 19.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.
Files changed (85) hide show
  1. package/CHANGELOG.md +1100 -0
  2. package/CONTENT.md +264 -33
  3. package/README.md +43 -4
  4. package/bin/content-build.mjs +94 -3
  5. package/config.mjs +9 -1
  6. package/content-config.mjs +99 -19
  7. package/docs/content-format.md +394 -72
  8. package/e2e.mjs +297 -3
  9. package/engine/actor-compiler.mjs +197 -7
  10. package/engine/address-charset.mjs +23 -5
  11. package/engine/base-compiler.mjs +63 -2
  12. package/engine/bundles.mjs +9 -0
  13. package/engine/content-address.mjs +92 -1
  14. package/engine/content-charset.mjs +434 -0
  15. package/engine/content-format.mjs +102 -0
  16. package/engine/content-icons.mjs +388 -0
  17. package/engine/content-index.mjs +11 -8
  18. package/engine/content-links.mjs +37 -21
  19. package/engine/field-reference.mjs +57 -5
  20. package/engine/field-spec.mjs +214 -7
  21. package/engine/folder-notes.mjs +24 -1
  22. package/engine/foreign-catalog.mjs +112 -7
  23. package/engine/foundry-entries.mjs +14 -0
  24. package/engine/frontmatter-lint.mjs +377 -56
  25. package/engine/frontmatter.mjs +11 -11
  26. package/engine/generate.mjs +72 -12
  27. package/engine/helpers.mjs +96 -10
  28. package/engine/index.mjs +9 -0
  29. package/engine/item-compiler.mjs +37 -0
  30. package/engine/journals.mjs +21 -4
  31. package/engine/macros.mjs +8 -0
  32. package/engine/map-notes.mjs +7 -7
  33. package/engine/note-claims.mjs +208 -5
  34. package/engine/note-ids.mjs +25 -1
  35. package/engine/note-vocabulary.mjs +76 -9
  36. package/engine/pack-config.mjs +102 -12
  37. package/engine/pack-router.mjs +0 -0
  38. package/engine/prose-config.mjs +42 -0
  39. package/engine/prose-lint.mjs +126 -0
  40. package/engine/retired-fields.mjs +57 -16
  41. package/engine/runtime-only-fields.mjs +204 -0
  42. package/engine/scenes.mjs +12 -19
  43. package/engine/schema-check.mjs +23 -1
  44. package/engine/schema-extract.mjs +13 -0
  45. package/engine/site-index.mjs +17 -0
  46. package/engine/subtype-registry.mjs +30 -0
  47. package/engine/system-block.mjs +81 -3
  48. package/engine/web-wikilinks.mjs +33 -27
  49. package/engine/wikilink-syntax.mjs +7 -0
  50. package/engine/wikilinks.mjs +74 -16
  51. package/hm3/actors.mjs +63 -13
  52. package/package.json +2 -2
  53. package/sohl/actors.mjs +106 -7
  54. package/sohl/item-fields.mjs +203 -0
  55. package/sohl/note-schemas.mjs +6 -3
  56. package/types/config.d.mts +7 -0
  57. package/types/e2e.d.mts +130 -3
  58. package/types/engine/actor-compiler.d.mts +83 -3
  59. package/types/engine/address-charset.d.mts +22 -4
  60. package/types/engine/base-compiler.d.mts +54 -3
  61. package/types/engine/content-address.d.mts +64 -0
  62. package/types/engine/content-charset.d.mts +127 -0
  63. package/types/engine/content-format.d.mts +9 -0
  64. package/types/engine/content-icons.d.mts +151 -0
  65. package/types/engine/field-spec.d.mts +271 -3
  66. package/types/engine/folder-notes.d.mts +20 -0
  67. package/types/engine/foreign-catalog.d.mts +38 -2
  68. package/types/engine/foundry-entries.d.mts +6 -0
  69. package/types/engine/frontmatter-lint.d.mts +164 -30
  70. package/types/engine/frontmatter.d.mts +11 -11
  71. package/types/engine/generate.d.mts +27 -0
  72. package/types/engine/helpers.d.mts +45 -9
  73. package/types/engine/index.d.mts +3 -0
  74. package/types/engine/map-notes.d.mts +2 -2
  75. package/types/engine/note-claims.d.mts +67 -0
  76. package/types/engine/note-ids.d.mts +14 -0
  77. package/types/engine/pack-config.d.mts +35 -0
  78. package/types/engine/prose-config.d.mts +41 -0
  79. package/types/engine/prose-lint.d.mts +36 -0
  80. package/types/engine/retired-fields.d.mts +29 -13
  81. package/types/engine/runtime-only-fields.d.mts +102 -0
  82. package/types/engine/schema-check.d.mts +10 -1
  83. package/types/engine/subtype-registry.d.mts +21 -0
  84. package/types/engine/system-block.d.mts +28 -2
  85. package/types/sohl/actors.d.mts +3 -3
@@ -41,6 +41,54 @@ export function deepMerge(base: any, overlay: any): any;
41
41
  * @returns {string} The address, `subType:shortcode`.
42
42
  */
43
43
  export function itemAddress(subType: string, shortcode: string): string;
44
+ /**
45
+ * The key one predefined item is held under **for the package that publishes
46
+ * it** — the address a `model:` naming that package resolves through (#334).
47
+ *
48
+ * The unqualified {@link itemAddress} stays beside it, and the two answer
49
+ * different questions. A `model` that names no package means *this* one and
50
+ * takes the unqualified key, where a local definition still shadows a
51
+ * dependency's. A `model` that names a package takes this one, which nothing
52
+ * can shadow: that is the whole point of writing the package down.
53
+ *
54
+ * Not the canonical wikilink address, because this map is keyed in the
55
+ * **document's** vocabulary — a Foundry Item subtype — while a canonical address
56
+ * carries the *note* type. The two differ wherever a system maps a type to a
57
+ * differently-named subtype, and translating here would put the translation in
58
+ * two places.
59
+ *
60
+ * @param {string} pkg - The content package that publishes the item.
61
+ * @param {string} subType - The Foundry Item subtype.
62
+ * @param {string} shortcode - The item's `system.shortcode`.
63
+ * @returns {string} The address, `package:subType:shortcode`.
64
+ */
65
+ export function packagedItemAddress(pkg: string, subType: string, shortcode: string): string;
66
+ /**
67
+ * The key one predefined item is held under **in the catalogue**, with the
68
+ * shortcode folded to lower case.
69
+ *
70
+ * A shortcode is case-sensitive and routinely mixed — `Clb`, `LtShoe`,
71
+ * `HsTunic` — while an **address** is not: `readQualifier` normalises what it
72
+ * reads, and every canonical address is lowercase. So the moment a `model:` is
73
+ * read as an address (#334), `weapongear-clb` has to find the document whose
74
+ * `system.shortcode` is `Clb`, and an exact match cannot (#346).
75
+ *
76
+ * Folding is safe because the fold is already the address: no two items in any
77
+ * published tree differ only by the case of their shortcode, and #340 will make
78
+ * that impossible rather than merely true.
79
+ *
80
+ * **This is not {@link itemAddress}, and must not become it.** That one seeds
81
+ * {@link embeddedItemId}, so folding there would change the `_id` of every
82
+ * embedded item whose identity carries a capital — a silent re-identification of
83
+ * documents nothing about which had changed. The catalogue is a lookup table;
84
+ * an id is a promise.
85
+ *
86
+ * @param {string} subType - The Foundry Item subtype.
87
+ * @param {string} shortcode - The item's `system.shortcode`, in any case.
88
+ * @param {string} [pkg] - The publishing package, for the qualified form.
89
+ * @returns {string} The catalogue key.
90
+ */
91
+ export function catalogueKey(subType: string, shortcode: string, pkg?: string): string;
44
92
  /**
45
93
  * What identifies one embedded item on its actor.
46
94
  *
@@ -136,6 +184,13 @@ export class SystemActorCompiler extends BasePackCompiler {
136
184
  /** @type {readonly string[]} */
137
185
  itemsSourceDirs: readonly string[];
138
186
  foreignSourceDirs: any;
187
+ /**
188
+ * Every package a `model:` may name besides this one — the dependencies
189
+ * whose item catalogues were supplied (#334).
190
+ *
191
+ * @returns {Set<string>} The dependency package ids.
192
+ */
193
+ get foreignPackages(): Set<string>;
139
194
  /**
140
195
  * This pass's system map, or a message naming the class that forgot it.
141
196
  *
@@ -195,9 +250,34 @@ export class SystemActorCompiler extends BasePackCompiler {
195
250
  * @returns {object|null} The embedded item, or null when it resolved to
196
251
  * nothing — always with a finding emitted.
197
252
  */
198
- resolveEmbedded(itemsMap: Map<string, object>, actorId: string, type: string, shortcode: string | null, overlay?: object, indexKey: string, ctx: string, { fmKey }?: {
199
- fmKey?: string | undefined;
200
- }): object | null;
253
+ /**
254
+ * Read an entry's `model:` — the address of the item it is a copy of.
255
+ *
256
+ * The address grammar is the wikilink one (#336), so a `model` is written at
257
+ * whatever length says what it means: `skill-wpnc` within this package,
258
+ * `sohl-sohl-skill-wpnc` to reach another. The system segment defaults from
259
+ * the block the entry sits in — `<system>.items` — which is what makes the
260
+ * short form name an *Item* here while the same string in body prose names
261
+ * a page.
262
+ *
263
+ * It replaced a top-level `shortcode:` that meant something different from
264
+ * the `system.shortcode` beside it and could not say which package a
265
+ * template came from (#334).
266
+ *
267
+ * @param {unknown} model - The authored value.
268
+ * @param {number} index - The entry's position, for the message.
269
+ * @param {string} ctx - Diagnostic context (the actor's label).
270
+ * @returns {{type: string, shortcode: string, package: string|null}|null}
271
+ * The parsed address, or `null` after reporting why it is not one.
272
+ */
273
+ readModel(model: unknown, index: number, ctx: string): {
274
+ type: string;
275
+ shortcode: string;
276
+ package: string | null;
277
+ } | null;
278
+ resolveEmbedded(itemsMap: any, actorId: any, type: any, shortcode: any, overlay: any, indexKey: any, ctx: any, { fmKey, modelPackage }?: {
279
+ modelPackage?: null | undefined;
280
+ }): any;
201
281
  #private;
202
282
  }
203
283
  import { BasePackCompiler } from "./base-compiler.mjs";
@@ -37,11 +37,29 @@ export function isAddressSegment(value: unknown): boolean;
37
37
  * @module
38
38
  */
39
39
  /**
40
- * The shape every address segment must match: ASCII letters and digits only.
40
+ * The shape every address segment must match: **lowercase** ASCII letters and
41
+ * digits only.
41
42
  *
42
- * Case is deliberately **not** constrained. Hundreds of authored shortcodes are
43
- * mixed-case and collide with nothing, and case has no bearing on the
44
- * separator, which is the whole of what this pattern is protecting.
43
+ * Case *was* deliberately unconstrained, on the reasoning that case has no
44
+ * bearing on the separator — which is true, and beside the point (#340).
45
+ *
46
+ * **Two names that differ only in case are two names nobody can tell apart.** A
47
+ * shortcode is how a person names a thing when writing a reference —
48
+ * `model: weapongear-dgr`, `[[skill-melee|…]]` — and `Dgr` beside `dgr` is a
49
+ * distinction you cannot say out loud and can only see by looking twice.
50
+ *
51
+ * The toolchain had already half-decided it: {@link canonicalKey} lowercases the
52
+ * address it builds, so a note declaring `Clb` published
53
+ * `sohl-sohl-weapongear-clb` and its `_id` derived from that. The authored name
54
+ * and its address disagreed, and everything downstream keys on the address —
55
+ * which left two notes differing only in case sharing one address, one `_id` and
56
+ * one URL, with nothing to report it. It also forced two exceptions elsewhere:
57
+ * #336 had to exempt the shortcode from the lowercase rule it pinned on every
58
+ * other segment, and #346 had to fold the shortcode's case in the item catalogue
59
+ * because an address is lowercased when read.
60
+ *
61
+ * One case, one spelling, no exceptions. Every tree already complies but two,
62
+ * and nothing in any of them collides when folded.
45
63
  *
46
64
  * @type {RegExp}
47
65
  */
@@ -99,14 +99,42 @@ export class BasePackCompiler {
99
99
  * @type {boolean}
100
100
  */
101
101
  static requiresSystemBlock: boolean;
102
+ /**
103
+ * The **art fields** this pass reads off a note and writes onto its
104
+ * document — `img`, `portrait`, whichever of them reaches the output.
105
+ *
106
+ * Empty by default, and every shipped pass states its own, for the reason
107
+ * {@link BasePackCompiler.readsPackOutputOf} does: the fact belongs to the
108
+ * class that does the writing, and a second list of it somewhere else is a
109
+ * list free to disagree with what is actually emitted.
110
+ *
111
+ * The reader is the frontmatter lint. `img` is a *shared top-level* field —
112
+ * legal on every note whatever its type, because
113
+ * `BLOCK_DOCUMENT_PROPERTIES` maps it onto `document.img` — so a note whose
114
+ * document has no such property authors it, validates, compiles, and loses
115
+ * the value with nothing said. That is #349: `Parrot` in `sohl-thalorna`
116
+ * had declared `img:` since long before the art rule existed and compiled
117
+ * `img: null` exactly as a note declaring nothing does. Naming the fields
118
+ * here is what lets the lint tell an inert key from a live one.
119
+ *
120
+ * A pass that emits art **anywhere** in its document declares it, not only
121
+ * one that writes a top-level `img`: the scenes pass puts the path on the
122
+ * scene's background rather than on a property called `img`, and the value
123
+ * is no less live for it. The question this answers is whether the authored
124
+ * path reaches the output at all.
125
+ *
126
+ * @type {readonly string[]}
127
+ */
128
+ static emitsArt: readonly string[];
102
129
  /**
103
130
  * @param {object} options
104
131
  * @param {string} options.contentBase - Root of the content tree.
105
132
  * @param {string} options.dest - Where this pass writes its JSON.
106
133
  * @param {readonly string[]} options.skipDirectories - Directories the walk
107
134
  * never descends into. Required: see {@link assertStatedScope}.
108
- * @param {(path: string|null) => string|null} [options.folderResolver] -
109
- * Resolves a `sohl.folder` id against this pack's folder hierarchy.
135
+ * @param {(address: string|null) => string|null} [options.folderResolver] -
136
+ * Resolves a `packFolder` a folder note's address — to the Foundry
137
+ * folder id it materialises as in this pack (#255, #257).
110
138
  * @param {string} [options.packName] - The pack this pass writes.
111
139
  * @param {string} [options.docType] - The Foundry document type it holds.
112
140
  * @param {{resolve: Function}} [options.router] - The pack router. Omit it
@@ -118,7 +146,7 @@ export class BasePackCompiler {
118
146
  contentBase: string;
119
147
  dest: string;
120
148
  skipDirectories: readonly string[];
121
- folderResolver?: ((path: string | null) => string | null) | undefined;
149
+ folderResolver?: ((address: string | null) => string | null) | undefined;
122
150
  packName?: string | undefined;
123
151
  docType?: string | undefined;
124
152
  router?: {
@@ -236,6 +264,29 @@ export class BasePackCompiler {
236
264
  * the note's own file can be read.
237
265
  */
238
266
  eligibleFor(fm: object): boolean;
267
+ /**
268
+ * A refusal only this pass can make, because its subject is the note's
269
+ * **type** (#330).
270
+ *
271
+ * The `assertNo*Field` family above it in the walk is type-agnostic by
272
+ * construction: it runs before `selects`, so that a note declaring a
273
+ * retired field is answered whichever pass would have claimed it. A rule
274
+ * about what a *`trauma`* may write cannot live there — it needs the type's
275
+ * field declaration, which only the pass that compiles the type can reach.
276
+ *
277
+ * So it is a hook, called once the note is known to be this pass's, and its
278
+ * throw is counted and located exactly as the family's is: the note is
279
+ * declined rather than skipped, and the build fails naming the line.
280
+ *
281
+ * The default refuses nothing, which is the honest position for a pass
282
+ * whose documents have no schema to have opinions about.
283
+ *
284
+ * @param {object} fm - The note's frontmatter.
285
+ * @returns {void}
286
+ * @throws {Error} When the note authors something its type forbids. The
287
+ * error may carry a `position` for the diagnostic.
288
+ */
289
+ assertAuthorable(fm: object): void;
239
290
  /**
240
291
  * Whether this pass claims a note. **Required.**
241
292
  *
@@ -113,6 +113,70 @@ export function packageAddress(fm: object): string;
113
113
  * @returns {string} `package-system-type-shortcode`, lowercased.
114
114
  */
115
115
  export function canonicalKey(pkg: string, system: string, type: string, shortcode: string): string;
116
+ /**
117
+ * Which system a frontmatter key path is written under.
118
+ *
119
+ * The **enclosing system block** decides, at any depth within it, and nothing
120
+ * else does: `sohl.items[3].model` and `sohl.system.body.structure` are both
121
+ * `sohl` because both sit under `sohl:`. Everywhere else is {@link NO_SYSTEM} —
122
+ * top-level frontmatter, the shared `data:` container, and body prose, which has
123
+ * no key path at all and passes `undefined`.
124
+ *
125
+ * It is the block rather than the field, so a `WikiLink` field needs no opinion
126
+ * about systems and no per-field table has to be kept in step with the schema.
127
+ *
128
+ * The first segment must **be** a declared system, not merely look like one:
129
+ * `sohlish.items` is a key called `sohlish`, and `notes.sohl.thing` names no
130
+ * block at all.
131
+ *
132
+ * @param {string} [keyPath] - The dotted frontmatter key path, or `undefined`
133
+ * for body prose.
134
+ * @returns {string} The system id, or `none`.
135
+ */
136
+ export function blockSystem(keyPath?: string): string;
137
+ /**
138
+ * Expand a written address to the one canonical address it names.
139
+ *
140
+ * **An omitted segment defaults from where the link is written** (#336) — it is
141
+ * not a wildcard, and resolution is not a search. Package omitted means the
142
+ * citing note's own; system omitted means {@link blockSystem} of the key path it
143
+ * was written under. So every short form has exactly one expansion, computed
144
+ * before anything is looked up, and there is no candidate set to disambiguate.
145
+ *
146
+ * **Under `none`, a system-bearing type addresses its documentation journal.**
147
+ * A note's `none` address *is* its `doc<type>` entry — the Item is the one with
148
+ * a system — so a prose `[[affiliation-sirvadar|…]]` names the page, which is
149
+ * almost always what prose means. A link that means the Item states the system
150
+ * and gets it. This is the defaulting rule applied, not an exception carved out
151
+ * of it.
152
+ *
153
+ * **Only a type whose own document carries a system is redirected.** A `macro`
154
+ * and the map types have documentation journals too, but their own documents
155
+ * are core ones and already live at `none` — so `<pkg>-none-macro-x` names the
156
+ * Macro and `<pkg>-none-docmacro-x` its journal, two live addresses that the
157
+ * redirect would collapse into one. The test is the note type's own system,
158
+ * not merely whether it has a doc entry.
159
+ *
160
+ * A `doc<type>` written explicitly is `none` **wherever** it appears, even
161
+ * inside a system block: no game system defines a JournalEntry, so there is no
162
+ * other system for one to belong to.
163
+ *
164
+ * @param {{type: string, shortcode: string, package?: string, system?: string,
165
+ * itemDoc?: boolean}} read - A qualifier, as `readQualifier` returns one.
166
+ * @param {{package: string, system?: string}} where - The citing context: the
167
+ * tree's own content package, and the system of the block the link sits in.
168
+ * @returns {string} The canonical `package-system-type-shortcode`.
169
+ */
170
+ export function expandAddress(read: {
171
+ type: string;
172
+ shortcode: string;
173
+ package?: string;
174
+ system?: string;
175
+ itemDoc?: boolean;
176
+ }, where: {
177
+ package: string;
178
+ system?: string;
179
+ }): string;
116
180
  /**
117
181
  * Reads a canonical key back into its parts.
118
182
  *
@@ -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>;
@@ -40,6 +40,9 @@ export const CONTENT_FORMAT_PATH: string;
40
40
  * property — what a note actually writes. `appearance.eye_color` is authored
41
41
  * as `appearance`, so that is the key recorded.
42
42
  * @property {Set<string>} dataPaths - The declared paths, whole.
43
+ * @property {string[]} subTypes - The `subType` values the section enumerates,
44
+ * in document order — empty when it states none, which is the ordinary case
45
+ * for a type that has no `subType` at all.
43
46
  */
44
47
  /**
45
48
  * One `system.*` target the specification names for one note type.
@@ -99,6 +102,12 @@ export type TypeSpec = {
99
102
  * - The declared paths, whole.
100
103
  */
101
104
  dataPaths: Set<string>;
105
+ /**
106
+ * - The `subType` values the section enumerates,
107
+ * in document order — empty when it states none, which is the ordinary case
108
+ * for a type that has no `subType` at all.
109
+ */
110
+ subTypes: string[];
102
111
  };
103
112
  /**
104
113
  * One `system.*` target the specification names for one note type.
@@ -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;