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.
Files changed (3) hide show
  1. package/README.md +91 -0
  2. package/dist/index.js +675 -0
  3. 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
+ }