@heroiclands/package-build 20.2.1 → 20.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/engine/content-charset.mjs +20 -7
- package/engine/content-icons.mjs +289 -41
- package/package.json +1 -1
- package/types/engine/content-charset.d.mts +4 -4
- package/types/engine/content-icons.d.mts +134 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 20.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 43188f2: A registry entry can now name an icon family, so a package that draws from two
|
|
8
|
+
of them can say so.
|
|
9
|
+
|
|
10
|
+
The SoHL icon legend states in its own prose that it uses Font Awesome plus
|
|
11
|
+
Game-Icons.net "for the arms, gear, and condition glyphs that Font Awesome does
|
|
12
|
+
not cover" — eighteen `ginf-*` classes the registry had no way to express. They
|
|
13
|
+
differ in more than a prefix: Game-Icons has no weights, so a `style` on such an
|
|
14
|
+
entry names something that does not exist, and it resolves to a different font,
|
|
15
|
+
which a PDF has to embed separately.
|
|
16
|
+
|
|
17
|
+
`family` defaults to `fontawesome`, so every existing entry is unchanged. A
|
|
18
|
+
style on an unstyled family is reported rather than silently ignored, because it
|
|
19
|
+
means the author expected a filled and hollow pair the family cannot spell.
|
|
20
|
+
|
|
21
|
+
Also adds `gem` and `gem-outline`, and repoints `diamond` at them. `fa-diamond`
|
|
22
|
+
is Font Awesome's playing-card suit and ships in solid only, so it could not
|
|
23
|
+
spell the hollow half of the Success Value scale it was being used for.
|
|
24
|
+
|
|
25
|
+
## 20.3.0
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- 6be4a7e: An icon can now carry attributes, and `affiliation` joins the registry.
|
|
30
|
+
|
|
31
|
+
`:icon-affiliation:{size: 2x}` — `key: value` pairs in a trailing brace, the
|
|
32
|
+
shape `markdown-it-attrs` and remark-directive already use. Attributes rather
|
|
33
|
+
than a bare size because `size` is only the first one anybody needed: a
|
|
34
|
+
fixed-width flag, a rotation, a title override are the same shape of thing, and
|
|
35
|
+
a syntax that could only express size would have to be replaced to gain any of
|
|
36
|
+
them.
|
|
37
|
+
|
|
38
|
+
Size is a closed vocabulary — `lg`, `xl`, `2x`, `3x` — mapping to Font Awesome's
|
|
39
|
+
own classes on the web and the same multiples in a PDF. It exists because the
|
|
40
|
+
icon legend enlarges its glyphs so a reader can tell them apart, which makes the
|
|
41
|
+
size part of what that page says rather than styling applied to it. A free-form
|
|
42
|
+
`font-size` would have been CSS, which reaches two surfaces of three.
|
|
43
|
+
|
|
44
|
+
An attribute that cannot be honoured is reported rather than ignored, and the
|
|
45
|
+
icon still renders: hiding a good icon over a bad size would be the worse answer.
|
|
46
|
+
|
|
47
|
+
`affiliation` is `fa-certificate` — a charter under seal. It replaces a
|
|
48
|
+
`fa-duotone fa-handshake` that was reaching for Font Awesome Pro because the
|
|
49
|
+
interlocking hands washed out at small sizes; the fix for a silhouette problem
|
|
50
|
+
is a bolder silhouette, not a paid tier.
|
|
51
|
+
- e209110: Add `close` and `run` to the icon registry, and correct `delete`.
|
|
52
|
+
|
|
53
|
+
Converting the SoHL user guide turned up two controls with no name to convert
|
|
54
|
+
to. A `✕` there means three different things — "not applicable" in a Healing
|
|
55
|
+
Rate column, "remove" on a control, and **close** on a dialog's corner — and a
|
|
56
|
+
`▶` **runs** an action, where the nearest existing entry was `expand`, whose
|
|
57
|
+
accessible label would have told a screen reader the wrong thing. Both now have
|
|
58
|
+
their own name and their own label; that Font Awesome draws the three `xmark`
|
|
59
|
+
senses identically is this table's business rather than the reader's.
|
|
60
|
+
|
|
61
|
+
`delete` was written as `fa-trash-can` and is now `fa-trash`, which is what the
|
|
62
|
+
system's templates actually draw — twenty-five times, against no `fa-trash-can`
|
|
63
|
+
at all. A registry checked against the interface is the whole point of having
|
|
64
|
+
one; unchecked, it is just a second place to be wrong.
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- 1d8e289: The charset and icon checks now report warnings rather than errors, so neither
|
|
69
|
+
can fail a build.
|
|
70
|
+
|
|
71
|
+
Both emitted `severity: "error"`, and `reportFindings` fails a run on an error —
|
|
72
|
+
so adopting the charset check turned consumers' builds red for content that was
|
|
73
|
+
already correct. A character outside the charset does not make a note wrong: it
|
|
74
|
+
compiles to the same document and publishes the same page, and only a book that
|
|
75
|
+
does not exist yet cares. An undeclared icon name is the same shape of thing,
|
|
76
|
+
visible on the page as literal text.
|
|
77
|
+
|
|
78
|
+
The NFC rule is a warning too, despite having the best claim to being an error,
|
|
79
|
+
because a lint that fails a build for one of its rules and not the others is one
|
|
80
|
+
nobody can predict.
|
|
81
|
+
|
|
3
82
|
## 20.2.1
|
|
4
83
|
|
|
5
84
|
### Patch Changes
|
|
@@ -64,8 +64,21 @@
|
|
|
64
64
|
* embeds: it carries the box-drawing, geometric and arrow repertoire that no
|
|
65
65
|
* candidate serif reliably has.
|
|
66
66
|
*
|
|
67
|
-
* **
|
|
68
|
-
*
|
|
67
|
+
* **Every finding here is a `warning`, and that is deliberate.** `reportFindings`
|
|
68
|
+
* fails a run on an error and not on a warning, so nothing this module says can
|
|
69
|
+
* break a build.
|
|
70
|
+
*
|
|
71
|
+
* A character outside the charset does not make a note wrong. It compiles to
|
|
72
|
+
* the same document, publishes the same page, and reads correctly everywhere
|
|
73
|
+
* except a book that does not exist yet — so failing a consumer's build over it
|
|
74
|
+
* would stop work that is already correct in order to serve a renderer that is
|
|
75
|
+
* still being written. The trees this shipped against had 112 findings between
|
|
76
|
+
* them on the day it landed, all of them real and none of them urgent.
|
|
77
|
+
*
|
|
78
|
+
* The NFC rule is the one with a claim to being an error, since a decomposed
|
|
79
|
+
* name is silently invisible to an exact-match filter. It is a warning too,
|
|
80
|
+
* because a lint that fails a build for one of its rules and not the others is
|
|
81
|
+
* a lint nobody can predict.
|
|
69
82
|
*
|
|
70
83
|
* @module
|
|
71
84
|
*/
|
|
@@ -295,7 +308,7 @@ export function decomposedRuns(text) {
|
|
|
295
308
|
* @param {string} text - The file's contents.
|
|
296
309
|
* @param {string} file - Path to report, relative to the tree.
|
|
297
310
|
* @returns {Array<{file: string, line: number, column: number,
|
|
298
|
-
* severity: "
|
|
311
|
+
* severity: "warning", message: string}>} What is wrong, in file order.
|
|
299
312
|
*/
|
|
300
313
|
export function checkText(text, file) {
|
|
301
314
|
const findings = [];
|
|
@@ -347,7 +360,7 @@ export function checkText(text, file) {
|
|
|
347
360
|
file,
|
|
348
361
|
line: i + 1,
|
|
349
362
|
column,
|
|
350
|
-
severity: /** @type {const} */ ("
|
|
363
|
+
severity: /** @type {const} */ ("warning"),
|
|
351
364
|
message: `\`${ch}\` ${hex(cp)} is ${refusalFor(cp)}`,
|
|
352
365
|
});
|
|
353
366
|
}
|
|
@@ -364,7 +377,7 @@ export function checkText(text, file) {
|
|
|
364
377
|
file,
|
|
365
378
|
line,
|
|
366
379
|
column,
|
|
367
|
-
severity: /** @type {const} */ ("
|
|
380
|
+
severity: /** @type {const} */ ("warning"),
|
|
368
381
|
message:
|
|
369
382
|
`\`${run.sequence}\` is written decomposed as ${points}; write the ` +
|
|
370
383
|
`precomposed \`${run.composed}\` (${[...run.composed]
|
|
@@ -391,13 +404,13 @@ export function checkText(text, file) {
|
|
|
391
404
|
* in addition to the dot-directories always skipped.
|
|
392
405
|
* @param {readonly string[]} [opts.extensions] - File extensions to read.
|
|
393
406
|
* @returns {{findings: Array<{file: string, line: number, column: number,
|
|
394
|
-
* severity: "
|
|
407
|
+
* severity: "warning", message: string}>, files: number}} The findings, and how
|
|
395
408
|
* many files produced them.
|
|
396
409
|
*/
|
|
397
410
|
export function lintContentCharset(contentBase, { skipDirectories = [], extensions } = {}) {
|
|
398
411
|
const exts = new Set(extensions ?? [".md", ".markdown", ".yaml", ".yml", ".json"]);
|
|
399
412
|
const skip = new Set(skipDirectories);
|
|
400
|
-
/** @type {Array<{file: string, line: number, column: number, severity: "
|
|
413
|
+
/** @type {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} */
|
|
401
414
|
const findings = [];
|
|
402
415
|
let files = 0;
|
|
403
416
|
|
package/engine/content-icons.mjs
CHANGED
|
@@ -56,6 +56,12 @@
|
|
|
56
56
|
* 1.1, so a distributed PDF may embed the subset it uses. Attribution belongs
|
|
57
57
|
* in the book's colophon, not in every note.
|
|
58
58
|
*
|
|
59
|
+
* **Every finding here is a `warning`**, so nothing this module says can fail a
|
|
60
|
+
* build — `reportFindings` fails on an error and not on a warning. An
|
|
61
|
+
* undeclared name renders as its own literal text, which is visible on the page
|
|
62
|
+
* and wrong in a way a reader will notice; that deserves to be reported and
|
|
63
|
+
* does not deserve to stop a build that is otherwise correct.
|
|
64
|
+
*
|
|
59
65
|
* @module
|
|
60
66
|
*/
|
|
61
67
|
|
|
@@ -73,28 +79,201 @@ import path from "node:path";
|
|
|
73
79
|
*/
|
|
74
80
|
export const ICON_STYLES = Object.freeze(["solid", "regular", "brands"]);
|
|
75
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The icon families a registry entry may draw from.
|
|
84
|
+
*
|
|
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).
|
|
89
|
+
*
|
|
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.
|
|
100
|
+
*
|
|
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.
|
|
104
|
+
*
|
|
105
|
+
* @type {Readonly<Record<string, {class: string, styled: boolean, describe: string}>>}
|
|
106
|
+
*/
|
|
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
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/** The family an entry that does not name one belongs to. */
|
|
121
|
+
export const DEFAULT_ICON_FAMILY = "fontawesome";
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The family an entry draws from, named or defaulted.
|
|
125
|
+
*
|
|
126
|
+
* @param {{family?: string}} entry - A registry entry.
|
|
127
|
+
* @returns {string} The family name.
|
|
128
|
+
*/
|
|
129
|
+
export function familyOf(entry) {
|
|
130
|
+
return entry?.family ?? DEFAULT_ICON_FAMILY;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The sizes a note may ask for, and what each means on a page.
|
|
135
|
+
*
|
|
136
|
+
* **A closed set, because size is content here rather than styling.** The icon
|
|
137
|
+
* legend exists to let a reader tell one glyph from another, and several of
|
|
138
|
+
* them genuinely cannot be told apart at the size of running text — so the
|
|
139
|
+
* enlargement is part of what that page *says*, not decoration applied to it.
|
|
140
|
+
* That is the one case worth a size at all.
|
|
141
|
+
*
|
|
142
|
+
* Closed rather than free-form for the usual reason: `font-size: 2em` in a note
|
|
143
|
+
* is CSS, which reaches two of the three surfaces and means nothing to the
|
|
144
|
+
* third. A name means the same thing everywhere, and the multiples below are
|
|
145
|
+
* Font Awesome's own, so the web class and the PDF scale cannot drift.
|
|
146
|
+
*
|
|
147
|
+
* @type {Readonly<Record<string, {class: string, scale: number}>>}
|
|
148
|
+
*/
|
|
149
|
+
export const ICON_SIZES = Object.freeze({
|
|
150
|
+
lg: { class: "fa-lg", scale: 1.25 },
|
|
151
|
+
xl: { class: "fa-xl", scale: 1.5 },
|
|
152
|
+
"2x": { class: "fa-2x", scale: 2 },
|
|
153
|
+
"3x": { class: "fa-3x", scale: 3 },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The attribute names a note may write, and how each is validated.
|
|
158
|
+
*
|
|
159
|
+
* One entry today. It is a table rather than an `if (key === "size")` because
|
|
160
|
+
* the next attribute — a fixed-width flag, a rotation, a title override — should
|
|
161
|
+
* cost a line here and nothing else, and because an unknown key has to be
|
|
162
|
+
* *reported*: silently ignoring `{sixe: 2x}` would leave the author looking at a
|
|
163
|
+
* page that is not what they asked for, with nothing to say why.
|
|
164
|
+
*
|
|
165
|
+
* @type {Readonly<Record<string, {values: readonly string[], describe: string}>>}
|
|
166
|
+
*/
|
|
167
|
+
export const ICON_ATTRIBUTES = Object.freeze({
|
|
168
|
+
size: {
|
|
169
|
+
values: Object.freeze(Object.keys(ICON_SIZES)),
|
|
170
|
+
describe: "how much larger than running text to draw the icon",
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Read the brace of an icon token.
|
|
176
|
+
*
|
|
177
|
+
* Values are validated against {@link ICON_ATTRIBUTES} here rather than at the
|
|
178
|
+
* point of rendering, so a mistake is one finding with a position rather than a
|
|
179
|
+
* silently different page.
|
|
180
|
+
*
|
|
181
|
+
* @param {string} [raw] - The text between the braces, without them.
|
|
182
|
+
* @returns {{attrs: Record<string, string>, problems: string[]}} What was
|
|
183
|
+
* written, and what cannot be honoured.
|
|
184
|
+
*/
|
|
185
|
+
export function parseIconAttributes(raw) {
|
|
186
|
+
/** @type {Record<string, string>} */
|
|
187
|
+
const attrs = {};
|
|
188
|
+
const problems = [];
|
|
189
|
+
if (!raw || !raw.trim()) return { attrs, problems };
|
|
190
|
+
|
|
191
|
+
// Split on commas, not whitespace: `size: 2x` is one pair with a space in
|
|
192
|
+
// it, and the space after the colon is the whole point of the spelling.
|
|
193
|
+
for (const part of raw
|
|
194
|
+
.split(",")
|
|
195
|
+
.map((s) => s.trim())
|
|
196
|
+
.filter(Boolean)) {
|
|
197
|
+
const colon = part.indexOf(":");
|
|
198
|
+
if (colon === -1) {
|
|
199
|
+
problems.push(
|
|
200
|
+
`\`${part}\` is not a \`key: value\` attribute — an icon takes ` +
|
|
201
|
+
`${Object.keys(ICON_ATTRIBUTES).join(", ")}`,
|
|
202
|
+
);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const key = part.slice(0, colon).trim();
|
|
206
|
+
const value = part.slice(colon + 1).trim();
|
|
207
|
+
const spec =
|
|
208
|
+
Object.prototype.hasOwnProperty.call(ICON_ATTRIBUTES, key) ?
|
|
209
|
+
ICON_ATTRIBUTES[key]
|
|
210
|
+
: undefined;
|
|
211
|
+
if (!spec) {
|
|
212
|
+
problems.push(
|
|
213
|
+
`\`${key}\` is not an icon attribute — the ones there are: ` +
|
|
214
|
+
`${Object.keys(ICON_ATTRIBUTES).join(", ")}`,
|
|
215
|
+
);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!spec.values.includes(value)) {
|
|
219
|
+
problems.push(
|
|
220
|
+
`\`${key}: ${value}\` is not one of ${spec.values.join(", ")} — ` +
|
|
221
|
+
`${key} says ${spec.describe}`,
|
|
222
|
+
);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
attrs[key] = value;
|
|
226
|
+
}
|
|
227
|
+
return { attrs, problems };
|
|
228
|
+
}
|
|
229
|
+
|
|
76
230
|
/**
|
|
77
231
|
* The icons the user guide already depicts, under the names it should call them.
|
|
78
232
|
*
|
|
79
|
-
* Each entry
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* `trash-can`
|
|
84
|
-
*
|
|
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.
|
|
85
254
|
*
|
|
86
255
|
* @type {Readonly<Record<string, {style: string, icon: string, label: string}>>}
|
|
87
256
|
*/
|
|
88
257
|
export const DEFAULT_ICONS = Object.freeze({
|
|
258
|
+
affiliation: { style: "solid", icon: "certificate", label: "affiliation" },
|
|
89
259
|
star: { style: "solid", icon: "star", label: "star" },
|
|
90
260
|
"star-outline": { style: "regular", icon: "star", label: "hollow star" },
|
|
91
|
-
|
|
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" },
|
|
92
269
|
edit: { style: "solid", icon: "pen-to-square", label: "edit" },
|
|
93
|
-
delete: { style: "solid", icon: "trash
|
|
270
|
+
delete: { style: "solid", icon: "trash", label: "delete" },
|
|
94
271
|
add: { style: "solid", icon: "plus", label: "add" },
|
|
95
272
|
remove: { style: "solid", icon: "xmark", label: "remove" },
|
|
96
273
|
"not-applicable": { style: "solid", icon: "xmark", label: "not applicable" },
|
|
274
|
+
close: { style: "solid", icon: "xmark", label: "close" },
|
|
97
275
|
menu: { style: "solid", icon: "ellipsis-vertical", label: "actions menu" },
|
|
276
|
+
run: { style: "solid", icon: "play", label: "run this action" },
|
|
98
277
|
expand: { style: "solid", icon: "caret-right", label: "expand" },
|
|
99
278
|
shield: { style: "solid", icon: "shield-halved", label: "armour" },
|
|
100
279
|
compass: { style: "solid", icon: "compass", label: "guided tour" },
|
|
@@ -113,9 +292,24 @@ export const DEFAULT_ICONS = Object.freeze({
|
|
|
113
292
|
* Not `:name[content]`. That is remark-directive syntax, and this toolchain
|
|
114
293
|
* parses with markdown-it; a directive would render as its own literal text.
|
|
115
294
|
*
|
|
295
|
+
* An optional trailing brace carries **attributes**:
|
|
296
|
+
*
|
|
297
|
+
* :icon-affiliation:{size: 2x}
|
|
298
|
+
*
|
|
299
|
+
* `key: value` pairs, comma-separated, in the shape `markdown-it-attrs` and
|
|
300
|
+
* remark-directive already use — so it is a convention a reader may recognise
|
|
301
|
+
* rather than one this module invented. Attributes rather than a bare value
|
|
302
|
+
* because `size` is merely the first one anybody needed: a fixed-width flag, a
|
|
303
|
+
* rotation, a title override are the same shape of thing, and a syntax that
|
|
304
|
+
* could only ever express size would have to be replaced to gain any of them.
|
|
305
|
+
*
|
|
306
|
+
* One inline rule consumes the token **and** its brace, so there is no state in
|
|
307
|
+
* which the icon resolves and the brace is left stranded on the page. An
|
|
308
|
+
* unhandled token degrades whole, exactly as the bare form does.
|
|
309
|
+
*
|
|
116
310
|
* @type {RegExp}
|
|
117
311
|
*/
|
|
118
|
-
export const ICON_PATTERN = /:icon-([a-z0-9]+(?:-[a-z0-9]+)*)
|
|
312
|
+
export const ICON_PATTERN = /:icon-([a-z0-9]+(?:-[a-z0-9]+)*):(?:\{([^}]*)\})?/g;
|
|
119
313
|
|
|
120
314
|
/**
|
|
121
315
|
* Look one name up.
|
|
@@ -144,11 +338,24 @@ const attr = (value) =>
|
|
|
144
338
|
* @param {{style: string, icon: string, label: string}} entry - A registry entry.
|
|
145
339
|
* @returns {string} An `<i>` element.
|
|
146
340
|
*/
|
|
147
|
-
export function iconHtml(entry) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
341
|
+
export function iconHtml(entry, attrs = {}) {
|
|
342
|
+
const family = ICON_FAMILIES[familyOf(entry)] ?? ICON_FAMILIES[DEFAULT_ICON_FAMILY];
|
|
343
|
+
const prefix = family.class;
|
|
344
|
+
|
|
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.
|
|
347
|
+
const classes =
|
|
348
|
+
family.styled ?
|
|
349
|
+
[`${prefix}-${attr(entry.style)}`, `${prefix}-${attr(entry.icon)}`]
|
|
350
|
+
: [`${prefix}-${attr(entry.icon)}`];
|
|
351
|
+
|
|
352
|
+
// The size classes are Font Awesome's, and the Game-Icons stylesheet this
|
|
353
|
+
// toolchain's consumers generate mirrors its box metrics deliberately, so
|
|
354
|
+
// they apply to both families.
|
|
355
|
+
const sized = attrs.size ? ICON_SIZES[attrs.size] : undefined;
|
|
356
|
+
if (sized) classes.push(sized.class);
|
|
357
|
+
|
|
358
|
+
return `<i class="${classes.join(" ")}" role="img" aria-label="${attr(entry.label)}"></i>`;
|
|
152
359
|
}
|
|
153
360
|
|
|
154
361
|
/**
|
|
@@ -160,7 +367,8 @@ export function iconHtml(entry) {
|
|
|
160
367
|
export function iconsIn(text) {
|
|
161
368
|
const out = [];
|
|
162
369
|
for (const m of text.matchAll(ICON_PATTERN)) {
|
|
163
|
-
|
|
370
|
+
const { attrs, problems } = parseIconAttributes(m[2]);
|
|
371
|
+
out.push({ name: m[1], index: m.index ?? 0, raw: m[0], attrs, problems });
|
|
164
372
|
}
|
|
165
373
|
return out;
|
|
166
374
|
}
|
|
@@ -176,28 +384,38 @@ export function iconsIn(text) {
|
|
|
176
384
|
* @param {string} file - Path to report.
|
|
177
385
|
* @param {Record<string, object>} [registry] - Defaults to {@link DEFAULT_ICONS}.
|
|
178
386
|
* @returns {Array<{file: string, line: number, column: number,
|
|
179
|
-
* severity: "
|
|
387
|
+
* severity: "warning", message: string}>} The unknown names.
|
|
180
388
|
*/
|
|
181
389
|
export function lintIcons(text, file, registry = DEFAULT_ICONS) {
|
|
182
390
|
const findings = [];
|
|
183
|
-
for (const { name, index, raw } of iconsIn(text)) {
|
|
184
|
-
if (resolveIcon(name, registry)) continue;
|
|
391
|
+
for (const { name, index, raw, problems } of iconsIn(text)) {
|
|
185
392
|
const before = text.slice(0, index);
|
|
186
393
|
const line = before.split("\n").length;
|
|
187
394
|
const column = index - (before.lastIndexOf("\n") + 1) + 1;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
395
|
+
const at = { file, line, column, severity: /** @type {const} */ ("warning") };
|
|
396
|
+
|
|
397
|
+
if (!resolveIcon(name, registry)) {
|
|
398
|
+
// Nearest declared name, when there is an obvious one: a typo is the
|
|
399
|
+
// common case and the registry is short enough to say what was meant.
|
|
400
|
+
const suggestion = nearestName(name, Object.keys(registry));
|
|
401
|
+
findings.push({
|
|
402
|
+
...at,
|
|
403
|
+
message:
|
|
404
|
+
`\`${raw}\` names an icon the registry does not declare` +
|
|
405
|
+
(suggestion ? `; did you mean \`:icon-${suggestion}:\`?` : "") +
|
|
406
|
+
` — an undeclared name renders as its own literal text`,
|
|
407
|
+
});
|
|
408
|
+
// The name is the bigger fault; reporting its attributes as well
|
|
409
|
+
// would be two findings for one token the author has to rewrite.
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// A bad attribute on a good name is its own finding: the icon renders,
|
|
414
|
+
// and renders differently from what was asked for, which is the case
|
|
415
|
+
// nobody notices without being told.
|
|
416
|
+
for (const problem of problems) {
|
|
417
|
+
findings.push({ ...at, message: `\`${raw}\`: ${problem}` });
|
|
418
|
+
}
|
|
201
419
|
}
|
|
202
420
|
return findings;
|
|
203
421
|
}
|
|
@@ -250,7 +468,7 @@ function editDistance(a, b) {
|
|
|
250
468
|
*
|
|
251
469
|
* @param {Record<string, object>} registry - A package's icon table.
|
|
252
470
|
* @param {string} [where="icons"] - Where to say the fault is.
|
|
253
|
-
* @returns {Array<{severity: "
|
|
471
|
+
* @returns {Array<{severity: "warning", message: string}>} What is wrong with it.
|
|
254
472
|
*/
|
|
255
473
|
export function checkIconRegistry(registry, where = "icons") {
|
|
256
474
|
const findings = [];
|
|
@@ -258,29 +476,54 @@ export function checkIconRegistry(registry, where = "icons") {
|
|
|
258
476
|
const at = `\`${where}.${name}\``;
|
|
259
477
|
if (!entry || typeof entry !== "object") {
|
|
260
478
|
findings.push({
|
|
261
|
-
severity: /** @type {const} */ ("
|
|
479
|
+
severity: /** @type {const} */ ("warning"),
|
|
262
480
|
message: `${at} is not an icon entry — it takes \`style\`, \`icon\` and \`label\``,
|
|
263
481
|
});
|
|
264
482
|
continue;
|
|
265
483
|
}
|
|
266
|
-
|
|
484
|
+
const familyName = familyOf(entry);
|
|
485
|
+
const family = ICON_FAMILIES[familyName];
|
|
486
|
+
if (!family) {
|
|
267
487
|
findings.push({
|
|
268
|
-
severity: /** @type {const} */ ("
|
|
488
|
+
severity: /** @type {const} */ ("warning"),
|
|
489
|
+
message:
|
|
490
|
+
`${at} names family \`${familyName}\`, and the families there ` +
|
|
491
|
+
`are: ${Object.keys(ICON_FAMILIES).join(", ")}`,
|
|
492
|
+
});
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (family.styled && !ICON_STYLES.includes(entry.style)) {
|
|
497
|
+
findings.push({
|
|
498
|
+
severity: /** @type {const} */ ("warning"),
|
|
269
499
|
message:
|
|
270
500
|
`${at} names style \`${entry.style}\`, and Font Awesome Free ships ` +
|
|
271
501
|
`only ${ICON_STYLES.join(", ")} — a glyph in any other style is ` +
|
|
272
502
|
`absent from the font a book would embed`,
|
|
273
503
|
});
|
|
274
504
|
}
|
|
505
|
+
|
|
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
|
+
});
|
|
517
|
+
}
|
|
275
518
|
if (typeof entry.icon !== "string" || !entry.icon) {
|
|
276
519
|
findings.push({
|
|
277
|
-
severity: /** @type {const} */ ("
|
|
520
|
+
severity: /** @type {const} */ ("warning"),
|
|
278
521
|
message: `${at} declares no \`icon\`, so nothing names the glyph to draw`,
|
|
279
522
|
});
|
|
280
523
|
}
|
|
281
524
|
if (typeof entry.label !== "string" || !entry.label) {
|
|
282
525
|
findings.push({
|
|
283
|
-
severity: /** @type {const} */ ("
|
|
526
|
+
severity: /** @type {const} */ ("warning"),
|
|
284
527
|
message:
|
|
285
528
|
`${at} declares no \`label\`, and an icon with no accessible name ` +
|
|
286
529
|
`is read aloud as a gap in the sentence`,
|
|
@@ -302,7 +545,7 @@ export function checkIconRegistry(registry, where = "icons") {
|
|
|
302
545
|
* @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore.
|
|
303
546
|
* @param {Record<string, object>} [opts.registry] - The package's icon table.
|
|
304
547
|
* @returns {{findings: Array<{file: string, line: number, column: number,
|
|
305
|
-
* severity: "
|
|
548
|
+
* severity: "warning", message: string}>, files: number}} What it found.
|
|
306
549
|
*/
|
|
307
550
|
export function lintContentIcons(contentBase, { skipDirectories = [], registry } = {}) {
|
|
308
551
|
const skip = new Set(skipDirectories);
|
|
@@ -355,7 +598,7 @@ export function iconPlugin(registry = DEFAULT_ICONS) {
|
|
|
355
598
|
return (md) => {
|
|
356
599
|
/** @type {any} */ (md).inline.ruler.before("emphasis", "heroiclands_icon", iconRule);
|
|
357
600
|
/** @type {any} */ (md).renderer.rules.heroiclands_icon = (tokens, idx) =>
|
|
358
|
-
iconHtml(tokens[idx].meta.entry);
|
|
601
|
+
iconHtml(tokens[idx].meta.entry, tokens[idx].meta.attrs);
|
|
359
602
|
|
|
360
603
|
/**
|
|
361
604
|
* @param {any} state - markdown-it inline state.
|
|
@@ -367,7 +610,7 @@ export function iconPlugin(registry = DEFAULT_ICONS) {
|
|
|
367
610
|
if (state.src.charCodeAt(start) !== 0x3a /* : */) return false;
|
|
368
611
|
// Anchored at the cursor, so the scan is O(token) rather than a
|
|
369
612
|
// search of the remaining source at every colon in the paragraph.
|
|
370
|
-
const re = /^:icon-([a-z0-9]+(?:-[a-z0-9]+)*)
|
|
613
|
+
const re = /^:icon-([a-z0-9]+(?:-[a-z0-9]+)*):(?:\{([^}]*)\})?/;
|
|
371
614
|
const m = re.exec(state.src.slice(start));
|
|
372
615
|
if (!m) return false;
|
|
373
616
|
|
|
@@ -376,9 +619,14 @@ export function iconPlugin(registry = DEFAULT_ICONS) {
|
|
|
376
619
|
// unrecognised name visible on the page instead of vanishing.
|
|
377
620
|
if (!entry) return false;
|
|
378
621
|
|
|
622
|
+
// An attribute that cannot be honoured is reported by the lint, not
|
|
623
|
+
// enforced here: refusing to render would hide a good icon over a
|
|
624
|
+
// bad size, and the page is the place the author is looking.
|
|
625
|
+
const { attrs } = parseIconAttributes(m[2]);
|
|
626
|
+
|
|
379
627
|
if (!silent) {
|
|
380
628
|
const token = state.push("heroiclands_icon", "", 0);
|
|
381
|
-
token.meta = { name: m[1], entry };
|
|
629
|
+
token.meta = { name: m[1], entry, attrs };
|
|
382
630
|
token.markup = m[0];
|
|
383
631
|
}
|
|
384
632
|
state.pos += m[0].length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "20.
|
|
3
|
+
"version": "20.3.1",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -61,13 +61,13 @@ export function decomposedRuns(text: string): Array<{
|
|
|
61
61
|
* @param {string} text - The file's contents.
|
|
62
62
|
* @param {string} file - Path to report, relative to the tree.
|
|
63
63
|
* @returns {Array<{file: string, line: number, column: number,
|
|
64
|
-
* severity: "
|
|
64
|
+
* severity: "warning", message: string}>} What is wrong, in file order.
|
|
65
65
|
*/
|
|
66
66
|
export function checkText(text: string, file: string): Array<{
|
|
67
67
|
file: string;
|
|
68
68
|
line: number;
|
|
69
69
|
column: number;
|
|
70
|
-
severity: "
|
|
70
|
+
severity: "warning";
|
|
71
71
|
message: string;
|
|
72
72
|
}>;
|
|
73
73
|
/**
|
|
@@ -84,7 +84,7 @@ export function checkText(text: string, file: string): Array<{
|
|
|
84
84
|
* in addition to the dot-directories always skipped.
|
|
85
85
|
* @param {readonly string[]} [opts.extensions] - File extensions to read.
|
|
86
86
|
* @returns {{findings: Array<{file: string, line: number, column: number,
|
|
87
|
-
* severity: "
|
|
87
|
+
* severity: "warning", message: string}>, files: number}} The findings, and how
|
|
88
88
|
* many files produced them.
|
|
89
89
|
*/
|
|
90
90
|
export function lintContentCharset(contentBase: string, { skipDirectories, extensions }?: {
|
|
@@ -95,7 +95,7 @@ export function lintContentCharset(contentBase: string, { skipDirectories, exten
|
|
|
95
95
|
file: string;
|
|
96
96
|
line: number;
|
|
97
97
|
column: number;
|
|
98
|
-
severity: "
|
|
98
|
+
severity: "warning";
|
|
99
99
|
message: string;
|
|
100
100
|
}>;
|
|
101
101
|
files: number;
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The family an entry draws from, named or defaulted.
|
|
3
|
+
*
|
|
4
|
+
* @param {{family?: string}} entry - A registry entry.
|
|
5
|
+
* @returns {string} The family name.
|
|
6
|
+
*/
|
|
7
|
+
export function familyOf(entry: {
|
|
8
|
+
family?: string;
|
|
9
|
+
}): string;
|
|
10
|
+
/**
|
|
11
|
+
* Read the brace of an icon token.
|
|
12
|
+
*
|
|
13
|
+
* Values are validated against {@link ICON_ATTRIBUTES} here rather than at the
|
|
14
|
+
* point of rendering, so a mistake is one finding with a position rather than a
|
|
15
|
+
* silently different page.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} [raw] - The text between the braces, without them.
|
|
18
|
+
* @returns {{attrs: Record<string, string>, problems: string[]}} What was
|
|
19
|
+
* written, and what cannot be honoured.
|
|
20
|
+
*/
|
|
21
|
+
export function parseIconAttributes(raw?: string): {
|
|
22
|
+
attrs: Record<string, string>;
|
|
23
|
+
problems: string[];
|
|
24
|
+
};
|
|
1
25
|
/**
|
|
2
26
|
* Look one name up.
|
|
3
27
|
*
|
|
@@ -26,7 +50,7 @@ export function iconHtml(entry: {
|
|
|
26
50
|
style: string;
|
|
27
51
|
icon: string;
|
|
28
52
|
label: string;
|
|
29
|
-
}): string;
|
|
53
|
+
}, attrs?: {}): string;
|
|
30
54
|
/**
|
|
31
55
|
* Every icon a string names, in the order written.
|
|
32
56
|
*
|
|
@@ -49,13 +73,13 @@ export function iconsIn(text: string): Array<{
|
|
|
49
73
|
* @param {string} file - Path to report.
|
|
50
74
|
* @param {Record<string, object>} [registry] - Defaults to {@link DEFAULT_ICONS}.
|
|
51
75
|
* @returns {Array<{file: string, line: number, column: number,
|
|
52
|
-
* severity: "
|
|
76
|
+
* severity: "warning", message: string}>} The unknown names.
|
|
53
77
|
*/
|
|
54
78
|
export function lintIcons(text: string, file: string, registry?: Record<string, object>): Array<{
|
|
55
79
|
file: string;
|
|
56
80
|
line: number;
|
|
57
81
|
column: number;
|
|
58
|
-
severity: "
|
|
82
|
+
severity: "warning";
|
|
59
83
|
message: string;
|
|
60
84
|
}>;
|
|
61
85
|
/**
|
|
@@ -63,10 +87,10 @@ export function lintIcons(text: string, file: string, registry?: Record<string,
|
|
|
63
87
|
*
|
|
64
88
|
* @param {Record<string, object>} registry - A package's icon table.
|
|
65
89
|
* @param {string} [where="icons"] - Where to say the fault is.
|
|
66
|
-
* @returns {Array<{severity: "
|
|
90
|
+
* @returns {Array<{severity: "warning", message: string}>} What is wrong with it.
|
|
67
91
|
*/
|
|
68
92
|
export function checkIconRegistry(registry: Record<string, object>, where?: string): Array<{
|
|
69
|
-
severity: "
|
|
93
|
+
severity: "warning";
|
|
70
94
|
message: string;
|
|
71
95
|
}>;
|
|
72
96
|
/**
|
|
@@ -81,7 +105,7 @@ export function checkIconRegistry(registry: Record<string, object>, where?: stri
|
|
|
81
105
|
* @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore.
|
|
82
106
|
* @param {Record<string, object>} [opts.registry] - The package's icon table.
|
|
83
107
|
* @returns {{findings: Array<{file: string, line: number, column: number,
|
|
84
|
-
* severity: "
|
|
108
|
+
* severity: "warning", message: string}>, files: number}} What it found.
|
|
85
109
|
*/
|
|
86
110
|
export function lintContentIcons(contentBase: string, { skipDirectories, registry }?: {
|
|
87
111
|
skipDirectories?: readonly string[] | undefined;
|
|
@@ -91,7 +115,7 @@ export function lintContentIcons(contentBase: string, { skipDirectories, registr
|
|
|
91
115
|
file: string;
|
|
92
116
|
line: number;
|
|
93
117
|
column: number;
|
|
94
|
-
severity: "
|
|
118
|
+
severity: "warning";
|
|
95
119
|
message: string;
|
|
96
120
|
}>;
|
|
97
121
|
files: number;
|
|
@@ -117,15 +141,97 @@ export function iconPlugin(registry?: Record<string, object>): (md: object) => v
|
|
|
117
141
|
* @type {readonly string[]}
|
|
118
142
|
*/
|
|
119
143
|
export const ICON_STYLES: readonly string[];
|
|
144
|
+
/**
|
|
145
|
+
* The icon families a registry entry may draw from.
|
|
146
|
+
*
|
|
147
|
+
* **Two, because the interface uses two.** The SoHL icon legend says so in its
|
|
148
|
+
* own prose: Font Awesome for most things, and Game-Icons.net *"for the arms,
|
|
149
|
+
* gear, and condition glyphs that Font Awesome does not cover"* — eighteen of
|
|
150
|
+
* them, `ginf-broadsword` and its kin (#391).
|
|
151
|
+
*
|
|
152
|
+
* They differ in more than a class prefix, which is why this is a family rather
|
|
153
|
+
* than a naming convention:
|
|
154
|
+
*
|
|
155
|
+
* - Font Awesome has **weights**, so an entry names a `style` and a filled and
|
|
156
|
+
* hollow pair is one icon twice. Game-Icons has none, so a `style` on such an
|
|
157
|
+
* entry names something that does not exist.
|
|
158
|
+
* - They resolve to **different fonts**. A PDF has to embed both, and find each
|
|
159
|
+
* codepoint in its own table — Font Awesome's from the file it ships,
|
|
160
|
+
* Game-Icons' from the `game-icons-codepoints.json` the consumer's own
|
|
161
|
+
* `build-icon-font.mjs` writes.
|
|
162
|
+
*
|
|
163
|
+
* `class` is the prefix the web surfaces use. `styled` says whether a `style`
|
|
164
|
+
* belongs on the entry at all, which is what lets a mistake be reported rather
|
|
165
|
+
* than rendered as a class nobody defined.
|
|
166
|
+
*
|
|
167
|
+
* @type {Readonly<Record<string, {class: string, styled: boolean, describe: string}>>}
|
|
168
|
+
*/
|
|
169
|
+
export const ICON_FAMILIES: Readonly<Record<string, {
|
|
170
|
+
class: string;
|
|
171
|
+
styled: boolean;
|
|
172
|
+
describe: string;
|
|
173
|
+
}>>;
|
|
174
|
+
/** The family an entry that does not name one belongs to. */
|
|
175
|
+
export const DEFAULT_ICON_FAMILY: "fontawesome";
|
|
176
|
+
/**
|
|
177
|
+
* The sizes a note may ask for, and what each means on a page.
|
|
178
|
+
*
|
|
179
|
+
* **A closed set, because size is content here rather than styling.** The icon
|
|
180
|
+
* legend exists to let a reader tell one glyph from another, and several of
|
|
181
|
+
* them genuinely cannot be told apart at the size of running text — so the
|
|
182
|
+
* enlargement is part of what that page *says*, not decoration applied to it.
|
|
183
|
+
* That is the one case worth a size at all.
|
|
184
|
+
*
|
|
185
|
+
* Closed rather than free-form for the usual reason: `font-size: 2em` in a note
|
|
186
|
+
* is CSS, which reaches two of the three surfaces and means nothing to the
|
|
187
|
+
* third. A name means the same thing everywhere, and the multiples below are
|
|
188
|
+
* Font Awesome's own, so the web class and the PDF scale cannot drift.
|
|
189
|
+
*
|
|
190
|
+
* @type {Readonly<Record<string, {class: string, scale: number}>>}
|
|
191
|
+
*/
|
|
192
|
+
export const ICON_SIZES: Readonly<Record<string, {
|
|
193
|
+
class: string;
|
|
194
|
+
scale: number;
|
|
195
|
+
}>>;
|
|
196
|
+
/**
|
|
197
|
+
* The attribute names a note may write, and how each is validated.
|
|
198
|
+
*
|
|
199
|
+
* One entry today. It is a table rather than an `if (key === "size")` because
|
|
200
|
+
* the next attribute — a fixed-width flag, a rotation, a title override — should
|
|
201
|
+
* cost a line here and nothing else, and because an unknown key has to be
|
|
202
|
+
* *reported*: silently ignoring `{sixe: 2x}` would leave the author looking at a
|
|
203
|
+
* page that is not what they asked for, with nothing to say why.
|
|
204
|
+
*
|
|
205
|
+
* @type {Readonly<Record<string, {values: readonly string[], describe: string}>>}
|
|
206
|
+
*/
|
|
207
|
+
export const ICON_ATTRIBUTES: Readonly<Record<string, {
|
|
208
|
+
values: readonly string[];
|
|
209
|
+
describe: string;
|
|
210
|
+
}>>;
|
|
120
211
|
/**
|
|
121
212
|
* The icons the user guide already depicts, under the names it should call them.
|
|
122
213
|
*
|
|
123
|
-
* Each entry
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* `trash-can`
|
|
128
|
-
*
|
|
214
|
+
* Each entry is read off the interface it describes rather than invented, and
|
|
215
|
+
* that is meant literally — the table was checked against the system's own
|
|
216
|
+
* templates, which is how `delete` came to be `fa-trash` rather than the
|
|
217
|
+
* `fa-trash-can` first written here. The sheets draw `fa-trash` twenty-five
|
|
218
|
+
* times and `fa-trash-can` never, so the first spelling would have printed an
|
|
219
|
+
* icon the reader has never seen on screen. A registry that is not checked
|
|
220
|
+
* against the interface is just a second place to be wrong.
|
|
221
|
+
*
|
|
222
|
+
* The names are what a *writer* would reach for — `delete`, not `trash` —
|
|
223
|
+
* because the writer is the one typing them; the Font Awesome spelling is this
|
|
224
|
+
* table's business, not theirs.
|
|
225
|
+
*
|
|
226
|
+
* **Three entries share `xmark`, and that is the point of naming rather than
|
|
227
|
+
* drawing.** A `✕` in the guide means "not applicable" in a Healing Rate
|
|
228
|
+
* column, "remove this row" on a control, and "close" on a dialog's corner. One
|
|
229
|
+
* glyph, three sentences, three different things for a reader who cannot see
|
|
230
|
+
* it — so they are three names with three labels, and the fact that Font
|
|
231
|
+
* Awesome happens to draw them identically stays in this table.
|
|
232
|
+
*
|
|
233
|
+
* `run` and `expand` are likewise distinct: `▶` runs an action, and its label
|
|
234
|
+
* should say so. `fa-play` is what the sheets use for it.
|
|
129
235
|
*
|
|
130
236
|
* @type {Readonly<Record<string, {style: string, icon: string, label: string}>>}
|
|
131
237
|
*/
|
|
@@ -146,6 +252,21 @@ export const DEFAULT_ICONS: Readonly<Record<string, {
|
|
|
146
252
|
* Not `:name[content]`. That is remark-directive syntax, and this toolchain
|
|
147
253
|
* parses with markdown-it; a directive would render as its own literal text.
|
|
148
254
|
*
|
|
255
|
+
* An optional trailing brace carries **attributes**:
|
|
256
|
+
*
|
|
257
|
+
* :icon-affiliation:{size: 2x}
|
|
258
|
+
*
|
|
259
|
+
* `key: value` pairs, comma-separated, in the shape `markdown-it-attrs` and
|
|
260
|
+
* remark-directive already use — so it is a convention a reader may recognise
|
|
261
|
+
* rather than one this module invented. Attributes rather than a bare value
|
|
262
|
+
* because `size` is merely the first one anybody needed: a fixed-width flag, a
|
|
263
|
+
* rotation, a title override are the same shape of thing, and a syntax that
|
|
264
|
+
* could only ever express size would have to be replaced to gain any of them.
|
|
265
|
+
*
|
|
266
|
+
* One inline rule consumes the token **and** its brace, so there is no state in
|
|
267
|
+
* which the icon resolves and the brace is left stranded on the page. An
|
|
268
|
+
* unhandled token degrades whole, exactly as the bare form does.
|
|
269
|
+
*
|
|
149
270
|
* @type {RegExp}
|
|
150
271
|
*/
|
|
151
272
|
export const ICON_PATTERN: RegExp;
|