@coolicons/cli 5.3.0 → 5.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/dist/search.js CHANGED
@@ -2,35 +2,65 @@ export function searchTerms(query) {
2
2
  return String(query).toLowerCase().trim().split(/\s+/).filter(Boolean);
3
3
  }
4
4
 
5
+ const NOT_PLURAL = new Set(["news"]);
6
+
7
+ function singulars(term) {
8
+ if (
9
+ term.length < 4 ||
10
+ !term.endsWith("s") ||
11
+ /(ss|us|is)$/.test(term) ||
12
+ NOT_PLURAL.has(term)
13
+ )
14
+ return [];
15
+ const forms = [term.slice(0, -1)];
16
+ if (/(ss|x|z|ch|sh)es$/.test(term)) forms.push(term.slice(0, -2));
17
+ if (term.endsWith("ies")) forms.push(`${term.slice(0, -3)}y`);
18
+ return forms;
19
+ }
20
+
21
+ function termScore(icon, term, tokens, groups) {
22
+ const exact = icon.keywords.indexOf(term);
23
+ const prefix = icon.keywords.findIndex((word) => word.startsWith(term));
24
+ if (icon.name === term) return 100;
25
+ if (icon.name.startsWith(term)) return 80;
26
+ if (tokens.includes(term)) return 70;
27
+ if (tokens.some((token) => token.startsWith(term))) return 55;
28
+ if ((term.length > 3 || term.includes("-")) && icon.name.includes(term))
29
+ return 40;
30
+ if (exact >= 0) return 35 - Math.min(exact, 9);
31
+ if (groups.includes(term)) return 30;
32
+ if (prefix >= 0) return 25 - Math.min(prefix, 9);
33
+ if (icon.category.toLowerCase().includes(term)) return 15;
34
+ return 0;
35
+ }
36
+
37
+ function singularScore(icon, form, tokens, groups) {
38
+ const exact = icon.keywords.indexOf(form);
39
+ if (icon.name === form) return 25;
40
+ if (tokens.includes(form)) return 23;
41
+ if (exact >= 0) return 20 - Math.min(exact, 9);
42
+ if (groups.includes(form)) return 10;
43
+ return 0;
44
+ }
45
+
5
46
  export function scoreIcon(icon, terms) {
6
47
  let total = 0;
7
48
  const tokens = icon.name.split("-");
8
49
  const groups = icon.category.toLowerCase().split(/[^a-z0-9]+/);
9
50
  for (const term of terms) {
10
- let best = 0;
11
- const exact = icon.keywords.indexOf(term);
12
- const prefix = icon.keywords.findIndex((word) => word.startsWith(term));
13
- if (icon.name === term) best = 100;
14
- else if (icon.name.startsWith(term)) best = 80;
15
- else if (tokens.includes(term)) best = 70;
16
- else if (tokens.some((token) => token.startsWith(term))) best = 55;
17
- else if (
18
- (term.length > 3 || term.includes("-")) &&
19
- icon.name.includes(term)
20
- )
21
- best = 40;
22
- else if (exact >= 0) best = 35 - Math.min(exact, 9);
23
- else if (groups.includes(term)) best = 30;
24
- else if (prefix >= 0) best = 25 - Math.min(prefix, 9);
25
- else if (icon.category.toLowerCase().includes(term)) best = 15;
51
+ const best = Math.max(
52
+ termScore(icon, term, tokens, groups),
53
+ ...singulars(term).map((form) =>
54
+ singularScore(icon, form, tokens, groups),
55
+ ),
56
+ );
26
57
  if (!best) return 0;
27
58
  total += best;
28
59
  }
29
60
  return total;
30
61
  }
31
62
 
32
- export function matchIcons(icons, query) {
33
- const terms = searchTerms(query);
63
+ function rankIcons(icons, terms) {
34
64
  return icons
35
65
  .map((icon) => ({ icon, rank: scoreIcon(icon, terms) }))
36
66
  .filter((entry) => entry.rank)
@@ -42,3 +72,93 @@ export function matchIcons(icons, query) {
42
72
  )
43
73
  .map((entry) => entry.icon);
44
74
  }
75
+
76
+ export function matchIcons(icons, query) {
77
+ return rankIcons(icons, searchTerms(query));
78
+ }
79
+
80
+ const vocabularies = new WeakMap();
81
+
82
+ function vocabulary(icons) {
83
+ const cached = vocabularies.get(icons);
84
+ if (cached) return cached;
85
+ const uses = new Map();
86
+ for (const icon of icons) {
87
+ const words = new Set([
88
+ icon.name,
89
+ ...icon.name.split("-"),
90
+ ...icon.keywords,
91
+ ...icon.category.toLowerCase().split(/[^a-z0-9]+/),
92
+ ]);
93
+ for (const word of words)
94
+ if (word) uses.set(word, (uses.get(word) ?? 0) + 1);
95
+ }
96
+ vocabularies.set(icons, uses);
97
+ return uses;
98
+ }
99
+
100
+ function differences(a, b) {
101
+ return [...a].flatMap((char, k) => (char === b[k] ? [] : [k]));
102
+ }
103
+
104
+ function swapped(a, b) {
105
+ if (a.length !== b.length) return false;
106
+ const diff = differences(a, b);
107
+ return (
108
+ diff.length === 2 &&
109
+ diff[1] === diff[0] + 1 &&
110
+ a[diff[0]] === b[diff[1]] &&
111
+ a[diff[1]] === b[diff[0]]
112
+ );
113
+ }
114
+
115
+ function missingOne(term, word) {
116
+ if (word.length !== term.length + 1) return false;
117
+ let k = 0;
118
+ while (k < term.length && term[k] === word[k]) k++;
119
+ return term.slice(k) === word.slice(k + 1);
120
+ }
121
+
122
+ function typo(term, word) {
123
+ if (term.length < 4) return false;
124
+ if (swapped(term, word) || missingOne(term, word)) return true;
125
+ return (
126
+ term.length >= 8 &&
127
+ word.length === term.length &&
128
+ differences(term, word).length === 1
129
+ );
130
+ }
131
+
132
+ function closestWord(words, term) {
133
+ let best = null;
134
+ let bestUses = 0;
135
+ for (const [word, uses] of words) {
136
+ if (!typo(term, word)) continue;
137
+ if (uses > bestUses || (uses === bestUses && word < best)) {
138
+ best = word;
139
+ bestUses = uses;
140
+ }
141
+ }
142
+ return best;
143
+ }
144
+
145
+ export function matchIconsCorrected(icons, query) {
146
+ const terms = searchTerms(query);
147
+ const found = rankIcons(icons, terms);
148
+ if (found.length)
149
+ return { icons: found, corrected: [], query: terms.join(" ") };
150
+ let words = null;
151
+ const corrected = [];
152
+ const fixed = terms.map((term) => {
153
+ if (icons.some((icon) => scoreIcon(icon, [term]))) return term;
154
+ words ??= vocabulary(icons);
155
+ const to = closestWord(words, term);
156
+ if (to && !corrected.some((entry) => entry.from === term))
157
+ corrected.push({ from: term, to });
158
+ return to ?? term;
159
+ });
160
+ const again = corrected.length ? rankIcons(icons, fixed) : [];
161
+ return again.length
162
+ ? { icons: again, corrected, query: fixed.join(" ") }
163
+ : { icons: [], corrected: [], query: terms.join(" ") };
164
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coolicons/cli",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
4
4
  "description": "Search coolicons and copy them into your project from the terminal, without network",
5
5
  "keywords": [
6
6
  "coolicons",
@@ -9,7 +9,7 @@
9
9
  "cli",
10
10
  "icon-set"
11
11
  ],
12
- "homepage": "https://coolicons.cool",
12
+ "homepage": "https://coolicons.cool/?ref=npm-cli",
13
13
  "bugs": "https://github.com/krystonschwarze/coolicons/issues",
14
14
  "repository": {
15
15
  "type": "git",