@heroiclands/package-build 20.2.0 → 20.3.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 +76 -0
- package/engine/content-charset.mjs +20 -7
- package/engine/content-icons.mjs +191 -39
- package/package.json +1 -1
- package/types/engine/content-charset.d.mts +4 -4
- package/types/engine/content-icons.d.mts +93 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 20.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6be4a7e: An icon can now carry attributes, and `affiliation` joins the registry.
|
|
8
|
+
|
|
9
|
+
`:icon-affiliation:{size: 2x}` — `key: value` pairs in a trailing brace, the
|
|
10
|
+
shape `markdown-it-attrs` and remark-directive already use. Attributes rather
|
|
11
|
+
than a bare size because `size` is only the first one anybody needed: a
|
|
12
|
+
fixed-width flag, a rotation, a title override are the same shape of thing, and
|
|
13
|
+
a syntax that could only express size would have to be replaced to gain any of
|
|
14
|
+
them.
|
|
15
|
+
|
|
16
|
+
Size is a closed vocabulary — `lg`, `xl`, `2x`, `3x` — mapping to Font Awesome's
|
|
17
|
+
own classes on the web and the same multiples in a PDF. It exists because the
|
|
18
|
+
icon legend enlarges its glyphs so a reader can tell them apart, which makes the
|
|
19
|
+
size part of what that page says rather than styling applied to it. A free-form
|
|
20
|
+
`font-size` would have been CSS, which reaches two surfaces of three.
|
|
21
|
+
|
|
22
|
+
An attribute that cannot be honoured is reported rather than ignored, and the
|
|
23
|
+
icon still renders: hiding a good icon over a bad size would be the worse answer.
|
|
24
|
+
|
|
25
|
+
`affiliation` is `fa-certificate` — a charter under seal. It replaces a
|
|
26
|
+
`fa-duotone fa-handshake` that was reaching for Font Awesome Pro because the
|
|
27
|
+
interlocking hands washed out at small sizes; the fix for a silhouette problem
|
|
28
|
+
is a bolder silhouette, not a paid tier.
|
|
29
|
+
- e209110: Add `close` and `run` to the icon registry, and correct `delete`.
|
|
30
|
+
|
|
31
|
+
Converting the SoHL user guide turned up two controls with no name to convert
|
|
32
|
+
to. A `✕` there means three different things — "not applicable" in a Healing
|
|
33
|
+
Rate column, "remove" on a control, and **close** on a dialog's corner — and a
|
|
34
|
+
`▶` **runs** an action, where the nearest existing entry was `expand`, whose
|
|
35
|
+
accessible label would have told a screen reader the wrong thing. Both now have
|
|
36
|
+
their own name and their own label; that Font Awesome draws the three `xmark`
|
|
37
|
+
senses identically is this table's business rather than the reader's.
|
|
38
|
+
|
|
39
|
+
`delete` was written as `fa-trash-can` and is now `fa-trash`, which is what the
|
|
40
|
+
system's templates actually draw — twenty-five times, against no `fa-trash-can`
|
|
41
|
+
at all. A registry checked against the interface is the whole point of having
|
|
42
|
+
one; unchecked, it is just a second place to be wrong.
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- 1d8e289: The charset and icon checks now report warnings rather than errors, so neither
|
|
47
|
+
can fail a build.
|
|
48
|
+
|
|
49
|
+
Both emitted `severity: "error"`, and `reportFindings` fails a run on an error —
|
|
50
|
+
so adopting the charset check turned consumers' builds red for content that was
|
|
51
|
+
already correct. A character outside the charset does not make a note wrong: it
|
|
52
|
+
compiles to the same document and publishes the same page, and only a book that
|
|
53
|
+
does not exist yet cares. An undeclared icon name is the same shape of thing,
|
|
54
|
+
visible on the page as literal text.
|
|
55
|
+
|
|
56
|
+
The NFC rule is a warning too, despite having the best claim to being an error,
|
|
57
|
+
because a lint that fails a build for one of its rules and not the others is one
|
|
58
|
+
nobody can predict.
|
|
59
|
+
|
|
60
|
+
## 20.2.1
|
|
61
|
+
|
|
62
|
+
### Patch Changes
|
|
63
|
+
|
|
64
|
+
- f39c7e8: Restore the lockfile refresh to the release, and re-sync the lockfile 20.2.0
|
|
65
|
+
shipped without.
|
|
66
|
+
|
|
67
|
+
`changeset version` rewrites `package.json` and the CHANGELOG and never touches
|
|
68
|
+
`package-lock.json`, so the lockfile's root `version` keeps the previous
|
|
69
|
+
release's number. The `version-script` input is the only seam the changesets
|
|
70
|
+
action offers between versioning and committing, which is where the refresh has
|
|
71
|
+
to happen for it to land in the same commit.
|
|
72
|
+
|
|
73
|
+
That input was removed for one release on the theory that it was why `changeset
|
|
74
|
+
version` could not find its own binary. It was not: `node_modules` had been
|
|
75
|
+
committed as a symlink to an absolute path, and the action's `git reset --hard`
|
|
76
|
+
restored it over the install, so nothing resolved by any mechanism. With the
|
|
77
|
+
symlink gone the seam works again, and it is back.
|
|
78
|
+
|
|
3
79
|
## 20.2.0
|
|
4
80
|
|
|
5
81
|
### Minor 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,143 @@ import path from "node:path";
|
|
|
73
79
|
*/
|
|
74
80
|
export const ICON_STYLES = Object.freeze(["solid", "regular", "brands"]);
|
|
75
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The sizes a note may ask for, and what each means on a page.
|
|
84
|
+
*
|
|
85
|
+
* **A closed set, because size is content here rather than styling.** The icon
|
|
86
|
+
* legend exists to let a reader tell one glyph from another, and several of
|
|
87
|
+
* them genuinely cannot be told apart at the size of running text — so the
|
|
88
|
+
* enlargement is part of what that page *says*, not decoration applied to it.
|
|
89
|
+
* That is the one case worth a size at all.
|
|
90
|
+
*
|
|
91
|
+
* Closed rather than free-form for the usual reason: `font-size: 2em` in a note
|
|
92
|
+
* is CSS, which reaches two of the three surfaces and means nothing to the
|
|
93
|
+
* third. A name means the same thing everywhere, and the multiples below are
|
|
94
|
+
* Font Awesome's own, so the web class and the PDF scale cannot drift.
|
|
95
|
+
*
|
|
96
|
+
* @type {Readonly<Record<string, {class: string, scale: number}>>}
|
|
97
|
+
*/
|
|
98
|
+
export const ICON_SIZES = Object.freeze({
|
|
99
|
+
lg: { class: "fa-lg", scale: 1.25 },
|
|
100
|
+
xl: { class: "fa-xl", scale: 1.5 },
|
|
101
|
+
"2x": { class: "fa-2x", scale: 2 },
|
|
102
|
+
"3x": { class: "fa-3x", scale: 3 },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The attribute names a note may write, and how each is validated.
|
|
107
|
+
*
|
|
108
|
+
* One entry today. It is a table rather than an `if (key === "size")` because
|
|
109
|
+
* the next attribute — a fixed-width flag, a rotation, a title override — should
|
|
110
|
+
* cost a line here and nothing else, and because an unknown key has to be
|
|
111
|
+
* *reported*: silently ignoring `{sixe: 2x}` would leave the author looking at a
|
|
112
|
+
* page that is not what they asked for, with nothing to say why.
|
|
113
|
+
*
|
|
114
|
+
* @type {Readonly<Record<string, {values: readonly string[], describe: string}>>}
|
|
115
|
+
*/
|
|
116
|
+
export const ICON_ATTRIBUTES = Object.freeze({
|
|
117
|
+
size: {
|
|
118
|
+
values: Object.freeze(Object.keys(ICON_SIZES)),
|
|
119
|
+
describe: "how much larger than running text to draw the icon",
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Read the brace of an icon token.
|
|
125
|
+
*
|
|
126
|
+
* Values are validated against {@link ICON_ATTRIBUTES} here rather than at the
|
|
127
|
+
* point of rendering, so a mistake is one finding with a position rather than a
|
|
128
|
+
* silently different page.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} [raw] - The text between the braces, without them.
|
|
131
|
+
* @returns {{attrs: Record<string, string>, problems: string[]}} What was
|
|
132
|
+
* written, and what cannot be honoured.
|
|
133
|
+
*/
|
|
134
|
+
export function parseIconAttributes(raw) {
|
|
135
|
+
/** @type {Record<string, string>} */
|
|
136
|
+
const attrs = {};
|
|
137
|
+
const problems = [];
|
|
138
|
+
if (!raw || !raw.trim()) return { attrs, problems };
|
|
139
|
+
|
|
140
|
+
// Split on commas, not whitespace: `size: 2x` is one pair with a space in
|
|
141
|
+
// it, and the space after the colon is the whole point of the spelling.
|
|
142
|
+
for (const part of raw
|
|
143
|
+
.split(",")
|
|
144
|
+
.map((s) => s.trim())
|
|
145
|
+
.filter(Boolean)) {
|
|
146
|
+
const colon = part.indexOf(":");
|
|
147
|
+
if (colon === -1) {
|
|
148
|
+
problems.push(
|
|
149
|
+
`\`${part}\` is not a \`key: value\` attribute — an icon takes ` +
|
|
150
|
+
`${Object.keys(ICON_ATTRIBUTES).join(", ")}`,
|
|
151
|
+
);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const key = part.slice(0, colon).trim();
|
|
155
|
+
const value = part.slice(colon + 1).trim();
|
|
156
|
+
const spec =
|
|
157
|
+
Object.prototype.hasOwnProperty.call(ICON_ATTRIBUTES, key) ?
|
|
158
|
+
ICON_ATTRIBUTES[key]
|
|
159
|
+
: undefined;
|
|
160
|
+
if (!spec) {
|
|
161
|
+
problems.push(
|
|
162
|
+
`\`${key}\` is not an icon attribute — the ones there are: ` +
|
|
163
|
+
`${Object.keys(ICON_ATTRIBUTES).join(", ")}`,
|
|
164
|
+
);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (!spec.values.includes(value)) {
|
|
168
|
+
problems.push(
|
|
169
|
+
`\`${key}: ${value}\` is not one of ${spec.values.join(", ")} — ` +
|
|
170
|
+
`${key} says ${spec.describe}`,
|
|
171
|
+
);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
attrs[key] = value;
|
|
175
|
+
}
|
|
176
|
+
return { attrs, problems };
|
|
177
|
+
}
|
|
178
|
+
|
|
76
179
|
/**
|
|
77
180
|
* The icons the user guide already depicts, under the names it should call them.
|
|
78
181
|
*
|
|
79
|
-
* Each entry
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* `trash-can`
|
|
84
|
-
*
|
|
182
|
+
* Each entry is read off the interface it describes rather than invented, and
|
|
183
|
+
* that is meant literally — the table was checked against the system's own
|
|
184
|
+
* templates, which is how `delete` came to be `fa-trash` rather than the
|
|
185
|
+
* `fa-trash-can` first written here. The sheets draw `fa-trash` twenty-five
|
|
186
|
+
* times and `fa-trash-can` never, so the first spelling would have printed an
|
|
187
|
+
* icon the reader has never seen on screen. A registry that is not checked
|
|
188
|
+
* against the interface is just a second place to be wrong.
|
|
189
|
+
*
|
|
190
|
+
* The names are what a *writer* would reach for — `delete`, not `trash` —
|
|
191
|
+
* because the writer is the one typing them; the Font Awesome spelling is this
|
|
192
|
+
* table's business, not theirs.
|
|
193
|
+
*
|
|
194
|
+
* **Three entries share `xmark`, and that is the point of naming rather than
|
|
195
|
+
* drawing.** A `✕` in the guide means "not applicable" in a Healing Rate
|
|
196
|
+
* column, "remove this row" on a control, and "close" on a dialog's corner. One
|
|
197
|
+
* glyph, three sentences, three different things for a reader who cannot see
|
|
198
|
+
* it — so they are three names with three labels, and the fact that Font
|
|
199
|
+
* Awesome happens to draw them identically stays in this table.
|
|
200
|
+
*
|
|
201
|
+
* `run` and `expand` are likewise distinct: `▶` runs an action, and its label
|
|
202
|
+
* should say so. `fa-play` is what the sheets use for it.
|
|
85
203
|
*
|
|
86
204
|
* @type {Readonly<Record<string, {style: string, icon: string, label: string}>>}
|
|
87
205
|
*/
|
|
88
206
|
export const DEFAULT_ICONS = Object.freeze({
|
|
207
|
+
affiliation: { style: "solid", icon: "certificate", label: "affiliation" },
|
|
89
208
|
star: { style: "solid", icon: "star", label: "star" },
|
|
90
209
|
"star-outline": { style: "regular", icon: "star", label: "hollow star" },
|
|
91
210
|
diamond: { style: "solid", icon: "diamond", label: "diamond" },
|
|
92
211
|
edit: { style: "solid", icon: "pen-to-square", label: "edit" },
|
|
93
|
-
delete: { style: "solid", icon: "trash
|
|
212
|
+
delete: { style: "solid", icon: "trash", label: "delete" },
|
|
94
213
|
add: { style: "solid", icon: "plus", label: "add" },
|
|
95
214
|
remove: { style: "solid", icon: "xmark", label: "remove" },
|
|
96
215
|
"not-applicable": { style: "solid", icon: "xmark", label: "not applicable" },
|
|
216
|
+
close: { style: "solid", icon: "xmark", label: "close" },
|
|
97
217
|
menu: { style: "solid", icon: "ellipsis-vertical", label: "actions menu" },
|
|
218
|
+
run: { style: "solid", icon: "play", label: "run this action" },
|
|
98
219
|
expand: { style: "solid", icon: "caret-right", label: "expand" },
|
|
99
220
|
shield: { style: "solid", icon: "shield-halved", label: "armour" },
|
|
100
221
|
compass: { style: "solid", icon: "compass", label: "guided tour" },
|
|
@@ -113,9 +234,24 @@ export const DEFAULT_ICONS = Object.freeze({
|
|
|
113
234
|
* Not `:name[content]`. That is remark-directive syntax, and this toolchain
|
|
114
235
|
* parses with markdown-it; a directive would render as its own literal text.
|
|
115
236
|
*
|
|
237
|
+
* An optional trailing brace carries **attributes**:
|
|
238
|
+
*
|
|
239
|
+
* :icon-affiliation:{size: 2x}
|
|
240
|
+
*
|
|
241
|
+
* `key: value` pairs, comma-separated, in the shape `markdown-it-attrs` and
|
|
242
|
+
* remark-directive already use — so it is a convention a reader may recognise
|
|
243
|
+
* rather than one this module invented. Attributes rather than a bare value
|
|
244
|
+
* because `size` is merely the first one anybody needed: a fixed-width flag, a
|
|
245
|
+
* rotation, a title override are the same shape of thing, and a syntax that
|
|
246
|
+
* could only ever express size would have to be replaced to gain any of them.
|
|
247
|
+
*
|
|
248
|
+
* One inline rule consumes the token **and** its brace, so there is no state in
|
|
249
|
+
* which the icon resolves and the brace is left stranded on the page. An
|
|
250
|
+
* unhandled token degrades whole, exactly as the bare form does.
|
|
251
|
+
*
|
|
116
252
|
* @type {RegExp}
|
|
117
253
|
*/
|
|
118
|
-
export const ICON_PATTERN = /:icon-([a-z0-9]+(?:-[a-z0-9]+)*)
|
|
254
|
+
export const ICON_PATTERN = /:icon-([a-z0-9]+(?:-[a-z0-9]+)*):(?:\{([^}]*)\})?/g;
|
|
119
255
|
|
|
120
256
|
/**
|
|
121
257
|
* Look one name up.
|
|
@@ -144,11 +280,11 @@ const attr = (value) =>
|
|
|
144
280
|
* @param {{style: string, icon: string, label: string}} entry - A registry entry.
|
|
145
281
|
* @returns {string} An `<i>` element.
|
|
146
282
|
*/
|
|
147
|
-
export function iconHtml(entry) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
)
|
|
283
|
+
export function iconHtml(entry, attrs = {}) {
|
|
284
|
+
const classes = [`fa-${attr(entry.style)}`, `fa-${attr(entry.icon)}`];
|
|
285
|
+
const sized = attrs.size ? ICON_SIZES[attrs.size] : undefined;
|
|
286
|
+
if (sized) classes.push(sized.class);
|
|
287
|
+
return `<i class="${classes.join(" ")}" role="img" aria-label="${attr(entry.label)}"></i>`;
|
|
152
288
|
}
|
|
153
289
|
|
|
154
290
|
/**
|
|
@@ -160,7 +296,8 @@ export function iconHtml(entry) {
|
|
|
160
296
|
export function iconsIn(text) {
|
|
161
297
|
const out = [];
|
|
162
298
|
for (const m of text.matchAll(ICON_PATTERN)) {
|
|
163
|
-
|
|
299
|
+
const { attrs, problems } = parseIconAttributes(m[2]);
|
|
300
|
+
out.push({ name: m[1], index: m.index ?? 0, raw: m[0], attrs, problems });
|
|
164
301
|
}
|
|
165
302
|
return out;
|
|
166
303
|
}
|
|
@@ -176,28 +313,38 @@ export function iconsIn(text) {
|
|
|
176
313
|
* @param {string} file - Path to report.
|
|
177
314
|
* @param {Record<string, object>} [registry] - Defaults to {@link DEFAULT_ICONS}.
|
|
178
315
|
* @returns {Array<{file: string, line: number, column: number,
|
|
179
|
-
* severity: "
|
|
316
|
+
* severity: "warning", message: string}>} The unknown names.
|
|
180
317
|
*/
|
|
181
318
|
export function lintIcons(text, file, registry = DEFAULT_ICONS) {
|
|
182
319
|
const findings = [];
|
|
183
|
-
for (const { name, index, raw } of iconsIn(text)) {
|
|
184
|
-
if (resolveIcon(name, registry)) continue;
|
|
320
|
+
for (const { name, index, raw, problems } of iconsIn(text)) {
|
|
185
321
|
const before = text.slice(0, index);
|
|
186
322
|
const line = before.split("\n").length;
|
|
187
323
|
const column = index - (before.lastIndexOf("\n") + 1) + 1;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
324
|
+
const at = { file, line, column, severity: /** @type {const} */ ("warning") };
|
|
325
|
+
|
|
326
|
+
if (!resolveIcon(name, registry)) {
|
|
327
|
+
// Nearest declared name, when there is an obvious one: a typo is the
|
|
328
|
+
// common case and the registry is short enough to say what was meant.
|
|
329
|
+
const suggestion = nearestName(name, Object.keys(registry));
|
|
330
|
+
findings.push({
|
|
331
|
+
...at,
|
|
332
|
+
message:
|
|
333
|
+
`\`${raw}\` names an icon the registry does not declare` +
|
|
334
|
+
(suggestion ? `; did you mean \`:icon-${suggestion}:\`?` : "") +
|
|
335
|
+
` — an undeclared name renders as its own literal text`,
|
|
336
|
+
});
|
|
337
|
+
// The name is the bigger fault; reporting its attributes as well
|
|
338
|
+
// would be two findings for one token the author has to rewrite.
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// A bad attribute on a good name is its own finding: the icon renders,
|
|
343
|
+
// and renders differently from what was asked for, which is the case
|
|
344
|
+
// nobody notices without being told.
|
|
345
|
+
for (const problem of problems) {
|
|
346
|
+
findings.push({ ...at, message: `\`${raw}\`: ${problem}` });
|
|
347
|
+
}
|
|
201
348
|
}
|
|
202
349
|
return findings;
|
|
203
350
|
}
|
|
@@ -250,7 +397,7 @@ function editDistance(a, b) {
|
|
|
250
397
|
*
|
|
251
398
|
* @param {Record<string, object>} registry - A package's icon table.
|
|
252
399
|
* @param {string} [where="icons"] - Where to say the fault is.
|
|
253
|
-
* @returns {Array<{severity: "
|
|
400
|
+
* @returns {Array<{severity: "warning", message: string}>} What is wrong with it.
|
|
254
401
|
*/
|
|
255
402
|
export function checkIconRegistry(registry, where = "icons") {
|
|
256
403
|
const findings = [];
|
|
@@ -258,14 +405,14 @@ export function checkIconRegistry(registry, where = "icons") {
|
|
|
258
405
|
const at = `\`${where}.${name}\``;
|
|
259
406
|
if (!entry || typeof entry !== "object") {
|
|
260
407
|
findings.push({
|
|
261
|
-
severity: /** @type {const} */ ("
|
|
408
|
+
severity: /** @type {const} */ ("warning"),
|
|
262
409
|
message: `${at} is not an icon entry — it takes \`style\`, \`icon\` and \`label\``,
|
|
263
410
|
});
|
|
264
411
|
continue;
|
|
265
412
|
}
|
|
266
413
|
if (!ICON_STYLES.includes(entry.style)) {
|
|
267
414
|
findings.push({
|
|
268
|
-
severity: /** @type {const} */ ("
|
|
415
|
+
severity: /** @type {const} */ ("warning"),
|
|
269
416
|
message:
|
|
270
417
|
`${at} names style \`${entry.style}\`, and Font Awesome Free ships ` +
|
|
271
418
|
`only ${ICON_STYLES.join(", ")} — a glyph in any other style is ` +
|
|
@@ -274,13 +421,13 @@ export function checkIconRegistry(registry, where = "icons") {
|
|
|
274
421
|
}
|
|
275
422
|
if (typeof entry.icon !== "string" || !entry.icon) {
|
|
276
423
|
findings.push({
|
|
277
|
-
severity: /** @type {const} */ ("
|
|
424
|
+
severity: /** @type {const} */ ("warning"),
|
|
278
425
|
message: `${at} declares no \`icon\`, so nothing names the glyph to draw`,
|
|
279
426
|
});
|
|
280
427
|
}
|
|
281
428
|
if (typeof entry.label !== "string" || !entry.label) {
|
|
282
429
|
findings.push({
|
|
283
|
-
severity: /** @type {const} */ ("
|
|
430
|
+
severity: /** @type {const} */ ("warning"),
|
|
284
431
|
message:
|
|
285
432
|
`${at} declares no \`label\`, and an icon with no accessible name ` +
|
|
286
433
|
`is read aloud as a gap in the sentence`,
|
|
@@ -302,7 +449,7 @@ export function checkIconRegistry(registry, where = "icons") {
|
|
|
302
449
|
* @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore.
|
|
303
450
|
* @param {Record<string, object>} [opts.registry] - The package's icon table.
|
|
304
451
|
* @returns {{findings: Array<{file: string, line: number, column: number,
|
|
305
|
-
* severity: "
|
|
452
|
+
* severity: "warning", message: string}>, files: number}} What it found.
|
|
306
453
|
*/
|
|
307
454
|
export function lintContentIcons(contentBase, { skipDirectories = [], registry } = {}) {
|
|
308
455
|
const skip = new Set(skipDirectories);
|
|
@@ -355,7 +502,7 @@ export function iconPlugin(registry = DEFAULT_ICONS) {
|
|
|
355
502
|
return (md) => {
|
|
356
503
|
/** @type {any} */ (md).inline.ruler.before("emphasis", "heroiclands_icon", iconRule);
|
|
357
504
|
/** @type {any} */ (md).renderer.rules.heroiclands_icon = (tokens, idx) =>
|
|
358
|
-
iconHtml(tokens[idx].meta.entry);
|
|
505
|
+
iconHtml(tokens[idx].meta.entry, tokens[idx].meta.attrs);
|
|
359
506
|
|
|
360
507
|
/**
|
|
361
508
|
* @param {any} state - markdown-it inline state.
|
|
@@ -367,7 +514,7 @@ export function iconPlugin(registry = DEFAULT_ICONS) {
|
|
|
367
514
|
if (state.src.charCodeAt(start) !== 0x3a /* : */) return false;
|
|
368
515
|
// Anchored at the cursor, so the scan is O(token) rather than a
|
|
369
516
|
// search of the remaining source at every colon in the paragraph.
|
|
370
|
-
const re = /^:icon-([a-z0-9]+(?:-[a-z0-9]+)*)
|
|
517
|
+
const re = /^:icon-([a-z0-9]+(?:-[a-z0-9]+)*):(?:\{([^}]*)\})?/;
|
|
371
518
|
const m = re.exec(state.src.slice(start));
|
|
372
519
|
if (!m) return false;
|
|
373
520
|
|
|
@@ -376,9 +523,14 @@ export function iconPlugin(registry = DEFAULT_ICONS) {
|
|
|
376
523
|
// unrecognised name visible on the page instead of vanishing.
|
|
377
524
|
if (!entry) return false;
|
|
378
525
|
|
|
526
|
+
// An attribute that cannot be honoured is reported by the lint, not
|
|
527
|
+
// enforced here: refusing to render would hide a good icon over a
|
|
528
|
+
// bad size, and the page is the place the author is looking.
|
|
529
|
+
const { attrs } = parseIconAttributes(m[2]);
|
|
530
|
+
|
|
379
531
|
if (!silent) {
|
|
380
532
|
const token = state.push("heroiclands_icon", "", 0);
|
|
381
|
-
token.meta = { name: m[1], entry };
|
|
533
|
+
token.meta = { name: m[1], entry, attrs };
|
|
382
534
|
token.markup = m[0];
|
|
383
535
|
}
|
|
384
536
|
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.0",
|
|
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,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the brace of an icon token.
|
|
3
|
+
*
|
|
4
|
+
* Values are validated against {@link ICON_ATTRIBUTES} here rather than at the
|
|
5
|
+
* point of rendering, so a mistake is one finding with a position rather than a
|
|
6
|
+
* silently different page.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} [raw] - The text between the braces, without them.
|
|
9
|
+
* @returns {{attrs: Record<string, string>, problems: string[]}} What was
|
|
10
|
+
* written, and what cannot be honoured.
|
|
11
|
+
*/
|
|
12
|
+
export function parseIconAttributes(raw?: string): {
|
|
13
|
+
attrs: Record<string, string>;
|
|
14
|
+
problems: string[];
|
|
15
|
+
};
|
|
1
16
|
/**
|
|
2
17
|
* Look one name up.
|
|
3
18
|
*
|
|
@@ -26,7 +41,7 @@ export function iconHtml(entry: {
|
|
|
26
41
|
style: string;
|
|
27
42
|
icon: string;
|
|
28
43
|
label: string;
|
|
29
|
-
}): string;
|
|
44
|
+
}, attrs?: {}): string;
|
|
30
45
|
/**
|
|
31
46
|
* Every icon a string names, in the order written.
|
|
32
47
|
*
|
|
@@ -49,13 +64,13 @@ export function iconsIn(text: string): Array<{
|
|
|
49
64
|
* @param {string} file - Path to report.
|
|
50
65
|
* @param {Record<string, object>} [registry] - Defaults to {@link DEFAULT_ICONS}.
|
|
51
66
|
* @returns {Array<{file: string, line: number, column: number,
|
|
52
|
-
* severity: "
|
|
67
|
+
* severity: "warning", message: string}>} The unknown names.
|
|
53
68
|
*/
|
|
54
69
|
export function lintIcons(text: string, file: string, registry?: Record<string, object>): Array<{
|
|
55
70
|
file: string;
|
|
56
71
|
line: number;
|
|
57
72
|
column: number;
|
|
58
|
-
severity: "
|
|
73
|
+
severity: "warning";
|
|
59
74
|
message: string;
|
|
60
75
|
}>;
|
|
61
76
|
/**
|
|
@@ -63,10 +78,10 @@ export function lintIcons(text: string, file: string, registry?: Record<string,
|
|
|
63
78
|
*
|
|
64
79
|
* @param {Record<string, object>} registry - A package's icon table.
|
|
65
80
|
* @param {string} [where="icons"] - Where to say the fault is.
|
|
66
|
-
* @returns {Array<{severity: "
|
|
81
|
+
* @returns {Array<{severity: "warning", message: string}>} What is wrong with it.
|
|
67
82
|
*/
|
|
68
83
|
export function checkIconRegistry(registry: Record<string, object>, where?: string): Array<{
|
|
69
|
-
severity: "
|
|
84
|
+
severity: "warning";
|
|
70
85
|
message: string;
|
|
71
86
|
}>;
|
|
72
87
|
/**
|
|
@@ -81,7 +96,7 @@ export function checkIconRegistry(registry: Record<string, object>, where?: stri
|
|
|
81
96
|
* @param {readonly string[]} [opts.skipDirectories] - Directory names to ignore.
|
|
82
97
|
* @param {Record<string, object>} [opts.registry] - The package's icon table.
|
|
83
98
|
* @returns {{findings: Array<{file: string, line: number, column: number,
|
|
84
|
-
* severity: "
|
|
99
|
+
* severity: "warning", message: string}>, files: number}} What it found.
|
|
85
100
|
*/
|
|
86
101
|
export function lintContentIcons(contentBase: string, { skipDirectories, registry }?: {
|
|
87
102
|
skipDirectories?: readonly string[] | undefined;
|
|
@@ -91,7 +106,7 @@ export function lintContentIcons(contentBase: string, { skipDirectories, registr
|
|
|
91
106
|
file: string;
|
|
92
107
|
line: number;
|
|
93
108
|
column: number;
|
|
94
|
-
severity: "
|
|
109
|
+
severity: "warning";
|
|
95
110
|
message: string;
|
|
96
111
|
}>;
|
|
97
112
|
files: number;
|
|
@@ -117,15 +132,65 @@ export function iconPlugin(registry?: Record<string, object>): (md: object) => v
|
|
|
117
132
|
* @type {readonly string[]}
|
|
118
133
|
*/
|
|
119
134
|
export const ICON_STYLES: readonly string[];
|
|
135
|
+
/**
|
|
136
|
+
* The sizes a note may ask for, and what each means on a page.
|
|
137
|
+
*
|
|
138
|
+
* **A closed set, because size is content here rather than styling.** The icon
|
|
139
|
+
* legend exists to let a reader tell one glyph from another, and several of
|
|
140
|
+
* them genuinely cannot be told apart at the size of running text — so the
|
|
141
|
+
* enlargement is part of what that page *says*, not decoration applied to it.
|
|
142
|
+
* That is the one case worth a size at all.
|
|
143
|
+
*
|
|
144
|
+
* Closed rather than free-form for the usual reason: `font-size: 2em` in a note
|
|
145
|
+
* is CSS, which reaches two of the three surfaces and means nothing to the
|
|
146
|
+
* third. A name means the same thing everywhere, and the multiples below are
|
|
147
|
+
* Font Awesome's own, so the web class and the PDF scale cannot drift.
|
|
148
|
+
*
|
|
149
|
+
* @type {Readonly<Record<string, {class: string, scale: number}>>}
|
|
150
|
+
*/
|
|
151
|
+
export const ICON_SIZES: Readonly<Record<string, {
|
|
152
|
+
class: string;
|
|
153
|
+
scale: number;
|
|
154
|
+
}>>;
|
|
155
|
+
/**
|
|
156
|
+
* The attribute names a note may write, and how each is validated.
|
|
157
|
+
*
|
|
158
|
+
* One entry today. It is a table rather than an `if (key === "size")` because
|
|
159
|
+
* the next attribute — a fixed-width flag, a rotation, a title override — should
|
|
160
|
+
* cost a line here and nothing else, and because an unknown key has to be
|
|
161
|
+
* *reported*: silently ignoring `{sixe: 2x}` would leave the author looking at a
|
|
162
|
+
* page that is not what they asked for, with nothing to say why.
|
|
163
|
+
*
|
|
164
|
+
* @type {Readonly<Record<string, {values: readonly string[], describe: string}>>}
|
|
165
|
+
*/
|
|
166
|
+
export const ICON_ATTRIBUTES: Readonly<Record<string, {
|
|
167
|
+
values: readonly string[];
|
|
168
|
+
describe: string;
|
|
169
|
+
}>>;
|
|
120
170
|
/**
|
|
121
171
|
* The icons the user guide already depicts, under the names it should call them.
|
|
122
172
|
*
|
|
123
|
-
* Each entry
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* `trash-can`
|
|
128
|
-
*
|
|
173
|
+
* Each entry is read off the interface it describes rather than invented, and
|
|
174
|
+
* that is meant literally — the table was checked against the system's own
|
|
175
|
+
* templates, which is how `delete` came to be `fa-trash` rather than the
|
|
176
|
+
* `fa-trash-can` first written here. The sheets draw `fa-trash` twenty-five
|
|
177
|
+
* times and `fa-trash-can` never, so the first spelling would have printed an
|
|
178
|
+
* icon the reader has never seen on screen. A registry that is not checked
|
|
179
|
+
* against the interface is just a second place to be wrong.
|
|
180
|
+
*
|
|
181
|
+
* The names are what a *writer* would reach for — `delete`, not `trash` —
|
|
182
|
+
* because the writer is the one typing them; the Font Awesome spelling is this
|
|
183
|
+
* table's business, not theirs.
|
|
184
|
+
*
|
|
185
|
+
* **Three entries share `xmark`, and that is the point of naming rather than
|
|
186
|
+
* drawing.** A `✕` in the guide means "not applicable" in a Healing Rate
|
|
187
|
+
* column, "remove this row" on a control, and "close" on a dialog's corner. One
|
|
188
|
+
* glyph, three sentences, three different things for a reader who cannot see
|
|
189
|
+
* it — so they are three names with three labels, and the fact that Font
|
|
190
|
+
* Awesome happens to draw them identically stays in this table.
|
|
191
|
+
*
|
|
192
|
+
* `run` and `expand` are likewise distinct: `▶` runs an action, and its label
|
|
193
|
+
* should say so. `fa-play` is what the sheets use for it.
|
|
129
194
|
*
|
|
130
195
|
* @type {Readonly<Record<string, {style: string, icon: string, label: string}>>}
|
|
131
196
|
*/
|
|
@@ -146,6 +211,21 @@ export const DEFAULT_ICONS: Readonly<Record<string, {
|
|
|
146
211
|
* Not `:name[content]`. That is remark-directive syntax, and this toolchain
|
|
147
212
|
* parses with markdown-it; a directive would render as its own literal text.
|
|
148
213
|
*
|
|
214
|
+
* An optional trailing brace carries **attributes**:
|
|
215
|
+
*
|
|
216
|
+
* :icon-affiliation:{size: 2x}
|
|
217
|
+
*
|
|
218
|
+
* `key: value` pairs, comma-separated, in the shape `markdown-it-attrs` and
|
|
219
|
+
* remark-directive already use — so it is a convention a reader may recognise
|
|
220
|
+
* rather than one this module invented. Attributes rather than a bare value
|
|
221
|
+
* because `size` is merely the first one anybody needed: a fixed-width flag, a
|
|
222
|
+
* rotation, a title override are the same shape of thing, and a syntax that
|
|
223
|
+
* could only ever express size would have to be replaced to gain any of them.
|
|
224
|
+
*
|
|
225
|
+
* One inline rule consumes the token **and** its brace, so there is no state in
|
|
226
|
+
* which the icon resolves and the brace is left stranded on the page. An
|
|
227
|
+
* unhandled token degrades whole, exactly as the bare form does.
|
|
228
|
+
*
|
|
149
229
|
* @type {RegExp}
|
|
150
230
|
*/
|
|
151
231
|
export const ICON_PATTERN: RegExp;
|