@heroiclands/package-build 20.3.1 → 20.5.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 (195) hide show
  1. package/CHANGELOG.md +351 -0
  2. package/CONTENT.md +322 -110
  3. package/README.md +21 -10
  4. package/bin/content-build.mjs +204 -92
  5. package/bin/package-build.mjs +48 -15
  6. package/config.mjs +6 -8
  7. package/content-config.mjs +518 -151
  8. package/docs/api.md +1352 -0
  9. package/docs/commands.md +1609 -0
  10. package/docs/configuration.md +1432 -0
  11. package/docs/content-format.md +73 -74
  12. package/docs/diagnostics.md +356 -0
  13. package/docs/getting-started.md +813 -0
  14. package/docs/project-setup.md +469 -0
  15. package/e2e.mjs +4 -4
  16. package/engine/actor-compiler.mjs +64 -61
  17. package/engine/address-charset.mjs +6 -6
  18. package/engine/address-diff.mjs +57 -52
  19. package/engine/anchored-sections.mjs +1 -1
  20. package/engine/anchors.mjs +1 -1
  21. package/engine/base-compiler.mjs +42 -38
  22. package/engine/bundle-notes.mjs +4 -4
  23. package/engine/bundles.mjs +17 -8
  24. package/engine/code-fences.mjs +3 -3
  25. package/engine/compendiums.mjs +8 -8
  26. package/engine/compile-corpus.mjs +4 -5
  27. package/engine/content-address.mjs +24 -24
  28. package/engine/content-charset.mjs +1 -1
  29. package/engine/content-format-check.mjs +24 -25
  30. package/engine/content-format.mjs +8 -8
  31. package/engine/content-html.mjs +202 -0
  32. package/engine/content-icons.mjs +196 -165
  33. package/engine/content-index.mjs +61 -40
  34. package/engine/content-links.mjs +74 -41
  35. package/engine/content-lint.mjs +64 -34
  36. package/engine/content-package.mjs +7 -9
  37. package/engine/content-slug.mjs +2 -2
  38. package/engine/content-tables.mjs +38 -34
  39. package/engine/content-tree.mjs +1 -1
  40. package/engine/derived-fields.mjs +174 -0
  41. package/engine/diagnostics.mjs +3 -4
  42. package/engine/document-subtypes.mjs +6 -7
  43. package/engine/field-reference.mjs +8 -8
  44. package/engine/field-spec.mjs +29 -30
  45. package/engine/folder-notes.mjs +18 -16
  46. package/engine/foreign-catalog.mjs +8 -9
  47. package/engine/foundry-entries.mjs +19 -20
  48. package/engine/frontmatter-lint.mjs +95 -103
  49. package/engine/frontmatter.mjs +11 -11
  50. package/engine/generate.mjs +44 -39
  51. package/engine/helpers.mjs +147 -84
  52. package/engine/homepage.mjs +20 -22
  53. package/engine/ids.mjs +15 -15
  54. package/engine/index-records.mjs +4 -4
  55. package/engine/index.mjs +26 -10
  56. package/engine/item-compiler.mjs +54 -29
  57. package/engine/item-docs.mjs +11 -13
  58. package/engine/item-registry.mjs +13 -13
  59. package/engine/journals.mjs +24 -15
  60. package/engine/macros.mjs +3 -3
  61. package/engine/map-notes.mjs +6 -6
  62. package/engine/metadata-index.mjs +5 -5
  63. package/engine/note-claims.mjs +51 -41
  64. package/engine/note-ids.mjs +5 -5
  65. package/engine/note-package.mjs +4 -4
  66. package/engine/note-renames.mjs +3 -3
  67. package/engine/note-schemas.mjs +3 -8
  68. package/engine/note-vocabulary.mjs +56 -54
  69. package/engine/pack-config.mjs +39 -25
  70. package/engine/pack-router.mjs +0 -0
  71. package/engine/pdf-build.mjs +464 -0
  72. package/engine/pdf-fonts.mjs +420 -0
  73. package/engine/pdf-render.mjs +798 -0
  74. package/engine/pdf-toc.mjs +525 -0
  75. package/engine/prose-config.mjs +9 -9
  76. package/engine/prose-lint.mjs +12 -13
  77. package/engine/region-events.mjs +2 -2
  78. package/engine/retired-fields.mjs +24 -22
  79. package/engine/runtime-only-fields.mjs +1 -1
  80. package/engine/scene-levels.mjs +1 -1
  81. package/engine/scenes.mjs +33 -25
  82. package/engine/schema-check.mjs +21 -21
  83. package/engine/schema-extract.mjs +1 -1
  84. package/engine/site-build.mjs +66 -50
  85. package/engine/site-index.mjs +17 -18
  86. package/engine/sql-tables.mjs +21 -13
  87. package/engine/subtype-registry.mjs +5 -4
  88. package/engine/system-block.mjs +18 -19
  89. package/engine/systems.mjs +2 -2
  90. package/engine/web-wikilinks.mjs +41 -38
  91. package/engine/wikilink-syntax.mjs +16 -16
  92. package/engine/wikilinks.mjs +40 -42
  93. package/hm3/actors.mjs +30 -22
  94. package/hm3/document-subtypes.mjs +5 -5
  95. package/hm3/index.mjs +2 -2
  96. package/hm3/item-builders.mjs +1 -1
  97. package/hm3/item-fields.mjs +3 -3
  98. package/hm3/items.mjs +28 -8
  99. package/hm3/template-priority.mjs +2 -2
  100. package/lang.mjs +3 -3
  101. package/manifest.mjs +17 -20
  102. package/package.json +1 -2
  103. package/release.mjs +66 -6
  104. package/sohl/actors.mjs +36 -28
  105. package/sohl/affiliation-standings.mjs +2 -2
  106. package/sohl/being-info.mjs +5 -5
  107. package/sohl/default-item-art.mjs +5 -5
  108. package/sohl/document-subtypes.mjs +5 -5
  109. package/sohl/index.mjs +3 -3
  110. package/sohl/item-builders.mjs +7 -7
  111. package/sohl/item-fields.mjs +8 -9
  112. package/sohl/items.mjs +28 -6
  113. package/sohl/kb-passes.mjs +5 -5
  114. package/sohl/note-schemas.mjs +12 -12
  115. package/sohl/skill-base.mjs +1 -1
  116. package/types/content-config.d.mts +91 -39
  117. package/types/e2e.d.mts +3 -3
  118. package/types/engine/actor-compiler.d.mts +45 -40
  119. package/types/engine/address-charset.d.mts +6 -6
  120. package/types/engine/address-diff.d.mts +60 -6
  121. package/types/engine/base-compiler.d.mts +27 -21
  122. package/types/engine/bundle-notes.d.mts +3 -3
  123. package/types/engine/bundles.d.mts +10 -1
  124. package/types/engine/code-fences.d.mts +3 -3
  125. package/types/engine/compendiums.d.mts +3 -3
  126. package/types/engine/compile-corpus.d.mts +1 -1
  127. package/types/engine/content-address.d.mts +20 -20
  128. package/types/engine/content-format-check.d.mts +6 -6
  129. package/types/engine/content-format.d.mts +2 -2
  130. package/types/engine/content-html.d.mts +78 -0
  131. package/types/engine/content-icons.d.mts +139 -92
  132. package/types/engine/content-index.d.mts +59 -15
  133. package/types/engine/content-links.d.mts +8 -8
  134. package/types/engine/content-lint.d.mts +8 -6
  135. package/types/engine/content-package.d.mts +6 -8
  136. package/types/engine/content-tables.d.mts +49 -18
  137. package/types/engine/derived-fields.d.mts +101 -0
  138. package/types/engine/diagnostics.d.mts +2 -2
  139. package/types/engine/document-subtypes.d.mts +3 -3
  140. package/types/engine/field-spec.d.mts +37 -39
  141. package/types/engine/folder-notes.d.mts +4 -5
  142. package/types/engine/foreign-catalog.d.mts +3 -3
  143. package/types/engine/foundry-entries.d.mts +6 -7
  144. package/types/engine/frontmatter-lint.d.mts +10 -9
  145. package/types/engine/frontmatter.d.mts +7 -7
  146. package/types/engine/generate.d.mts +7 -7
  147. package/types/engine/helpers.d.mts +128 -56
  148. package/types/engine/homepage.d.mts +16 -18
  149. package/types/engine/ids.d.mts +13 -13
  150. package/types/engine/index-records.d.mts +3 -3
  151. package/types/engine/index.d.mts +6 -0
  152. package/types/engine/item-compiler.d.mts +21 -5
  153. package/types/engine/item-docs.d.mts +2 -2
  154. package/types/engine/item-registry.d.mts +6 -6
  155. package/types/engine/journals.d.mts +12 -3
  156. package/types/engine/map-notes.d.mts +2 -2
  157. package/types/engine/metadata-index.d.mts +4 -4
  158. package/types/engine/note-claims.d.mts +30 -22
  159. package/types/engine/note-ids.d.mts +4 -4
  160. package/types/engine/note-package.d.mts +1 -1
  161. package/types/engine/note-renames.d.mts +3 -3
  162. package/types/engine/note-vocabulary.d.mts +31 -203
  163. package/types/engine/pack-config.d.mts +7 -7
  164. package/types/engine/pack-router.d.mts +1 -1
  165. package/types/engine/pdf-build.d.mts +42 -0
  166. package/types/engine/pdf-fonts.d.mts +30 -0
  167. package/types/engine/pdf-render.d.mts +144 -0
  168. package/types/engine/pdf-toc.d.mts +114 -0
  169. package/types/engine/prose-config.d.mts +9 -9
  170. package/types/engine/prose-lint.d.mts +3 -4
  171. package/types/engine/region-events.d.mts +2 -2
  172. package/types/engine/retired-fields.d.mts +10 -9
  173. package/types/engine/scenes.d.mts +10 -1
  174. package/types/engine/schema-check.d.mts +13 -13
  175. package/types/engine/site-build.d.mts +52 -25
  176. package/types/engine/site-index.d.mts +3 -4
  177. package/types/engine/sql-tables.d.mts +11 -5
  178. package/types/engine/subtype-registry.d.mts +3 -3
  179. package/types/engine/system-block.d.mts +3 -3
  180. package/types/engine/web-wikilinks.d.mts +7 -7
  181. package/types/engine/wikilink-syntax.d.mts +17 -17
  182. package/types/engine/wikilinks.d.mts +14 -14
  183. package/types/hm3/actors.d.mts +9 -1
  184. package/types/hm3/document-subtypes.d.mts +1 -1
  185. package/types/hm3/items.d.mts +9 -2
  186. package/types/hm3/template-priority.d.mts +1 -1
  187. package/types/manifest.d.mts +8 -8
  188. package/types/release.d.mts +15 -4
  189. package/types/sohl/actors.d.mts +12 -4
  190. package/types/sohl/affiliation-standings.d.mts +2 -2
  191. package/types/sohl/being-info.d.mts +2 -2
  192. package/types/sohl/document-subtypes.d.mts +1 -1
  193. package/types/sohl/items.d.mts +9 -2
  194. package/types/sohl/note-schemas.d.mts +1 -1
  195. package/MIGRATING.md +0 -608
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  /**
15
- * Naming an interface icon in a note, without drawing it there (#378).
15
+ * Naming an interface icon in a note, without drawing it there.
16
16
  *
17
17
  * The user guide describes Foundry's interface, and it did so by pasting
18
18
  * Unicode lookalikes of the icons the sheets actually draw: `☆` for the improve
@@ -23,11 +23,11 @@
23
23
  * every sheet change.
24
24
  *
25
25
  * They are also the worst characters in the corpus to typeset. Of the eight
26
- * book faces probed for #377, **none** carries `✕ ✗ ✎ ☆ ⚗ ➕`; in a Libertinus
26
+ * book faces probed, **none** carries `✕ ✗ ✎ ☆ ⚗ ➕`; in a Libertinus
27
27
  * setting `✕` resolves to macOS LastResort, which draws a tofu box.
28
28
  *
29
29
  * **Neither obvious fix works.** Keeping the dingbats pins the book to some
30
- * icon-capable font forever, which is the coupling #377 exists to remove.
30
+ * icon-capable font forever, which is the coupling this exists to remove.
31
31
  * Pasting Font Awesome's own codepoints is worse: they live in the Private Use
32
32
  * Area, which is unassigned by definition, so they break search, copy-paste and
33
33
  * screen readers, and no charset check can validate them.
@@ -69,65 +69,80 @@ import fs from "node:fs";
69
69
  import path from "node:path";
70
70
 
71
71
  /**
72
- * The Font Awesome styles a registry entry may name.
73
- *
74
- * Free ships these three and no others, so a `light` or `duotone` entry would
75
- * name a glyph the shipped font does not contain refused here rather than
76
- * discovered as a blank space in a printed book.
77
- *
78
- * @type {readonly string[]}
72
+ * A **family** is an icon font, and a consumer declares the ones it ships.
73
+ *
74
+ * Nothing here names Font Awesome, or Game-Icons, or any other font. A registry
75
+ * entry is a promise that a glyph will render, and only the package that ships
76
+ * the font can keep it: the Game-Icons webfont is built by a consumer from its
77
+ * own templates, and even Font Awesome — which Foundry supplies in-app — is
78
+ * present on neither the knowledgebase nor the page of a book unless somebody
79
+ * puts it there. A toolchain that shipped a table would be promising on a
80
+ * consumer's behalf.
81
+ *
82
+ * The vocabulary is the consumer's for a second reason, independent of fonts. A
83
+ * name like `victory-star-tester` is one game system's concept; another system
84
+ * compiled by this same toolchain has different icons meaning different things.
85
+ * What is shared is the *mechanism* — the syntax, the resolution, the checks —
86
+ * and that is what lives here.
87
+ *
88
+ * A family declares:
89
+ *
90
+ * - `class` — the prefix its stylesheet uses (`fa`, `ginf`, `bi`).
91
+ * - `styles` — the weights it ships, in the spelling its classes use. An empty
92
+ * list means the font has none, and then a `style` on an entry names
93
+ * something that does not exist and is reported rather than rendered.
94
+ * - `describe` — one line, so a finding can say which font it means.
95
+ *
96
+ * @typedef {object} IconFamily
97
+ * @property {string} class - The stylesheet's class prefix.
98
+ * @property {readonly string[]} styles - The weights it ships; empty for none.
99
+ * @property {string} describe - One line, for a finding.
79
100
  */
80
- export const ICON_STYLES = Object.freeze(["solid", "regular", "brands"]);
81
101
 
82
102
  /**
83
- * The icon families a registry entry may draw from.
103
+ * A resolved registry: the families a package ships, and the icons it names.
84
104
  *
85
- * **Two, because the interface uses two.** The SoHL icon legend says so in its
86
- * own prose: Font Awesome for most things, and Game-Icons.net *"for the arms,
87
- * gear, and condition glyphs that Font Awesome does not cover"* — eighteen of
88
- * them, `ginf-broadsword` and its kin (#391).
105
+ * `defaultFamily` is what an entry that names none belongs to. It is optional,
106
+ * and where a package declares exactly one family that one is it — so a
107
+ * single-font package writes no `family` anywhere.
89
108
  *
90
- * They differ in more than a class prefix, which is why this is a family rather
91
- * than a naming convention:
92
- *
93
- * - Font Awesome has **weights**, so an entry names a `style` and a filled and
94
- * hollow pair is one icon twice. Game-Icons has none, so a `style` on such an
95
- * entry names something that does not exist.
96
- * - They resolve to **different fonts**. A PDF has to embed both, and find each
97
- * codepoint in its own table Font Awesome's from the file it ships,
98
- * Game-Icons' from the `game-icons-codepoints.json` the consumer's own
99
- * `build-icon-font.mjs` writes.
109
+ * @typedef {object} IconRegistry
110
+ * @property {Readonly<Record<string, IconFamily>>} families
111
+ * @property {string|undefined} defaultFamily
112
+ * @property {Readonly<Record<string, object>>} icons
113
+ */
114
+
115
+ /**
116
+ * The registry a package that declares none gets: nothing at all.
100
117
  *
101
- * `class` is the prefix the web surfaces use. `styled` says whether a `style`
102
- * belongs on the entry at all, which is what lets a mistake be reported rather
103
- * than rendered as a class nobody defined.
118
+ * Empty rather than a starter set, because a starter set is a promise about
119
+ * fonts this package does not ship. A tree with no `icons:` configured names no
120
+ * icons, and `:icon-star:` in one of its notes renders as its own literal text
121
+ * and is reported — which is the visible failure, not a silent one.
104
122
  *
105
- * @type {Readonly<Record<string, {class: string, styled: boolean, describe: string}>>}
123
+ * @type {IconRegistry}
106
124
  */
107
- export const ICON_FAMILIES = Object.freeze({
108
- fontawesome: {
109
- class: "fa",
110
- styled: true,
111
- describe: "Font Awesome Free",
112
- },
113
- "game-icons": {
114
- class: "ginf",
115
- styled: false,
116
- describe: "the Game-Icons.net webfont a package builds for itself",
117
- },
125
+ export const EMPTY_ICON_REGISTRY = Object.freeze({
126
+ families: Object.freeze({}),
127
+ defaultFamily: undefined,
128
+ icons: Object.freeze({}),
118
129
  });
119
130
 
120
- /** The family an entry that does not name one belongs to. */
121
- export const DEFAULT_ICON_FAMILY = "fontawesome";
122
-
123
131
  /**
124
132
  * The family an entry draws from, named or defaulted.
125
133
  *
126
134
  * @param {{family?: string}} entry - A registry entry.
127
- * @returns {string} The family name.
135
+ * @param {IconRegistry} [registry] - The registry it came from.
136
+ * @returns {string|undefined} The family name, or nothing when neither the
137
+ * entry nor the registry says.
128
138
  */
129
- export function familyOf(entry) {
130
- return entry?.family ?? DEFAULT_ICON_FAMILY;
139
+ export function familyOf(entry, registry = EMPTY_ICON_REGISTRY) {
140
+ if (entry?.family) return entry.family;
141
+ if (registry?.defaultFamily) return registry.defaultFamily;
142
+ // One declared family is unambiguous, so a single-font package writes no
143
+ // `family` on any entry and states no default either.
144
+ const names = Object.keys(registry?.families ?? {});
145
+ return names.length === 1 ? names[0] : undefined;
131
146
  }
132
147
 
133
148
  /**
@@ -227,59 +242,6 @@ export function parseIconAttributes(raw) {
227
242
  return { attrs, problems };
228
243
  }
229
244
 
230
- /**
231
- * The icons the user guide already depicts, under the names it should call them.
232
- *
233
- * Each entry is read off the interface it describes rather than invented, and
234
- * that is meant literally — the table was checked against the system's own
235
- * templates, which is how `delete` came to be `fa-trash` rather than the
236
- * `fa-trash-can` first written here. The sheets draw `fa-trash` twenty-five
237
- * times and `fa-trash-can` never, so the first spelling would have printed an
238
- * icon the reader has never seen on screen. A registry that is not checked
239
- * against the interface is just a second place to be wrong.
240
- *
241
- * The names are what a *writer* would reach for — `delete`, not `trash` —
242
- * because the writer is the one typing them; the Font Awesome spelling is this
243
- * table's business, not theirs.
244
- *
245
- * **Three entries share `xmark`, and that is the point of naming rather than
246
- * drawing.** A `✕` in the guide means "not applicable" in a Healing Rate
247
- * column, "remove this row" on a control, and "close" on a dialog's corner. One
248
- * glyph, three sentences, three different things for a reader who cannot see
249
- * it — so they are three names with three labels, and the fact that Font
250
- * Awesome happens to draw them identically stays in this table.
251
- *
252
- * `run` and `expand` are likewise distinct: `▶` runs an action, and its label
253
- * should say so. `fa-play` is what the sheets use for it.
254
- *
255
- * @type {Readonly<Record<string, {style: string, icon: string, label: string}>>}
256
- */
257
- export const DEFAULT_ICONS = Object.freeze({
258
- affiliation: { style: "solid", icon: "certificate", label: "affiliation" },
259
- star: { style: "solid", icon: "star", label: "star" },
260
- "star-outline": { style: "regular", icon: "star", label: "hollow star" },
261
- // The Success Value scale. `fa-diamond` is Font Awesome's playing-card
262
- // suit and ships in solid only, so it can spell no hollow half of a
263
- // filled/hollow pair; `fa-gem` is a gemstone, has both weights, and is what
264
- // a quality scale actually means. `diamond` stays as an alias of it so a
265
- // note that already says `:icon-diamond:` keeps working.
266
- gem: { style: "solid", icon: "gem", label: "value gem" },
267
- "gem-outline": { style: "regular", icon: "gem", label: "unearned value gem" },
268
- diamond: { style: "solid", icon: "gem", label: "value gem" },
269
- edit: { style: "solid", icon: "pen-to-square", label: "edit" },
270
- delete: { style: "solid", icon: "trash", label: "delete" },
271
- add: { style: "solid", icon: "plus", label: "add" },
272
- remove: { style: "solid", icon: "xmark", label: "remove" },
273
- "not-applicable": { style: "solid", icon: "xmark", label: "not applicable" },
274
- close: { style: "solid", icon: "xmark", label: "close" },
275
- menu: { style: "solid", icon: "ellipsis-vertical", label: "actions menu" },
276
- run: { style: "solid", icon: "play", label: "run this action" },
277
- expand: { style: "solid", icon: "caret-right", label: "expand" },
278
- shield: { style: "solid", icon: "shield-halved", label: "armour" },
279
- compass: { style: "solid", icon: "compass", label: "guided tour" },
280
- flask: { style: "solid", icon: "flask", label: "under construction" },
281
- });
282
-
283
245
  /**
284
246
  * The shape a note writes, and the one this module claims.
285
247
  *
@@ -287,7 +249,7 @@ export const DEFAULT_ICONS = Object.freeze({
287
249
  * surface that also renders `:smile:` can tell the two apart without a lookup,
288
250
  * and a reader can tell what `:icon-star:` is without knowing this module
289
251
  * exists. Names are lowercase, digits and hyphens — the charset an address
290
- * segment already uses (#59), so nothing new has to be explained.
252
+ * segment already uses, so nothing new has to be explained.
291
253
  *
292
254
  * Not `:name[content]`. That is remark-directive syntax, and this toolchain
293
255
  * parses with markdown-it; a directive would render as its own literal text.
@@ -315,12 +277,13 @@ export const ICON_PATTERN = /:icon-([a-z0-9]+(?:-[a-z0-9]+)*):(?:\{([^}]*)\})?/g
315
277
  * Look one name up.
316
278
  *
317
279
  * @param {string} name - The name written between the colons, without `icon-`.
318
- * @param {Record<string, object>} [registry] - Defaults to {@link DEFAULT_ICONS}.
319
- * @returns {{style: string, icon: string, label: string}|null} The entry, or
320
- * `null` when the registry does not declare it.
280
+ * @param {IconRegistry} [registry] - The package's registry.
281
+ * @returns {object|null} The entry, or `null` when the registry does not
282
+ * declare it.
321
283
  */
322
- export function resolveIcon(name, registry = DEFAULT_ICONS) {
323
- return Object.prototype.hasOwnProperty.call(registry, name) ? registry[name] : null;
284
+ export function resolveIcon(name, registry = EMPTY_ICON_REGISTRY) {
285
+ const icons = registry?.icons ?? {};
286
+ return Object.prototype.hasOwnProperty.call(icons, name) ? icons[name] : null;
324
287
  }
325
288
 
326
289
  /** HTML-escape a value going into an attribute. */
@@ -335,20 +298,35 @@ const attr = (value) =>
335
298
  * an icon dropped into a sentence has no such parent, and "the ☆ toggles it"
336
299
  * read aloud as "the toggles it" is a sentence with a hole in it.
337
300
  *
338
- * @param {{style: string, icon: string, label: string}} entry - A registry entry.
301
+ * @param {object} entry - A registry entry.
302
+ * @param {Record<string, string>} [attrs] - The token's attributes.
303
+ * @param {IconRegistry} [registry] - The registry the entry came from, which is
304
+ * what says how its family spells a class.
339
305
  * @returns {string} An `<i>` element.
340
306
  */
341
- export function iconHtml(entry, attrs = {}) {
342
- const family = ICON_FAMILIES[familyOf(entry)] ?? ICON_FAMILIES[DEFAULT_ICON_FAMILY];
307
+ export function iconHtml(entry, attrs = {}, registry = EMPTY_ICON_REGISTRY) {
308
+ const family = registry?.families?.[familyOf(entry, registry)];
309
+ // A family nothing declares is reported by `checkIconRegistry`, not
310
+ // invented here: guessing a prefix would put a class on the page that no
311
+ // stylesheet defines, which looks like a missing glyph rather than a
312
+ // configuration mistake.
313
+ if (!family) return "";
343
314
  const prefix = family.class;
315
+ const styles = family.styles ?? [];
344
316
 
345
- // A styled family spells the weight and the name as two classes; an
346
- // unstyled one has a single class and no weight to spell.
317
+ // A family with weights spells the weight and the name as two classes; one
318
+ // without has a single class and no weight to spell.
347
319
  const classes =
348
- family.styled ?
320
+ styles.length ?
349
321
  [`${prefix}-${attr(entry.style)}`, `${prefix}-${attr(entry.icon)}`]
350
322
  : [`${prefix}-${attr(entry.icon)}`];
351
323
 
324
+ // Fixed width, where the glyph needs it. This is the table's to say, not a
325
+ // note's: whether an ellipsis is too narrow to sit in a column of controls
326
+ // is a fact about that glyph, and the same everywhere it is drawn. A note
327
+ // names the meaning and the table owns how it is set.
328
+ if (styles.length && entry.fixedWidth) classes.push(`${prefix}-fw`);
329
+
352
330
  // The size classes are Font Awesome's, and the Game-Icons stylesheet this
353
331
  // toolchain's consumers generate mirrors its box metrics deliberately, so
354
332
  // they apply to both families.
@@ -382,11 +360,11 @@ export function iconsIn(text) {
382
360
  *
383
361
  * @param {string} text - The file's contents.
384
362
  * @param {string} file - Path to report.
385
- * @param {Record<string, object>} [registry] - Defaults to {@link DEFAULT_ICONS}.
363
+ * @param {IconRegistry} [registry] - The package's registry.
386
364
  * @returns {Array<{file: string, line: number, column: number,
387
365
  * severity: "warning", message: string}>} The unknown names.
388
366
  */
389
- export function lintIcons(text, file, registry = DEFAULT_ICONS) {
367
+ export function lintIcons(text, file, registry = EMPTY_ICON_REGISTRY) {
390
368
  const findings = [];
391
369
  for (const { name, index, raw, problems } of iconsIn(text)) {
392
370
  const before = text.slice(0, index);
@@ -397,7 +375,7 @@ export function lintIcons(text, file, registry = DEFAULT_ICONS) {
397
375
  if (!resolveIcon(name, registry)) {
398
376
  // Nearest declared name, when there is an obvious one: a typo is the
399
377
  // common case and the registry is short enough to say what was meant.
400
- const suggestion = nearestName(name, Object.keys(registry));
378
+ const suggestion = nearestName(name, Object.keys(registry?.icons ?? {}));
401
379
  findings.push({
402
380
  ...at,
403
381
  message:
@@ -464,72 +442,117 @@ function editDistance(a, b) {
464
442
  }
465
443
 
466
444
  /**
467
- * Refuse a registry that names a style Font Awesome Free does not ship.
445
+ * What is wrong with a package's declared registry.
468
446
  *
469
- * @param {Record<string, object>} registry - A package's icon table.
447
+ * Both halves are checked, because either alone is unusable: an icon naming a
448
+ * family nothing declares has no class prefix, and a family nothing names is a
449
+ * font declared for no reason.
450
+ *
451
+ * A **style** is checked against the family's own `styles`, not against a list
452
+ * here. Font Awesome Free ships three weights and another font ships none or
453
+ * five, and only the declaration knows which — so a `duotone` entry is refused
454
+ * because the family that entry belongs to does not list `duotone`, which is a
455
+ * statement the consumer made about the font it actually ships.
456
+ *
457
+ * @param {{families?: object, icons?: object}} registry - A package's declared
458
+ * registry, before it is resolved.
470
459
  * @param {string} [where="icons"] - Where to say the fault is.
471
460
  * @returns {Array<{severity: "warning", message: string}>} What is wrong with it.
472
461
  */
473
462
  export function checkIconRegistry(registry, where = "icons") {
474
463
  const findings = [];
475
- for (const [name, entry] of Object.entries(registry ?? {})) {
476
- const at = `\`${where}.${name}\``;
464
+ const warn = (message) =>
465
+ findings.push({ severity: /** @type {const} */ ("warning"), message });
466
+
467
+ const families = registry?.families ?? {};
468
+ const icons = registry?.icons ?? {};
469
+
470
+ for (const [name, family] of Object.entries(families)) {
471
+ const at = `\`${where}.families.${name}\``;
472
+ if (!family || typeof family !== "object") {
473
+ warn(`${at} is not a family — it takes \`class\`, \`styles\` and \`describe\``);
474
+ continue;
475
+ }
476
+ if (typeof family.class !== "string" || !family.class) {
477
+ warn(`${at} declares no \`class\`, so nothing says how its glyphs are spelled`);
478
+ }
479
+ if (family.styles !== undefined && !Array.isArray(family.styles)) {
480
+ warn(
481
+ `${at} declares \`styles\` that is not a list — write \`[]\` for a font with no weights`,
482
+ );
483
+ }
484
+ if (typeof family.describe !== "string" || !family.describe) {
485
+ warn(`${at} declares no \`describe\`, so a finding cannot say which font it means`);
486
+ }
487
+ }
488
+
489
+ for (const [name, entry] of Object.entries(icons)) {
490
+ const at = `\`${where}.icons.${name}\``;
477
491
  if (!entry || typeof entry !== "object") {
478
- findings.push({
479
- severity: /** @type {const} */ ("warning"),
480
- message: `${at} is not an icon entry — it takes \`style\`, \`icon\` and \`label\``,
481
- });
492
+ warn(`${at} is not an icon entry — it takes \`icon\` and \`label\``);
482
493
  continue;
483
494
  }
484
- const familyName = familyOf(entry);
485
- const family = ICON_FAMILIES[familyName];
495
+
496
+ const familyName = familyOf(entry, { families, defaultFamily: registry?.defaultFamily });
497
+ if (!familyName) {
498
+ warn(
499
+ `${at} names no family and the registry declares ${Object.keys(families).length} of ` +
500
+ `them, so nothing says which font draws it — name one on the entry, or ` +
501
+ `declare a \`defaultFamily\``,
502
+ );
503
+ continue;
504
+ }
505
+ const family = families[familyName];
486
506
  if (!family) {
487
- findings.push({
488
- severity: /** @type {const} */ ("warning"),
489
- message:
490
- `${at} names family \`${familyName}\`, and the families there ` +
491
- `are: ${Object.keys(ICON_FAMILIES).join(", ")}`,
492
- });
507
+ const declared = Object.keys(families);
508
+ warn(
509
+ `${at} names family \`${familyName}\`, and the families this package ` +
510
+ `declares are: ${declared.length ? declared.join(", ") : "none"}`,
511
+ );
493
512
  continue;
494
513
  }
495
514
 
496
- if (family.styled && !ICON_STYLES.includes(entry.style)) {
497
- findings.push({
498
- severity: /** @type {const} */ ("warning"),
499
- message:
500
- `${at} names style \`${entry.style}\`, and Font Awesome Free ships ` +
501
- `only ${ICON_STYLES.join(", ")} a glyph in any other style is ` +
502
- `absent from the font a book would embed`,
503
- });
515
+ const styles = Array.isArray(family.styles) ? family.styles : [];
516
+ if (styles.length && !styles.includes(entry.style)) {
517
+ warn(
518
+ `${at} names style \`${entry.style}\`, and ${family.describe} ships ` +
519
+ `only ${styles.join(", ")} a glyph in any other style is absent ` +
520
+ `from the font a book would embed`,
521
+ );
504
522
  }
505
523
 
506
- // A style on an unstyled family is not a harmless extra key: it says
507
- // the author expected a weight, and the family has none, so what they
508
- // get is not what they asked for.
509
- if (!family.styled && entry.style !== undefined) {
510
- findings.push({
511
- severity: /** @type {const} */ ("warning"),
512
- message:
513
- `${at} names style \`${entry.style}\`, and ${family.describe} has ` +
514
- `no weights — the style is ignored, so a filled and hollow pair ` +
515
- `cannot be spelled this way`,
516
- });
524
+ // A style on a font with no weights is not a harmless extra key: it
525
+ // says the author expected a weight, and what they get is not what
526
+ // they asked for.
527
+ if (!styles.length && entry.style !== undefined) {
528
+ warn(
529
+ `${at} names style \`${entry.style}\`, and ${family.describe} has no ` +
530
+ `weights — the style is ignored, so a filled and hollow pair cannot ` +
531
+ `be spelled this way`,
532
+ );
517
533
  }
534
+
535
+ // The same rule `style` gets: a font with no weights ships no `-fw`
536
+ // class either, so asking for one asks for a width it cannot give.
537
+ if (!styles.length && entry.fixedWidth !== undefined) {
538
+ warn(
539
+ `${at} asks for fixed width, and ${family.describe} ships no such ` +
540
+ `class — the request is ignored, so a glyph that needs the width ` +
541
+ `will not get it`,
542
+ );
543
+ }
544
+
518
545
  if (typeof entry.icon !== "string" || !entry.icon) {
519
- findings.push({
520
- severity: /** @type {const} */ ("warning"),
521
- message: `${at} declares no \`icon\`, so nothing names the glyph to draw`,
522
- });
546
+ warn(`${at} declares no \`icon\`, so nothing names the glyph to draw`);
523
547
  }
524
548
  if (typeof entry.label !== "string" || !entry.label) {
525
- findings.push({
526
- severity: /** @type {const} */ ("warning"),
527
- message:
528
- `${at} declares no \`label\`, and an icon with no accessible name ` +
529
- `is read aloud as a gap in the sentence`,
530
- });
549
+ warn(
550
+ `${at} declares no \`label\`, and an icon with no accessible name is ` +
551
+ `read aloud as a gap in the sentence`,
552
+ );
531
553
  }
532
554
  }
555
+
533
556
  return findings;
534
557
  }
535
558
 
@@ -543,7 +566,7 @@ export function checkIconRegistry(registry, where = "icons") {
543
566
  * @param {string} contentBase - Root of the content tree.
544
567
  * @param {object} [opts]
545
568
  * @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore.
546
- * @param {Record<string, object>} [opts.registry] - The package's icon table.
569
+ * @param {IconRegistry} [opts.registry] - The package's registry.
547
570
  * @returns {{findings: Array<{file: string, line: number, column: number,
548
571
  * severity: "warning", message: string}>, files: number}} What it found.
549
572
  */
@@ -591,14 +614,22 @@ export function lintContentIcons(contentBase, { skipDirectories = [], registry }
591
614
  * is reported by {@link lintIcons}, and a rendered page that still shows
592
615
  * `:icon-stra:` is how the author finds it without reading a log.
593
616
  *
594
- * @param {Record<string, object>} [registry] - Defaults to {@link DEFAULT_ICONS}.
617
+ * **A function is accepted as well as a table**, and resolved per render. The
618
+ * shared markdown-it instance is a module-level constant, so it is built before
619
+ * any configuration is read; a getter lets it draw the package's own registry
620
+ * without the module load order deciding whether that registry exists yet.
621
+ *
622
+ * @param {Record<string, object>|(() => Record<string, object>)} [registry] -
623
+ * The registry, or something that returns it.
595
624
  * @returns {(md: object) => void} A markdown-it plugin.
596
625
  */
597
- export function iconPlugin(registry = DEFAULT_ICONS) {
626
+ export function iconPlugin(registry = EMPTY_ICON_REGISTRY) {
627
+ const tableOf = () =>
628
+ typeof registry === "function" ? (registry() ?? EMPTY_ICON_REGISTRY) : registry;
598
629
  return (md) => {
599
630
  /** @type {any} */ (md).inline.ruler.before("emphasis", "heroiclands_icon", iconRule);
600
631
  /** @type {any} */ (md).renderer.rules.heroiclands_icon = (tokens, idx) =>
601
- iconHtml(tokens[idx].meta.entry, tokens[idx].meta.attrs);
632
+ iconHtml(tokens[idx].meta.entry, tokens[idx].meta.attrs, tableOf());
602
633
 
603
634
  /**
604
635
  * @param {any} state - markdown-it inline state.
@@ -614,7 +645,7 @@ export function iconPlugin(registry = DEFAULT_ICONS) {
614
645
  const m = re.exec(state.src.slice(start));
615
646
  if (!m) return false;
616
647
 
617
- const entry = resolveIcon(m[1], registry);
648
+ const entry = resolveIcon(m[1], tableOf());
618
649
  // Not ours to consume: leaving the source untouched is what makes an
619
650
  // unrecognised name visible on the page instead of vanishing.
620
651
  if (!entry) return false;