iconify-search 0.0.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/README.md +91 -0
- package/dist/index.js +675 -0
- package/package.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# iconify-search
|
|
2
|
+
|
|
3
|
+
A command-line client for the [Iconify API](https://iconify.design/docs/api/), built with TypeScript and Bun.
|
|
4
|
+
|
|
5
|
+
Browse and search 200k+ open-source icons, and build SVG URLs with transformations applied — straight from the terminal.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run via Bun:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun run index.ts <command> [options]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or link it as `iconify-search`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun link
|
|
23
|
+
iconify-search <command> [options]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
Icons are referenced as `prefix:name` (e.g. `mdi:home`), matching Iconify's own convention.
|
|
29
|
+
|
|
30
|
+
Structured output (`search`, `collections`, `list`) is rendered as [TOON](https://github.com/toon-format/toon) by default — compact and readable. Pass `--json` for raw JSON.
|
|
31
|
+
|
|
32
|
+
### `url <prefix:name>`
|
|
33
|
+
|
|
34
|
+
Build an SVG URL for an icon, with optional transformations.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
iconify-search url mdi:home
|
|
38
|
+
iconify-search url mdi:home --color '#ba3329' --width 24
|
|
39
|
+
iconify-search url mdi:home --rotate 90deg --flip horizontal --box
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Options: `--color`, `--width`, `--height`, `--flip`, `--rotate`, `--box`, `--download`.
|
|
43
|
+
|
|
44
|
+
### `search <query>`
|
|
45
|
+
|
|
46
|
+
Search icons across all icon sets.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
iconify-search search home
|
|
50
|
+
iconify-search search arrow --limit 999 --prefixes mdi-,bi
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Options: `--limit`, `--prefix`, `--prefixes`, `--category`, `--json`.
|
|
54
|
+
|
|
55
|
+
### `collections`
|
|
56
|
+
|
|
57
|
+
List available icon sets.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
iconify-search collections
|
|
61
|
+
iconify-search collections --prefixes fa,bi
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Options: `--prefix`, `--prefixes`, `--json`.
|
|
65
|
+
|
|
66
|
+
### `list <prefix>`
|
|
67
|
+
|
|
68
|
+
List the icon names within an icon set.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
iconify-search list mdi-light
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Options: `--info`, `--chars`, `--json`.
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
bun test # run the test suite
|
|
80
|
+
bunx tsc --noEmit # typecheck
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The codebase separates pure logic from I/O:
|
|
84
|
+
|
|
85
|
+
- `src/api.ts` — URL builders for every endpoint
|
|
86
|
+
- `src/icon-ref.ts` — `prefix:name` parsing
|
|
87
|
+
- `src/transform.ts` — reshaping API responses for output
|
|
88
|
+
- `src/output.ts` — TOON / JSON formatting
|
|
89
|
+
- `src/fetch.ts` — the network boundary
|
|
90
|
+
- `src/commands/*` — command handlers (network injected for testability)
|
|
91
|
+
- `src/cli.ts` — argument parsing and dispatch
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
|
|
6
|
+
// src/api.ts
|
|
7
|
+
var API_BASE = "https://api.iconify.design";
|
|
8
|
+
function buildSvgUrl(ref, options) {
|
|
9
|
+
const url = new URL(`${API_BASE}/${ref.prefix}/${ref.name}.svg`);
|
|
10
|
+
applyParams(url, options);
|
|
11
|
+
return url.toString();
|
|
12
|
+
}
|
|
13
|
+
function buildSearchUrl(query, options) {
|
|
14
|
+
const url = new URL(`${API_BASE}/search`);
|
|
15
|
+
url.searchParams.set("query", query);
|
|
16
|
+
applyParams(url, options);
|
|
17
|
+
return url.toString();
|
|
18
|
+
}
|
|
19
|
+
function buildCollectionsUrl(options) {
|
|
20
|
+
const url = new URL(`${API_BASE}/collections`);
|
|
21
|
+
applyParams(url, options);
|
|
22
|
+
return url.toString();
|
|
23
|
+
}
|
|
24
|
+
function buildCollectionUrl(prefix, options) {
|
|
25
|
+
const url = new URL(`${API_BASE}/collection`);
|
|
26
|
+
url.searchParams.set("prefix", prefix);
|
|
27
|
+
applyParams(url, options);
|
|
28
|
+
return url.toString();
|
|
29
|
+
}
|
|
30
|
+
function applyParams(url, params) {
|
|
31
|
+
for (const [key, value] of Object.entries(params)) {
|
|
32
|
+
if (value === undefined || value === false)
|
|
33
|
+
continue;
|
|
34
|
+
url.searchParams.set(key, value === true ? "1" : String(value));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// node_modules/@toon-format/toon/dist/index.mjs
|
|
39
|
+
var NULL_LITERAL = "null";
|
|
40
|
+
var DELIMITERS = {
|
|
41
|
+
comma: ",",
|
|
42
|
+
tab: "\t",
|
|
43
|
+
pipe: "|"
|
|
44
|
+
};
|
|
45
|
+
var DEFAULT_DELIMITER = DELIMITERS.comma;
|
|
46
|
+
function escapeString(value) {
|
|
47
|
+
return value.replace(/\\/g, `\\\\`).replace(/"/g, `\\"`).replace(/\n/g, `\\n`).replace(/\r/g, `\\r`).replace(/\t/g, `\\t`).replace(/[\u0000-\u001F]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
48
|
+
}
|
|
49
|
+
function isBooleanOrNullLiteral(token) {
|
|
50
|
+
return token === "true" || token === "false" || token === "null";
|
|
51
|
+
}
|
|
52
|
+
function normalizeValue(value) {
|
|
53
|
+
if (value === null)
|
|
54
|
+
return null;
|
|
55
|
+
if (typeof value === "object" && value !== null && "toJSON" in value && typeof value.toJSON === "function") {
|
|
56
|
+
const next = value.toJSON();
|
|
57
|
+
if (next !== value)
|
|
58
|
+
return normalizeValue(next);
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === "string" || typeof value === "boolean")
|
|
61
|
+
return value;
|
|
62
|
+
if (typeof value === "number") {
|
|
63
|
+
if (Object.is(value, -0))
|
|
64
|
+
return 0;
|
|
65
|
+
if (!Number.isFinite(value))
|
|
66
|
+
return null;
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === "bigint") {
|
|
70
|
+
if (value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER)
|
|
71
|
+
return Number(value);
|
|
72
|
+
return value.toString();
|
|
73
|
+
}
|
|
74
|
+
if (value instanceof Date)
|
|
75
|
+
return value.toISOString();
|
|
76
|
+
if (Array.isArray(value))
|
|
77
|
+
return value.map(normalizeValue);
|
|
78
|
+
if (value instanceof Set)
|
|
79
|
+
return Array.from(value).map(normalizeValue);
|
|
80
|
+
if (value instanceof Map)
|
|
81
|
+
return Object.fromEntries(Array.from(value, ([k, v]) => [String(k), normalizeValue(v)]));
|
|
82
|
+
if (isPlainObject(value)) {
|
|
83
|
+
const encodedValues = {};
|
|
84
|
+
for (const key in value)
|
|
85
|
+
if (Object.hasOwn(value, key))
|
|
86
|
+
encodedValues[key] = normalizeValue(value[key]);
|
|
87
|
+
return encodedValues;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function isJsonPrimitive(value) {
|
|
92
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
93
|
+
}
|
|
94
|
+
function isJsonArray(value) {
|
|
95
|
+
return Array.isArray(value);
|
|
96
|
+
}
|
|
97
|
+
function isJsonObject(value) {
|
|
98
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
99
|
+
}
|
|
100
|
+
function isEmptyObject(value) {
|
|
101
|
+
return Object.keys(value).length === 0;
|
|
102
|
+
}
|
|
103
|
+
function isPlainObject(value) {
|
|
104
|
+
if (value === null || typeof value !== "object")
|
|
105
|
+
return false;
|
|
106
|
+
const prototype = Object.getPrototypeOf(value);
|
|
107
|
+
return prototype === null || prototype === Object.prototype;
|
|
108
|
+
}
|
|
109
|
+
function isArrayOfPrimitives(value) {
|
|
110
|
+
return value.length === 0 || value.every((item) => isJsonPrimitive(item));
|
|
111
|
+
}
|
|
112
|
+
function isArrayOfArrays(value) {
|
|
113
|
+
return value.length === 0 || value.every((item) => isJsonArray(item));
|
|
114
|
+
}
|
|
115
|
+
function isArrayOfObjects(value) {
|
|
116
|
+
return value.length === 0 || value.every((item) => isJsonObject(item));
|
|
117
|
+
}
|
|
118
|
+
var NUMERIC_LIKE_PATTERN = /^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i;
|
|
119
|
+
var LEADING_ZERO_PATTERN = /^0\d+$/;
|
|
120
|
+
function isValidUnquotedKey(key) {
|
|
121
|
+
return /^[A-Z_][\w.]*$/i.test(key);
|
|
122
|
+
}
|
|
123
|
+
function isIdentifierSegment(key) {
|
|
124
|
+
return /^[A-Z_]\w*$/i.test(key);
|
|
125
|
+
}
|
|
126
|
+
function isSafeUnquoted(value, delimiter = DEFAULT_DELIMITER) {
|
|
127
|
+
if (!value)
|
|
128
|
+
return false;
|
|
129
|
+
if (value !== value.trim())
|
|
130
|
+
return false;
|
|
131
|
+
if (isBooleanOrNullLiteral(value) || isNumericLike(value))
|
|
132
|
+
return false;
|
|
133
|
+
if (value.includes(":"))
|
|
134
|
+
return false;
|
|
135
|
+
if (value.includes('"') || value.includes("\\"))
|
|
136
|
+
return false;
|
|
137
|
+
if (/[[\]{}]/.test(value))
|
|
138
|
+
return false;
|
|
139
|
+
if (/[\u0000-\u001F]/.test(value))
|
|
140
|
+
return false;
|
|
141
|
+
if (value.includes(delimiter))
|
|
142
|
+
return false;
|
|
143
|
+
if (value.startsWith("-"))
|
|
144
|
+
return false;
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
function isNumericLike(value) {
|
|
148
|
+
return NUMERIC_LIKE_PATTERN.test(value) || LEADING_ZERO_PATTERN.test(value);
|
|
149
|
+
}
|
|
150
|
+
var QUOTED_KEY_MARKER = Symbol("quotedKey");
|
|
151
|
+
function tryFoldKeyChain(key, value, siblings, options, rootLiteralKeys, pathPrefix, flattenDepth) {
|
|
152
|
+
if (options.keyFolding !== "safe")
|
|
153
|
+
return;
|
|
154
|
+
if (!isJsonObject(value))
|
|
155
|
+
return;
|
|
156
|
+
const { segments, tail, leafValue } = collectSingleKeyChain(key, value, flattenDepth ?? options.flattenDepth);
|
|
157
|
+
if (segments.length < 2)
|
|
158
|
+
return;
|
|
159
|
+
if (!segments.every((seg) => isIdentifierSegment(seg)))
|
|
160
|
+
return;
|
|
161
|
+
const foldedKey = buildFoldedKey(segments);
|
|
162
|
+
const absolutePath = pathPrefix ? `${pathPrefix}.${foldedKey}` : foldedKey;
|
|
163
|
+
if (siblings.includes(foldedKey))
|
|
164
|
+
return;
|
|
165
|
+
if (rootLiteralKeys && rootLiteralKeys.has(absolutePath))
|
|
166
|
+
return;
|
|
167
|
+
return {
|
|
168
|
+
foldedKey,
|
|
169
|
+
remainder: tail,
|
|
170
|
+
leafValue,
|
|
171
|
+
segmentCount: segments.length
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function collectSingleKeyChain(startKey, startValue, maxDepth) {
|
|
175
|
+
const segments = [startKey];
|
|
176
|
+
let currentValue = startValue;
|
|
177
|
+
while (segments.length < maxDepth) {
|
|
178
|
+
if (!isJsonObject(currentValue))
|
|
179
|
+
break;
|
|
180
|
+
const keys = Object.keys(currentValue);
|
|
181
|
+
if (keys.length !== 1)
|
|
182
|
+
break;
|
|
183
|
+
const nextKey = keys[0];
|
|
184
|
+
const nextValue = currentValue[nextKey];
|
|
185
|
+
segments.push(nextKey);
|
|
186
|
+
currentValue = nextValue;
|
|
187
|
+
}
|
|
188
|
+
if (!isJsonObject(currentValue) || isEmptyObject(currentValue))
|
|
189
|
+
return {
|
|
190
|
+
segments,
|
|
191
|
+
tail: undefined,
|
|
192
|
+
leafValue: currentValue
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
segments,
|
|
196
|
+
tail: currentValue,
|
|
197
|
+
leafValue: currentValue
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function buildFoldedKey(segments) {
|
|
201
|
+
return segments.join(".");
|
|
202
|
+
}
|
|
203
|
+
function encodePrimitive(value, delimiter) {
|
|
204
|
+
if (value === null)
|
|
205
|
+
return NULL_LITERAL;
|
|
206
|
+
if (typeof value === "boolean")
|
|
207
|
+
return String(value);
|
|
208
|
+
if (typeof value === "number")
|
|
209
|
+
return String(value);
|
|
210
|
+
return encodeStringLiteral(value, delimiter);
|
|
211
|
+
}
|
|
212
|
+
function encodeStringLiteral(value, delimiter = DEFAULT_DELIMITER) {
|
|
213
|
+
if (isSafeUnquoted(value, delimiter))
|
|
214
|
+
return value;
|
|
215
|
+
return `"${escapeString(value)}"`;
|
|
216
|
+
}
|
|
217
|
+
function encodeKey(key) {
|
|
218
|
+
if (isValidUnquotedKey(key))
|
|
219
|
+
return key;
|
|
220
|
+
return `"${escapeString(key)}"`;
|
|
221
|
+
}
|
|
222
|
+
function encodeAndJoinPrimitives(values, delimiter = DEFAULT_DELIMITER) {
|
|
223
|
+
return values.map((v) => encodePrimitive(v, delimiter)).join(delimiter);
|
|
224
|
+
}
|
|
225
|
+
function formatHeader(length, options) {
|
|
226
|
+
const key = options?.key;
|
|
227
|
+
const fields = options?.fields;
|
|
228
|
+
const delimiter = options?.delimiter ?? ",";
|
|
229
|
+
let header = "";
|
|
230
|
+
if (key != null)
|
|
231
|
+
header += encodeKey(key);
|
|
232
|
+
header += `[${length}${delimiter !== DEFAULT_DELIMITER ? delimiter : ""}]`;
|
|
233
|
+
if (fields) {
|
|
234
|
+
const quotedFields = fields.map((f) => encodeKey(f));
|
|
235
|
+
header += `{${quotedFields.join(delimiter)}}`;
|
|
236
|
+
}
|
|
237
|
+
header += ":";
|
|
238
|
+
return header;
|
|
239
|
+
}
|
|
240
|
+
function* encodeJsonValue(value, options, depth) {
|
|
241
|
+
if (isJsonPrimitive(value)) {
|
|
242
|
+
const encodedPrimitive = encodePrimitive(value, options.delimiter);
|
|
243
|
+
if (encodedPrimitive !== "")
|
|
244
|
+
yield encodedPrimitive;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (isJsonArray(value))
|
|
248
|
+
yield* encodeArrayLines(undefined, value, depth, options);
|
|
249
|
+
else if (isJsonObject(value))
|
|
250
|
+
yield* encodeObjectLines(value, depth, options);
|
|
251
|
+
}
|
|
252
|
+
function* encodeObjectLines(value, depth, options, rootLiteralKeys, pathPrefix, remainingDepth) {
|
|
253
|
+
const keys = Object.keys(value);
|
|
254
|
+
if (depth === 0 && !rootLiteralKeys)
|
|
255
|
+
rootLiteralKeys = new Set(keys.filter((k) => k.includes(".")));
|
|
256
|
+
const effectiveFlattenDepth = remainingDepth ?? options.flattenDepth;
|
|
257
|
+
for (const [key, val] of Object.entries(value))
|
|
258
|
+
yield* encodeKeyValuePairLines(key, val, depth, options, keys, rootLiteralKeys, pathPrefix, effectiveFlattenDepth);
|
|
259
|
+
}
|
|
260
|
+
function* encodeKeyValuePairLines(key, value, depth, options, siblings, rootLiteralKeys, pathPrefix, flattenDepth) {
|
|
261
|
+
const currentPath = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
262
|
+
const effectiveFlattenDepth = flattenDepth ?? options.flattenDepth;
|
|
263
|
+
if (options.keyFolding === "safe" && siblings) {
|
|
264
|
+
const foldResult = tryFoldKeyChain(key, value, siblings, options, rootLiteralKeys, pathPrefix, effectiveFlattenDepth);
|
|
265
|
+
if (foldResult) {
|
|
266
|
+
const { foldedKey, remainder, leafValue, segmentCount } = foldResult;
|
|
267
|
+
const encodedFoldedKey = encodeKey(foldedKey);
|
|
268
|
+
if (remainder === undefined) {
|
|
269
|
+
if (isJsonPrimitive(leafValue)) {
|
|
270
|
+
yield indentedLine(depth, `${encodedFoldedKey}: ${encodePrimitive(leafValue, options.delimiter)}`, options.indent);
|
|
271
|
+
return;
|
|
272
|
+
} else if (isJsonArray(leafValue)) {
|
|
273
|
+
yield* encodeArrayLines(foldedKey, leafValue, depth, options);
|
|
274
|
+
return;
|
|
275
|
+
} else if (isJsonObject(leafValue) && isEmptyObject(leafValue)) {
|
|
276
|
+
yield indentedLine(depth, `${encodedFoldedKey}:`, options.indent);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (isJsonObject(remainder)) {
|
|
281
|
+
yield indentedLine(depth, `${encodedFoldedKey}:`, options.indent);
|
|
282
|
+
const remainingDepth = effectiveFlattenDepth - segmentCount;
|
|
283
|
+
const foldedPath = pathPrefix ? `${pathPrefix}.${foldedKey}` : foldedKey;
|
|
284
|
+
yield* encodeObjectLines(remainder, depth + 1, options, rootLiteralKeys, foldedPath, remainingDepth);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const encodedKey = encodeKey(key);
|
|
290
|
+
if (isJsonPrimitive(value))
|
|
291
|
+
yield indentedLine(depth, `${encodedKey}: ${encodePrimitive(value, options.delimiter)}`, options.indent);
|
|
292
|
+
else if (isJsonArray(value))
|
|
293
|
+
yield* encodeArrayLines(key, value, depth, options);
|
|
294
|
+
else if (isJsonObject(value)) {
|
|
295
|
+
yield indentedLine(depth, `${encodedKey}:`, options.indent);
|
|
296
|
+
if (!isEmptyObject(value))
|
|
297
|
+
yield* encodeObjectLines(value, depth + 1, options, rootLiteralKeys, currentPath, effectiveFlattenDepth);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function* encodeArrayLines(key, value, depth, options) {
|
|
301
|
+
if (value.length === 0) {
|
|
302
|
+
yield indentedLine(depth, key != null ? `${encodeKey(key)}: []` : "[]", options.indent);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (isArrayOfPrimitives(value)) {
|
|
306
|
+
yield indentedLine(depth, encodeInlineArrayLine(value, options.delimiter, key), options.indent);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (isArrayOfArrays(value)) {
|
|
310
|
+
if (value.every((arr) => isArrayOfPrimitives(arr))) {
|
|
311
|
+
yield* encodeArrayOfArraysAsListItemsLines(key, value, depth, options);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (isArrayOfObjects(value)) {
|
|
316
|
+
const header = extractTabularHeader(value);
|
|
317
|
+
if (header)
|
|
318
|
+
yield* encodeArrayOfObjectsAsTabularLines(key, value, header, depth, options);
|
|
319
|
+
else
|
|
320
|
+
yield* encodeMixedArrayAsListItemsLines(key, value, depth, options);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
yield* encodeMixedArrayAsListItemsLines(key, value, depth, options);
|
|
324
|
+
}
|
|
325
|
+
function* encodeArrayOfArraysAsListItemsLines(prefix, values, depth, options) {
|
|
326
|
+
yield indentedLine(depth, formatHeader(values.length, {
|
|
327
|
+
key: prefix,
|
|
328
|
+
delimiter: options.delimiter
|
|
329
|
+
}), options.indent);
|
|
330
|
+
for (const arr of values)
|
|
331
|
+
if (isArrayOfPrimitives(arr)) {
|
|
332
|
+
const arrayLine = encodeInlineArrayLine(arr, options.delimiter);
|
|
333
|
+
yield indentedListItem(depth + 1, arrayLine, options.indent);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function encodeInlineArrayLine(values, delimiter, prefix) {
|
|
337
|
+
const header = formatHeader(values.length, {
|
|
338
|
+
key: prefix,
|
|
339
|
+
delimiter
|
|
340
|
+
});
|
|
341
|
+
const joinedValue = encodeAndJoinPrimitives(values, delimiter);
|
|
342
|
+
if (values.length === 0)
|
|
343
|
+
return header;
|
|
344
|
+
return `${header} ${joinedValue}`;
|
|
345
|
+
}
|
|
346
|
+
function* encodeArrayOfObjectsAsTabularLines(prefix, rows, header, depth, options) {
|
|
347
|
+
yield indentedLine(depth, formatHeader(rows.length, {
|
|
348
|
+
key: prefix,
|
|
349
|
+
fields: header,
|
|
350
|
+
delimiter: options.delimiter
|
|
351
|
+
}), options.indent);
|
|
352
|
+
yield* writeTabularRowsLines(rows, header, depth + 1, options);
|
|
353
|
+
}
|
|
354
|
+
function extractTabularHeader(rows) {
|
|
355
|
+
if (rows.length === 0)
|
|
356
|
+
return;
|
|
357
|
+
const firstRow = rows[0];
|
|
358
|
+
const firstKeys = Object.keys(firstRow);
|
|
359
|
+
if (firstKeys.length === 0)
|
|
360
|
+
return;
|
|
361
|
+
if (isTabularArray(rows, firstKeys))
|
|
362
|
+
return firstKeys;
|
|
363
|
+
}
|
|
364
|
+
function isTabularArray(rows, header) {
|
|
365
|
+
for (const row of rows) {
|
|
366
|
+
if (Object.keys(row).length !== header.length)
|
|
367
|
+
return false;
|
|
368
|
+
for (const key of header) {
|
|
369
|
+
if (!(key in row))
|
|
370
|
+
return false;
|
|
371
|
+
if (!isJsonPrimitive(row[key]))
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
function* writeTabularRowsLines(rows, header, depth, options) {
|
|
378
|
+
for (const row of rows)
|
|
379
|
+
yield indentedLine(depth, encodeAndJoinPrimitives(header.map((key) => row[key]), options.delimiter), options.indent);
|
|
380
|
+
}
|
|
381
|
+
function* encodeMixedArrayAsListItemsLines(prefix, items, depth, options) {
|
|
382
|
+
yield indentedLine(depth, formatHeader(items.length, {
|
|
383
|
+
key: prefix,
|
|
384
|
+
delimiter: options.delimiter
|
|
385
|
+
}), options.indent);
|
|
386
|
+
for (const item of items)
|
|
387
|
+
yield* encodeListItemValueLines(item, depth + 1, options);
|
|
388
|
+
}
|
|
389
|
+
function* encodeObjectAsListItemLines(obj, depth, options) {
|
|
390
|
+
if (isEmptyObject(obj)) {
|
|
391
|
+
yield indentedLine(depth, "-", options.indent);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const entries = Object.entries(obj);
|
|
395
|
+
const [firstKey, firstValue] = entries[0];
|
|
396
|
+
const restEntries = entries.slice(1);
|
|
397
|
+
if (isJsonArray(firstValue) && isArrayOfObjects(firstValue)) {
|
|
398
|
+
const header = extractTabularHeader(firstValue);
|
|
399
|
+
if (header) {
|
|
400
|
+
yield indentedListItem(depth, formatHeader(firstValue.length, {
|
|
401
|
+
key: firstKey,
|
|
402
|
+
fields: header,
|
|
403
|
+
delimiter: options.delimiter
|
|
404
|
+
}), options.indent);
|
|
405
|
+
yield* writeTabularRowsLines(firstValue, header, depth + 2, options);
|
|
406
|
+
if (restEntries.length > 0)
|
|
407
|
+
yield* encodeObjectLines(Object.fromEntries(restEntries), depth + 1, options);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const encodedKey = encodeKey(firstKey);
|
|
412
|
+
if (isJsonPrimitive(firstValue))
|
|
413
|
+
yield indentedListItem(depth, `${encodedKey}: ${encodePrimitive(firstValue, options.delimiter)}`, options.indent);
|
|
414
|
+
else if (isJsonArray(firstValue))
|
|
415
|
+
if (firstValue.length === 0)
|
|
416
|
+
yield indentedListItem(depth, `${encodedKey}: []`, options.indent);
|
|
417
|
+
else if (isArrayOfPrimitives(firstValue))
|
|
418
|
+
yield indentedListItem(depth, `${encodedKey}${encodeInlineArrayLine(firstValue, options.delimiter)}`, options.indent);
|
|
419
|
+
else {
|
|
420
|
+
yield indentedListItem(depth, `${encodedKey}${formatHeader(firstValue.length, { delimiter: options.delimiter })}`, options.indent);
|
|
421
|
+
for (const item of firstValue)
|
|
422
|
+
yield* encodeListItemValueLines(item, depth + 2, options);
|
|
423
|
+
}
|
|
424
|
+
else if (isJsonObject(firstValue)) {
|
|
425
|
+
yield indentedListItem(depth, `${encodedKey}:`, options.indent);
|
|
426
|
+
if (!isEmptyObject(firstValue))
|
|
427
|
+
yield* encodeObjectLines(firstValue, depth + 2, options);
|
|
428
|
+
}
|
|
429
|
+
if (restEntries.length > 0)
|
|
430
|
+
yield* encodeObjectLines(Object.fromEntries(restEntries), depth + 1, options);
|
|
431
|
+
}
|
|
432
|
+
function* encodeListItemValueLines(value, depth, options) {
|
|
433
|
+
if (isJsonPrimitive(value))
|
|
434
|
+
yield indentedListItem(depth, encodePrimitive(value, options.delimiter), options.indent);
|
|
435
|
+
else if (isJsonArray(value))
|
|
436
|
+
if (isArrayOfPrimitives(value))
|
|
437
|
+
yield indentedListItem(depth, encodeInlineArrayLine(value, options.delimiter), options.indent);
|
|
438
|
+
else {
|
|
439
|
+
yield indentedListItem(depth, formatHeader(value.length, { delimiter: options.delimiter }), options.indent);
|
|
440
|
+
for (const item of value)
|
|
441
|
+
yield* encodeListItemValueLines(item, depth + 1, options);
|
|
442
|
+
}
|
|
443
|
+
else if (isJsonObject(value))
|
|
444
|
+
yield* encodeObjectAsListItemLines(value, depth, options);
|
|
445
|
+
}
|
|
446
|
+
function indentedLine(depth, content, indentSize) {
|
|
447
|
+
return " ".repeat(indentSize * depth) + content;
|
|
448
|
+
}
|
|
449
|
+
function indentedListItem(depth, content, indentSize) {
|
|
450
|
+
return indentedLine(depth, "- " + content, indentSize);
|
|
451
|
+
}
|
|
452
|
+
function applyReplacer(root, replacer) {
|
|
453
|
+
const replacedRoot = replacer("", root, []);
|
|
454
|
+
if (replacedRoot === undefined)
|
|
455
|
+
return transformChildren(root, replacer, []);
|
|
456
|
+
return transformChildren(normalizeValue(replacedRoot), replacer, []);
|
|
457
|
+
}
|
|
458
|
+
function transformChildren(value, replacer, path) {
|
|
459
|
+
if (isJsonObject(value))
|
|
460
|
+
return transformObject(value, replacer, path);
|
|
461
|
+
if (isJsonArray(value))
|
|
462
|
+
return transformArray(value, replacer, path);
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
function transformObject(obj, replacer, path) {
|
|
466
|
+
const result = {};
|
|
467
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
468
|
+
const childPath = [...path, key];
|
|
469
|
+
const replacedValue = replacer(key, value, childPath);
|
|
470
|
+
if (replacedValue === undefined)
|
|
471
|
+
continue;
|
|
472
|
+
result[key] = transformChildren(normalizeValue(replacedValue), replacer, childPath);
|
|
473
|
+
}
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
function transformArray(arr, replacer, path) {
|
|
477
|
+
const result = [];
|
|
478
|
+
for (let i = 0;i < arr.length; i++) {
|
|
479
|
+
const value = arr[i];
|
|
480
|
+
const childPath = [...path, i];
|
|
481
|
+
const replacedValue = replacer(String(i), value, childPath);
|
|
482
|
+
if (replacedValue === undefined)
|
|
483
|
+
continue;
|
|
484
|
+
const normalizedValue = normalizeValue(replacedValue);
|
|
485
|
+
result.push(transformChildren(normalizedValue, replacer, childPath));
|
|
486
|
+
}
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
function encode(input, options) {
|
|
490
|
+
return Array.from(encodeLines(input, options)).join(`
|
|
491
|
+
`);
|
|
492
|
+
}
|
|
493
|
+
function encodeLines(input, options) {
|
|
494
|
+
const normalizedValue = normalizeValue(input);
|
|
495
|
+
const resolvedOptions = resolveOptions(options);
|
|
496
|
+
return encodeJsonValue(resolvedOptions.replacer ? applyReplacer(normalizedValue, resolvedOptions.replacer) : normalizedValue, resolvedOptions, 0);
|
|
497
|
+
}
|
|
498
|
+
function resolveOptions(options) {
|
|
499
|
+
return {
|
|
500
|
+
indent: options?.indent ?? 2,
|
|
501
|
+
delimiter: options?.delimiter ?? DEFAULT_DELIMITER,
|
|
502
|
+
keyFolding: options?.keyFolding ?? "off",
|
|
503
|
+
flattenDepth: options?.flattenDepth ?? Number.POSITIVE_INFINITY,
|
|
504
|
+
replacer: options?.replacer
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/output.ts
|
|
509
|
+
function formatOutput(data, options) {
|
|
510
|
+
return options.json ? JSON.stringify(data, null, 2) : encode(data);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/transform.ts
|
|
514
|
+
function collectionsTable(response) {
|
|
515
|
+
return Object.entries(response).map(([prefix, info]) => ({
|
|
516
|
+
prefix,
|
|
517
|
+
name: info.name ?? "",
|
|
518
|
+
total: info.total ?? 0,
|
|
519
|
+
category: info.category ?? ""
|
|
520
|
+
})).sort((a, b) => a.prefix.localeCompare(b.prefix));
|
|
521
|
+
}
|
|
522
|
+
function collectionIcons(response) {
|
|
523
|
+
const icons = new Set(response.uncategorized ?? []);
|
|
524
|
+
for (const names of Object.values(response.categories ?? {})) {
|
|
525
|
+
for (const name of names)
|
|
526
|
+
icons.add(name);
|
|
527
|
+
}
|
|
528
|
+
return [...icons];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/commands/collections.ts
|
|
532
|
+
async function collectionsCommand(args, deps) {
|
|
533
|
+
const url = buildCollectionsUrl(args.options);
|
|
534
|
+
const response = await deps.fetchJson(url);
|
|
535
|
+
return formatOutput(collectionsTable(response), { json: args.json });
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/commands/list.ts
|
|
539
|
+
async function listCommand(args, deps) {
|
|
540
|
+
const url = buildCollectionUrl(args.prefix, args.options);
|
|
541
|
+
const response = await deps.fetchJson(url);
|
|
542
|
+
return formatOutput(collectionIcons(response), { json: args.json });
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/commands/search.ts
|
|
546
|
+
async function searchCommand(args, deps) {
|
|
547
|
+
const url = buildSearchUrl(args.query, args.options);
|
|
548
|
+
const response = await deps.fetchJson(url);
|
|
549
|
+
return formatOutput({ total: response.total, icons: response.icons }, { json: args.json });
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/icon-ref.ts
|
|
553
|
+
function parseIconRef(input) {
|
|
554
|
+
const [prefix, name, ...rest] = input.split(":");
|
|
555
|
+
if (!prefix || !name || rest.length > 0) {
|
|
556
|
+
throw new Error(`invalid icon "${input}": expected format prefix:name`);
|
|
557
|
+
}
|
|
558
|
+
return { prefix, name };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/commands/url.ts
|
|
562
|
+
function urlCommand(args) {
|
|
563
|
+
const ref = parseIconRef(args.icon);
|
|
564
|
+
return buildSvgUrl(ref, args.options);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/cli.ts
|
|
568
|
+
var USAGE = `Usage: iconify <command> [options]
|
|
569
|
+
|
|
570
|
+
Commands:
|
|
571
|
+
url <prefix:name> Build an SVG URL for an icon
|
|
572
|
+
--color --width --height --flip --rotate --box --download
|
|
573
|
+
search <query> Search icons across all sets
|
|
574
|
+
--limit --prefix --prefixes --category
|
|
575
|
+
collections List available icon sets
|
|
576
|
+
--prefix --prefixes
|
|
577
|
+
list <prefix> List icon names in an icon set
|
|
578
|
+
--info --chars
|
|
579
|
+
|
|
580
|
+
Structured output is TOON by default; pass --json for JSON.`;
|
|
581
|
+
async function run(argv, deps) {
|
|
582
|
+
const [command, ...rest] = argv;
|
|
583
|
+
switch (command) {
|
|
584
|
+
case undefined:
|
|
585
|
+
case "help":
|
|
586
|
+
case "--help":
|
|
587
|
+
case "-h":
|
|
588
|
+
return USAGE;
|
|
589
|
+
case "url": {
|
|
590
|
+
const { values, positionals } = parseArgs({
|
|
591
|
+
args: rest,
|
|
592
|
+
allowPositionals: true,
|
|
593
|
+
options: {
|
|
594
|
+
color: { type: "string" },
|
|
595
|
+
width: { type: "string" },
|
|
596
|
+
height: { type: "string" },
|
|
597
|
+
flip: { type: "string" },
|
|
598
|
+
rotate: { type: "string" },
|
|
599
|
+
box: { type: "boolean" },
|
|
600
|
+
download: { type: "boolean" }
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
const icon = positionals[0];
|
|
604
|
+
if (!icon)
|
|
605
|
+
throw new Error("url: missing icon (e.g. mdi:home)");
|
|
606
|
+
return urlCommand({ icon, options: values });
|
|
607
|
+
}
|
|
608
|
+
case "search": {
|
|
609
|
+
const { values, positionals } = parseArgs({
|
|
610
|
+
args: rest,
|
|
611
|
+
allowPositionals: true,
|
|
612
|
+
options: {
|
|
613
|
+
limit: { type: "string" },
|
|
614
|
+
prefix: { type: "string" },
|
|
615
|
+
prefixes: { type: "string" },
|
|
616
|
+
category: { type: "string" },
|
|
617
|
+
json: { type: "boolean", default: false }
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
const query = positionals[0];
|
|
621
|
+
if (!query)
|
|
622
|
+
throw new Error("search: missing query");
|
|
623
|
+
const { json, ...options } = values;
|
|
624
|
+
return searchCommand({ query, options, json }, deps);
|
|
625
|
+
}
|
|
626
|
+
case "collections": {
|
|
627
|
+
const { values } = parseArgs({
|
|
628
|
+
args: rest,
|
|
629
|
+
options: {
|
|
630
|
+
prefix: { type: "string" },
|
|
631
|
+
prefixes: { type: "string" },
|
|
632
|
+
json: { type: "boolean", default: false }
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
const { json, ...options } = values;
|
|
636
|
+
return collectionsCommand({ options, json }, deps);
|
|
637
|
+
}
|
|
638
|
+
case "list": {
|
|
639
|
+
const { values, positionals } = parseArgs({
|
|
640
|
+
args: rest,
|
|
641
|
+
allowPositionals: true,
|
|
642
|
+
options: {
|
|
643
|
+
info: { type: "boolean" },
|
|
644
|
+
chars: { type: "boolean" },
|
|
645
|
+
json: { type: "boolean", default: false }
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
const prefix = positionals[0];
|
|
649
|
+
if (!prefix)
|
|
650
|
+
throw new Error("list: missing prefix");
|
|
651
|
+
const { json, ...options } = values;
|
|
652
|
+
return listCommand({ prefix, options, json }, deps);
|
|
653
|
+
}
|
|
654
|
+
default:
|
|
655
|
+
throw new Error(`unknown command: ${command}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/fetch.ts
|
|
660
|
+
async function fetchJson(url) {
|
|
661
|
+
const response = await fetch(url);
|
|
662
|
+
if (!response.ok) {
|
|
663
|
+
throw new Error(`request failed (${response.status}): ${url}`);
|
|
664
|
+
}
|
|
665
|
+
return response.json();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// index.ts
|
|
669
|
+
try {
|
|
670
|
+
const output = await run(process.argv.slice(2), { fetchJson });
|
|
671
|
+
console.log(output);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "iconify-search",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "iconify-search": "dist/index.js" },
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "bun test",
|
|
10
|
+
"start": "bun run index.ts",
|
|
11
|
+
"build": "bun build index.ts --target node --outfile dist/index.js",
|
|
12
|
+
"prepublishOnly": "bun run build"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "latest",
|
|
16
|
+
"@toon-format/toon": "^2.3.0"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"typescript": "^5"
|
|
20
|
+
}
|
|
21
|
+
}
|