@heroiclands/package-build 20.3.1 → 20.4.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.
- package/CHANGELOG.md +75 -0
- package/CONTENT.md +111 -92
- package/README.md +2 -9
- package/bin/content-build.mjs +70 -61
- package/bin/package-build.mjs +2 -2
- package/config.mjs +6 -8
- package/content-config.mjs +187 -64
- package/docs/content-format.md +57 -68
- package/e2e.mjs +4 -4
- package/engine/actor-compiler.mjs +36 -36
- package/engine/address-charset.mjs +6 -6
- package/engine/address-diff.mjs +12 -11
- package/engine/anchored-sections.mjs +1 -1
- package/engine/anchors.mjs +1 -1
- package/engine/base-compiler.mjs +36 -38
- package/engine/bundle-notes.mjs +4 -4
- package/engine/bundles.mjs +8 -8
- package/engine/code-fences.mjs +3 -3
- package/engine/compendiums.mjs +8 -8
- package/engine/compile-corpus.mjs +4 -5
- package/engine/content-address.mjs +15 -15
- package/engine/content-charset.mjs +1 -1
- package/engine/content-format-check.mjs +24 -25
- package/engine/content-format.mjs +8 -8
- package/engine/content-html.mjs +202 -0
- package/engine/content-icons.mjs +196 -165
- package/engine/content-index.mjs +17 -17
- package/engine/content-links.mjs +30 -30
- package/engine/content-lint.mjs +20 -24
- package/engine/content-package.mjs +7 -9
- package/engine/content-slug.mjs +2 -2
- package/engine/content-tables.mjs +7 -8
- package/engine/content-tree.mjs +1 -1
- package/engine/derived-fields.mjs +174 -0
- package/engine/diagnostics.mjs +3 -4
- package/engine/document-subtypes.mjs +6 -7
- package/engine/field-reference.mjs +8 -8
- package/engine/field-spec.mjs +29 -30
- package/engine/folder-notes.mjs +14 -14
- package/engine/foreign-catalog.mjs +8 -9
- package/engine/foundry-entries.mjs +19 -20
- package/engine/frontmatter-lint.mjs +64 -69
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +39 -39
- package/engine/helpers.mjs +63 -54
- package/engine/homepage.mjs +20 -22
- package/engine/ids.mjs +15 -15
- package/engine/index-records.mjs +4 -4
- package/engine/index.mjs +14 -8
- package/engine/item-compiler.mjs +54 -29
- package/engine/item-docs.mjs +11 -13
- package/engine/item-registry.mjs +13 -13
- package/engine/journals.mjs +15 -15
- package/engine/macros.mjs +3 -3
- package/engine/map-notes.mjs +6 -6
- package/engine/metadata-index.mjs +5 -5
- package/engine/note-claims.mjs +36 -34
- package/engine/note-ids.mjs +5 -5
- package/engine/note-package.mjs +4 -4
- package/engine/note-renames.mjs +3 -3
- package/engine/note-schemas.mjs +3 -3
- package/engine/note-vocabulary.mjs +27 -26
- package/engine/pack-config.mjs +13 -13
- package/engine/pack-router.mjs +0 -0
- package/engine/prose-config.mjs +9 -9
- package/engine/prose-lint.mjs +12 -13
- package/engine/region-events.mjs +2 -2
- package/engine/retired-fields.mjs +24 -22
- package/engine/runtime-only-fields.mjs +1 -1
- package/engine/scene-levels.mjs +1 -1
- package/engine/scenes.mjs +19 -20
- package/engine/schema-check.mjs +20 -20
- package/engine/schema-extract.mjs +1 -1
- package/engine/site-build.mjs +45 -47
- package/engine/site-index.mjs +17 -18
- package/engine/sql-tables.mjs +21 -13
- package/engine/subtype-registry.mjs +5 -4
- package/engine/system-block.mjs +18 -19
- package/engine/systems.mjs +2 -2
- package/engine/web-wikilinks.mjs +35 -35
- package/engine/wikilink-syntax.mjs +16 -16
- package/engine/wikilinks.mjs +39 -39
- package/hm3/actors.mjs +22 -22
- package/hm3/document-subtypes.mjs +5 -5
- package/hm3/index.mjs +2 -2
- package/hm3/item-builders.mjs +1 -1
- package/hm3/item-fields.mjs +3 -3
- package/hm3/items.mjs +20 -8
- package/hm3/template-priority.mjs +2 -2
- package/lang.mjs +3 -3
- package/manifest.mjs +17 -20
- package/package.json +1 -2
- package/release.mjs +3 -3
- package/sohl/actors.mjs +28 -28
- package/sohl/affiliation-standings.mjs +2 -2
- package/sohl/being-info.mjs +5 -5
- package/sohl/default-item-art.mjs +5 -5
- package/sohl/document-subtypes.mjs +5 -5
- package/sohl/index.mjs +3 -3
- package/sohl/item-builders.mjs +7 -7
- package/sohl/item-fields.mjs +8 -9
- package/sohl/items.mjs +20 -6
- package/sohl/kb-passes.mjs +5 -5
- package/sohl/note-schemas.mjs +7 -7
- package/sohl/skill-base.mjs +1 -1
- package/types/content-config.d.mts +25 -24
- package/types/e2e.d.mts +3 -3
- package/types/engine/actor-compiler.d.mts +14 -13
- package/types/engine/address-charset.d.mts +6 -6
- package/types/engine/address-diff.d.mts +3 -3
- package/types/engine/base-compiler.d.mts +17 -19
- package/types/engine/bundle-notes.d.mts +3 -3
- package/types/engine/bundles.d.mts +1 -1
- package/types/engine/code-fences.d.mts +3 -3
- package/types/engine/compendiums.d.mts +3 -3
- package/types/engine/compile-corpus.d.mts +1 -1
- package/types/engine/content-address.d.mts +11 -11
- package/types/engine/content-format-check.d.mts +6 -6
- package/types/engine/content-format.d.mts +2 -2
- package/types/engine/content-html.d.mts +78 -0
- package/types/engine/content-icons.d.mts +139 -92
- package/types/engine/content-index.d.mts +2 -2
- package/types/engine/content-links.d.mts +8 -8
- package/types/engine/content-lint.d.mts +2 -2
- package/types/engine/content-package.d.mts +6 -8
- package/types/engine/derived-fields.d.mts +101 -0
- package/types/engine/diagnostics.d.mts +2 -2
- package/types/engine/document-subtypes.d.mts +3 -3
- package/types/engine/field-spec.d.mts +37 -39
- package/types/engine/folder-notes.d.mts +4 -5
- package/types/engine/foreign-catalog.d.mts +3 -3
- package/types/engine/foundry-entries.d.mts +6 -7
- package/types/engine/frontmatter-lint.d.mts +7 -7
- package/types/engine/frontmatter.d.mts +7 -7
- package/types/engine/generate.d.mts +7 -7
- package/types/engine/helpers.d.mts +25 -27
- package/types/engine/homepage.d.mts +16 -18
- package/types/engine/ids.d.mts +13 -13
- package/types/engine/index-records.d.mts +3 -3
- package/types/engine/index.d.mts +2 -0
- package/types/engine/item-compiler.d.mts +21 -5
- package/types/engine/item-docs.d.mts +2 -2
- package/types/engine/item-registry.d.mts +6 -6
- package/types/engine/journals.d.mts +3 -3
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/metadata-index.d.mts +4 -4
- package/types/engine/note-claims.d.mts +16 -15
- package/types/engine/note-ids.d.mts +4 -4
- package/types/engine/note-package.d.mts +1 -1
- package/types/engine/note-renames.d.mts +3 -3
- package/types/engine/note-vocabulary.d.mts +9 -8
- package/types/engine/pack-config.d.mts +3 -3
- package/types/engine/pack-router.d.mts +1 -1
- package/types/engine/prose-config.d.mts +9 -9
- package/types/engine/prose-lint.d.mts +3 -4
- package/types/engine/region-events.d.mts +2 -2
- package/types/engine/retired-fields.d.mts +10 -9
- package/types/engine/schema-check.d.mts +11 -11
- package/types/engine/site-build.d.mts +18 -19
- package/types/engine/site-index.d.mts +3 -4
- package/types/engine/sql-tables.d.mts +11 -5
- package/types/engine/subtype-registry.d.mts +3 -3
- package/types/engine/system-block.d.mts +3 -3
- package/types/engine/web-wikilinks.d.mts +7 -7
- package/types/engine/wikilink-syntax.d.mts +17 -17
- package/types/engine/wikilinks.d.mts +13 -12
- package/types/hm3/actors.d.mts +1 -1
- package/types/hm3/document-subtypes.d.mts +1 -1
- package/types/hm3/items.d.mts +1 -2
- package/types/hm3/template-priority.d.mts +1 -1
- package/types/manifest.d.mts +8 -8
- package/types/sohl/actors.d.mts +2 -2
- package/types/sohl/affiliation-standings.d.mts +2 -2
- package/types/sohl/being-info.d.mts +2 -2
- package/types/sohl/document-subtypes.d.mts +1 -1
- package/types/sohl/items.d.mts +1 -2
- package/types/sohl/note-schemas.d.mts +1 -1
- package/MIGRATING.md +0 -608
package/engine/content-icons.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Naming an interface icon in a note, without drawing it there
|
|
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
|
|
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
|
|
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
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
-
*
|
|
103
|
+
* A resolved registry: the families a package ships, and the icons it names.
|
|
84
104
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
*
|
|
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
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
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 {
|
|
123
|
+
* @type {IconRegistry}
|
|
106
124
|
*/
|
|
107
|
-
export const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
|
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 {
|
|
319
|
-
* @returns {
|
|
320
|
-
*
|
|
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 =
|
|
323
|
-
|
|
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 {
|
|
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 =
|
|
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
|
|
346
|
-
//
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
*
|
|
445
|
+
* What is wrong with a package's declared registry.
|
|
468
446
|
*
|
|
469
|
-
*
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
485
|
-
const
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
`
|
|
502
|
-
|
|
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
|
|
507
|
-
// the author expected a weight, and
|
|
508
|
-
//
|
|
509
|
-
if (!
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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 {
|
|
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
|
-
*
|
|
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 =
|
|
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],
|
|
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;
|
package/engine/content-index.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Emitting this package's content index
|
|
15
|
+
* Emitting this package's content index.
|
|
16
16
|
*
|
|
17
17
|
* Every content build already walks the whole note tree and parses every note's
|
|
18
18
|
* frontmatter — the pack compilers, the site build, and the content-table
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* "which beings carry no `kbcat`?", "what does this table actually select?",
|
|
22
22
|
* "did that type rename leave anything behind?" have no answer short of writing
|
|
23
23
|
* a throwaway script that re-walks the tree. Eight dead Bestiary tables shipped
|
|
24
|
-
* for weeks behind exactly that gap
|
|
24
|
+
* for weeks behind exactly that gap.
|
|
25
25
|
*
|
|
26
26
|
* This module publishes the walk. One line of JSON per note, in
|
|
27
27
|
* [JSON Lines](https://jsonlines.org/) — the whole frontmatter, plus where the
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
* to every player, and a build artifact has no business there.
|
|
63
63
|
*
|
|
64
64
|
* **What it deliberately does not carry: the note's text, and positions within
|
|
65
|
-
* it.**
|
|
65
|
+
* it.** The question is whether the index should record a position for every
|
|
66
66
|
* frontmatter key, so that a pass reading the index could report a field defect
|
|
67
67
|
* without opening the file. It should not, and the numbers are not close: over
|
|
68
68
|
* `sohl`'s 1,685 notes the index is 3.0 MB and holds 50,598 leaf values, so a
|
|
@@ -94,7 +94,7 @@ import unidecode from "unidecode";
|
|
|
94
94
|
import { metadataFileName } from "./metadata-index.mjs";
|
|
95
95
|
import { addressSlug, canonicalKey } from "./content-address.mjs";
|
|
96
96
|
// One reader for a note's anchors, shared with the link checker and with the
|
|
97
|
-
// builds that emit a link
|
|
97
|
+
// builds that emit a link. Re-exported because this is where callers
|
|
98
98
|
// have always addressed it.
|
|
99
99
|
import { collectAnchors } from "./anchors.mjs";
|
|
100
100
|
import { subtypeRow, NO_SYSTEM, systemOf } from "./document-subtypes.mjs";
|
|
@@ -105,7 +105,7 @@ import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./note-claims.mjs";
|
|
|
105
105
|
*
|
|
106
106
|
* The specification's word, not this module's: the canonical address carries it
|
|
107
107
|
* in the same position — `harnadventures-none-being-grod` — so the index and
|
|
108
|
-
* the address say "no system" the same way
|
|
108
|
+
* the address say "no system" the same way.
|
|
109
109
|
*
|
|
110
110
|
* @type {string}
|
|
111
111
|
*/
|
|
@@ -115,12 +115,12 @@ import { entriesForNote, foundryIdentities } from "./foundry-entries.mjs";
|
|
|
115
115
|
import { walkMarkdownTree } from "./helpers.mjs";
|
|
116
116
|
import { resolveNoteId } from "./note-ids.mjs";
|
|
117
117
|
// The retired-field refusal and the key locator, so a note authoring a derived
|
|
118
|
-
// key is reported where it is rather than as a bare abort
|
|
118
|
+
// key is reported where it is rather than as a bare abort.
|
|
119
119
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
120
120
|
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
121
121
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
122
122
|
// The record accessors, which live apart so that a module the compilers load
|
|
123
|
-
// can read a record without importing this one and closing a cycle
|
|
123
|
+
// can read a record without importing this one and closing a cycle.
|
|
124
124
|
// Re-exported because this is where callers have always addressed them.
|
|
125
125
|
import { authoredFrontmatter, DERIVED_KEYS, isNoteRecord, noteFile } from "./index-records.mjs";
|
|
126
126
|
|
|
@@ -281,7 +281,7 @@ export function asciiAliases(aliases) {
|
|
|
281
281
|
* plus the pack router — frontmatter and configuration, nothing from a compiled
|
|
282
282
|
* pack — so the index's frontmatter walk already has every input. Deriving it
|
|
283
283
|
* twice is how two artifacts describing one note start disagreeing, which is
|
|
284
|
-
* the failure the merge is meant to end
|
|
284
|
+
* the failure the merge is meant to end.
|
|
285
285
|
*
|
|
286
286
|
* The shape flattens the manifest's *two* entries for an item note onto the one
|
|
287
287
|
* record the index keeps per note. An item compiles into a document **and** a
|
|
@@ -339,7 +339,7 @@ function foundryEntries({ frontmatter, address, body, manifest }) {
|
|
|
339
339
|
* shipped map produced and said nothing about the other.
|
|
340
340
|
*
|
|
341
341
|
* Only `sohl` can appear today, because `KNOWN_DOCUMENT_SUBTYPE_MAPS` holds one
|
|
342
|
-
* map
|
|
342
|
+
* map, the `hm3/` half being separate. The shape is system-keyed so
|
|
343
343
|
* that adding it is one more key rather than a second breaking change to an
|
|
344
344
|
* artifact consumers have already started reading.
|
|
345
345
|
*
|
|
@@ -369,7 +369,7 @@ function foundryBlock(entry, system) {
|
|
|
369
369
|
* Refuse a note that authors a key the index derives, and say where.
|
|
370
370
|
*
|
|
371
371
|
* **Located, because every reader of the index is now a reporter of this.**
|
|
372
|
-
*
|
|
372
|
+
* With the emitter the only pass building a record, aborting
|
|
373
373
|
* with a bare message was the whole story. Now the link check and the address
|
|
374
374
|
* diff read the index too, and a bare abort in one of them reports *nothing*
|
|
375
375
|
* about the tree — the one malformed note takes every other finding with it,
|
|
@@ -378,7 +378,7 @@ function foundryBlock(entry, system) {
|
|
|
378
378
|
* can emit `file:line:column: error: …` like any other finding.
|
|
379
379
|
*
|
|
380
380
|
* **`package:` keeps its own words.** It is not a name collision but a *retired
|
|
381
|
-
* field
|
|
381
|
+
* field*, and the correction is to delete it, not to rename it — which is
|
|
382
382
|
* what {@link module:engine/note-package.assertNoDeclaredPackage} has always
|
|
383
383
|
* said, and had no caller to say it to. Deferring to it means one message for
|
|
384
384
|
* one mistake rather than two that contradict each other about the fix.
|
|
@@ -517,7 +517,7 @@ function buildDocRecord({ frontmatter, address, entry, file, contentPackage, anc
|
|
|
517
517
|
// The journal's own `_id`, taken from the entry rather than
|
|
518
518
|
// re-derived: every entry the index gives an identity to publishes
|
|
519
519
|
// both the id and the UUID, computed once by whatever owns that
|
|
520
|
-
// entry's derivation
|
|
520
|
+
// entry's derivation.
|
|
521
521
|
id: entry.id,
|
|
522
522
|
nameAscii: asciiName(frontmatter?.name?.full),
|
|
523
523
|
address: { slug: address.slug, canonical: entry.key },
|
|
@@ -537,7 +537,7 @@ export function collectContentIndex(
|
|
|
537
537
|
) {
|
|
538
538
|
const records = [];
|
|
539
539
|
// Passed through rather than defaulted away: an absent scope is the
|
|
540
|
-
// caller's omission, and `walkMarkdownTree` says so
|
|
540
|
+
// caller's omission, and `walkMarkdownTree` says so.
|
|
541
541
|
const walkOpts = { skipDirectories };
|
|
542
542
|
|
|
543
543
|
for (const { frontmatter, body, bodyLine, absPath } of walkMarkdownTree(
|
|
@@ -545,7 +545,7 @@ export function collectContentIndex(
|
|
|
545
545
|
walkOpts,
|
|
546
546
|
)) {
|
|
547
547
|
const fm = frontmatter ?? {};
|
|
548
|
-
// The id the note's document is filed under
|
|
548
|
+
// The id the note's document is filed under, resolved before
|
|
549
549
|
// the record is built so the index publishes the address *and* the id
|
|
550
550
|
// that address derives.
|
|
551
551
|
resolveNoteId(fm, { pkg: contentPackage });
|
|
@@ -579,7 +579,7 @@ export function collectContentIndex(
|
|
|
579
579
|
}
|
|
580
580
|
records.push(record);
|
|
581
581
|
|
|
582
|
-
// An item note is two documents, so it is two records
|
|
582
|
+
// An item note is two documents, so it is two records.
|
|
583
583
|
const doc = foundryEntries({
|
|
584
584
|
frontmatter: fm,
|
|
585
585
|
address: record.address,
|
|
@@ -637,7 +637,7 @@ export function serializeContentIndex(records) {
|
|
|
637
637
|
*
|
|
638
638
|
* The half of {@link emitContentIndex} that derives rather than emits, so a
|
|
639
639
|
* pass that needs the corpus in memory — a SQL content table, the link check,
|
|
640
|
-
* and in time every reader
|
|
640
|
+
* and in time every converted reader — builds it the same way the artifact
|
|
641
641
|
* is built, rather than by walking and parsing again with its own idea of the
|
|
642
642
|
* scope.
|
|
643
643
|
*
|
|
@@ -648,7 +648,7 @@ export function serializeContentIndex(records) {
|
|
|
648
648
|
* caller that resolved one of its own; defaults to the resolved
|
|
649
649
|
* configuration's. Stated separately from `config` because a caller that was
|
|
650
650
|
* *handed* a scope must be able to pass it on rather than have it silently
|
|
651
|
-
* replaced by the one its configuration happens to carry
|
|
651
|
+
* replaced by the one its configuration happens to carry.
|
|
652
652
|
* @param {object[]} [opts.problems] - Supplied by a **reader**: a note that
|
|
653
653
|
* cannot be recorded is pushed here as a diagnostic and skipped, instead of
|
|
654
654
|
* aborting the derivation. Omitted, the note throws — which is the contract
|