@buoy-design/scanners 0.3.38 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-design/scanners",
3
- "version": "0.3.38",
3
+ "version": "0.4.1",
4
4
  "description": "Source scanners for Buoy (React, Vue, Svelte, Angular, Figma, Storybook)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -51,10 +51,11 @@
51
51
  }
52
52
  },
53
53
  "dependencies": {
54
+ "@google/design.md": "0.1.1",
54
55
  "glob": "^11.0.0",
55
56
  "typescript": "^5.7.2",
56
57
  "zod": "^3.24.1",
57
- "@buoy-design/core": "0.3.38"
58
+ "@buoy-design/core": "0.4.1"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@types/node": "^22.10.2",
@@ -1,1381 +0,0 @@
1
- import { readFileSync, existsSync } from 'fs';
2
- import { resolve, relative } from 'path';
3
- import { glob } from 'glob';
4
- import { createTokenId } from '@buoy-design/core';
5
- export class TailwindConfigParser {
6
- projectRoot;
7
- options;
8
- constructor(projectRoot, options = {}) {
9
- this.projectRoot = projectRoot;
10
- this.options = options;
11
- }
12
- async parse() {
13
- // Try v3 JS/TS config first
14
- const v3ConfigPath = this.findV3ConfigFile();
15
- if (v3ConfigPath) {
16
- try {
17
- const theme = await this.extractThemeFromJS(v3ConfigPath);
18
- const tokens = this.themeToTokens(theme, v3ConfigPath);
19
- return {
20
- theme,
21
- tokens,
22
- configPath: v3ConfigPath,
23
- version: 3,
24
- };
25
- }
26
- catch (err) {
27
- console.error('Failed to parse Tailwind v3 config:', err);
28
- }
29
- }
30
- // Try v4 CSS config
31
- const v4ConfigPath = await this.findV4CSSFile();
32
- if (v4ConfigPath) {
33
- try {
34
- const { theme, tokens } = await this.extractFromCSS(v4ConfigPath);
35
- return {
36
- theme,
37
- tokens,
38
- configPath: v4ConfigPath,
39
- version: 4,
40
- };
41
- }
42
- catch (err) {
43
- console.error('Failed to parse Tailwind v4 config:', err);
44
- }
45
- }
46
- return null;
47
- }
48
- findV3ConfigFile() {
49
- const configNames = [
50
- 'tailwind.config.js',
51
- 'tailwind.config.ts',
52
- 'tailwind.config.mjs',
53
- 'tailwind.config.cjs',
54
- ];
55
- for (const name of configNames) {
56
- const path = resolve(this.projectRoot, name);
57
- if (existsSync(path)) {
58
- return path;
59
- }
60
- }
61
- return null;
62
- }
63
- async findV4CSSFile() {
64
- // If CSS paths are provided, use those
65
- if (this.options.cssConfigPaths?.length) {
66
- for (const cssPath of this.options.cssConfigPaths) {
67
- if (existsSync(cssPath)) {
68
- const content = readFileSync(cssPath, 'utf-8');
69
- if (this.isTailwindV4CSS(content)) {
70
- return cssPath;
71
- }
72
- }
73
- }
74
- }
75
- // Auto-discover CSS files - check common locations first
76
- const cssPatterns = [
77
- 'app/globals.css',
78
- 'src/app/globals.css',
79
- 'styles/globals.css',
80
- 'src/styles/globals.css',
81
- 'src/index.css',
82
- 'app/global.css',
83
- 'styles/global.css',
84
- ];
85
- for (const pattern of cssPatterns) {
86
- const path = resolve(this.projectRoot, pattern);
87
- if (existsSync(path)) {
88
- const content = readFileSync(path, 'utf-8');
89
- if (this.isTailwindV4CSS(content)) {
90
- return path;
91
- }
92
- }
93
- }
94
- // Try glob search, prioritizing files with @theme blocks
95
- try {
96
- const files = await glob('**/*.css', {
97
- cwd: this.projectRoot,
98
- ignore: [
99
- '**/node_modules/**',
100
- '**/dist/**',
101
- '**/build/**',
102
- '**/templates/**',
103
- '**/fixtures/**',
104
- '**/test/**',
105
- '**/tests/**',
106
- '**/.next/**',
107
- '**/coverage/**',
108
- ],
109
- absolute: true,
110
- });
111
- // Sort candidates by content quality (prefer files with @theme blocks)
112
- const candidates = [];
113
- for (const file of files) {
114
- try {
115
- const content = readFileSync(file, 'utf-8');
116
- if (this.isTailwindV4CSS(content)) {
117
- let score = 0;
118
- // Prefer files with @theme blocks
119
- if (content.includes('@theme'))
120
- score += 100;
121
- // Prefer files with :root variables
122
- if (content.includes(':root'))
123
- score += 50;
124
- // Prefer files with more content
125
- score += Math.min(content.length / 100, 50);
126
- // Prefer files in app/ or styles/ directories
127
- if (file.includes('/app/') || file.includes('/styles/'))
128
- score += 20;
129
- candidates.push({ path: file, score });
130
- }
131
- }
132
- catch {
133
- // Skip unreadable files
134
- }
135
- }
136
- // Return the highest scoring candidate
137
- if (candidates.length > 0) {
138
- candidates.sort((a, b) => b.score - a.score);
139
- return candidates[0].path;
140
- }
141
- }
142
- catch {
143
- // Glob failed, skip
144
- }
145
- return null;
146
- }
147
- isTailwindV4CSS(content) {
148
- // Check for v4 indicators
149
- return (content.includes('@import "tailwindcss"') ||
150
- content.includes("@import 'tailwindcss'") ||
151
- content.includes('@theme') ||
152
- content.includes('@custom-variant') ||
153
- content.includes('@utility'));
154
- }
155
- async extractFromCSS(cssPath) {
156
- const content = readFileSync(cssPath, 'utf-8');
157
- const relativePath = relative(this.projectRoot, cssPath);
158
- const source = { type: 'css', path: relativePath };
159
- const theme = {
160
- colors: {},
161
- spacing: {},
162
- fontSize: {},
163
- fontFamily: {},
164
- borderRadius: {},
165
- boxShadow: {},
166
- customVariants: [],
167
- utilities: [],
168
- plugins: [],
169
- imports: [],
170
- breakpoints: {},
171
- };
172
- const tokens = [];
173
- // Extract @plugin declarations
174
- const plugins = this.extractPlugins(content);
175
- theme.plugins = plugins;
176
- // Extract @import declarations (excluding tailwindcss itself)
177
- const imports = this.extractImports(content);
178
- theme.imports = imports;
179
- // Extract @theme inline blocks
180
- const themeBlocks = this.extractThemeBlocks(content);
181
- for (const block of themeBlocks) {
182
- const blockTokens = this.parseThemeBlock(block, source, theme);
183
- tokens.push(...blockTokens);
184
- // Also populate theme object
185
- for (const token of blockTokens) {
186
- this.addToTheme(theme, token);
187
- }
188
- }
189
- // Extract :root CSS variables
190
- const rootVars = this.extractRootVariables(content);
191
- for (const [name, value] of Object.entries(rootVars)) {
192
- const token = this.cssVarToToken(name, value, source, 'light');
193
- if (token) {
194
- tokens.push(token);
195
- this.addToTheme(theme, token);
196
- }
197
- }
198
- // Extract .dark CSS variables
199
- const darkVars = this.extractDarkVariables(content);
200
- for (const [name, value] of Object.entries(darkVars)) {
201
- const token = this.cssVarToToken(name, value, source, 'dark');
202
- if (token) {
203
- tokens.push(token);
204
- }
205
- }
206
- // Extract @custom-variant declarations
207
- const customVariants = this.extractCustomVariants(content);
208
- theme.customVariants = customVariants;
209
- // Extract @utility declarations
210
- const utilities = this.extractUtilities(content);
211
- theme.utilities = utilities;
212
- // Extract @source directives (v4 content paths)
213
- const sources = this.extractSources(content);
214
- theme.sources = sources;
215
- return { theme, tokens };
216
- }
217
- extractThemeBlocks(content) {
218
- const blocks = [];
219
- // Match @theme inline { }, @theme { }, but NOT @theme reference { }
220
- // The 'reference' keyword is used to reset theme values, not define new ones
221
- const regex = /@theme\s+(?:inline\s*)?\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/gs;
222
- let match;
223
- while ((match = regex.exec(content)) !== null) {
224
- // Skip @theme reference blocks
225
- const fullMatch = match[0];
226
- if (fullMatch.includes('@theme reference') || fullMatch.includes('@theme reference')) {
227
- continue;
228
- }
229
- blocks.push(match[1] || '');
230
- }
231
- return blocks;
232
- }
233
- parseThemeBlock(block, source, theme) {
234
- const tokens = [];
235
- // Match CSS custom property declarations
236
- const propRegex = /--([\w-]+):\s*([^;]+);/g;
237
- let match;
238
- while ((match = propRegex.exec(block)) !== null) {
239
- const name = match[1];
240
- const value = match[2].trim();
241
- // Extract breakpoints to theme.breakpoints
242
- if (name.startsWith('breakpoint-')) {
243
- const bpName = name.replace('breakpoint-', '');
244
- theme.breakpoints[bpName] = value;
245
- }
246
- const token = this.themeVarToToken(name, value, source);
247
- if (token) {
248
- tokens.push(token);
249
- }
250
- }
251
- return tokens;
252
- }
253
- themeVarToToken(name, value, source) {
254
- // Parse the variable name to determine category
255
- const category = this.inferCategoryFromName(name);
256
- // Handle var() references - store as raw with reference info in tags
257
- if (value.startsWith('var(')) {
258
- // Parse var() with optional fallback: var(--name, fallback)
259
- const refMatch = value.match(/var\(--([^,)]+)(?:,\s*([^)]+))?\)/);
260
- const refName = refMatch?.[1] || '';
261
- const fallback = refMatch?.[2]?.trim();
262
- const hasFallback = !!fallback;
263
- const aliases = [name];
264
- if (refName)
265
- aliases.push(refName);
266
- const tags = ['tailwind', 'v4', 'reference'];
267
- if (hasFallback)
268
- tags.push('fallback');
269
- // Determine the appropriate category for the token
270
- let tokenCategory = category;
271
- if (category === 'other') {
272
- tokenCategory = 'color'; // Default to color for unknown categories
273
- }
274
- return {
275
- id: createTokenId(source, `tw-${name}`),
276
- name: `tw-${name}`,
277
- category: tokenCategory === 'other' ? 'color' : tokenCategory,
278
- value: { type: 'raw', value },
279
- source,
280
- aliases,
281
- usedBy: [],
282
- metadata: { tags },
283
- scannedAt: new Date(),
284
- };
285
- }
286
- // Parse color values (hex, rgb, rgba, hsl, oklch, etc.)
287
- if (category === 'color') {
288
- return this.createColorToken(name, value, source, 'v4');
289
- }
290
- // Parse spacing/sizing values
291
- if (category === 'spacing' || category === 'border') {
292
- return this.createV4SpacingToken(name, value, source, category);
293
- }
294
- // Handle typography tokens (font-*, text-*, etc.)
295
- if (category === 'typography') {
296
- return {
297
- id: createTokenId(source, `tw-${name}`),
298
- name: `tw-${name}`,
299
- category: 'typography',
300
- value: { type: 'raw', value },
301
- source,
302
- aliases: [name],
303
- usedBy: [],
304
- metadata: { tags: ['tailwind', 'v4'] },
305
- scannedAt: new Date(),
306
- };
307
- }
308
- // Default raw token
309
- return {
310
- id: createTokenId(source, `tw-${name}`),
311
- name: `tw-${name}`,
312
- category: 'color',
313
- value: { type: 'raw', value },
314
- source,
315
- aliases: [name],
316
- usedBy: [],
317
- metadata: { tags: ['tailwind', 'v4'] },
318
- scannedAt: new Date(),
319
- };
320
- }
321
- inferCategoryFromName(name) {
322
- if (name.startsWith('color-') ||
323
- name.includes('background') ||
324
- name.includes('foreground') ||
325
- name.includes('primary') ||
326
- name.includes('secondary') ||
327
- name.includes('accent') ||
328
- name.includes('muted') ||
329
- name.includes('destructive') ||
330
- name.includes('border') ||
331
- name.includes('input') ||
332
- name.includes('ring') ||
333
- name.includes('chart-') ||
334
- name.includes('sidebar') ||
335
- name.includes('surface') ||
336
- name.includes('code') ||
337
- name.includes('selection') ||
338
- name.includes('popover') ||
339
- name.includes('card')) {
340
- return 'color';
341
- }
342
- if (name.startsWith('spacing-') ||
343
- name.startsWith('breakpoint-') ||
344
- name.includes('gap') ||
345
- name.includes('padding') ||
346
- name.includes('margin')) {
347
- return 'spacing';
348
- }
349
- if (name.startsWith('radius-') || name.includes('border-radius')) {
350
- return 'border';
351
- }
352
- if (name.startsWith('font-') ||
353
- name.startsWith('text-') ||
354
- name.includes('leading') ||
355
- name.includes('tracking')) {
356
- return 'typography';
357
- }
358
- return 'other';
359
- }
360
- createV4SpacingToken(name, value, source, category) {
361
- const numMatch = value.match(/^([\d.]+)(rem|px|em|%)?$/);
362
- if (numMatch) {
363
- const num = parseFloat(numMatch[1]);
364
- const rawUnit = numMatch[2] || 'px';
365
- // Only use valid spacing units, fall back to raw for %
366
- if (rawUnit === 'rem' || rawUnit === 'px' || rawUnit === 'em') {
367
- return {
368
- id: createTokenId(source, `tw-${name}`),
369
- name: `tw-${name}`,
370
- category,
371
- value: { type: 'spacing', value: num, unit: rawUnit },
372
- source,
373
- aliases: [name],
374
- usedBy: [],
375
- metadata: { tags: ['tailwind', 'v4'] },
376
- scannedAt: new Date(),
377
- };
378
- }
379
- }
380
- // Handle calc() expressions
381
- if (value.startsWith('calc(')) {
382
- return {
383
- id: createTokenId(source, `tw-${name}`),
384
- name: `tw-${name}`,
385
- category,
386
- value: { type: 'raw', value },
387
- source,
388
- aliases: [name],
389
- usedBy: [],
390
- metadata: { tags: ['tailwind', 'v4', 'calc'] },
391
- scannedAt: new Date(),
392
- };
393
- }
394
- return {
395
- id: createTokenId(source, `tw-${name}`),
396
- name: `tw-${name}`,
397
- category,
398
- value: { type: 'raw', value },
399
- source,
400
- aliases: [name],
401
- usedBy: [],
402
- metadata: { tags: ['tailwind', 'v4'] },
403
- scannedAt: new Date(),
404
- };
405
- }
406
- extractRootVariables(content) {
407
- const vars = {};
408
- // Match :root { ... }
409
- const rootMatch = content.match(/:root\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/s);
410
- if (rootMatch) {
411
- const block = rootMatch[1];
412
- const propRegex = /--([\w-]+):\s*([^;]+);/g;
413
- let match;
414
- while ((match = propRegex.exec(block)) !== null) {
415
- vars[match[1]] = match[2].trim();
416
- }
417
- }
418
- return vars;
419
- }
420
- extractDarkVariables(content) {
421
- const vars = {};
422
- // Match .dark { ... }
423
- const darkMatch = content.match(/\.dark\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/s);
424
- if (darkMatch) {
425
- const block = darkMatch[1];
426
- const propRegex = /--([\w-]+):\s*([^;]+);/g;
427
- let match;
428
- while ((match = propRegex.exec(block)) !== null) {
429
- vars[match[1]] = match[2].trim();
430
- }
431
- }
432
- return vars;
433
- }
434
- cssVarToToken(name, value, source, mode) {
435
- const category = this.inferCategoryFromCSSVar(name);
436
- // Skip non-semantic variables
437
- if (category === 'other') {
438
- return null;
439
- }
440
- const tokenName = mode === 'dark' ? `tw-${name}-dark` : `tw-${name}`;
441
- if (category === 'color') {
442
- return this.createColorToken(name, value, source, 'v4', mode);
443
- }
444
- if (category === 'spacing' || category === 'border') {
445
- return this.createV4SpacingToken(tokenName.replace('tw-', ''), value, source, category);
446
- }
447
- return {
448
- id: createTokenId(source, tokenName),
449
- name: tokenName,
450
- category,
451
- value: { type: 'raw', value },
452
- source,
453
- aliases: mode === 'dark' ? [`${name}-dark`] : [name],
454
- usedBy: [],
455
- metadata: { tags: ['tailwind', 'v4', mode] },
456
- scannedAt: new Date(),
457
- };
458
- }
459
- inferCategoryFromCSSVar(name) {
460
- // Common shadcn/tailwind v4 variable names
461
- const colorVars = [
462
- 'background',
463
- 'foreground',
464
- 'primary',
465
- 'secondary',
466
- 'accent',
467
- 'muted',
468
- 'destructive',
469
- 'border',
470
- 'input',
471
- 'ring',
472
- 'card',
473
- 'popover',
474
- 'sidebar',
475
- 'surface',
476
- 'code',
477
- 'selection',
478
- 'chart-',
479
- ];
480
- for (const cv of colorVars) {
481
- if (name === cv || name.startsWith(`${cv}-`)) {
482
- return 'color';
483
- }
484
- }
485
- if (name.startsWith('radius')) {
486
- return 'border';
487
- }
488
- if (name.startsWith('spacing') || name.startsWith('gap')) {
489
- return 'spacing';
490
- }
491
- if (name.startsWith('font') || name.startsWith('text')) {
492
- return 'typography';
493
- }
494
- return 'other';
495
- }
496
- extractCustomVariants(content) {
497
- const variants = [];
498
- // Match @custom-variant declarations
499
- const customVariantRegex = /@custom-variant\s+([\w-]+)/g;
500
- let match;
501
- while ((match = customVariantRegex.exec(content)) !== null) {
502
- variants.push(match[1]);
503
- }
504
- // Also match @variant declarations (Tailwind v4 syntax)
505
- const variantRegex = /@variant\s+([\w-]+)/g;
506
- while ((match = variantRegex.exec(content)) !== null) {
507
- if (!variants.includes(match[1])) {
508
- variants.push(match[1]);
509
- }
510
- }
511
- return variants;
512
- }
513
- extractUtilities(content) {
514
- const utilities = [];
515
- const regex = /@utility\s+([\w-]+)/g;
516
- let match;
517
- while ((match = regex.exec(content)) !== null) {
518
- utilities.push(match[1]);
519
- }
520
- return utilities;
521
- }
522
- extractPlugins(content) {
523
- const plugins = [];
524
- // Match @plugin 'package-name' or @plugin "package-name"
525
- const regex = /@plugin\s+['"]([^'"]+)['"]/g;
526
- let match;
527
- while ((match = regex.exec(content)) !== null) {
528
- plugins.push(match[1]);
529
- }
530
- return plugins;
531
- }
532
- extractImports(content) {
533
- const imports = [];
534
- // Match @import 'path' or @import "path" with optional layer()
535
- const regex = /@import\s+['"]([^'"]+)['"](?:\s+layer\([^)]+\))?/g;
536
- let match;
537
- while ((match = regex.exec(content)) !== null) {
538
- const importPath = match[1];
539
- // Exclude tailwindcss itself
540
- if (importPath !== 'tailwindcss') {
541
- imports.push(importPath);
542
- }
543
- }
544
- return imports;
545
- }
546
- /**
547
- * Extract @source directives from v4 CSS config
548
- * Handles both @source "path" and @source inline("path") syntax
549
- */
550
- extractSources(content) {
551
- const sources = [];
552
- // Match @source "path"; or @source 'path';
553
- const simpleRegex = /@source\s+['"]([^'"]+)['"];?/g;
554
- let match;
555
- while ((match = simpleRegex.exec(content)) !== null) {
556
- sources.push(match[1]);
557
- }
558
- // Match @source inline("path"); or @source inline('path');
559
- const inlineRegex = /@source\s+inline\(\s*['"]([^'"]+)['"]\s*\);?/g;
560
- while ((match = inlineRegex.exec(content)) !== null) {
561
- sources.push(match[1]);
562
- }
563
- return sources;
564
- }
565
- addToTheme(theme, token) {
566
- if (token.category === 'color') {
567
- const name = token.name.replace('tw-', '').replace('-dark', '');
568
- if (typeof token.value === 'object' && 'hex' in token.value) {
569
- theme.colors[name] = token.value.hex;
570
- }
571
- else if (typeof token.value === 'object' && token.value.type === 'raw') {
572
- // Store raw value (including oklch) as-is
573
- theme.colors[name] = token.value.value;
574
- }
575
- }
576
- else if (token.category === 'spacing') {
577
- const name = token.name.replace('tw-spacing-', '').replace('tw-', '');
578
- if (typeof token.value === 'object' && token.value.type === 'spacing') {
579
- theme.spacing[name] = `${token.value.value}${token.value.unit || ''}`;
580
- }
581
- }
582
- else if (token.category === 'border') {
583
- const name = token.name.replace('tw-radius-', '').replace('tw-', '');
584
- if (typeof token.value === 'object' && token.value.type === 'spacing') {
585
- theme.borderRadius[name] = `${token.value.value}${token.value.unit || ''}`;
586
- }
587
- else if (typeof token.value === 'object' && token.value.type === 'raw') {
588
- theme.borderRadius[name] = token.value.value;
589
- }
590
- }
591
- }
592
- // ============ V3 JS Config Parsing ============
593
- async extractThemeFromJS(configPath) {
594
- const content = readFileSync(configPath, 'utf-8');
595
- const theme = {
596
- colors: {},
597
- spacing: {},
598
- fontSize: {},
599
- fontFamily: {},
600
- borderRadius: {},
601
- boxShadow: {},
602
- dropShadow: {},
603
- keyframes: [],
604
- animation: {},
605
- maxWidth: {},
606
- plugins: [],
607
- };
608
- // Extract colors from theme.extend.colors or theme.colors
609
- const colorMatches = this.extractObjectFromConfig(content, 'colors');
610
- if (colorMatches) {
611
- theme.colors = this.parseObjectLiteral(colorMatches);
612
- }
613
- // Extract spacing
614
- const spacingMatches = this.extractObjectFromConfig(content, 'spacing');
615
- if (spacingMatches) {
616
- theme.spacing = this.parseObjectLiteral(spacingMatches);
617
- }
618
- // Extract fontSize (handles both simple values and tuples)
619
- const fontSizeMatches = this.extractObjectFromConfig(content, 'fontSize');
620
- if (fontSizeMatches) {
621
- theme.fontSize = this.parseFontSizeObject(fontSizeMatches);
622
- }
623
- // Extract fontFamily (handles array syntax)
624
- const fontFamilyMatches = this.extractObjectFromConfig(content, 'fontFamily');
625
- if (fontFamilyMatches) {
626
- theme.fontFamily = this.parseFontFamilyObject(fontFamilyMatches);
627
- }
628
- // Extract borderRadius
629
- const borderRadiusMatches = this.extractObjectFromConfig(content, 'borderRadius');
630
- if (borderRadiusMatches) {
631
- theme.borderRadius = this.parseObjectLiteral(borderRadiusMatches);
632
- }
633
- // Extract boxShadow (handles multi-line values)
634
- const boxShadowMatches = this.extractObjectFromConfig(content, 'boxShadow');
635
- if (boxShadowMatches) {
636
- theme.boxShadow = this.parseMultiLineObjectLiteral(boxShadowMatches);
637
- }
638
- // Extract dropShadow
639
- const dropShadowMatches = this.extractObjectFromConfig(content, 'dropShadow');
640
- if (dropShadowMatches) {
641
- theme.dropShadow = this.parseMultiLineObjectLiteral(dropShadowMatches);
642
- }
643
- // Extract keyframes (just get the names)
644
- const keyframesMatches = this.extractObjectFromConfig(content, 'keyframes');
645
- if (keyframesMatches) {
646
- theme.keyframes = this.extractKeyframeNames(keyframesMatches);
647
- }
648
- // Extract animation
649
- const animationMatches = this.extractObjectFromConfig(content, 'animation');
650
- if (animationMatches) {
651
- theme.animation = this.parseObjectLiteral(animationMatches);
652
- }
653
- // Extract maxWidth
654
- const maxWidthMatches = this.extractObjectFromConfig(content, 'maxWidth');
655
- if (maxWidthMatches) {
656
- theme.maxWidth = this.parseObjectLiteral(maxWidthMatches);
657
- }
658
- // Extract plugins from the plugins array
659
- theme.plugins = this.extractPluginsFromJS(content);
660
- // Extract container configuration
661
- const containerMatches = this.extractObjectFromConfig(content, 'container');
662
- if (containerMatches) {
663
- theme.container = this.parseContainerConfig(containerMatches);
664
- }
665
- // Extract screens configuration - check both theme.screens and theme.extend.screens
666
- theme.screens = {};
667
- const allScreensMatches = this.extractAllObjectsFromConfig(content, 'screens');
668
- for (const screensContent of allScreensMatches) {
669
- const parsed = this.parseObjectLiteral(screensContent);
670
- Object.assign(theme.screens, parsed);
671
- }
672
- // Extract darkMode configuration
673
- theme.darkMode = this.extractDarkMode(content);
674
- // Extract zIndex
675
- const zIndexMatches = this.extractObjectFromConfig(content, 'zIndex');
676
- if (zIndexMatches) {
677
- theme.zIndex = this.parseObjectLiteral(zIndexMatches);
678
- }
679
- // Extract lineHeight
680
- const lineHeightMatches = this.extractObjectFromConfig(content, 'lineHeight');
681
- if (lineHeightMatches) {
682
- theme.lineHeight = this.parseObjectLiteral(lineHeightMatches);
683
- }
684
- // Extract letterSpacing
685
- const letterSpacingMatches = this.extractObjectFromConfig(content, 'letterSpacing');
686
- if (letterSpacingMatches) {
687
- theme.letterSpacing = this.parseObjectLiteral(letterSpacingMatches);
688
- }
689
- // Extract opacity
690
- const opacityMatches = this.extractObjectFromConfig(content, 'opacity');
691
- if (opacityMatches) {
692
- theme.opacity = this.parseObjectLiteral(opacityMatches);
693
- }
694
- // Extract transitionDuration
695
- const transitionDurationMatches = this.extractObjectFromConfig(content, 'transitionDuration');
696
- if (transitionDurationMatches) {
697
- theme.transitionDuration = this.parseObjectLiteral(transitionDurationMatches);
698
- }
699
- // Extract backgroundImage
700
- const backgroundImageMatches = this.extractObjectFromConfig(content, 'backgroundImage');
701
- if (backgroundImageMatches) {
702
- theme.backgroundImage = this.parseMultiLineObjectLiteral(backgroundImageMatches);
703
- }
704
- // Extract fontWeight
705
- const fontWeightMatches = this.extractObjectFromConfig(content, 'fontWeight');
706
- if (fontWeightMatches) {
707
- theme.fontWeight = this.parseObjectLiteral(fontWeightMatches);
708
- }
709
- return theme;
710
- }
711
- /**
712
- * Parse container configuration object
713
- */
714
- parseContainerConfig(content) {
715
- const config = {};
716
- // Extract center: true/false
717
- const centerMatch = content.match(/center\s*:\s*(true|false)/);
718
- if (centerMatch) {
719
- config.center = centerMatch[1] === 'true';
720
- }
721
- // Extract padding as string or object
722
- const paddingStrMatch = content.match(/padding\s*:\s*["']([^"']+)["']/);
723
- if (paddingStrMatch) {
724
- config.padding = paddingStrMatch[1];
725
- }
726
- // Extract screens object inside container
727
- const screensMatch = this.extractObjectFromConfig(content, 'screens');
728
- if (screensMatch) {
729
- config.screens = this.parseObjectLiteral(screensMatch);
730
- }
731
- return config;
732
- }
733
- /**
734
- * Extract darkMode configuration
735
- */
736
- extractDarkMode(content) {
737
- // Match darkMode: ["class"] or darkMode: ["class", "[data-theme='dark']"]
738
- const arrayMatch = content.match(/darkMode\s*:\s*\[([^\]]+)\]/);
739
- if (arrayMatch) {
740
- const items = [];
741
- const itemRegex = /["']([^"']+)["']/g;
742
- let match;
743
- while ((match = itemRegex.exec(arrayMatch[1])) !== null) {
744
- items.push(match[1]);
745
- }
746
- return items;
747
- }
748
- // Match darkMode: "class" or darkMode: "selector" or darkMode: 'media'
749
- const stringMatch = content.match(/darkMode\s*:\s*["']([^"']+)["']/);
750
- if (stringMatch) {
751
- return stringMatch[1];
752
- }
753
- return undefined;
754
- }
755
- extractObjectFromConfig(content, key) {
756
- // For deeply nested structures like keyframes, use balanced brace matching
757
- const keyPatterns = [
758
- new RegExp(`${key}:\\s*\\{`, 'g'),
759
- new RegExp(`['"]${key}['"]:\\s*\\{`, 'g'),
760
- ];
761
- for (const pattern of keyPatterns) {
762
- const keyMatch = pattern.exec(content);
763
- if (keyMatch) {
764
- const startIdx = keyMatch.index + keyMatch[0].length;
765
- let depth = 1;
766
- let endIdx = startIdx;
767
- // Find matching closing brace
768
- for (let i = startIdx; i < content.length && depth > 0; i++) {
769
- if (content[i] === '{')
770
- depth++;
771
- else if (content[i] === '}')
772
- depth--;
773
- endIdx = i;
774
- }
775
- if (depth === 0) {
776
- return content.substring(startIdx, endIdx);
777
- }
778
- }
779
- }
780
- return null;
781
- }
782
- /**
783
- * Extract ALL occurrences of an object with a given key from config
784
- * This handles cases where same key appears in both theme and theme.extend
785
- */
786
- extractAllObjectsFromConfig(content, key) {
787
- const results = [];
788
- const keyPatterns = [
789
- new RegExp(`${key}:\\s*\\{`, 'g'),
790
- new RegExp(`['"]${key}['"]:\\s*\\{`, 'g'),
791
- ];
792
- for (const pattern of keyPatterns) {
793
- let keyMatch;
794
- while ((keyMatch = pattern.exec(content)) !== null) {
795
- const startIdx = keyMatch.index + keyMatch[0].length;
796
- let depth = 1;
797
- let endIdx = startIdx;
798
- // Find matching closing brace
799
- for (let i = startIdx; i < content.length && depth > 0; i++) {
800
- if (content[i] === '{')
801
- depth++;
802
- else if (content[i] === '}')
803
- depth--;
804
- endIdx = i;
805
- }
806
- if (depth === 0) {
807
- results.push(content.substring(startIdx, endIdx));
808
- }
809
- }
810
- }
811
- return results;
812
- }
813
- parseObjectLiteral(content) {
814
- const result = {};
815
- // Simple key-value extraction
816
- const kvPattern = /['"]?(\w+[-\w]*)['"]?\s*:\s*['"]([^'"]+)['"]/g;
817
- let match;
818
- while ((match = kvPattern.exec(content)) !== null) {
819
- result[match[1]] = match[2];
820
- }
821
- // Also try to match nested objects for color scales
822
- const nestedPattern = /['"]?(\w+[-\w]*)['"]?\s*:\s*\{([^{}]+)\}/g;
823
- while ((match = nestedPattern.exec(content)) !== null) {
824
- const nestedObj = {};
825
- const nestedContent = match[2];
826
- const nestedKv = /['"]?(\w+[-\w]*)['"]?\s*:\s*['"]([^'"]+)['"]/g;
827
- let nestedMatch;
828
- while ((nestedMatch = nestedKv.exec(nestedContent)) !== null) {
829
- nestedObj[nestedMatch[1]] = nestedMatch[2];
830
- }
831
- if (Object.keys(nestedObj).length > 0) {
832
- result[match[1]] = nestedObj;
833
- }
834
- }
835
- return result;
836
- }
837
- themeToTokens(theme, configPath) {
838
- const tokens = [];
839
- const relativePath = configPath.replace(this.projectRoot + '/', '');
840
- const source = {
841
- type: 'json',
842
- path: relativePath,
843
- };
844
- // Convert colors to tokens
845
- if (theme.colors) {
846
- for (const [name, value] of Object.entries(theme.colors)) {
847
- if (typeof value === 'string') {
848
- tokens.push(this.createColorToken(name, value, source, 'v3'));
849
- }
850
- else if (typeof value === 'object') {
851
- for (const [shade, color] of Object.entries(value)) {
852
- // Handle DEFAULT key - use just the parent name for the token
853
- if (shade === 'DEFAULT') {
854
- tokens.push(this.createColorToken(name, color, source, 'v3'));
855
- }
856
- else {
857
- tokens.push(this.createColorToken(`${name}-${shade}`, color, source, 'v3'));
858
- }
859
- }
860
- }
861
- }
862
- }
863
- // Convert spacing to tokens
864
- if (theme.spacing) {
865
- for (const [name, value] of Object.entries(theme.spacing)) {
866
- tokens.push(this.createSpacingToken(name, value, source));
867
- }
868
- }
869
- // Convert boxShadow to tokens
870
- if (theme.boxShadow) {
871
- for (const [name, value] of Object.entries(theme.boxShadow)) {
872
- tokens.push(this.createShadowToken(name, value, source));
873
- }
874
- }
875
- // Convert dropShadow to tokens
876
- if (theme.dropShadow) {
877
- for (const [name, value] of Object.entries(theme.dropShadow)) {
878
- tokens.push(this.createDropShadowToken(name, value, source));
879
- }
880
- }
881
- // Convert borderRadius to tokens
882
- if (theme.borderRadius) {
883
- for (const [name, value] of Object.entries(theme.borderRadius)) {
884
- tokens.push(this.createBorderRadiusToken(name, value, source));
885
- }
886
- }
887
- // Convert fontFamily to tokens
888
- if (theme.fontFamily) {
889
- for (const [name, fonts] of Object.entries(theme.fontFamily)) {
890
- if (Array.isArray(fonts)) {
891
- tokens.push(this.createFontFamilyToken(name, fonts, source));
892
- }
893
- }
894
- }
895
- // Convert backgroundImage to tokens
896
- if (theme.backgroundImage) {
897
- for (const [name, value] of Object.entries(theme.backgroundImage)) {
898
- tokens.push(this.createBackgroundImageToken(name, value, source));
899
- }
900
- }
901
- // Convert fontWeight to tokens
902
- if (theme.fontWeight) {
903
- for (const [name, value] of Object.entries(theme.fontWeight)) {
904
- tokens.push(this.createFontWeightToken(name, value, source));
905
- }
906
- }
907
- // Convert fontSize to tokens (handles both string and tuple values)
908
- if (theme.fontSize) {
909
- for (const [name, value] of Object.entries(theme.fontSize)) {
910
- tokens.push(this.createFontSizeToken(name, value, source));
911
- }
912
- }
913
- return tokens;
914
- }
915
- createColorToken(name, value, source, version, mode) {
916
- const tokenName = mode === 'dark' ? `tw-${name}-dark` : `tw-${name}`;
917
- const tags = ['tailwind', version];
918
- if (mode)
919
- tags.push(mode);
920
- // Check for oklch colors (v4) - store as raw since schema doesn't support oklch
921
- if (value.startsWith('oklch(')) {
922
- const colorTags = [...tags, 'oklch'];
923
- // Check for alpha values (oklch with / for alpha)
924
- if (value.includes('/')) {
925
- colorTags.push('alpha');
926
- }
927
- return {
928
- id: createTokenId(source, tokenName),
929
- name: tokenName,
930
- category: 'color',
931
- value: { type: 'raw', value },
932
- source,
933
- aliases: mode === 'dark' ? [`${name}-dark`] : [name],
934
- usedBy: [],
935
- metadata: { tags: colorTags },
936
- scannedAt: new Date(),
937
- };
938
- }
939
- // Check for hsl colors - store as raw with hsl tag
940
- if (value.startsWith('hsl(') || value.startsWith('hsla(')) {
941
- const colorTags = [...tags, 'hsl'];
942
- if (value.includes('/') || value.startsWith('hsla(')) {
943
- colorTags.push('alpha');
944
- }
945
- // Check for var() inside hsl() - e.g., hsl(var(--border))
946
- const hslVarMatch = value.match(/var\(--([^,)]+)\)/);
947
- if (hslVarMatch) {
948
- colorTags.push('reference');
949
- const refName = hslVarMatch[1];
950
- return {
951
- id: createTokenId(source, tokenName),
952
- name: tokenName,
953
- category: 'color',
954
- value: { type: 'raw', value },
955
- source,
956
- aliases: mode === 'dark' ? [`${name}-dark`, refName] : [name, refName],
957
- usedBy: [],
958
- metadata: { tags: colorTags },
959
- scannedAt: new Date(),
960
- };
961
- }
962
- return {
963
- id: createTokenId(source, tokenName),
964
- name: tokenName,
965
- category: 'color',
966
- value: { type: 'raw', value },
967
- source,
968
- aliases: mode === 'dark' ? [`${name}-dark`] : [name],
969
- usedBy: [],
970
- metadata: { tags: colorTags },
971
- scannedAt: new Date(),
972
- };
973
- }
974
- // Check for rgb colors - store as raw with rgb tag
975
- if (value.startsWith('rgb(') || value.startsWith('rgba(')) {
976
- const colorTags = [...tags, 'rgb'];
977
- if (value.includes('/') || value.startsWith('rgba(') || value.includes('<alpha-value>')) {
978
- colorTags.push('alpha');
979
- }
980
- return {
981
- id: createTokenId(source, tokenName),
982
- name: tokenName,
983
- category: 'color',
984
- value: { type: 'raw', value },
985
- source,
986
- aliases: mode === 'dark' ? [`${name}-dark`] : [name],
987
- usedBy: [],
988
- metadata: { tags: colorTags },
989
- scannedAt: new Date(),
990
- };
991
- }
992
- // Check for var() references - store as raw with reference info in aliases
993
- if (value.startsWith('var(')) {
994
- const refMatch = value.match(/var\(--([^)]+)\)/);
995
- const refName = refMatch?.[1] || '';
996
- return {
997
- id: createTokenId(source, tokenName),
998
- name: tokenName,
999
- category: 'color',
1000
- value: { type: 'raw', value },
1001
- source,
1002
- aliases: mode === 'dark' ? [`${name}-dark`, refName] : [name, refName],
1003
- usedBy: [],
1004
- metadata: { tags: [...tags, 'reference'] },
1005
- scannedAt: new Date(),
1006
- };
1007
- }
1008
- // Check for color-mix() expressions (CSS Color Level 5)
1009
- if (value.startsWith('color-mix(')) {
1010
- const colorTags = [...tags, 'color-mix'];
1011
- return {
1012
- id: createTokenId(source, tokenName),
1013
- name: tokenName,
1014
- category: 'color',
1015
- value: { type: 'raw', value },
1016
- source,
1017
- aliases: mode === 'dark' ? [`${name}-dark`] : [name],
1018
- usedBy: [],
1019
- metadata: { tags: colorTags },
1020
- scannedAt: new Date(),
1021
- };
1022
- }
1023
- // Check for other CSS color functions that should be raw
1024
- // This includes light-dark(), lab(), lch(), color(), hwb(), etc.
1025
- const cssColorFunctions = [
1026
- 'light-dark(',
1027
- 'lab(',
1028
- 'lch(',
1029
- 'color(',
1030
- 'hwb(',
1031
- ];
1032
- for (const fn of cssColorFunctions) {
1033
- if (value.startsWith(fn)) {
1034
- return {
1035
- id: createTokenId(source, tokenName),
1036
- name: tokenName,
1037
- category: 'color',
1038
- value: { type: 'raw', value },
1039
- source,
1040
- aliases: mode === 'dark' ? [`${name}-dark`] : [name],
1041
- usedBy: [],
1042
- metadata: { tags: [...tags, fn.replace('(', '')] },
1043
- scannedAt: new Date(),
1044
- };
1045
- }
1046
- }
1047
- // Only treat as hex if it actually starts with #
1048
- if (value.startsWith('#')) {
1049
- return {
1050
- id: createTokenId(source, tokenName),
1051
- name: tokenName,
1052
- category: 'color',
1053
- value: { type: 'color', hex: value },
1054
- source,
1055
- aliases: mode === 'dark' ? [`${name}-dark`] : [name],
1056
- usedBy: [],
1057
- metadata: { tags },
1058
- scannedAt: new Date(),
1059
- };
1060
- }
1061
- // Default to raw for any other color format we don't recognize
1062
- return {
1063
- id: createTokenId(source, tokenName),
1064
- name: tokenName,
1065
- category: 'color',
1066
- value: { type: 'raw', value },
1067
- source,
1068
- aliases: mode === 'dark' ? [`${name}-dark`] : [name],
1069
- usedBy: [],
1070
- metadata: { tags },
1071
- scannedAt: new Date(),
1072
- };
1073
- }
1074
- createSpacingToken(name, value, source) {
1075
- const numMatch = value.match(/^([\d.]+)(rem|px|em)?$/);
1076
- if (numMatch) {
1077
- const num = parseFloat(numMatch[1]);
1078
- const unit = numMatch[2] || 'px';
1079
- return {
1080
- id: createTokenId(source, `tw-spacing-${name}`),
1081
- name: `tw-spacing-${name}`,
1082
- category: 'spacing',
1083
- value: { type: 'spacing', value: num, unit },
1084
- source,
1085
- aliases: [name],
1086
- usedBy: [],
1087
- metadata: { tags: ['tailwind'] },
1088
- scannedAt: new Date(),
1089
- };
1090
- }
1091
- return this.createRawToken(`spacing-${name}`, value, 'spacing', source);
1092
- }
1093
- createShadowToken(name, value, source) {
1094
- return {
1095
- id: createTokenId(source, `tw-shadow-${name}`),
1096
- name: `tw-shadow-${name}`,
1097
- category: 'shadow',
1098
- value: { type: 'raw', value },
1099
- source,
1100
- aliases: [name],
1101
- usedBy: [],
1102
- metadata: { tags: ['tailwind'] },
1103
- scannedAt: new Date(),
1104
- };
1105
- }
1106
- /**
1107
- * Create a border radius token with proper tags for calc() and var() expressions
1108
- */
1109
- createBorderRadiusToken(name, value, source) {
1110
- const tags = ['tailwind'];
1111
- // Check for calc() expressions
1112
- if (value.startsWith('calc(')) {
1113
- tags.push('calc');
1114
- }
1115
- // Check for var() references
1116
- if (value.includes('var(')) {
1117
- tags.push('reference');
1118
- }
1119
- return {
1120
- id: createTokenId(source, `tw-radius-${name}`),
1121
- name: `tw-radius-${name}`,
1122
- category: 'border',
1123
- value: { type: 'raw', value },
1124
- source,
1125
- aliases: [name],
1126
- usedBy: [],
1127
- metadata: { tags },
1128
- scannedAt: new Date(),
1129
- };
1130
- }
1131
- createRawToken(name, value, category, source) {
1132
- return {
1133
- id: createTokenId(source, `tw-${name}`),
1134
- name: `tw-${name}`,
1135
- category,
1136
- value: { type: 'raw', value },
1137
- source,
1138
- aliases: [],
1139
- usedBy: [],
1140
- metadata: { tags: ['tailwind'] },
1141
- scannedAt: new Date(),
1142
- };
1143
- }
1144
- /**
1145
- * Parse font family objects that contain array values
1146
- * e.g., mono: ["Menlo", "Consolas", "monospace"]
1147
- */
1148
- parseFontFamilyObject(content) {
1149
- const result = {};
1150
- // Match key: [array values]
1151
- const arrayPattern = /['"]?(\w+[-\w]*)['"]?\s*:\s*\[([^\]]+)\]/g;
1152
- let match;
1153
- while ((match = arrayPattern.exec(content)) !== null) {
1154
- const key = match[1];
1155
- const arrayContent = match[2];
1156
- // Extract individual strings from the array
1157
- const strings = [];
1158
- const stringPattern = /['"]([^'"]+)['"]/g;
1159
- let strMatch;
1160
- while ((strMatch = stringPattern.exec(arrayContent)) !== null) {
1161
- strings.push(strMatch[1]);
1162
- }
1163
- if (strings.length > 0) {
1164
- result[key] = strings;
1165
- }
1166
- }
1167
- return result;
1168
- }
1169
- /**
1170
- * Parse fontSize object which can contain:
1171
- * - Simple string values: 'xs': '0.75rem'
1172
- * - Tuple values: 'xs': ['0.75rem', { lineHeight: '1rem' }]
1173
- * - Extended tuples: 'hero': ['4rem', { lineHeight: '1.1', letterSpacing: '-0.05em', fontWeight: '700' }]
1174
- */
1175
- parseFontSizeObject(content) {
1176
- const result = {};
1177
- // First, extract simple string values: 'key': 'value'
1178
- const simpleKvPattern = /['"]?([\w-]+)['"]?\s*:\s*['"]([^'"]+)['"]\s*(?:,|}|$)/g;
1179
- let match;
1180
- while ((match = simpleKvPattern.exec(content)) !== null) {
1181
- // Only add if not already parsed as tuple
1182
- if (!result[match[1]]) {
1183
- result[match[1]] = match[2];
1184
- }
1185
- }
1186
- // Then extract tuple values: 'key': ['size', { lineHeight: 'value', ... }]
1187
- // Use balanced bracket matching for arrays with nested objects
1188
- const tuplePattern = /['"]?([\w-]+)['"]?\s*:\s*\[/g;
1189
- while ((match = tuplePattern.exec(content)) !== null) {
1190
- const key = match[1];
1191
- const startIdx = match.index + match[0].length;
1192
- // Find the matching closing bracket for the array
1193
- let depth = 1;
1194
- let endIdx = startIdx;
1195
- for (let i = startIdx; i < content.length && depth > 0; i++) {
1196
- if (content[i] === '[')
1197
- depth++;
1198
- else if (content[i] === ']')
1199
- depth--;
1200
- endIdx = i;
1201
- }
1202
- if (depth === 0) {
1203
- const arrayContent = content.substring(startIdx, endIdx);
1204
- // Parse the tuple: first element is the size, second is the config object
1205
- // Format: 'size', { lineHeight: 'value', ... }
1206
- const sizeMatch = arrayContent.match(/^\s*['"]([^'"]+)['"]/);
1207
- if (sizeMatch) {
1208
- const size = sizeMatch[1];
1209
- // Extract the config object
1210
- const configMatch = arrayContent.match(/\{\s*([^}]+)\s*\}/);
1211
- if (configMatch) {
1212
- const configContent = configMatch[1];
1213
- const config = {};
1214
- // Parse each property in the config object
1215
- const propPattern = /['"]?([\w-]+)['"]?\s*:\s*['"]([^'"]+)['"]/g;
1216
- let propMatch;
1217
- while ((propMatch = propPattern.exec(configContent)) !== null) {
1218
- config[propMatch[1]] = propMatch[2];
1219
- }
1220
- result[key] = [size, config];
1221
- }
1222
- else {
1223
- // Array with just size value, no config object
1224
- result[key] = size;
1225
- }
1226
- }
1227
- }
1228
- }
1229
- return result;
1230
- }
1231
- /**
1232
- * Parse object literals that may have multi-line string values
1233
- * Handles cases like:
1234
- * "md-dark":
1235
- * "0 4px 6px...",
1236
- */
1237
- parseMultiLineObjectLiteral(content) {
1238
- const result = {};
1239
- // First pass: simple key-value on same line
1240
- const simpleKvPattern = /['"]?([\w-]+)['"]?\s*:\s*['"]([^'"]+)['"]/g;
1241
- let match;
1242
- while ((match = simpleKvPattern.exec(content)) !== null) {
1243
- result[match[1]] = match[2];
1244
- }
1245
- // Second pass: key on one line, value on next line (multi-line)
1246
- const multiLinePattern = /['"]?([\w-]+)['"]?\s*:\s*\n\s*['"]([^'"]+)['"]/g;
1247
- while ((match = multiLinePattern.exec(content)) !== null) {
1248
- result[match[1]] = match[2];
1249
- }
1250
- return result;
1251
- }
1252
- /**
1253
- * Extract keyframe animation names from a keyframes object
1254
- */
1255
- extractKeyframeNames(content) {
1256
- const names = [];
1257
- // Match keyframe names (keys that have objects as values)
1258
- const keyframePattern = /['"]?(\w+[-\w]*)['"]?\s*:\s*\{/g;
1259
- let match;
1260
- while ((match = keyframePattern.exec(content)) !== null) {
1261
- names.push(match[1]);
1262
- }
1263
- return names;
1264
- }
1265
- /**
1266
- * Extract plugin names from plugins array in JS config
1267
- */
1268
- extractPluginsFromJS(content) {
1269
- const plugins = [];
1270
- // Match require("plugin-name") or require('plugin-name')
1271
- const requirePattern = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
1272
- let match;
1273
- while ((match = requirePattern.exec(content)) !== null) {
1274
- plugins.push(match[1]);
1275
- }
1276
- return plugins;
1277
- }
1278
- /**
1279
- * Create a drop shadow token
1280
- */
1281
- createDropShadowToken(name, value, source) {
1282
- return {
1283
- id: createTokenId(source, `tw-drop-shadow-${name}`),
1284
- name: `tw-drop-shadow-${name}`,
1285
- category: 'shadow',
1286
- value: { type: 'raw', value },
1287
- source,
1288
- aliases: [name],
1289
- usedBy: [],
1290
- metadata: { tags: ['tailwind', 'drop-shadow'] },
1291
- scannedAt: new Date(),
1292
- };
1293
- }
1294
- /**
1295
- * Create a font family token
1296
- */
1297
- createFontFamilyToken(name, fonts, source) {
1298
- return {
1299
- id: createTokenId(source, `tw-font-${name}`),
1300
- name: `tw-font-${name}`,
1301
- category: 'typography',
1302
- value: { type: 'raw', value: fonts.join(', ') },
1303
- source,
1304
- aliases: [name],
1305
- usedBy: [],
1306
- metadata: { tags: ['tailwind', 'font-family'] },
1307
- scannedAt: new Date(),
1308
- };
1309
- }
1310
- /**
1311
- * Create a background image token
1312
- */
1313
- createBackgroundImageToken(name, value, source) {
1314
- return {
1315
- id: createTokenId(source, `tw-bg-${name}`),
1316
- name: `tw-bg-${name}`,
1317
- category: 'other',
1318
- value: { type: 'raw', value },
1319
- source,
1320
- aliases: [name],
1321
- usedBy: [],
1322
- metadata: { tags: ['tailwind', 'background-image'] },
1323
- scannedAt: new Date(),
1324
- };
1325
- }
1326
- /**
1327
- * Create a font weight token
1328
- */
1329
- createFontWeightToken(name, value, source) {
1330
- return {
1331
- id: createTokenId(source, `tw-font-weight-${name}`),
1332
- name: `tw-font-weight-${name}`,
1333
- category: 'typography',
1334
- value: { type: 'raw', value },
1335
- source,
1336
- aliases: [name],
1337
- usedBy: [],
1338
- metadata: { tags: ['tailwind', 'font-weight'] },
1339
- scannedAt: new Date(),
1340
- };
1341
- }
1342
- /**
1343
- * Create a font size token
1344
- * Handles both simple string values and tuple values [size, { lineHeight, ... }]
1345
- */
1346
- createFontSizeToken(name, value, source) {
1347
- if (typeof value === 'string') {
1348
- return {
1349
- id: createTokenId(source, `tw-text-${name}`),
1350
- name: `tw-text-${name}`,
1351
- category: 'typography',
1352
- value: { type: 'raw', value },
1353
- source,
1354
- aliases: [name],
1355
- usedBy: [],
1356
- metadata: { tags: ['tailwind', 'font-size'] },
1357
- scannedAt: new Date(),
1358
- };
1359
- }
1360
- // Handle tuple: [size, { lineHeight, letterSpacing, fontWeight }]
1361
- const [size, config] = value;
1362
- const configStr = Object.entries(config)
1363
- .map(([k, v]) => `${k}: ${v}`)
1364
- .join(', ');
1365
- return {
1366
- id: createTokenId(source, `tw-text-${name}`),
1367
- name: `tw-text-${name}`,
1368
- category: 'typography',
1369
- value: { type: 'raw', value: `${size} [${configStr}]` },
1370
- source,
1371
- aliases: [name],
1372
- usedBy: [],
1373
- metadata: {
1374
- tags: ['tailwind', 'font-size', 'tuple'],
1375
- description: `Font size ${size} with ${configStr}`,
1376
- },
1377
- scannedAt: new Date(),
1378
- };
1379
- }
1380
- }
1381
- //# sourceMappingURL=config-parser.js.map