@keyline-icons/cli 0.1.0 → 0.1.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/icons.json +1 -1
- package/package.json +1 -1
- package/src/index.mjs +62 -10
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -22,7 +22,7 @@ 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
|
-
const { icons, styles } = data
|
|
25
|
+
const { icons, styles, keywords = {} } = data
|
|
26
26
|
const NAMES = Object.keys(icons)
|
|
27
27
|
const VERSION = "0.1.0"
|
|
28
28
|
|
|
@@ -66,7 +66,15 @@ function parseArgs(argv) {
|
|
|
66
66
|
const key = alias[k] ?? k
|
|
67
67
|
if (inline !== undefined) flags[key] = inline
|
|
68
68
|
else if (key === "help" || key === "version") flags[key] = true
|
|
69
|
-
else if (
|
|
69
|
+
else if (
|
|
70
|
+
argv[i + 1] &&
|
|
71
|
+
// A bare negative number is a value, not a flag. Without this `-l -3`
|
|
72
|
+
// read `-3` as its own short flag, left `limit` set to `true`, and fell
|
|
73
|
+
// back to the default, so it showed 25 results while `--limit=-3`
|
|
74
|
+
// clamped to 1. No name in the set starts with a digit, so nothing else
|
|
75
|
+
// can be caught by it.
|
|
76
|
+
(!argv[i + 1].startsWith("-") || /^-\d+(\.\d+)?$/.test(argv[i + 1]))
|
|
77
|
+
)
|
|
70
78
|
flags[key] = argv[++i]
|
|
71
79
|
else flags[key] = true
|
|
72
80
|
} else positional.push(a)
|
|
@@ -94,7 +102,8 @@ function svgFor(name, style) {
|
|
|
94
102
|
|
|
95
103
|
/** Kept in step with `@keyline-icons/mcp`, which carries the reasoning. */
|
|
96
104
|
function wordsOf(query) {
|
|
97
|
-
const identifier =
|
|
105
|
+
const identifier =
|
|
106
|
+
/[a-z][A-Z]/.test(query) || /^[A-Z][A-Za-z]*\d+$/.test(query)
|
|
98
107
|
const split = identifier
|
|
99
108
|
? query
|
|
100
109
|
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
@@ -112,6 +121,27 @@ function wordsOf(query) {
|
|
|
112
121
|
* for "check-circle" when the icon is `circle-check`. Kept in step with the
|
|
113
122
|
* copy in `@keyline-icons/mcp`, which carries the full reasoning.
|
|
114
123
|
*/
|
|
124
|
+
/**
|
|
125
|
+
* The words a name can be found by: the name itself, plus whatever is written
|
|
126
|
+
* about it.
|
|
127
|
+
*
|
|
128
|
+
* Keywords are keyed by base name, because one component set in Figma covers
|
|
129
|
+
* all three containers, so `square-arrow-down` reads `arrow-down`'s words. The
|
|
130
|
+
* prefix only counts when that base exists: `circle-half` is a shape in its own
|
|
131
|
+
* right and there is no `half` for it to contain.
|
|
132
|
+
*
|
|
133
|
+
* Searched as one string rather than as a separate tier, which is what lets a
|
|
134
|
+
* query take a word from each: "help circle" finds `circle-question` because
|
|
135
|
+
* `circle` is in the name and `help` is in the words. Matching the whole query
|
|
136
|
+
* against one keyword, which is what this used to do, could never do that.
|
|
137
|
+
*/
|
|
138
|
+
function keywordsFor(name) {
|
|
139
|
+
const m = /^(square|circle)-(.+)$/.exec(name)
|
|
140
|
+
return keywords[m && icons[m[2]] ? m[2] : name] || []
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const haystackFor = (name) => `${name} ${keywordsFor(name).join(" ")}`
|
|
144
|
+
|
|
115
145
|
function search(query, style, limit) {
|
|
116
146
|
const q = query.toLowerCase().trim()
|
|
117
147
|
const words = wordsOf(query)
|
|
@@ -120,13 +150,17 @@ function search(query, style, limit) {
|
|
|
120
150
|
return NAMES.filter(
|
|
121
151
|
(n) =>
|
|
122
152
|
(!style || icons[n][style]) &&
|
|
123
|
-
(n.includes(q) ||
|
|
153
|
+
(n.includes(q) ||
|
|
154
|
+
words.every((w) => n.includes(w)) ||
|
|
155
|
+
words.every((w) => haystackFor(n).includes(w)))
|
|
124
156
|
)
|
|
125
157
|
.map((n) => {
|
|
126
158
|
const i = n.indexOf(q)
|
|
127
159
|
const rank =
|
|
128
160
|
i === -1
|
|
129
|
-
?
|
|
161
|
+
? words.every((w) => n.includes(w))
|
|
162
|
+
? 4
|
|
163
|
+
: 5
|
|
130
164
|
: n === q
|
|
131
165
|
? 0
|
|
132
166
|
: n.startsWith(q)
|
|
@@ -144,8 +178,16 @@ function search(query, style, limit) {
|
|
|
144
178
|
.map((h) => h.n)
|
|
145
179
|
}
|
|
146
180
|
|
|
181
|
+
/**
|
|
182
|
+
* The longest query worth answering. Kept in step with `@keyline-icons/mcp`,
|
|
183
|
+
* which carries the reasoning: `nearest` costs O(query x name) per name, so the
|
|
184
|
+
* caller sets the price, and a 200KB query took ten seconds here too.
|
|
185
|
+
*/
|
|
186
|
+
const MAX_QUERY = 200
|
|
187
|
+
|
|
147
188
|
/** Only reached when a name misses, so it can afford to walk every name. */
|
|
148
189
|
function nearest(name) {
|
|
190
|
+
if (name.length > MAX_QUERY) return []
|
|
149
191
|
const q = name.toLowerCase()
|
|
150
192
|
const hit = search(q, null, 5)
|
|
151
193
|
if (hit.length) return hit
|
|
@@ -236,11 +278,18 @@ const commands = {
|
|
|
236
278
|
search({ flags, positional }) {
|
|
237
279
|
const query = positional.join(" ")
|
|
238
280
|
if (!query) die("search needs a query. Try: keyline-icons search arrow")
|
|
281
|
+
if (query.length > MAX_QUERY)
|
|
282
|
+
die(
|
|
283
|
+
`that query is ${query.length} characters. The longest name in the set ` +
|
|
284
|
+
`is under 40, so anything over ${MAX_QUERY} is refused.`
|
|
285
|
+
)
|
|
239
286
|
const style = flags.style ? styleOf(flags) : null
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
287
|
+
/* `|| 25` turned every falsy limit into the default, which swallowed `0`
|
|
288
|
+
along with the genuinely unusable ones: `--limit=0` should clamp up to 1,
|
|
289
|
+
the way `--limit=-3` does, rather than quietly show 25. Only a value that
|
|
290
|
+
is not a number at all earns the default. */
|
|
291
|
+
const raw = parseInt(flags.limit ?? "25", 10)
|
|
292
|
+
const limit = Math.min(Math.max(Number.isNaN(raw) ? 25 : raw, 1), 200)
|
|
244
293
|
const hits = search(query, style, limit)
|
|
245
294
|
if (!hits.length) {
|
|
246
295
|
const near = nearest(query)
|
|
@@ -298,7 +347,10 @@ if (flags.version) {
|
|
|
298
347
|
}
|
|
299
348
|
if (flags.help || !positional.length) {
|
|
300
349
|
commands.help()
|
|
301
|
-
|
|
350
|
+
// Both print the same text and only one of them is a failure. Asking for help
|
|
351
|
+
// succeeded; running with no command did not. `--help` exiting non-zero stops
|
|
352
|
+
// any `set -e` script that offers its own usage line.
|
|
353
|
+
process.exit(flags.help ? 0 : 1)
|
|
302
354
|
}
|
|
303
355
|
|
|
304
356
|
const name = positional.shift()
|