@keyline-icons/cli 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyline-icons/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Search Keyline Icons and copy them into your project from the terminal.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
package/src/index.mjs CHANGED
@@ -26,7 +26,7 @@ const data = JSON.parse(
26
26
  // older icons.json still runs rather than reading `undefined.includes`.
27
27
  const { icons, styles, corners = ["regular"], keywords = {} } = data
28
28
  const NAMES = Object.keys(icons)
29
- const VERSION = "0.3.0"
29
+ const VERSION = "0.4.0"
30
30
 
31
31
  /** Colour only when a human is looking. Piped output stays clean. */
32
32
  const tty = process.stdout.isTTY
@@ -159,6 +159,54 @@ function keywordsFor(name) {
159
159
 
160
160
  const haystackFor = (name) => `${name} ${keywordsFor(name).join(" ")}`
161
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
+
162
210
  /**
163
211
  * What another set calls a drawing here: `message-square` for `message`,
164
212
  * `arrow-big-up` for `arrow-up`, `loader-2` for `loader`.
@@ -198,6 +246,12 @@ const wholeWord = (hay, needle) => {
198
246
  return false
199
247
  }
200
248
 
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
+
201
255
  function search(query, style, limit, corners = "regular") {
202
256
  const q = query.toLowerCase().trim()
203
257
  const words = wordsOf(query)
@@ -208,8 +262,8 @@ function search(query, style, limit, corners = "regular") {
208
262
  (!style || svgFor(n, style, corners)) &&
209
263
  (foreign[q] === n ||
210
264
  wholeWord(n, q) ||
211
- words.every((w) => wholeWord(n, w)) ||
212
- words.every((w) => wholeWord(haystackFor(n), w)))
265
+ answers(n, words) ||
266
+ answers(haystackFor(n), words))
213
267
  )
214
268
  .map((n) => {
215
269
  const rank =
@@ -221,7 +275,7 @@ function search(query, style, limit, corners = "regular") {
221
275
  ? 2
222
276
  : wholeWord(n, q)
223
277
  ? 3
224
- : words.every((w) => wholeWord(n, w))
278
+ : answers(n, words)
225
279
  ? 4
226
280
  : 5
227
281
  return { n, rank }