@ai.to.design/design-token-extractor 1.3.4

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 +109 -0
  2. package/dist/cli.js +1597 -0
  3. package/package.json +63 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1597 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import ora from "ora";
6
+ import { z as z2 } from "zod";
7
+
8
+ // src/errors.ts
9
+ var DteError = class extends Error {
10
+ exitCode;
11
+ constructor(message, exitCode, options) {
12
+ super(message, options);
13
+ this.exitCode = exitCode;
14
+ this.name = "DteError";
15
+ }
16
+ };
17
+ var UserError = class extends DteError {
18
+ constructor(message, options) {
19
+ super(message, 1, options);
20
+ this.name = "UserError";
21
+ }
22
+ };
23
+ var ExtractionError = class extends DteError {
24
+ constructor(message, options) {
25
+ super(message, 2, options);
26
+ this.name = "ExtractionError";
27
+ }
28
+ };
29
+
30
+ // src/extract.ts
31
+ import { basename } from "path";
32
+
33
+ // src/score.ts
34
+ function scoreConfidence(usageCount) {
35
+ if (usageCount >= 10) return 0.9;
36
+ if (usageCount >= 5) return 0.7;
37
+ if (usageCount >= 2) return 0.5;
38
+ return 0.2;
39
+ }
40
+
41
+ // src/apply-score.ts
42
+ function applyScores(tokens) {
43
+ return tokens.map((token) => ({
44
+ ...token,
45
+ $extensions: {
46
+ ...token.$extensions,
47
+ "com.dte.confidence": scoreConfidence(
48
+ token.$extensions["com.dte.usage"].count
49
+ )
50
+ }
51
+ }));
52
+ }
53
+
54
+ // src/categorize/color.ts
55
+ var COLOR_PROPERTIES = /* @__PURE__ */ new Set([
56
+ "color",
57
+ "background-color",
58
+ "border-color",
59
+ "border-top-color",
60
+ "border-right-color",
61
+ "border-bottom-color",
62
+ "border-left-color",
63
+ "outline-color"
64
+ ]);
65
+ var SKIPPED_KEYWORDS = /* @__PURE__ */ new Set([
66
+ "currentcolor",
67
+ "inherit",
68
+ "initial",
69
+ "unset",
70
+ "transparent",
71
+ "revert",
72
+ "revert-layer",
73
+ "none",
74
+ "auto"
75
+ ]);
76
+ var NAMED_COLORS = {
77
+ black: "#000000",
78
+ silver: "#c0c0c0",
79
+ gray: "#808080",
80
+ grey: "#808080",
81
+ white: "#ffffff",
82
+ maroon: "#800000",
83
+ red: "#ff0000",
84
+ purple: "#800080",
85
+ fuchsia: "#ff00ff",
86
+ magenta: "#ff00ff",
87
+ green: "#008000",
88
+ lime: "#00ff00",
89
+ olive: "#808000",
90
+ yellow: "#ffff00",
91
+ navy: "#000080",
92
+ blue: "#0000ff",
93
+ teal: "#008080",
94
+ aqua: "#00ffff",
95
+ cyan: "#00ffff",
96
+ orange: "#ffa500",
97
+ pink: "#ffc0cb"
98
+ };
99
+ function isGradient(value) {
100
+ const trimmed = value.trim().toLowerCase();
101
+ return trimmed.startsWith("linear-gradient(") || trimmed.startsWith("radial-gradient(") || trimmed.startsWith("conic-gradient(") || trimmed.startsWith("repeating-linear-gradient(") || trimmed.startsWith("repeating-radial-gradient(") || trimmed.startsWith("repeating-conic-gradient(");
102
+ }
103
+ function normalizeHex(value) {
104
+ const trimmed = value.trim();
105
+ if (!trimmed.startsWith("#")) return null;
106
+ const digits = trimmed.slice(1);
107
+ if (!/^[0-9a-fA-F]+$/.test(digits)) return null;
108
+ if (digits.length === 3 || digits.length === 4) {
109
+ const expanded = digits.split("").map((ch) => ch + ch).join("");
110
+ return `#${expanded.toLowerCase()}`;
111
+ }
112
+ if (digits.length === 6 || digits.length === 8) {
113
+ return `#${digits.toLowerCase()}`;
114
+ }
115
+ return null;
116
+ }
117
+ function rgbToHex(value) {
118
+ const match = /^rgb\(\s*([^)]+)\s*\)$/i.exec(value.trim());
119
+ if (!match) return null;
120
+ const parts = match[1].split(",").map((p) => p.trim());
121
+ if (parts.length !== 3) return null;
122
+ const channels = [];
123
+ for (const part of parts) {
124
+ if (!/^\d+$/.test(part)) return null;
125
+ const n = Number.parseInt(part, 10);
126
+ if (n < 0 || n > 255) return null;
127
+ channels.push(n);
128
+ }
129
+ const hex = channels.map((n) => n.toString(16).padStart(2, "0")).join("");
130
+ return `#${hex}`;
131
+ }
132
+ function canonicalizeRgba(value) {
133
+ const match = /^rgba\(\s*([^)]+)\s*\)$/i.exec(value.trim());
134
+ if (!match) return null;
135
+ const parts = match[1].split(",").map((p) => p.trim());
136
+ if (parts.length !== 4) return null;
137
+ return `rgba(${parts.join(", ")})`;
138
+ }
139
+ function canonicalizeHsl(value) {
140
+ const match = /^(hsla?)\(\s*([^)]+)\s*\)$/i.exec(value.trim());
141
+ if (!match) return null;
142
+ const fn = match[1].toLowerCase();
143
+ const parts = match[2].split(",").map((p) => p.trim());
144
+ return `${fn}(${parts.join(", ")})`;
145
+ }
146
+ function normalizeColor(value) {
147
+ const trimmed = value.trim();
148
+ if (trimmed.length === 0) return null;
149
+ if (SKIPPED_KEYWORDS.has(trimmed.toLowerCase())) return null;
150
+ if (isGradient(trimmed)) return trimmed;
151
+ const hex = normalizeHex(trimmed);
152
+ if (hex !== null) return hex;
153
+ if (/^rgb\(/i.test(trimmed)) {
154
+ const hexForm = rgbToHex(trimmed);
155
+ if (hexForm !== null) return hexForm;
156
+ }
157
+ if (/^rgba\(/i.test(trimmed)) {
158
+ const canonical = canonicalizeRgba(trimmed);
159
+ if (canonical !== null) return canonical;
160
+ }
161
+ if (/^hsla?\(/i.test(trimmed)) {
162
+ const canonical = canonicalizeHsl(trimmed);
163
+ if (canonical !== null) return canonical;
164
+ }
165
+ const named = NAMED_COLORS[trimmed.toLowerCase()];
166
+ if (named !== void 0) return named;
167
+ return trimmed;
168
+ }
169
+ function categorizeColors(records) {
170
+ const groups = /* @__PURE__ */ new Map();
171
+ for (const record of records) {
172
+ if (!COLOR_PROPERTIES.has(record.property)) continue;
173
+ const canonical = normalizeColor(record.value);
174
+ if (canonical === null) continue;
175
+ const existing = groups.get(canonical);
176
+ if (existing === void 0) {
177
+ groups.set(canonical, {
178
+ canonical,
179
+ isGradientValue: isGradient(canonical),
180
+ first: record,
181
+ selectors: [record.selector]
182
+ });
183
+ continue;
184
+ }
185
+ existing.selectors.push(record.selector);
186
+ }
187
+ const tokens = [];
188
+ for (const group of groups.values()) {
189
+ tokens.push({
190
+ $value: group.canonical,
191
+ $type: group.isGradientValue ? "other" : "color",
192
+ $extensions: {
193
+ "com.dte.usage": {
194
+ selectors: group.selectors,
195
+ count: group.selectors.length
196
+ },
197
+ "com.dte.confidence": 0,
198
+ "com.dte.source": group.first.source,
199
+ "com.dte.theme": group.first.theme
200
+ }
201
+ });
202
+ }
203
+ return tokens;
204
+ }
205
+
206
+ // src/categorize/motion.ts
207
+ var DURATION_PROPERTIES = /* @__PURE__ */ new Set([
208
+ "transition-duration",
209
+ "animation-duration"
210
+ ]);
211
+ var EASING_PROPERTIES = /* @__PURE__ */ new Set([
212
+ "transition-timing-function",
213
+ "animation-timing-function"
214
+ ]);
215
+ var SKIPPED_VALUES = /* @__PURE__ */ new Set([
216
+ "none",
217
+ "initial",
218
+ "inherit"
219
+ ]);
220
+ var EASING_KEYWORDS = /* @__PURE__ */ new Set([
221
+ "ease",
222
+ "ease-in",
223
+ "ease-out",
224
+ "ease-in-out",
225
+ "linear",
226
+ "step-start",
227
+ "step-end"
228
+ ]);
229
+ function classifyEasing(value) {
230
+ const lower = value.trim().toLowerCase();
231
+ if (lower.startsWith("cubic-bezier(")) return "cubicBezier";
232
+ if (EASING_KEYWORDS.has(lower)) return "other";
233
+ return "other";
234
+ }
235
+ function upsert(map, key, type, record) {
236
+ const existing = map.get(key);
237
+ if (existing) {
238
+ existing.selectors.add(record.selector);
239
+ existing.sources.add(record.source);
240
+ return;
241
+ }
242
+ map.set(key, {
243
+ $value: record.value,
244
+ $type: type,
245
+ selectors: /* @__PURE__ */ new Set([record.selector]),
246
+ sources: /* @__PURE__ */ new Set([record.source])
247
+ });
248
+ }
249
+ function toToken(agg) {
250
+ const selectors = [...agg.selectors];
251
+ const token = {
252
+ $value: agg.$value,
253
+ $type: agg.$type,
254
+ $extensions: {
255
+ "com.dte.usage": {
256
+ selectors,
257
+ count: selectors.length
258
+ },
259
+ "com.dte.confidence": 0
260
+ }
261
+ };
262
+ if (agg.sources.size === 1) {
263
+ const [onlySource] = agg.sources;
264
+ token.$extensions["com.dte.source"] = onlySource;
265
+ }
266
+ return token;
267
+ }
268
+ function categorizeMotion(records) {
269
+ const durationAgg = /* @__PURE__ */ new Map();
270
+ const easingAgg = /* @__PURE__ */ new Map();
271
+ for (const record of records) {
272
+ const value = record.value.trim();
273
+ if (SKIPPED_VALUES.has(value.toLowerCase())) continue;
274
+ if (DURATION_PROPERTIES.has(record.property)) {
275
+ upsert(durationAgg, value, "duration", record);
276
+ continue;
277
+ }
278
+ if (EASING_PROPERTIES.has(record.property)) {
279
+ const type = classifyEasing(value);
280
+ upsert(easingAgg, value, type, record);
281
+ continue;
282
+ }
283
+ }
284
+ return {
285
+ duration: [...durationAgg.values()].map(toToken),
286
+ easing: [...easingAgg.values()].map(toToken)
287
+ };
288
+ }
289
+
290
+ // src/categorize/radius.ts
291
+ var RADIUS_PROPERTIES = [
292
+ "border-radius",
293
+ "border-top-left-radius",
294
+ "border-top-right-radius",
295
+ "border-bottom-right-radius",
296
+ "border-bottom-left-radius"
297
+ ];
298
+ var RADIUS_PROPERTY_SET = new Set(RADIUS_PROPERTIES);
299
+ function normalizeRadiusValue(raw) {
300
+ const trimmed = raw.trim();
301
+ if (trimmed.length === 0) return null;
302
+ if (trimmed === "0" || trimmed === "0px") return "0";
303
+ return trimmed;
304
+ }
305
+ function dedupKey(property, normalizedValue) {
306
+ return `${property}::${normalizedValue}`;
307
+ }
308
+ function categorizeRadius(records) {
309
+ const byKey = /* @__PURE__ */ new Map();
310
+ for (const record of records) {
311
+ if (!RADIUS_PROPERTY_SET.has(record.property)) continue;
312
+ const normalized = normalizeRadiusValue(record.value);
313
+ if (normalized === null) continue;
314
+ const key = dedupKey(record.property, normalized);
315
+ const existing = byKey.get(key);
316
+ if (existing) {
317
+ const selectors = existing.$extensions["com.dte.usage"].selectors;
318
+ if (!selectors.includes(record.selector)) {
319
+ selectors.push(record.selector);
320
+ }
321
+ continue;
322
+ }
323
+ byKey.set(key, {
324
+ $value: normalized,
325
+ $type: "dimension",
326
+ $extensions: {
327
+ "com.dte.usage": {
328
+ selectors: [record.selector],
329
+ count: 0
330
+ },
331
+ "com.dte.confidence": 0
332
+ }
333
+ });
334
+ }
335
+ return Array.from(byKey.values());
336
+ }
337
+
338
+ // src/categorize/shadow.ts
339
+ var SHADOW_PROPERTIES = /* @__PURE__ */ new Set([
340
+ "box-shadow",
341
+ "text-shadow"
342
+ ]);
343
+ var SKIPPED_VALUES2 = /* @__PURE__ */ new Set([
344
+ "none",
345
+ "inherit",
346
+ "initial",
347
+ "unset"
348
+ ]);
349
+ function isShadowRecord(record) {
350
+ if (!SHADOW_PROPERTIES.has(record.property)) return false;
351
+ if (SKIPPED_VALUES2.has(record.value)) return false;
352
+ return true;
353
+ }
354
+ function bucketToToken(bucket) {
355
+ return {
356
+ $value: bucket.value,
357
+ $type: "shadow",
358
+ $extensions: {
359
+ "com.dte.usage": {
360
+ selectors: bucket.selectors,
361
+ count: bucket.selectors.length
362
+ },
363
+ "com.dte.confidence": 0,
364
+ "com.dte.source": bucket.firstRecord.source,
365
+ "com.dte.theme": bucket.firstRecord.theme
366
+ }
367
+ };
368
+ }
369
+ function categorizeShadow(records) {
370
+ const buckets = /* @__PURE__ */ new Map();
371
+ for (const record of records) {
372
+ if (!isShadowRecord(record)) continue;
373
+ const existing = buckets.get(record.value);
374
+ if (existing) {
375
+ existing.selectors.push(record.selector);
376
+ continue;
377
+ }
378
+ buckets.set(record.value, {
379
+ value: record.value,
380
+ selectors: [record.selector],
381
+ firstRecord: record
382
+ });
383
+ }
384
+ return Array.from(buckets.values(), bucketToToken);
385
+ }
386
+
387
+ // src/categorize/spacing.ts
388
+ var SPACING_PROPERTIES = [
389
+ "padding",
390
+ "padding-top",
391
+ "padding-right",
392
+ "padding-bottom",
393
+ "padding-left",
394
+ "margin",
395
+ "margin-top",
396
+ "margin-right",
397
+ "margin-bottom",
398
+ "margin-left",
399
+ "gap",
400
+ "row-gap",
401
+ "column-gap"
402
+ ];
403
+ var SKIPPED_VALUES3 = /* @__PURE__ */ new Set(["auto", "inherit"]);
404
+ function isSpacingProperty(property) {
405
+ return SPACING_PROPERTIES.includes(property);
406
+ }
407
+ function canonicalize(rawValue) {
408
+ const value = rawValue.trim();
409
+ if (value === "") return null;
410
+ const lowered = value.toLowerCase();
411
+ if (SKIPPED_VALUES3.has(lowered)) return null;
412
+ if (value === "0" || lowered === "0px") return "0";
413
+ return value;
414
+ }
415
+ function buildToken(bucket) {
416
+ const extensions = {
417
+ "com.dte.usage": {
418
+ selectors: bucket.selectors.slice(),
419
+ count: bucket.count
420
+ },
421
+ "com.dte.confidence": 0,
422
+ "com.dte.source": bucket.source
423
+ };
424
+ return {
425
+ $value: bucket.value,
426
+ $type: "dimension",
427
+ $extensions: extensions
428
+ };
429
+ }
430
+ function categorizeSpacing(records) {
431
+ const buckets = /* @__PURE__ */ new Map();
432
+ for (const record of records) {
433
+ if (!isSpacingProperty(record.property)) continue;
434
+ const canonicalValue = canonicalize(record.value);
435
+ if (canonicalValue === null) continue;
436
+ const existing = buckets.get(canonicalValue);
437
+ if (existing === void 0) {
438
+ buckets.set(canonicalValue, {
439
+ value: canonicalValue,
440
+ selectors: [record.selector],
441
+ count: 1,
442
+ source: record.source
443
+ });
444
+ continue;
445
+ }
446
+ existing.count += 1;
447
+ if (!existing.selectors.includes(record.selector)) {
448
+ existing.selectors.push(record.selector);
449
+ }
450
+ }
451
+ const tokens = [];
452
+ for (const bucket of buckets.values()) {
453
+ tokens.push(buildToken(bucket));
454
+ }
455
+ return tokens;
456
+ }
457
+
458
+ // src/categorize/typography.ts
459
+ var SKIP_KEYWORDS = /* @__PURE__ */ new Set(["normal", "inherit", "initial"]);
460
+ function parsePlainNumber(value) {
461
+ const trimmed = value.trim();
462
+ if (!/^-?\d+(?:\.\d+)?$/.test(trimmed)) return void 0;
463
+ const parsed = Number(trimmed);
464
+ return Number.isFinite(parsed) ? parsed : void 0;
465
+ }
466
+ function normalize(record) {
467
+ const { property, value } = record;
468
+ const trimmed = value.trim();
469
+ if (trimmed.length === 0) return void 0;
470
+ if (SKIP_KEYWORDS.has(trimmed.toLowerCase())) return void 0;
471
+ switch (property) {
472
+ case "font-family":
473
+ return { bucket: "family", value: trimmed, type: "fontFamily" };
474
+ case "font-size":
475
+ return { bucket: "size", value: trimmed, type: "dimension" };
476
+ case "font-weight": {
477
+ const numeric = parsePlainNumber(trimmed);
478
+ if (numeric !== void 0) {
479
+ return { bucket: "weight", value: numeric, type: "fontWeight" };
480
+ }
481
+ return { bucket: "weight", value: trimmed, type: "fontWeight" };
482
+ }
483
+ case "line-height": {
484
+ const numeric = parsePlainNumber(trimmed);
485
+ if (numeric !== void 0) {
486
+ return { bucket: "lineHeight", value: numeric, type: "number" };
487
+ }
488
+ return { bucket: "lineHeight", value: trimmed, type: "dimension" };
489
+ }
490
+ case "letter-spacing":
491
+ return { bucket: "letterSpacing", value: trimmed, type: "dimension" };
492
+ default:
493
+ return void 0;
494
+ }
495
+ }
496
+ function makeToken(entry, record) {
497
+ const token = {
498
+ $value: entry.value,
499
+ $type: entry.type,
500
+ $extensions: {
501
+ "com.dte.usage": { selectors: [record.selector], count: 1 },
502
+ "com.dte.confidence": 0
503
+ }
504
+ };
505
+ token.$extensions["com.dte.source"] = record.source;
506
+ token.$extensions["com.dte.theme"] = record.theme;
507
+ if (record.originalVar !== void 0) {
508
+ token.$extensions["com.dte.unresolvedVar"] = record.originalVar;
509
+ }
510
+ return token;
511
+ }
512
+ function accumulate(existing, record) {
513
+ const usage = existing.$extensions["com.dte.usage"];
514
+ usage.selectors.push(record.selector);
515
+ usage.count += 1;
516
+ }
517
+ function categorizeTypography(records) {
518
+ const buckets = {
519
+ family: [],
520
+ size: [],
521
+ weight: [],
522
+ lineHeight: [],
523
+ letterSpacing: []
524
+ };
525
+ const dedup = {
526
+ family: /* @__PURE__ */ new Map(),
527
+ size: /* @__PURE__ */ new Map(),
528
+ weight: /* @__PURE__ */ new Map(),
529
+ lineHeight: /* @__PURE__ */ new Map(),
530
+ letterSpacing: /* @__PURE__ */ new Map()
531
+ };
532
+ for (const record of records) {
533
+ const entry = normalize(record);
534
+ if (entry === void 0) continue;
535
+ const key = `${typeof entry.value}:${String(entry.value)}`;
536
+ const bucketMap = dedup[entry.bucket];
537
+ const existing = bucketMap.get(key);
538
+ if (existing !== void 0) {
539
+ accumulate(existing, record);
540
+ continue;
541
+ }
542
+ const token = makeToken(entry, record);
543
+ bucketMap.set(key, token);
544
+ buckets[entry.bucket].push(token);
545
+ }
546
+ return buckets;
547
+ }
548
+
549
+ // src/categorize/zindex.ts
550
+ function categorizeZIndex(records) {
551
+ const tokensByValue = /* @__PURE__ */ new Map();
552
+ for (const record of records) {
553
+ if (record.property !== "z-index") continue;
554
+ const numericValue = parseZIndexValue(record.value);
555
+ if (numericValue === null) continue;
556
+ const existing = tokensByValue.get(numericValue);
557
+ if (existing) {
558
+ appendUsage(existing, record.selector);
559
+ continue;
560
+ }
561
+ tokensByValue.set(numericValue, buildToken2(numericValue, record.selector));
562
+ }
563
+ return Array.from(tokensByValue.values());
564
+ }
565
+ function parseZIndexValue(raw) {
566
+ const trimmed = raw.trim();
567
+ if (trimmed === "") return null;
568
+ const parsed = parseInt(trimmed, 10);
569
+ if (Number.isNaN(parsed)) return null;
570
+ return parsed;
571
+ }
572
+ function buildToken2(value, selector) {
573
+ return {
574
+ $value: value,
575
+ $type: "number",
576
+ $extensions: {
577
+ "com.dte.usage": { selectors: [selector], count: 1 },
578
+ "com.dte.confidence": 0
579
+ }
580
+ };
581
+ }
582
+ function appendUsage(token, selector) {
583
+ const usage = token.$extensions["com.dte.usage"];
584
+ usage.count += 1;
585
+ if (!usage.selectors.includes(selector)) {
586
+ usage.selectors.push(selector);
587
+ }
588
+ }
589
+
590
+ // src/dedup.ts
591
+ function stringifyValue(value) {
592
+ if (typeof value === "string") return value;
593
+ if (typeof value === "number") return String(value);
594
+ const keys = Object.keys(value).sort();
595
+ const ordered = {};
596
+ for (const key of keys) {
597
+ ordered[key] = value[key];
598
+ }
599
+ return JSON.stringify(ordered);
600
+ }
601
+ function mergeKey(token) {
602
+ const theme = token.$extensions["com.dte.theme"] ?? "none";
603
+ return `${token.$type}::${stringifyValue(token.$value)}::${theme}`;
604
+ }
605
+ function unionSelectors(a, b) {
606
+ const seen = /* @__PURE__ */ new Set();
607
+ const out = [];
608
+ for (const selector of a) {
609
+ if (seen.has(selector)) continue;
610
+ seen.add(selector);
611
+ out.push(selector);
612
+ }
613
+ for (const selector of b) {
614
+ if (seen.has(selector)) continue;
615
+ seen.add(selector);
616
+ out.push(selector);
617
+ }
618
+ return out;
619
+ }
620
+ function mergeTokens(existing, incoming) {
621
+ const existingUsage = existing.$extensions["com.dte.usage"];
622
+ const incomingUsage = incoming.$extensions["com.dte.usage"];
623
+ const mergedExtensions = {
624
+ ...existing.$extensions,
625
+ "com.dte.usage": {
626
+ selectors: unionSelectors(existingUsage.selectors, incomingUsage.selectors),
627
+ count: existingUsage.count + incomingUsage.count
628
+ }
629
+ };
630
+ const existingSource = existing.$extensions["com.dte.source"];
631
+ const incomingSource = incoming.$extensions["com.dte.source"];
632
+ if (existingSource === "stylesheet" || incomingSource === "stylesheet") {
633
+ mergedExtensions["com.dte.source"] = "stylesheet";
634
+ }
635
+ return {
636
+ ...existing,
637
+ $extensions: mergedExtensions
638
+ };
639
+ }
640
+ function dedupTokens(tokens) {
641
+ const order = [];
642
+ const merged = /* @__PURE__ */ new Map();
643
+ for (const token of tokens) {
644
+ const key = mergeKey(token);
645
+ const existing = merged.get(key);
646
+ if (existing === void 0) {
647
+ order.push(key);
648
+ merged.set(key, token);
649
+ continue;
650
+ }
651
+ merged.set(key, mergeTokens(existing, token));
652
+ }
653
+ return order.map((key) => merged.get(key));
654
+ }
655
+
656
+ // src/name.ts
657
+ function tieBreakKey(value) {
658
+ if (typeof value === "string") return value;
659
+ if (typeof value === "number") return String(value);
660
+ const keys = Object.keys(value).sort();
661
+ const ordered = {};
662
+ for (const key of keys) {
663
+ ordered[key] = value[key];
664
+ }
665
+ return JSON.stringify(ordered);
666
+ }
667
+ function nameBucket(tokens, prefix) {
668
+ const sorted = tokens.slice().sort((a, b) => {
669
+ const countDiff = b.$extensions["com.dte.usage"].count - a.$extensions["com.dte.usage"].count;
670
+ if (countDiff !== 0) return countDiff;
671
+ const aKey = tieBreakKey(a.$value);
672
+ const bKey = tieBreakKey(b.$value);
673
+ if (aKey < bKey) return -1;
674
+ if (aKey > bKey) return 1;
675
+ return 0;
676
+ });
677
+ const named = {};
678
+ sorted.forEach((token, index) => {
679
+ named[`${prefix}-${index + 1}`] = token;
680
+ });
681
+ return named;
682
+ }
683
+
684
+ // src/render/playwright.ts
685
+ import { stat } from "fs/promises";
686
+ import { resolve } from "path";
687
+ import { pathToFileURL } from "url";
688
+ import { chromium } from "playwright";
689
+
690
+ // src/render/extract-in-page.ts
691
+ var extractInPageFromGlobals = (theme) => {
692
+ const properties = [
693
+ // Color
694
+ "color",
695
+ "background-color",
696
+ "border-color",
697
+ "border-top-color",
698
+ "border-right-color",
699
+ "border-bottom-color",
700
+ "border-left-color",
701
+ "outline-color",
702
+ // Typography
703
+ "font-family",
704
+ "font-size",
705
+ "font-weight",
706
+ "line-height",
707
+ "letter-spacing",
708
+ "text-transform",
709
+ "text-decoration",
710
+ // Spacing — padding
711
+ "padding",
712
+ "padding-top",
713
+ "padding-right",
714
+ "padding-bottom",
715
+ "padding-left",
716
+ // Spacing — margin & gap
717
+ "margin",
718
+ "margin-top",
719
+ "margin-right",
720
+ "margin-bottom",
721
+ "margin-left",
722
+ "gap",
723
+ // Radius
724
+ "border-radius",
725
+ "border-top-left-radius",
726
+ "border-top-right-radius",
727
+ "border-bottom-right-radius",
728
+ "border-bottom-left-radius",
729
+ // Shadow
730
+ "box-shadow",
731
+ "text-shadow",
732
+ // Z-index
733
+ "z-index",
734
+ // Motion
735
+ "transition-duration",
736
+ "transition-timing-function",
737
+ "animation-duration",
738
+ "animation-timing-function"
739
+ ];
740
+ const buildSelectorLocal = (element) => {
741
+ const tag = element.tagName.toLowerCase();
742
+ const id = element.id ? `#${element.id}` : "";
743
+ const classList = element.classList.length > 0 ? `.${Array.from(element.classList).join(".")}` : "";
744
+ return `${tag}${id}${classList}`;
745
+ };
746
+ const resolveSourceLocal = (element, property) => {
747
+ const styledElement = element;
748
+ const inline = styledElement.style?.getPropertyValue(property) ?? "";
749
+ return inline !== "" ? "inline" : "stylesheet";
750
+ };
751
+ const records = [];
752
+ const elements = /* @__PURE__ */ new Set();
753
+ elements.add(document.documentElement);
754
+ document.querySelectorAll("*").forEach((el) => elements.add(el));
755
+ for (const element of elements) {
756
+ const computed = window.getComputedStyle(element);
757
+ const selector = buildSelectorLocal(element);
758
+ for (const property of properties) {
759
+ const value = computed.getPropertyValue(property).trim();
760
+ if (value === "") continue;
761
+ if (property === "z-index" && value === "auto") continue;
762
+ records.push({
763
+ selector,
764
+ property,
765
+ value,
766
+ source: resolveSourceLocal(element, property),
767
+ theme,
768
+ scope: ":root"
769
+ });
770
+ }
771
+ }
772
+ return records;
773
+ };
774
+
775
+ // src/render/playwright.ts
776
+ var isPrivateHost = (hostname) => {
777
+ const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
778
+ if (h === "localhost" || h === "ip6-localhost" || h === "ip6-loopback") {
779
+ return true;
780
+ }
781
+ if (h === "::1" || h.startsWith("fe80:") || /^f[cd][0-9a-f]{2}:/i.test(h)) {
782
+ return true;
783
+ }
784
+ const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
785
+ if (!m) return false;
786
+ const [a, b] = [Number(m[1]), Number(m[2])];
787
+ if (a === 10) return true;
788
+ if (a === 127) return true;
789
+ if (a === 169 && b === 254) return true;
790
+ if (a === 172 && b >= 16 && b <= 31) return true;
791
+ if (a === 192 && b === 168) return true;
792
+ if (a === 0) return true;
793
+ return false;
794
+ };
795
+ var resolveTargetUrl = async (input) => {
796
+ if (input.kind === "url") {
797
+ return input.url;
798
+ }
799
+ const absolute = resolve(input.path);
800
+ try {
801
+ await stat(absolute);
802
+ } catch (err) {
803
+ throw new ExtractionError(`File not found: ${absolute}`, { cause: err });
804
+ }
805
+ return pathToFileURL(absolute).href;
806
+ };
807
+ var withTimeout = (operation, timeoutMs) => {
808
+ let timer;
809
+ const timeout = new Promise((_, reject) => {
810
+ timer = setTimeout(() => {
811
+ const seconds = timeoutMs / 1e3;
812
+ reject(
813
+ new ExtractionError(
814
+ `Extraction timed out after ${seconds}s \u2014 consider --file or --fast`
815
+ )
816
+ );
817
+ }, timeoutMs);
818
+ });
819
+ return {
820
+ result: Promise.race([operation, timeout]),
821
+ cancel: () => {
822
+ if (timer !== void 0) clearTimeout(timer);
823
+ }
824
+ };
825
+ };
826
+ var asExtractionError = (err) => {
827
+ if (err instanceof ExtractionError) return err;
828
+ const message = err instanceof Error ? err.message : String(err);
829
+ return new ExtractionError(`Render failed: ${message}`, { cause: err });
830
+ };
831
+ var render = async (input, theme, timeoutMs, opts) => {
832
+ const targetUrl = await resolveTargetUrl(input);
833
+ let browser;
834
+ const onSigint = () => {
835
+ if (browser && browser.isConnected()) {
836
+ void browser.close();
837
+ }
838
+ };
839
+ process.once("SIGINT", onSigint);
840
+ const allowPrivate = opts?.allowPrivateHosts === true;
841
+ if (input.kind === "url" && !allowPrivate) {
842
+ const hostname = new URL(targetUrl).hostname;
843
+ if (isPrivateHost(hostname)) {
844
+ throw new ExtractionError(
845
+ `Refusing to navigate to private host ${hostname} \u2014 pass --allow-private-hosts to override`
846
+ );
847
+ }
848
+ }
849
+ const operation = (async () => {
850
+ browser = await chromium.launch({ headless: true });
851
+ opts?.onBrowser?.(browser);
852
+ const context = await browser.newContext();
853
+ if (!allowPrivate) {
854
+ await context.route("**/*", async (route) => {
855
+ try {
856
+ const hostname = new URL(route.request().url()).hostname;
857
+ if (hostname && isPrivateHost(hostname)) {
858
+ await route.abort("blockedbyclient");
859
+ return;
860
+ }
861
+ } catch {
862
+ }
863
+ await route.continue();
864
+ });
865
+ }
866
+ const page = await context.newPage();
867
+ await page.emulateMedia({ colorScheme: theme });
868
+ await page.goto(targetUrl, { waitUntil: "networkidle" });
869
+ const records = await page.evaluate(extractInPageFromGlobals, theme);
870
+ return records;
871
+ })();
872
+ const { result, cancel } = withTimeout(operation, timeoutMs);
873
+ try {
874
+ return await result;
875
+ } catch (err) {
876
+ throw asExtractionError(err);
877
+ } finally {
878
+ cancel();
879
+ process.removeListener("SIGINT", onSigint);
880
+ if (browser) {
881
+ try {
882
+ await browser.close();
883
+ } catch {
884
+ }
885
+ }
886
+ }
887
+ };
888
+
889
+ // src/sources/file.ts
890
+ import { lstat, readFile } from "fs/promises";
891
+ import { extname, resolve as resolve2 } from "path";
892
+ async function loadFile(inputPath) {
893
+ const absPath = resolve2(inputPath.trim());
894
+ const stats = await statOrThrow(absPath);
895
+ if (stats.isDirectory()) {
896
+ throw new UserError(`Not a file: ${absPath}`);
897
+ }
898
+ if (stats.isSymbolicLink()) {
899
+ throw new UserError(`Symlinks are not supported: ${absPath}`);
900
+ }
901
+ if (extname(absPath).toLowerCase() !== ".html") {
902
+ process.stderr.write(
903
+ `warning: ${absPath} does not have a .html extension
904
+ `
905
+ );
906
+ }
907
+ const html = await readFile(absPath, "utf8");
908
+ return { absPath, html };
909
+ }
910
+ async function statOrThrow(absPath) {
911
+ try {
912
+ return await lstat(absPath);
913
+ } catch (error) {
914
+ if (isNodeErrnoException(error) && error.code === "ENOENT") {
915
+ throw new UserError(`File not found: ${absPath}`, { cause: error });
916
+ }
917
+ throw error;
918
+ }
919
+ }
920
+ function isNodeErrnoException(error) {
921
+ return error instanceof Error && typeof error.code === "string";
922
+ }
923
+
924
+ // src/sources/url.ts
925
+ var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
926
+ function parseUrl(input) {
927
+ const trimmed = input.trim();
928
+ if (trimmed.length === 0) {
929
+ throw new UserError("URL is required");
930
+ }
931
+ let parsed;
932
+ try {
933
+ parsed = new URL(trimmed);
934
+ } catch (cause) {
935
+ throw new UserError(`Invalid URL: ${redactUserinfo(trimmed)}`, { cause });
936
+ }
937
+ if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
938
+ throw new UserError(
939
+ `Only http:// or https:// URLs are supported, got: ${parsed.protocol}`
940
+ );
941
+ }
942
+ return parsed;
943
+ }
944
+ function redactUserinfo(raw) {
945
+ return raw.replace(/^(\w+:\/\/)[^/@]+@/, "$1[REDACTED]@");
946
+ }
947
+
948
+ // src/extract.ts
949
+ var VERSION = true ? "1.3.4" : "0.0.0-dev";
950
+ async function extract(opts) {
951
+ const sourceValue = await resolveInput(opts);
952
+ const records = await gatherRecords(opts);
953
+ const tokenSet = buildTokenSet(records, {
954
+ extractor: "design-token-extractor",
955
+ version: VERSION,
956
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
957
+ source: { kind: opts.input.kind, value: sourceValue }
958
+ });
959
+ return tokenSet;
960
+ }
961
+ async function resolveInput(opts) {
962
+ if (opts.input.kind === "url") {
963
+ const parsed = parseUrl(opts.input.url);
964
+ parsed.username = "";
965
+ parsed.password = "";
966
+ return parsed.toString();
967
+ }
968
+ const { absPath } = await loadFile(opts.input.path);
969
+ return basename(absPath);
970
+ }
971
+ async function gatherRecords(opts) {
972
+ const renderOpts = { allowPrivateHosts: opts.allowPrivateHosts };
973
+ if (opts.theme === "light" || opts.theme === "dark") {
974
+ return render(opts.input, opts.theme, opts.timeoutMs, renderOpts);
975
+ }
976
+ const lightRecords = await render(opts.input, "light", opts.timeoutMs, renderOpts);
977
+ const darkRecords = await render(opts.input, "dark", opts.timeoutMs, renderOpts);
978
+ if (recordsEqual(lightRecords, darkRecords)) {
979
+ return lightRecords.map((r) => ({ ...r, theme: "light" }));
980
+ }
981
+ return [...lightRecords, ...darkRecords];
982
+ }
983
+ function recordsEqual(left, right) {
984
+ if (left.length !== right.length) return false;
985
+ for (let i = 0; i < left.length; i++) {
986
+ const a = left[i];
987
+ const b = right[i];
988
+ if (a === void 0 || b === void 0) return false;
989
+ if (a.selector !== b.selector || a.property !== b.property || a.value !== b.value || a.source !== b.source || a.scope !== b.scope || a.originalVar !== b.originalVar) {
990
+ return false;
991
+ }
992
+ }
993
+ return true;
994
+ }
995
+ function finalizeBucket(tokens, prefix) {
996
+ const deduped = dedupTokens(tokens);
997
+ const scored = applyScores(deduped);
998
+ return nameBucket(scored, prefix);
999
+ }
1000
+ function categorizeOne(records) {
1001
+ const typography = categorizeTypography(records);
1002
+ const motion = categorizeMotion(records);
1003
+ return {
1004
+ color: categorizeColors(records),
1005
+ family: typography.family,
1006
+ size: typography.size,
1007
+ weight: typography.weight,
1008
+ lineHeight: typography.lineHeight,
1009
+ letterSpacing: typography.letterSpacing,
1010
+ spacing: categorizeSpacing(records),
1011
+ radius: categorizeRadius(records),
1012
+ shadow: categorizeShadow(records),
1013
+ zIndex: categorizeZIndex(records),
1014
+ durationMotion: motion.duration,
1015
+ easingMotion: motion.easing
1016
+ };
1017
+ }
1018
+ function categorizeByTheme(records) {
1019
+ const lightRecords = records.filter((r) => r.theme === "light");
1020
+ const darkRecords = records.filter((r) => r.theme === "dark");
1021
+ if (lightRecords.length === 0 || darkRecords.length === 0) {
1022
+ return categorizeOne(records);
1023
+ }
1024
+ const light = categorizeOne(lightRecords);
1025
+ const dark = categorizeOne(darkRecords);
1026
+ return {
1027
+ color: [...light.color, ...dark.color],
1028
+ family: [...light.family, ...dark.family],
1029
+ size: [...light.size, ...dark.size],
1030
+ weight: [...light.weight, ...dark.weight],
1031
+ lineHeight: [...light.lineHeight, ...dark.lineHeight],
1032
+ letterSpacing: [...light.letterSpacing, ...dark.letterSpacing],
1033
+ spacing: [...light.spacing, ...dark.spacing],
1034
+ radius: [...light.radius, ...dark.radius],
1035
+ shadow: [...light.shadow, ...dark.shadow],
1036
+ zIndex: [...light.zIndex, ...dark.zIndex],
1037
+ durationMotion: [...light.durationMotion, ...dark.durationMotion],
1038
+ easingMotion: [...light.easingMotion, ...dark.easingMotion]
1039
+ };
1040
+ }
1041
+ function buildTokenSet(records, metadata) {
1042
+ const buckets = categorizeByTheme(records);
1043
+ const color = finalizeBucket(buckets.color, "color");
1044
+ const typography = {
1045
+ family: finalizeBucket(buckets.family, "font-family"),
1046
+ size: finalizeBucket(buckets.size, "font-size"),
1047
+ weight: finalizeBucket(buckets.weight, "font-weight"),
1048
+ lineHeight: finalizeBucket(buckets.lineHeight, "line-height"),
1049
+ letterSpacing: finalizeBucket(buckets.letterSpacing, "letter-spacing")
1050
+ };
1051
+ const spacing = finalizeBucket(buckets.spacing, "spacing");
1052
+ const radius = finalizeBucket(buckets.radius, "radius");
1053
+ const shadow = finalizeBucket(buckets.shadow, "shadow");
1054
+ const zIndex = finalizeBucket(buckets.zIndex, "z");
1055
+ const breakpoint = {};
1056
+ const motion = {
1057
+ duration: finalizeBucket(buckets.durationMotion, "duration"),
1058
+ easing: finalizeBucket(buckets.easingMotion, "easing")
1059
+ };
1060
+ return {
1061
+ $schema: "https://design-tokens.github.io/community-group/format/",
1062
+ $metadata: metadata,
1063
+ color,
1064
+ typography,
1065
+ spacing,
1066
+ radius,
1067
+ shadow,
1068
+ zIndex,
1069
+ breakpoint,
1070
+ motion
1071
+ };
1072
+ }
1073
+
1074
+ // src/format/css.ts
1075
+ import postcss from "postcss";
1076
+ var FLAT_CATEGORIES = [
1077
+ "color",
1078
+ "spacing",
1079
+ "radius",
1080
+ "shadow",
1081
+ "zIndex",
1082
+ "breakpoint"
1083
+ ];
1084
+ var SUB_CATEGORIES = ["typography", "motion"];
1085
+ var CATEGORY_ORDER = [
1086
+ "color",
1087
+ "typography",
1088
+ "spacing",
1089
+ "radius",
1090
+ "shadow",
1091
+ "zIndex",
1092
+ "breakpoint",
1093
+ "motion"
1094
+ ];
1095
+ function themeOf(token) {
1096
+ return token.$extensions["com.dte.theme"] ?? "light";
1097
+ }
1098
+ function renderValue(value) {
1099
+ const raw = typeof value === "string" ? value : typeof value === "number" ? String(value) : JSON.stringify(value);
1100
+ if (/[{}]/.test(raw)) {
1101
+ return raw.replace(/[{}]/g, "");
1102
+ }
1103
+ return raw;
1104
+ }
1105
+ function flatEntriesForTheme(collection, theme) {
1106
+ return Object.entries(collection).filter(
1107
+ ([, token]) => themeOf(token) === theme
1108
+ );
1109
+ }
1110
+ function subEntriesForTheme(collection, theme) {
1111
+ const out = [];
1112
+ for (const subkey of Object.keys(collection)) {
1113
+ const bucket = collection[subkey] ?? {};
1114
+ for (const [name, token] of Object.entries(bucket)) {
1115
+ if (themeOf(token) === theme) {
1116
+ out.push([name, token]);
1117
+ }
1118
+ }
1119
+ }
1120
+ return out;
1121
+ }
1122
+ function entriesForCategory(set, category, theme) {
1123
+ if (SUB_CATEGORIES.includes(category)) {
1124
+ return subEntriesForTheme(set[category], theme);
1125
+ }
1126
+ return flatEntriesForTheme(set[category], theme);
1127
+ }
1128
+ function emitCategory(name, entries, indent) {
1129
+ if (entries.length === 0) return "";
1130
+ const header = `${indent}/* ==== ${name} ==== */
1131
+ `;
1132
+ const decls = entries.map(([key, token]) => `${indent}--${key}: ${renderValue(token.$value)};`).join("\n");
1133
+ return `${header}${decls}
1134
+ `;
1135
+ }
1136
+ function emitRoot(set, theme, indent) {
1137
+ const sections = [];
1138
+ for (const category of CATEGORY_ORDER) {
1139
+ const entries = entriesForCategory(set, category, theme);
1140
+ const section = emitCategory(category, entries, indent);
1141
+ if (section.length > 0) sections.push(section);
1142
+ }
1143
+ return sections.join("\n");
1144
+ }
1145
+ function hasDarkTokens(set) {
1146
+ for (const category of FLAT_CATEGORIES) {
1147
+ if (flatEntriesForTheme(set[category], "dark").length > 0) return true;
1148
+ }
1149
+ for (const category of SUB_CATEGORIES) {
1150
+ if (subEntriesForTheme(set[category], "dark").length > 0) return true;
1151
+ }
1152
+ return false;
1153
+ }
1154
+ function formatCss(tokenSet) {
1155
+ const lightBody = emitRoot(tokenSet, "light", " ");
1156
+ let output = `:root {
1157
+ ${lightBody}}
1158
+ `;
1159
+ if (hasDarkTokens(tokenSet)) {
1160
+ const darkBody = emitRoot(tokenSet, "dark", " ");
1161
+ output += `
1162
+ @media (prefers-color-scheme: dark) {
1163
+ :root {
1164
+ ${darkBody} }
1165
+ }
1166
+ `;
1167
+ }
1168
+ return output;
1169
+ }
1170
+
1171
+ // src/format/js.ts
1172
+ var INDENT = 2;
1173
+ function formatJs(tokenSet) {
1174
+ const body = JSON.stringify(tokenSet, null, INDENT);
1175
+ return `export default ${body};
1176
+ `;
1177
+ }
1178
+
1179
+ // src/format/json.ts
1180
+ import { z } from "zod";
1181
+ var DTCG_SCHEMA_URL = "https://design-tokens.github.io/community-group/format/";
1182
+ var tokenTypeSchema = z.enum([
1183
+ "color",
1184
+ "dimension",
1185
+ "fontFamily",
1186
+ "fontWeight",
1187
+ "duration",
1188
+ "cubicBezier",
1189
+ "shadow",
1190
+ "number",
1191
+ "other"
1192
+ ]);
1193
+ var tokenValueSchema = z.union([
1194
+ z.string(),
1195
+ z.number(),
1196
+ z.record(z.string(), z.unknown())
1197
+ ]);
1198
+ var tokenUsageSchema = z.object({
1199
+ selectors: z.array(z.string()),
1200
+ count: z.number().int().min(0)
1201
+ });
1202
+ var tokenExtensionsSchema = z.object({
1203
+ "com.dte.usage": tokenUsageSchema,
1204
+ "com.dte.confidence": z.number().min(0).max(1),
1205
+ "com.dte.source": z.enum(["stylesheet", "inline"]).optional(),
1206
+ "com.dte.unresolvedVar": z.string().optional(),
1207
+ "com.dte.theme": z.enum(["light", "dark"]).optional()
1208
+ });
1209
+ var tokenSchema = z.object({
1210
+ $value: tokenValueSchema,
1211
+ $type: tokenTypeSchema,
1212
+ $description: z.string().optional(),
1213
+ $extensions: tokenExtensionsSchema
1214
+ });
1215
+ var tokenCollectionSchema = z.record(z.string(), tokenSchema);
1216
+ var subcategoryCollectionSchema = z.record(z.string(), tokenCollectionSchema);
1217
+ var tokenSetMetadataSchema = z.object({
1218
+ extractor: z.literal("design-token-extractor"),
1219
+ version: z.string(),
1220
+ extractedAt: z.string(),
1221
+ source: z.object({
1222
+ kind: z.enum(["url", "file"]),
1223
+ value: z.string()
1224
+ })
1225
+ });
1226
+ var tokenSetSchema = z.object({
1227
+ $schema: z.literal(DTCG_SCHEMA_URL),
1228
+ $metadata: tokenSetMetadataSchema,
1229
+ color: tokenCollectionSchema,
1230
+ typography: subcategoryCollectionSchema,
1231
+ spacing: tokenCollectionSchema,
1232
+ radius: tokenCollectionSchema,
1233
+ shadow: tokenCollectionSchema,
1234
+ zIndex: tokenCollectionSchema,
1235
+ breakpoint: tokenCollectionSchema,
1236
+ motion: subcategoryCollectionSchema
1237
+ });
1238
+ function formatJson(tokenSet) {
1239
+ const validated = tokenSetSchema.parse(tokenSet);
1240
+ return JSON.stringify(validated, null, 2);
1241
+ }
1242
+
1243
+ // src/format/md.ts
1244
+ var FLAT_CATEGORIES2 = [
1245
+ "color",
1246
+ "spacing",
1247
+ "radius",
1248
+ "shadow",
1249
+ "zIndex",
1250
+ "breakpoint"
1251
+ ];
1252
+ var NESTED_CATEGORIES = [
1253
+ "typography",
1254
+ "motion"
1255
+ ];
1256
+ function formatMarkdown(tokenSet) {
1257
+ const sections = [];
1258
+ sections.push(renderIntro(tokenSet));
1259
+ for (const key of FLAT_CATEGORIES2) {
1260
+ const section = renderFlatSection(key, tokenSet[key]);
1261
+ if (section !== null) {
1262
+ sections.push(section);
1263
+ }
1264
+ }
1265
+ for (const key of NESTED_CATEGORIES) {
1266
+ const section = renderNestedSection(key, tokenSet[key]);
1267
+ if (section !== null) {
1268
+ sections.push(section);
1269
+ }
1270
+ }
1271
+ return sections.join("\n") + "\n";
1272
+ }
1273
+ function renderIntro(tokenSet) {
1274
+ const { extractedAt } = tokenSet.$metadata;
1275
+ return `# Design Tokens
1276
+
1277
+ Extracted at ${extractedAt}.
1278
+ `;
1279
+ }
1280
+ function renderFlatSection(key, tokens) {
1281
+ const entries = Object.entries(tokens);
1282
+ if (entries.length === 0) {
1283
+ return null;
1284
+ }
1285
+ const heading = `## ${capitalize(key)}`;
1286
+ const table = key === "color" ? renderColorTable(entries) : renderPlainTable(entries);
1287
+ return `${heading}
1288
+
1289
+ ${table}`;
1290
+ }
1291
+ function renderNestedSection(key, subcategories) {
1292
+ const nonEmptySubcategories = Object.entries(subcategories).filter(
1293
+ ([, collection]) => Object.keys(collection).length > 0
1294
+ );
1295
+ if (nonEmptySubcategories.length === 0) {
1296
+ return null;
1297
+ }
1298
+ const parts = [`## ${capitalize(key)}`, ""];
1299
+ for (const [subKey, collection] of nonEmptySubcategories) {
1300
+ const entries = Object.entries(collection);
1301
+ parts.push(`### ${capitalize(subKey)}`);
1302
+ parts.push("");
1303
+ parts.push(renderPlainTable(entries));
1304
+ parts.push("");
1305
+ }
1306
+ while (parts.length > 0 && parts[parts.length - 1] === "") {
1307
+ parts.pop();
1308
+ }
1309
+ return parts.join("\n") + "\n";
1310
+ }
1311
+ function renderColorTable(entries) {
1312
+ const header = "| Name | Swatch | Value | Count | Confidence |";
1313
+ const separator = "| --- | --- | --- | --- | --- |";
1314
+ const rows = entries.map(([name, token]) => {
1315
+ const value = formatValue(token.$value);
1316
+ const swatch = renderSwatch(value);
1317
+ const count = token.$extensions["com.dte.usage"].count;
1318
+ const confidence = token.$extensions["com.dte.confidence"];
1319
+ return `| ${escapeMdCell(name)} | ${swatch} | ${escapeMdCell(value)} | ${count} | ${confidence} |`;
1320
+ });
1321
+ return [header, separator, ...rows].join("\n") + "\n";
1322
+ }
1323
+ function renderPlainTable(entries) {
1324
+ const header = "| Name | Value | Count | Confidence |";
1325
+ const separator = "| --- | --- | --- | --- |";
1326
+ const rows = entries.map(([name, token]) => {
1327
+ const value = formatValue(token.$value);
1328
+ const count = token.$extensions["com.dte.usage"].count;
1329
+ const confidence = token.$extensions["com.dte.confidence"];
1330
+ return `| ${escapeMdCell(name)} | ${escapeMdCell(value)} | ${count} | ${confidence} |`;
1331
+ });
1332
+ return [header, separator, ...rows].join("\n") + "\n";
1333
+ }
1334
+ function renderSwatch(value) {
1335
+ return `<span style="display:inline-block;width:16px;height:16px;background:${escapeHtml(value)};border:1px solid #ccc;"></span>`;
1336
+ }
1337
+ function escapeHtml(input) {
1338
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1339
+ }
1340
+ function escapeMdCell(input) {
1341
+ return input.replace(/\|/g, "\\|").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1342
+ }
1343
+ function formatValue(value) {
1344
+ if (typeof value === "string") {
1345
+ return value;
1346
+ }
1347
+ if (typeof value === "number") {
1348
+ return String(value);
1349
+ }
1350
+ return JSON.stringify(value);
1351
+ }
1352
+ function capitalize(input) {
1353
+ if (input.length === 0) {
1354
+ return input;
1355
+ }
1356
+ return input.charAt(0).toUpperCase() + input.slice(1);
1357
+ }
1358
+
1359
+ // src/io/write.ts
1360
+ import { rename, unlink, writeFile } from "fs/promises";
1361
+ import { dirname, resolve as resolve3, basename as basename2 } from "path";
1362
+ import { randomBytes } from "crypto";
1363
+ async function writeAtomic(content, outPath) {
1364
+ if (outPath === void 0 || outPath === "-") {
1365
+ process.stdout.write(content);
1366
+ return;
1367
+ }
1368
+ const absolute = resolve3(outPath);
1369
+ const targetDir = dirname(absolute);
1370
+ const targetName = basename2(absolute);
1371
+ const suffix = randomBytes(8).toString("hex");
1372
+ const tempPath = resolve3(targetDir, `.${targetName}.tmp-${suffix}`);
1373
+ try {
1374
+ await writeFile(tempPath, content, "utf8");
1375
+ await rename(tempPath, absolute);
1376
+ } catch (error) {
1377
+ try {
1378
+ await unlink(tempPath);
1379
+ } catch {
1380
+ }
1381
+ throw error;
1382
+ }
1383
+ }
1384
+
1385
+ // src/cli.ts
1386
+ var CLI_VERSION = true ? "1.3.4" : "0.0.0-dev";
1387
+ var inputSchema = z2.union([
1388
+ z2.object({ kind: z2.literal("url"), url: z2.string().url() }),
1389
+ z2.object({ kind: z2.literal("file"), path: z2.string().min(1) })
1390
+ ]);
1391
+ var cliOptionsSchema = z2.object({
1392
+ input: inputSchema,
1393
+ format: z2.enum(["json", "css", "js", "md"]),
1394
+ out: z2.string().min(1).optional(),
1395
+ timeoutMs: z2.number().int().positive(),
1396
+ minConfidence: z2.number().min(0).max(1),
1397
+ theme: z2.enum(["auto", "light", "dark"]),
1398
+ fast: z2.boolean(),
1399
+ allowPrivateHosts: z2.boolean()
1400
+ });
1401
+ async function resolveInput2(urlArg, fileFlag) {
1402
+ if (urlArg !== void 0 && fileFlag !== void 0) {
1403
+ throw new UserError("Specify either a URL or --file, not both");
1404
+ }
1405
+ if (urlArg === void 0 && fileFlag === void 0) {
1406
+ throw new UserError("Must specify a URL or --file <path>");
1407
+ }
1408
+ if (urlArg !== void 0) {
1409
+ const parsed = parseUrl(urlArg);
1410
+ return { kind: "url", url: parsed.toString() };
1411
+ }
1412
+ const { absPath } = await loadFile(fileFlag);
1413
+ return { kind: "file", path: absPath };
1414
+ }
1415
+ var MAX_TIMEOUT_SECONDS = 3600;
1416
+ function parseTimeoutMs(raw, fallbackMs) {
1417
+ if (raw === void 0) return fallbackMs;
1418
+ const seconds = Number.parseFloat(raw);
1419
+ if (!Number.isFinite(seconds) || seconds <= 0) {
1420
+ throw new UserError(`--timeout must be a positive number, got: ${raw}`);
1421
+ }
1422
+ if (seconds > MAX_TIMEOUT_SECONDS) {
1423
+ throw new UserError(
1424
+ `--timeout must not exceed ${MAX_TIMEOUT_SECONDS}s, got: ${raw}`
1425
+ );
1426
+ }
1427
+ return Math.round(seconds * 1e3);
1428
+ }
1429
+ function parseMinConfidence(raw, fallback) {
1430
+ if (raw === void 0) return fallback;
1431
+ const value = Number.parseFloat(raw);
1432
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
1433
+ throw new UserError(
1434
+ `--min-confidence must be between 0 and 1, got: ${raw}`
1435
+ );
1436
+ }
1437
+ return value;
1438
+ }
1439
+ function parseFormatFlag(raw) {
1440
+ const value = raw ?? "json";
1441
+ if (value !== "json" && value !== "css" && value !== "js" && value !== "md") {
1442
+ throw new UserError(
1443
+ `--format must be one of json|css|js|md, got: ${value}`
1444
+ );
1445
+ }
1446
+ return value;
1447
+ }
1448
+ function parseThemeFlag(raw) {
1449
+ const value = raw ?? "auto";
1450
+ if (value !== "auto" && value !== "light" && value !== "dark") {
1451
+ throw new UserError(
1452
+ `--theme must be one of auto|light|dark, got: ${value}`
1453
+ );
1454
+ }
1455
+ return value;
1456
+ }
1457
+ function filterByConfidence(tokenSet, threshold) {
1458
+ if (threshold <= 0) return tokenSet;
1459
+ const filterCollection = (collection) => {
1460
+ const out = {};
1461
+ for (const [key, token] of Object.entries(collection)) {
1462
+ if (token.$extensions["com.dte.confidence"] >= threshold) {
1463
+ out[key] = token;
1464
+ }
1465
+ }
1466
+ return out;
1467
+ };
1468
+ const filterSubcollection = (subcollection) => {
1469
+ const out = {};
1470
+ for (const [subKey, collection] of Object.entries(subcollection)) {
1471
+ out[subKey] = filterCollection(collection);
1472
+ }
1473
+ return out;
1474
+ };
1475
+ return {
1476
+ ...tokenSet,
1477
+ color: filterCollection(tokenSet.color),
1478
+ typography: filterSubcollection(tokenSet.typography),
1479
+ spacing: filterCollection(tokenSet.spacing),
1480
+ radius: filterCollection(tokenSet.radius),
1481
+ shadow: filterCollection(tokenSet.shadow),
1482
+ zIndex: filterCollection(tokenSet.zIndex),
1483
+ breakpoint: filterCollection(tokenSet.breakpoint),
1484
+ motion: filterSubcollection(tokenSet.motion)
1485
+ };
1486
+ }
1487
+ function serialize(tokenSet, format) {
1488
+ switch (format) {
1489
+ case "json":
1490
+ return formatJson(tokenSet);
1491
+ case "css":
1492
+ return formatCss(tokenSet);
1493
+ case "js":
1494
+ return formatJs(tokenSet);
1495
+ case "md":
1496
+ return formatMarkdown(tokenSet);
1497
+ }
1498
+ }
1499
+ function createSpinner(text) {
1500
+ if (!process.stdout.isTTY) return null;
1501
+ return ora({ text, stream: process.stderr }).start();
1502
+ }
1503
+ async function runExtract(urlArg, flags) {
1504
+ let spinner = null;
1505
+ try {
1506
+ const input = await resolveInput2(urlArg, flags.file);
1507
+ const rawOpts = {
1508
+ input,
1509
+ format: parseFormatFlag(flags.format),
1510
+ out: flags.out,
1511
+ timeoutMs: parseTimeoutMs(flags.timeout, 6e4),
1512
+ minConfidence: parseMinConfidence(flags.minConfidence, 0),
1513
+ theme: parseThemeFlag(flags.theme),
1514
+ fast: flags.fast === true,
1515
+ allowPrivateHosts: flags.allowPrivateHosts === true
1516
+ };
1517
+ const parsedOpts = cliOptionsSchema.parse(rawOpts);
1518
+ if (parsedOpts.fast) {
1519
+ process.stderr.write(
1520
+ "warning: --fast is not implemented in v1; proceeding with headless renderer\n"
1521
+ );
1522
+ }
1523
+ spinner = createSpinner("Extracting design tokens...");
1524
+ const tokenSet = await extract(parsedOpts);
1525
+ const filtered = filterByConfidence(tokenSet, parsedOpts.minConfidence);
1526
+ const serialized = serialize(filtered, parsedOpts.format);
1527
+ await writeAtomic(serialized, parsedOpts.out);
1528
+ if (spinner !== null) spinner.succeed("Done");
1529
+ } catch (err) {
1530
+ if (spinner !== null) spinner.fail();
1531
+ handleError(err);
1532
+ }
1533
+ }
1534
+ function handleError(err) {
1535
+ if (err instanceof z2.ZodError) {
1536
+ const messages = err.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
1537
+ process.stderr.write(`Invalid options: ${messages}
1538
+ `);
1539
+ process.exit(1);
1540
+ }
1541
+ if (err instanceof ExtractionError) {
1542
+ process.stderr.write(`${err.message}
1543
+ `);
1544
+ process.exit(err.exitCode);
1545
+ }
1546
+ if (err instanceof UserError) {
1547
+ process.stderr.write(`${err.message}
1548
+ `);
1549
+ process.exit(err.exitCode);
1550
+ }
1551
+ if (err instanceof DteError) {
1552
+ process.stderr.write(`${err.message}
1553
+ `);
1554
+ process.exit(err.exitCode);
1555
+ }
1556
+ const message = err instanceof Error ? err.message : String(err);
1557
+ process.stderr.write(`Internal error: ${message}
1558
+ `);
1559
+ process.exit(3);
1560
+ }
1561
+ function buildProgram() {
1562
+ const program = new Command();
1563
+ program.name("design-token-extractor").description("Extract design tokens from any website into W3C DTCG JSON").version(CLI_VERSION, "-V, --version", "Print version");
1564
+ program.command("extract").description("Extract tokens from a URL or local HTML file").argument(
1565
+ "[url]",
1566
+ "Target URL (http/https). Mutually exclusive with --file."
1567
+ ).option("--file <path>", "Extract from local HTML file").option("-f, --format <fmt>", "Output format: json | css | js | md", "json").option("-o, --out <path>", "Output file path (default: stdout)").option("--timeout <seconds>", "Extraction timeout", "60").option(
1568
+ "--min-confidence <num>",
1569
+ "Filter tokens below this confidence",
1570
+ "0"
1571
+ ).option(
1572
+ "--theme <mode>",
1573
+ "Theme emulation: auto | light | dark",
1574
+ "auto"
1575
+ ).option("--fast", "Skip headless browser; static HTML only (v2)").option("--user-agent <string>", "Override User-Agent (reserved for v2)").option(
1576
+ "--allow-private-hosts",
1577
+ "Allow navigation to private / loopback / link-local addresses"
1578
+ ).action(async (urlArg, flags) => {
1579
+ await runExtract(urlArg, flags);
1580
+ });
1581
+ program.exitOverride((err) => {
1582
+ if (err.code === "commander.helpDisplayed" || err.code === "commander.version") {
1583
+ process.exit(0);
1584
+ }
1585
+ process.exit(1);
1586
+ });
1587
+ return program;
1588
+ }
1589
+ async function main() {
1590
+ const program = buildProgram();
1591
+ try {
1592
+ await program.parseAsync(process.argv);
1593
+ } catch (err) {
1594
+ handleError(err);
1595
+ }
1596
+ }
1597
+ void main();