@keyline-icons/cli 0.2.0 → 0.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/icons.json +1 -1
- package/package.json +1 -1
- package/src/index.mjs +107 -20
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -22,9 +22,11 @@ import { existsSync } from "node:fs"
|
|
|
22
22
|
const data = JSON.parse(
|
|
23
23
|
await readFile(new URL("../icons.json", import.meta.url), "utf8")
|
|
24
24
|
)
|
|
25
|
-
|
|
25
|
+
// `corners` defaults for a bundle generated before the axis existed, so an
|
|
26
|
+
// older icons.json still runs rather than reading `undefined.includes`.
|
|
27
|
+
const { icons, styles, corners = ["regular"], keywords = {} } = data
|
|
26
28
|
const NAMES = Object.keys(icons)
|
|
27
|
-
const VERSION = "0.
|
|
29
|
+
const VERSION = "0.4.0"
|
|
28
30
|
|
|
29
31
|
/** Colour only when a human is looking. Piped output stays clean. */
|
|
30
32
|
const tty = process.stdout.isTTY
|
|
@@ -49,7 +51,7 @@ function die(msg) {
|
|
|
49
51
|
* positional, which is what lets `add a b c --style fill` work in any order.
|
|
50
52
|
*/
|
|
51
53
|
function parseArgs(argv) {
|
|
52
|
-
const alias = { s: "style", o: "out", l: "limit", h: "help", v: "version" }
|
|
54
|
+
const alias = { s: "style", c: "corners", o: "out", l: "limit", h: "help", v: "version" }
|
|
53
55
|
const flags = {}
|
|
54
56
|
const positional = []
|
|
55
57
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -89,10 +91,25 @@ const styleOf = (flags) => {
|
|
|
89
91
|
return s
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
const cornersOf = (flags) => {
|
|
95
|
+
const k = flags.corners ?? "regular"
|
|
96
|
+
if (!corners.includes(k))
|
|
97
|
+
die(`unknown corner treatment \`${k}\`. One of: ${corners.join(", ")}`)
|
|
98
|
+
return k
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
/* ------------------------------------------------------------------ icons */
|
|
93
102
|
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
/**
|
|
104
|
+
* A drawing in one style and one treatment.
|
|
105
|
+
*
|
|
106
|
+
* The rounded half sits at `icons[name][style]` where it has always sat, and
|
|
107
|
+
* the sharp half one level down under `sharp`, so a project pinned to an older
|
|
108
|
+
* bundle reads the same shape it read before.
|
|
109
|
+
*/
|
|
110
|
+
function svgFor(name, style, corners = "regular") {
|
|
111
|
+
const art =
|
|
112
|
+
corners === "sharp" ? icons[name]?.sharp?.[style] : icons[name]?.[style]
|
|
96
113
|
if (!art) return null
|
|
97
114
|
const attrs = Object.entries(art.root)
|
|
98
115
|
.map(([k, v]) => ` ${k}="${v}"`)
|
|
@@ -142,6 +159,54 @@ function keywordsFor(name) {
|
|
|
142
159
|
|
|
143
160
|
const haystackFor = (name) => `${name} ${keywordsFor(name).join(" ")}`
|
|
144
161
|
|
|
162
|
+
/**
|
|
163
|
+
* A word reduced to its singular, so a plural finds the family.
|
|
164
|
+
*
|
|
165
|
+
* Every name in the set is singular: `arrow-down`, `file`, `bar-chart`. Every
|
|
166
|
+
* heading on the site's category rail is plural. So "Arrows" sat on the screen
|
|
167
|
+
* while typing `arrows` returned one drawing out of 585, `git-compare-arrows`,
|
|
168
|
+
* the only name carrying the letter. `charts`, `stars` and `bells` returned
|
|
169
|
+
* nothing at all. The plural is what someone reads off our own sidebar and
|
|
170
|
+
* what every other set answers, and here it was the one word that could not
|
|
171
|
+
* work.
|
|
172
|
+
*
|
|
173
|
+
* Applied to both sides, the query word and each word of the haystack, which
|
|
174
|
+
* also carries the other direction: `chevron` reaches `chevrons-left`, which
|
|
175
|
+
* whole words alone could not.
|
|
176
|
+
*
|
|
177
|
+
* Deliberately blunt: no dictionary, no suffix ladder, one `s`. The guards are
|
|
178
|
+
* the whole design, and they are read off this vocabulary rather than guessed.
|
|
179
|
+
* Every `-ss`, `-us` and `-is` word in it is already a word: `progress`,
|
|
180
|
+
* `compass`, `plus`, `minus`, `status`, `axis`. Past those, `lens`, `atlas`
|
|
181
|
+
* and `sideways` are the only three that end in `s` without being a plural.
|
|
182
|
+
* `lens` is the one that mattered. Cut to `len` it lands inside `calendar` and
|
|
183
|
+
* `silence`, and the word someone types for the camera came back with
|
|
184
|
+
* seventeen drawings that are not it.
|
|
185
|
+
*
|
|
186
|
+
* `arrows` is in the list for the opposite reason. The rule would not damage
|
|
187
|
+
* it, it would erase it: the plural asks for the drawings carrying more than
|
|
188
|
+
* one arrowhead, `fullscreen`, `chevrons-up-down`, `refresh-cw`, and folding it
|
|
189
|
+
* into `arrow` answers with the ninety-nine single arrows instead. The word is
|
|
190
|
+
* a keyword on those drawings, in lib/icon-aliases.json, and this is what keeps
|
|
191
|
+
* the two questions apart.
|
|
192
|
+
*
|
|
193
|
+
* Same rule as the site, the MCP server and the Figma plugin, checked by
|
|
194
|
+
* pipeline/check-search.mjs.
|
|
195
|
+
*/
|
|
196
|
+
const singular = (w) =>
|
|
197
|
+
w.length < 4 ||
|
|
198
|
+
!w.endsWith("s") ||
|
|
199
|
+
/(ss|us|is|arrows|lens|atlas|sideways)$/.test(w)
|
|
200
|
+
? w
|
|
201
|
+
: w.endsWith("ies")
|
|
202
|
+
? `${w.slice(0, -3)}y`
|
|
203
|
+
: /(ch|sh|x|z)es$/.test(w)
|
|
204
|
+
? w.slice(0, -2)
|
|
205
|
+
: w.slice(0, -1)
|
|
206
|
+
|
|
207
|
+
/** Every word in `hay` singular, delimiters kept so `wholeWord` still works. */
|
|
208
|
+
const stemmed = (hay) => hay.replace(/[a-z0-9]+/g, singular)
|
|
209
|
+
|
|
145
210
|
/**
|
|
146
211
|
* What another set calls a drawing here: `message-square` for `message`,
|
|
147
212
|
* `arrow-big-up` for `arrow-up`, `loader-2` for `loader`.
|
|
@@ -181,18 +246,24 @@ const wholeWord = (hay, needle) => {
|
|
|
181
246
|
return false
|
|
182
247
|
}
|
|
183
248
|
|
|
184
|
-
|
|
249
|
+
/** Whether one haystack answers every word of a query, whole words only. */
|
|
250
|
+
const answers = (hay, words) =>
|
|
251
|
+
words.every(
|
|
252
|
+
(w) => wholeWord(hay, w) || wholeWord(stemmed(hay), singular(w))
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
function search(query, style, limit, corners = "regular") {
|
|
185
256
|
const q = query.toLowerCase().trim()
|
|
186
257
|
const words = wordsOf(query)
|
|
187
258
|
if (!words.length) return []
|
|
188
259
|
|
|
189
260
|
return NAMES.filter(
|
|
190
261
|
(n) =>
|
|
191
|
-
(!style ||
|
|
262
|
+
(!style || svgFor(n, style, corners)) &&
|
|
192
263
|
(foreign[q] === n ||
|
|
193
264
|
wholeWord(n, q) ||
|
|
194
|
-
|
|
195
|
-
|
|
265
|
+
answers(n, words) ||
|
|
266
|
+
answers(haystackFor(n), words))
|
|
196
267
|
)
|
|
197
268
|
.map((n) => {
|
|
198
269
|
const rank =
|
|
@@ -204,7 +275,7 @@ function search(query, style, limit) {
|
|
|
204
275
|
? 2
|
|
205
276
|
: wholeWord(n, q)
|
|
206
277
|
? 3
|
|
207
|
-
:
|
|
278
|
+
: answers(n, words)
|
|
208
279
|
? 4
|
|
209
280
|
: 5
|
|
210
281
|
return { n, rank }
|
|
@@ -255,17 +326,20 @@ function nearest(name) {
|
|
|
255
326
|
.map((h) => h.n)
|
|
256
327
|
}
|
|
257
328
|
|
|
258
|
-
function resolveOrDie(name, style) {
|
|
329
|
+
function resolveOrDie(name, style, corners = "regular") {
|
|
259
330
|
if (!icons[name]) {
|
|
260
331
|
const near = nearest(name)
|
|
261
332
|
die(
|
|
262
333
|
`no icon named \`${name}\`.${near.length ? ` Did you mean: ${near.join(", ")}?` : ""}`
|
|
263
334
|
)
|
|
264
335
|
}
|
|
265
|
-
const svg = svgFor(name, style)
|
|
336
|
+
const svg = svgFor(name, style, corners)
|
|
266
337
|
if (!svg) {
|
|
338
|
+
// `Object.keys` would list `sharp` here as though it were a fourth style,
|
|
339
|
+
// which is the axis leaking into a sentence about coverage.
|
|
340
|
+
const has = styles.filter((s) => svgFor(name, s, corners))
|
|
267
341
|
die(
|
|
268
|
-
`\`${name}\` has no ${style} style. It has: ${
|
|
342
|
+
`\`${name}\` has no ${style} style. It has: ${has.join(", ")}.\n` +
|
|
269
343
|
` Duotone and fill need a fillable region, and this glyph has none.`
|
|
270
344
|
)
|
|
271
345
|
}
|
|
@@ -288,13 +362,14 @@ ${bold("COMMANDS")}
|
|
|
288
362
|
|
|
289
363
|
${bold("OPTIONS")}
|
|
290
364
|
-s, --style <s> ${styles.join(" | ")} (default: stroke)
|
|
365
|
+
-c, --corners <c> ${corners.join(" | ")} (default: regular)
|
|
291
366
|
-o, --out <dir> Where \`add\` writes (default: ./icons)
|
|
292
367
|
-l, --limit <n> Max results for \`search\` (default: 25)
|
|
293
368
|
|
|
294
369
|
${bold("EXAMPLES")}
|
|
295
370
|
keyline-icons search arrow
|
|
296
371
|
keyline-icons search mail --style fill
|
|
297
|
-
keyline-icons show check > check.svg
|
|
372
|
+
keyline-icons show check --corners sharp > check-sharp.svg
|
|
298
373
|
keyline-icons add circle-arrow-down bell --out src/icons
|
|
299
374
|
keyline-icons list | grep chart
|
|
300
375
|
|
|
@@ -309,7 +384,8 @@ const commands = {
|
|
|
309
384
|
|
|
310
385
|
list({ flags }) {
|
|
311
386
|
const style = flags.style ? styleOf(flags) : null
|
|
312
|
-
const
|
|
387
|
+
const treatment = cornersOf(flags)
|
|
388
|
+
const names = style ? NAMES.filter((n) => svgFor(n, style, treatment)) : NAMES
|
|
313
389
|
for (const n of names) out(n)
|
|
314
390
|
note(dim(`\n${names.length} icons${style ? ` in ${style}` : ""}`))
|
|
315
391
|
},
|
|
@@ -329,7 +405,7 @@ const commands = {
|
|
|
329
405
|
is not a number at all earns the default. */
|
|
330
406
|
const raw = parseInt(flags.limit ?? "25", 10)
|
|
331
407
|
const limit = Math.min(Math.max(Number.isNaN(raw) ? 25 : raw, 1), 200)
|
|
332
|
-
const hits = search(query, style, limit)
|
|
408
|
+
const hits = search(query, style, limit, cornersOf(flags))
|
|
333
409
|
if (!hits.length) {
|
|
334
410
|
const near = nearest(query)
|
|
335
411
|
die(
|
|
@@ -337,7 +413,7 @@ const commands = {
|
|
|
337
413
|
)
|
|
338
414
|
}
|
|
339
415
|
for (const n of hits) {
|
|
340
|
-
const has = styles.filter((s) =>
|
|
416
|
+
const has = styles.filter((s) => svgFor(n, s, cornersOf(flags)))
|
|
341
417
|
out(`${n}${tty ? " " + dim(`[${has.join(", ")}]`) : ""}`)
|
|
342
418
|
}
|
|
343
419
|
note(dim(`\n${hits.length} shown${style ? ` in ${style}` : ""}`))
|
|
@@ -346,17 +422,27 @@ const commands = {
|
|
|
346
422
|
show({ flags, positional }) {
|
|
347
423
|
const [name] = positional
|
|
348
424
|
if (!name) die("show needs an icon name. Try: keyline-icons show check")
|
|
349
|
-
process.stdout.write(resolveOrDie(name, styleOf(flags)))
|
|
425
|
+
process.stdout.write(resolveOrDie(name, styleOf(flags), cornersOf(flags)))
|
|
350
426
|
},
|
|
351
427
|
|
|
352
428
|
async add({ flags, positional }) {
|
|
353
429
|
if (!positional.length) die("add needs at least one icon name.")
|
|
354
430
|
const style = styleOf(flags)
|
|
431
|
+
const treatment = cornersOf(flags)
|
|
355
432
|
const dir = resolve(flags.out === true || !flags.out ? "icons" : flags.out)
|
|
356
433
|
|
|
434
|
+
// The treatment rides in the filename, for the reason the style does not:
|
|
435
|
+
// two treatments of one drawing are the same name and the same style, so
|
|
436
|
+
// adding both would otherwise write one file twice and leave whichever ran
|
|
437
|
+
// last. Rounded keeps the bare name, which is what everyone means by it.
|
|
438
|
+
const suffix = treatment === "regular" ? "" : `-${treatment}`
|
|
439
|
+
|
|
357
440
|
// Every name is resolved before anything is written, so a typo in the
|
|
358
441
|
// third of five does not leave two files on disk and an error.
|
|
359
|
-
const files = positional.map((name) => [
|
|
442
|
+
const files = positional.map((name) => [
|
|
443
|
+
`${name}${suffix}`,
|
|
444
|
+
resolveOrDie(name, style, treatment),
|
|
445
|
+
])
|
|
360
446
|
|
|
361
447
|
await mkdir(dir, { recursive: true })
|
|
362
448
|
for (const [name, svg] of files) {
|
|
@@ -369,7 +455,8 @@ const commands = {
|
|
|
369
455
|
}
|
|
370
456
|
note(
|
|
371
457
|
dim(
|
|
372
|
-
`\n${files.length} ${
|
|
458
|
+
`\n${files.length} ${treatment === "sharp" ? "sharp " : ""}${style} ` +
|
|
459
|
+
`icon${files.length === 1 ? "" : "s"} to ${dir}`
|
|
373
460
|
)
|
|
374
461
|
)
|
|
375
462
|
},
|