@cwcss/crosswind 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +52 -0
  3. package/dist/bin/cli.js +14615 -0
  4. package/dist/build.d.ts +24 -0
  5. package/dist/config.d.ts +5 -0
  6. package/dist/generator.d.ts +31 -0
  7. package/dist/index.d.ts +10 -0
  8. package/dist/parser.d.ts +42 -0
  9. package/dist/plugin.d.ts +22 -0
  10. package/dist/preflight-forms.d.ts +5 -0
  11. package/dist/preflight.d.ts +2 -0
  12. package/dist/rules-advanced.d.ts +27 -0
  13. package/dist/rules-effects.d.ts +25 -0
  14. package/dist/rules-forms.d.ts +7 -0
  15. package/dist/rules-grid.d.ts +13 -0
  16. package/dist/rules-interactivity.d.ts +41 -0
  17. package/dist/rules-layout.d.ts +26 -0
  18. package/dist/rules-transforms.d.ts +33 -0
  19. package/dist/rules-typography.d.ts +41 -0
  20. package/dist/rules.d.ts +39 -0
  21. package/dist/scanner.d.ts +18 -0
  22. package/dist/src/index.js +12848 -0
  23. package/dist/transformer-compile-class.d.ts +37 -0
  24. package/{src/types.ts → dist/types.d.ts} +17 -86
  25. package/package.json +2 -16
  26. package/PLUGIN.md +0 -235
  27. package/benchmark/framework-comparison.bench.ts +0 -850
  28. package/bin/cli.ts +0 -365
  29. package/bin/crosswind +0 -0
  30. package/bin/headwind +0 -0
  31. package/build.ts +0 -8
  32. package/crosswind.config.ts +0 -9
  33. package/example/comprehensive.html +0 -70
  34. package/example/index.html +0 -21
  35. package/example/output.css +0 -236
  36. package/examples/plugin/README.md +0 -112
  37. package/examples/plugin/build.ts +0 -32
  38. package/examples/plugin/src/index.html +0 -34
  39. package/examples/plugin/src/index.ts +0 -7
  40. package/headwind +0 -2
  41. package/src/build.ts +0 -101
  42. package/src/config.ts +0 -529
  43. package/src/generator.ts +0 -2173
  44. package/src/index.ts +0 -10
  45. package/src/parser.ts +0 -1471
  46. package/src/plugin.ts +0 -118
  47. package/src/preflight-forms.ts +0 -229
  48. package/src/preflight.ts +0 -388
  49. package/src/rules-advanced.ts +0 -477
  50. package/src/rules-effects.ts +0 -461
  51. package/src/rules-forms.ts +0 -103
  52. package/src/rules-grid.ts +0 -241
  53. package/src/rules-interactivity.ts +0 -525
  54. package/src/rules-layout.ts +0 -385
  55. package/src/rules-transforms.ts +0 -412
  56. package/src/rules-typography.ts +0 -486
  57. package/src/rules.ts +0 -809
  58. package/src/scanner.ts +0 -84
  59. package/src/transformer-compile-class.ts +0 -275
  60. package/test/advanced-features.test.ts +0 -911
  61. package/test/arbitrary.test.ts +0 -396
  62. package/test/attributify.test.ts +0 -592
  63. package/test/bracket-syntax.test.ts +0 -1133
  64. package/test/build.test.ts +0 -99
  65. package/test/colors.test.ts +0 -934
  66. package/test/flexbox.test.ts +0 -669
  67. package/test/generator.test.ts +0 -597
  68. package/test/grid.test.ts +0 -584
  69. package/test/layout.test.ts +0 -404
  70. package/test/modifiers.test.ts +0 -417
  71. package/test/parser.test.ts +0 -564
  72. package/test/performance-regression.test.ts +0 -376
  73. package/test/performance.test.ts +0 -568
  74. package/test/plugin.test.ts +0 -160
  75. package/test/scanner.test.ts +0 -94
  76. package/test/sizing.test.ts +0 -481
  77. package/test/spacing.test.ts +0 -394
  78. package/test/transformer-compile-class.test.ts +0 -287
  79. package/test/transforms.test.ts +0 -448
  80. package/test/typography.test.ts +0 -632
  81. package/test/variants-form-states.test.ts +0 -225
  82. package/test/variants-group-peer.test.ts +0 -66
  83. package/test/variants-media.test.ts +0 -213
  84. package/test/variants-positional.test.ts +0 -58
  85. package/test/variants-pseudo-elements.test.ts +0 -47
  86. package/test/variants-state.test.ts +0 -62
  87. package/tsconfig.json +0 -18
package/src/parser.ts DELETED
@@ -1,1471 +0,0 @@
1
- import type { AttributifyConfig, BracketSyntaxConfig, ParsedClass } from './types'
2
-
3
- // Cache for parsed classes to avoid re-parsing
4
- const parseCache = new Map<string, ParsedClass>()
5
-
6
- // Cache for expanded bracket syntax
7
- const bracketExpansionCache = new Map<string, string[]>()
8
-
9
- /**
10
- * Options for class extraction
11
- */
12
- export interface ExtractClassesOptions {
13
- attributify?: AttributifyConfig
14
- bracketSyntax?: BracketSyntaxConfig
15
- }
16
-
17
- /**
18
- * Default aliases for bracket syntax
19
- * Maps shorthand to full utility part
20
- */
21
- const defaultBracketAliases: Record<string, string> = {
22
- // Flexbox direction
23
- 'col': 'col',
24
- 'row': 'row',
25
- // Justify/align abbreviations
26
- 'jc': 'justify',
27
- 'ji': 'justify-items',
28
- 'js': 'justify-self',
29
- 'ai': 'items',
30
- 'ac': 'content',
31
- 'as': 'self',
32
- // Wrap
33
- 'wrap': 'wrap',
34
- 'nowrap': 'nowrap',
35
- // Common values
36
- 'c': 'center',
37
- 's': 'start',
38
- 'e': 'end',
39
- 'sb': 'between',
40
- 'sa': 'around',
41
- 'se': 'evenly',
42
- 'st': 'stretch',
43
- // Font weights
44
- 'thin': 'thin',
45
- 'extralight': 'extralight',
46
- 'light': 'light',
47
- 'normal': 'normal',
48
- 'medium': 'medium',
49
- 'semibold': 'semibold',
50
- 'bold': 'bold',
51
- 'extrabold': 'extrabold',
52
- 'black': 'black',
53
- // Position
54
- 't': 'top',
55
- 'r': 'right',
56
- 'b': 'bottom',
57
- 'l': 'left',
58
- }
59
-
60
- /**
61
- * Pre-compiled regex patterns for performance (avoid regex compilation on each call)
62
- */
63
- const SPECIAL_CHARS_REGEX = /[%#()]/
64
- const CSS_UNITS_REGEX = /^\d+(\.\d+)?(px|rem|em|vh|vw|dvh|dvw|svh|svw|lvh|lvw|ch|ex|lh|cap|ic|rlh|vi|vb|vmin|vmax|cqw|cqh|cqi|cqb|cqmin|cqmax|cm|mm|in|pt|pc|Q)$/
65
-
66
- /**
67
- * Check if value needs arbitrary bracket syntax
68
- */
69
- function needsArbitraryBrackets(value: string): boolean {
70
- return SPECIAL_CHARS_REGEX.test(value) || CSS_UNITS_REGEX.test(value)
71
- }
72
-
73
- /**
74
- * Handle min/max prefix patterns for sizing utilities
75
- * w[min 200px] -> min-w-[200px], h[max screen] -> max-h-screen
76
- */
77
- function handleMinMaxPattern(prefix: string, parts: string[]): string[] | null {
78
- if (parts.length !== 2) return null
79
- const [modifier, value] = parts
80
- if (modifier !== 'min' && modifier !== 'max') return null
81
-
82
- const result = needsArbitraryBrackets(value)
83
- ? `${modifier}-${prefix}-[${value}]`
84
- : `${modifier}-${prefix}-${value}`
85
- return [result]
86
- }
87
-
88
- /**
89
- * Mapping of bracket utility prefixes to their expansion patterns
90
- * This defines how utilities like flex[col jc-center] expand to real classes
91
- */
92
- const bracketUtilityMappings: Record<string, {
93
- // How to expand each part within brackets
94
- expand: (part: string, aliases: Record<string, string>) => string | null
95
- // Optional: handle multi-value patterns like w[min 200px]
96
- multiValue?: (parts: string[], aliases: Record<string, string>) => string[] | null
97
- }> = {
98
- 'flex': {
99
- expand: (part, aliases) => {
100
- const aliased = aliases[part] || part
101
- // flex[col] -> flex-col, flex[row] -> flex-row
102
- if (['col', 'row', 'col-reverse', 'row-reverse', 'wrap', 'nowrap', 'wrap-reverse'].includes(aliased)) {
103
- return `flex-${aliased}`
104
- }
105
- // flex[jc-center] -> justify-center, flex[ai-center] -> items-center
106
- const match = part.match(/^(jc|ji|js|ai|ac|as)-(.+)$/)
107
- if (match) {
108
- const prefix = aliases[match[1]] || match[1]
109
- const value = aliases[match[2]] || match[2]
110
- return `${prefix}-${value}`
111
- }
112
- // flex[1] -> flex-1, flex[grow] -> flex-grow
113
- if (/^\d+$/.test(aliased) || ['grow', 'shrink', 'auto', 'initial', 'none'].includes(aliased)) {
114
- return `flex-${aliased}`
115
- }
116
- return null
117
- },
118
- },
119
- 'grid': {
120
- expand: (part, aliases) => {
121
- const aliased = aliases[part] || part
122
- // grid[cols-3] -> grid-cols-3
123
- if (part.startsWith('cols-') || part.startsWith('rows-')) {
124
- return `grid-${part}`
125
- }
126
- // grid[flow-row] -> grid-flow-row
127
- if (part.startsWith('flow-')) {
128
- return `grid-${part}`
129
- }
130
- // grid[gap-4] -> gap-4
131
- if (part.startsWith('gap-')) {
132
- return part
133
- }
134
- return null
135
- },
136
- },
137
- 'text': {
138
- expand: (part, aliases) => {
139
- const aliased = aliases[part] || part
140
- // text[2rem] or text[16px] -> text-[2rem] or text-[16px] (arbitrary size)
141
- if (needsArbitraryBrackets(aliased)) {
142
- return `text-[${aliased}]`
143
- }
144
- // text[700] -> font-bold (weight), text[500] -> font-medium
145
- if (/^\d{3}$/.test(aliased)) {
146
- const weightMap: Record<string, string> = {
147
- '100': 'font-thin',
148
- '200': 'font-extralight',
149
- '300': 'font-light',
150
- '400': 'font-normal',
151
- '500': 'font-medium',
152
- '600': 'font-semibold',
153
- '700': 'font-bold',
154
- '800': 'font-extrabold',
155
- '900': 'font-black',
156
- }
157
- return weightMap[aliased] || `font-[${aliased}]`
158
- }
159
- // text[arial] -> font-[arial] (font family)
160
- if (/^[a-z-]+$/i.test(aliased) && !['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl', '8xl', '9xl'].includes(aliased)) {
161
- // Check if it's a color name
162
- const colorNames = ['white', 'black', 'red', 'blue', 'green', 'yellow', 'purple', 'pink', 'gray', 'slate', 'zinc', 'neutral', 'stone', 'orange', 'amber', 'lime', 'emerald', 'teal', 'cyan', 'sky', 'indigo', 'violet', 'fuchsia', 'rose', 'inherit', 'current', 'transparent']
163
- if (colorNames.includes(aliased.toLowerCase())) {
164
- return `text-${aliased}`
165
- }
166
- // Otherwise treat as font family
167
- return `font-[${aliased}]`
168
- }
169
- // text[sm], text[lg], etc -> text-sm, text-lg
170
- return `text-${aliased}`
171
- },
172
- },
173
- 'font': {
174
- expand: (part, aliases) => {
175
- const aliased = aliases[part] || part
176
- // font[bold] -> font-bold, font[sans] -> font-sans
177
- const weights = ['thin', 'extralight', 'light', 'normal', 'medium', 'semibold', 'bold', 'extrabold', 'black']
178
- const families = ['sans', 'serif', 'mono']
179
- if (weights.includes(aliased) || families.includes(aliased)) {
180
- return `font-${aliased}`
181
- }
182
- // font[700] -> font-bold
183
- if (/^\d{3}$/.test(aliased)) {
184
- const weightMap: Record<string, string> = {
185
- '100': 'font-thin',
186
- '200': 'font-extralight',
187
- '300': 'font-light',
188
- '400': 'font-normal',
189
- '500': 'font-medium',
190
- '600': 'font-semibold',
191
- '700': 'font-bold',
192
- '800': 'font-extrabold',
193
- '900': 'font-black',
194
- }
195
- return weightMap[aliased] || `font-[${aliased}]`
196
- }
197
- // font[arial] -> font-[arial]
198
- return `font-[${aliased}]`
199
- },
200
- },
201
- 'bg': {
202
- expand: (part, aliases) => {
203
- const aliased = aliases[part] || part
204
- return `bg-${aliased}`
205
- },
206
- },
207
- 'w': {
208
- expand: (part, aliases) => {
209
- const aliased = aliases[part] || part
210
- if (needsArbitraryBrackets(aliased)) {
211
- return `w-[${aliased}]`
212
- }
213
- return `w-${aliased}`
214
- },
215
- multiValue: (parts) => handleMinMaxPattern('w', parts),
216
- },
217
- 'h': {
218
- expand: (part, aliases) => {
219
- const aliased = aliases[part] || part
220
- if (needsArbitraryBrackets(aliased)) {
221
- return `h-[${aliased}]`
222
- }
223
- return `h-${aliased}`
224
- },
225
- multiValue: (parts) => handleMinMaxPattern('h', parts),
226
- },
227
- 'p': {
228
- expand: (part) => `p-${part}`,
229
- },
230
- 'px': {
231
- expand: (part) => `px-${part}`,
232
- },
233
- 'py': {
234
- expand: (part) => `py-${part}`,
235
- },
236
- 'pt': {
237
- expand: (part) => `pt-${part}`,
238
- },
239
- 'pr': {
240
- expand: (part) => `pr-${part}`,
241
- },
242
- 'pb': {
243
- expand: (part) => `pb-${part}`,
244
- },
245
- 'pl': {
246
- expand: (part) => `pl-${part}`,
247
- },
248
- 'm': {
249
- expand: (part) => `m-${part}`,
250
- },
251
- 'mx': {
252
- expand: (part) => `mx-${part}`,
253
- },
254
- 'my': {
255
- expand: (part) => `my-${part}`,
256
- },
257
- 'mt': {
258
- expand: (part) => `mt-${part}`,
259
- },
260
- 'mr': {
261
- expand: (part) => `mr-${part}`,
262
- },
263
- 'mb': {
264
- expand: (part) => `mb-${part}`,
265
- },
266
- 'ml': {
267
- expand: (part) => `ml-${part}`,
268
- },
269
- 'scroll': {
270
- expand: (part) => {
271
- // scroll[y auto] -> overflow-y-auto
272
- if (['x', 'y'].includes(part)) {
273
- return null // Will be combined with next value
274
- }
275
- if (['auto', 'hidden', 'scroll', 'visible'].includes(part)) {
276
- return `overflow-${part}`
277
- }
278
- return null
279
- },
280
- multiValue: (parts) => {
281
- if (parts.length === 2) {
282
- const [axis, value] = parts
283
- if (['x', 'y'].includes(axis) && ['auto', 'hidden', 'scroll', 'visible'].includes(value)) {
284
- return [`overflow-${axis}-${value}`]
285
- }
286
- }
287
- return null
288
- },
289
- },
290
- 'overflow': {
291
- expand: (part) => `overflow-${part}`,
292
- multiValue: (parts) => {
293
- if (parts.length === 2) {
294
- const [axis, value] = parts
295
- if (['x', 'y'].includes(axis)) {
296
- return [`overflow-${axis}-${value}`]
297
- }
298
- }
299
- return null
300
- },
301
- },
302
- 'border': {
303
- expand: (part) => `border-${part}`,
304
- },
305
- 'rounded': {
306
- expand: (part) => `rounded-${part}`,
307
- },
308
- 'shadow': {
309
- expand: (part) => `shadow-${part}`,
310
- },
311
- 'gap': {
312
- expand: (part) => {
313
- // gap[x-4] -> gap-x-4
314
- if (part.startsWith('x-') || part.startsWith('y-')) {
315
- return `gap-${part}`
316
- }
317
- return `gap-${part}`
318
- },
319
- },
320
- 'space': {
321
- expand: (part) => `space-${part}`,
322
- },
323
- // New utilities
324
- 'opacity': {
325
- expand: (part) => `opacity-${part}`,
326
- },
327
- 'z': {
328
- expand: (part) => `z-${part}`,
329
- },
330
- 'inset': {
331
- expand: (part, aliases) => {
332
- const aliased = aliases[part] || part
333
- // inset[x-0] -> inset-x-0, inset[y-auto] -> inset-y-auto
334
- if (aliased.startsWith('x-') || aliased.startsWith('y-')) {
335
- return `inset-${aliased}`
336
- }
337
- if (needsArbitraryBrackets(aliased)) {
338
- return `inset-[${aliased}]`
339
- }
340
- return `inset-${aliased}`
341
- },
342
- },
343
- 'top': {
344
- expand: (part) => needsArbitraryBrackets(part) ? `top-[${part}]` : `top-${part}`,
345
- },
346
- 'right': {
347
- expand: (part) => needsArbitraryBrackets(part) ? `right-[${part}]` : `right-${part}`,
348
- },
349
- 'bottom': {
350
- expand: (part) => needsArbitraryBrackets(part) ? `bottom-[${part}]` : `bottom-${part}`,
351
- },
352
- 'left': {
353
- expand: (part) => needsArbitraryBrackets(part) ? `left-[${part}]` : `left-${part}`,
354
- },
355
- 'duration': {
356
- expand: (part) => `duration-${part}`,
357
- },
358
- 'delay': {
359
- expand: (part) => `delay-${part}`,
360
- },
361
- 'ease': {
362
- expand: (part) => `ease-${part}`,
363
- },
364
- 'transition': {
365
- expand: (part) => {
366
- // transition[all] -> transition-all
367
- if (['all', 'none', 'colors', 'opacity', 'shadow', 'transform'].includes(part)) {
368
- return `transition-${part}`
369
- }
370
- // transition[300] -> duration-300
371
- if (/^\d+$/.test(part)) {
372
- return `duration-${part}`
373
- }
374
- // transition[ease-in-out] -> ease-in-out
375
- if (['linear', 'in', 'out', 'in-out', 'ease-linear', 'ease-in', 'ease-out', 'ease-in-out'].includes(part)) {
376
- return part.startsWith('ease-') ? part : `ease-${part}`
377
- }
378
- return `transition-${part}`
379
- },
380
- },
381
- 'translate': {
382
- expand: (part) => {
383
- // translate[x-4] -> translate-x-4
384
- if (part.startsWith('x-') || part.startsWith('y-') || part.startsWith('z-')) {
385
- return `translate-${part}`
386
- }
387
- // translate[4] -> translate-4 (Tailwind v4 syntax)
388
- return `translate-${part}`
389
- },
390
- },
391
- 'rotate': {
392
- expand: (part) => {
393
- if (part.startsWith('x-') || part.startsWith('y-') || part.startsWith('z-')) {
394
- return `rotate-${part}`
395
- }
396
- return `rotate-${part}`
397
- },
398
- },
399
- 'scale': {
400
- expand: (part) => {
401
- if (part.startsWith('x-') || part.startsWith('y-') || part.startsWith('z-')) {
402
- return `scale-${part}`
403
- }
404
- return `scale-${part}`
405
- },
406
- },
407
- 'skew': {
408
- expand: (part) => {
409
- if (part.startsWith('x-') || part.startsWith('y-')) {
410
- return `skew-${part}`
411
- }
412
- return `skew-${part}`
413
- },
414
- },
415
- 'origin': {
416
- expand: (part) => `origin-${part}`,
417
- },
418
- 'cursor': {
419
- expand: (part) => `cursor-${part}`,
420
- },
421
- 'select': {
422
- expand: (part) => `select-${part}`,
423
- },
424
- 'resize': {
425
- expand: (part) => `resize-${part}`,
426
- },
427
- 'appearance': {
428
- expand: (part) => `appearance-${part}`,
429
- },
430
- 'pointer': {
431
- expand: (part) => `pointer-events-${part}`,
432
- },
433
- 'outline': {
434
- expand: (part) => `outline-${part}`,
435
- },
436
- 'ring': {
437
- expand: (part) => `ring-${part}`,
438
- },
439
- 'blur': {
440
- expand: (part) => `blur-${part}`,
441
- },
442
- 'brightness': {
443
- expand: (part) => `brightness-${part}`,
444
- },
445
- 'contrast': {
446
- expand: (part) => `contrast-${part}`,
447
- },
448
- 'grayscale': {
449
- expand: (part) => `grayscale-${part}`,
450
- },
451
- 'saturate': {
452
- expand: (part) => `saturate-${part}`,
453
- },
454
- 'sepia': {
455
- expand: (part) => `sepia-${part}`,
456
- },
457
- 'backdrop': {
458
- expand: (part) => `backdrop-${part}`,
459
- },
460
- 'aspect': {
461
- expand: (part) => `aspect-${part}`,
462
- },
463
- 'columns': {
464
- expand: (part) => `columns-${part}`,
465
- },
466
- 'break': {
467
- expand: (part) => `break-${part}`,
468
- },
469
- 'object': {
470
- expand: (part) => `object-${part}`,
471
- },
472
- 'overscroll': {
473
- expand: (part) => `overscroll-${part}`,
474
- },
475
- 'place': {
476
- expand: (part) => `place-${part}`,
477
- },
478
- 'items': {
479
- expand: (part) => `items-${part}`,
480
- },
481
- 'justify': {
482
- expand: (part) => `justify-${part}`,
483
- },
484
- 'content': {
485
- expand: (part) => `content-${part}`,
486
- },
487
- 'self': {
488
- expand: (part) => `self-${part}`,
489
- },
490
- 'order': {
491
- expand: (part) => `order-${part}`,
492
- },
493
- 'col': {
494
- expand: (part) => `col-${part}`,
495
- },
496
- 'row': {
497
- expand: (part) => `row-${part}`,
498
- },
499
- 'tracking': {
500
- expand: (part) => `tracking-${part}`,
501
- },
502
- 'leading': {
503
- expand: (part) => `leading-${part}`,
504
- },
505
- 'list': {
506
- expand: (part) => `list-${part}`,
507
- },
508
- 'decoration': {
509
- expand: (part) => `decoration-${part}`,
510
- },
511
- 'underline': {
512
- expand: (part) => `underline-${part}`,
513
- },
514
- 'accent': {
515
- expand: (part) => `accent-${part}`,
516
- },
517
- 'caret': {
518
- expand: (part) => `caret-${part}`,
519
- },
520
- 'scroll-m': {
521
- expand: (part) => `scroll-m-${part}`,
522
- },
523
- 'scroll-p': {
524
- expand: (part) => `scroll-p-${part}`,
525
- },
526
- 'snap': {
527
- expand: (part) => `snap-${part}`,
528
- },
529
- 'touch': {
530
- expand: (part) => `touch-${part}`,
531
- },
532
- 'will': {
533
- expand: (part) => `will-change-${part}`,
534
- },
535
- 'fill': {
536
- expand: (part) => `fill-${part}`,
537
- },
538
- 'stroke': {
539
- expand: (part) => `stroke-${part}`,
540
- },
541
- 'sr': {
542
- expand: (part) => `sr-${part}`,
543
- },
544
- // Filter utilities
545
- 'invert': {
546
- expand: (part) => `invert-${part}`,
547
- },
548
- 'hue-rotate': {
549
- expand: (part) => needsArbitraryBrackets(part) ? `hue-rotate-[${part}]` : `hue-rotate-${part}`,
550
- },
551
- 'drop-shadow': {
552
- expand: (part) => `drop-shadow-${part}`,
553
- },
554
- 'backdrop-invert': {
555
- expand: (part) => `backdrop-invert-${part}`,
556
- },
557
- 'backdrop-hue-rotate': {
558
- expand: (part) => needsArbitraryBrackets(part) ? `backdrop-hue-rotate-[${part}]` : `backdrop-hue-rotate-${part}`,
559
- },
560
- // Animation utilities
561
- 'animate': {
562
- expand: (part) => `animate-${part}`,
563
- },
564
- // Accessibility utilities
565
- 'forced-colors': {
566
- expand: (part) => `forced-colors-${part}`,
567
- },
568
- }
569
-
570
- /**
571
- * Common variant prefixes for responsive and state variants
572
- * Using Set for O(1) lookup instead of O(n) array search
573
- */
574
- const variantPrefixes = new Set([
575
- // Responsive
576
- 'sm', 'md', 'lg', 'xl', '2xl',
577
- // State
578
- 'hover', 'focus', 'active', 'visited', 'disabled', 'enabled',
579
- 'focus-within', 'focus-visible',
580
- // Group/peer
581
- 'group-hover', 'group-focus', 'peer-hover', 'peer-focus',
582
- // Dark mode
583
- 'dark',
584
- // First/last/odd/even
585
- 'first', 'last', 'odd', 'even', 'only',
586
- 'first-of-type', 'last-of-type',
587
- // Form states
588
- 'checked', 'indeterminate', 'default', 'required', 'valid', 'invalid',
589
- 'in-range', 'out-of-range', 'placeholder-shown', 'autofill', 'read-only',
590
- // Content
591
- 'empty', 'before', 'after',
592
- // Selection
593
- 'selection', 'marker', 'file',
594
- // Print
595
- 'print',
596
- // Motion
597
- 'motion-safe', 'motion-reduce',
598
- // Contrast
599
- 'contrast-more', 'contrast-less',
600
- // RTL/LTR
601
- 'rtl', 'ltr',
602
- // Portrait/Landscape
603
- 'portrait', 'landscape',
604
- // Container queries
605
- '@sm', '@md', '@lg', '@xl', '@2xl',
606
- ])
607
-
608
- /**
609
- * Expand bracket/grouped syntax into individual class names
610
- * e.g., flex[col jc-center ai-center] -> ['flex-col', 'justify-center', 'items-center']
611
- * e.g., text[white 2rem 700] -> ['text-white', 'text-[2rem]', 'font-bold']
612
- * e.g., h[min 100vh] -> ['min-h-[100vh]']
613
- * e.g., hover:flex[col] -> ['hover:flex-col']
614
- * e.g., flex[md:col lg:row] -> ['md:flex-col', 'lg:flex-row']
615
- * e.g., -m[4] or m[-4] -> ['-m-4']
616
- */
617
- export function expandBracketSyntax(className: string, config?: BracketSyntaxConfig): string[] {
618
- // Check cache first
619
- const cacheKey = `${className}:${config?.colonSyntax}:${JSON.stringify(config?.aliases || {})}`
620
- const cached = bracketExpansionCache.get(cacheKey)
621
- if (cached) {
622
- return cached
623
- }
624
-
625
- const aliases = { ...defaultBracketAliases, ...config?.aliases }
626
-
627
- // Handle colon syntax: bg:black -> bg-black, w:100% -> w-[100%]
628
- // Only if colonSyntax is explicitly enabled
629
- // But NOT if it looks like a variant (hover:bg, sm:flex, etc.)
630
- if (config?.colonSyntax === true) {
631
- const colonMatch = className.match(/^([a-z][a-z0-9-]*):([^[\]:]+)$/i)
632
- if (colonMatch) {
633
- const [, prefix, value] = colonMatch
634
- // Skip if prefix is a variant
635
- if (!variantPrefixes.has(prefix)) {
636
- // If value contains special characters, use arbitrary syntax
637
- if (needsArbitraryBrackets(value)) {
638
- const result = [`${prefix}-[${value}]`]
639
- bracketExpansionCache.set(cacheKey, result)
640
- return result
641
- }
642
- const result = [`${prefix}-${value}`]
643
- bracketExpansionCache.set(cacheKey, result)
644
- return result
645
- }
646
- }
647
- }
648
-
649
- // Handle variant prefix: hover:flex[col] -> expand with hover: prefix
650
- let variantPrefix = ''
651
- let workingClassName = className
652
-
653
- // Check for negative prefix: -m[4] -> negative class
654
- let isNegative = false
655
- if (workingClassName.startsWith('-')) {
656
- isNegative = true
657
- workingClassName = workingClassName.slice(1)
658
- }
659
-
660
- // Extract variant prefix if present (e.g., hover:flex[col] or dark:hover:bg[black])
661
- const variantMatch = workingClassName.match(/^((?:[a-z@][a-z0-9-]*:)+)(.+)$/i)
662
- if (variantMatch) {
663
- const potentialVariants = variantMatch[1].slice(0, -1).split(':')
664
- // Verify all are valid variants
665
- const allVariants = potentialVariants.every(v => variantPrefixes.has(v))
666
- if (allVariants) {
667
- variantPrefix = variantMatch[1]
668
- workingClassName = variantMatch[2]
669
- }
670
- }
671
-
672
- // Handle bracket syntax: flex[col jc-center]
673
- // But NOT arbitrary values like w-[100px] (note the dash before bracket)
674
- const bracketMatch = workingClassName.match(/^([a-z][a-z0-9-]*)\[([^\]]*)\]$/i)
675
- if (!bracketMatch || workingClassName.includes('-[')) {
676
- // No bracket syntax or it's an arbitrary value, return as-is
677
- const result = [className]
678
- bracketExpansionCache.set(cacheKey, result)
679
- return result
680
- }
681
-
682
- const [, prefix, content] = bracketMatch
683
- const parts = content.split(/\s+/).filter(Boolean)
684
-
685
- // Handle empty brackets
686
- if (parts.length === 0) {
687
- bracketExpansionCache.set(cacheKey, [])
688
- return []
689
- }
690
-
691
- const mapping = bracketUtilityMappings[prefix.toLowerCase()]
692
-
693
- // Try multi-value handler first if available
694
- if (mapping?.multiValue) {
695
- const multiResult = mapping.multiValue(parts, aliases)
696
- if (multiResult) {
697
- const results = multiResult.map(cls => {
698
- const neg = isNegative ? '-' : ''
699
- return `${variantPrefix}${neg}${cls}`
700
- })
701
- bracketExpansionCache.set(cacheKey, results)
702
- return results
703
- }
704
- }
705
-
706
- if (!mapping) {
707
- // Unknown prefix, try generic expansion: prefix[a b c] -> prefix-a prefix-b prefix-c
708
- const results: string[] = []
709
- for (const part of parts) {
710
- // Check for variant inside brackets: flex[md:col lg:row]
711
- const innerVariantMatch = part.match(/^([a-z@][a-z0-9-]*):(.+)$/i)
712
- let innerVariant = ''
713
- let partValue = part
714
-
715
- if (innerVariantMatch && variantPrefixes.has(innerVariantMatch[1])) {
716
- innerVariant = `${innerVariantMatch[1]}:`
717
- partValue = innerVariantMatch[2]
718
- }
719
-
720
- // Handle negative values inside brackets: m[-4]
721
- let partNegative = ''
722
- if (partValue.startsWith('-')) {
723
- partNegative = '-'
724
- partValue = partValue.slice(1)
725
- }
726
-
727
- // Handle important modifier: p[4!] -> !p-4
728
- let important = ''
729
- if (partValue.endsWith('!')) {
730
- important = '!'
731
- partValue = partValue.slice(0, -1)
732
- }
733
-
734
- const neg = isNegative ? '-' : ''
735
- if (needsArbitraryBrackets(partValue)) {
736
- results.push(`${variantPrefix}${innerVariant}${important}${neg}${partNegative}${prefix}-[${partValue}]`)
737
- } else {
738
- results.push(`${variantPrefix}${innerVariant}${important}${neg}${partNegative}${prefix}-${partValue}`)
739
- }
740
- }
741
- bracketExpansionCache.set(cacheKey, results)
742
- return results
743
- }
744
-
745
- const results: string[] = []
746
-
747
- // Normal expansion for each part
748
- for (const part of parts) {
749
- // Check for variant inside brackets: flex[md:col lg:row]
750
- const innerVariantMatch = part.match(/^([a-z@][a-z0-9-]*):(.+)$/i)
751
- let innerVariant = ''
752
- let partValue = part
753
-
754
- if (innerVariantMatch && variantPrefixes.has(innerVariantMatch[1])) {
755
- innerVariant = `${innerVariantMatch[1]}:`
756
- partValue = innerVariantMatch[2]
757
- }
758
-
759
- // Handle negative values inside brackets: m[-4]
760
- let partNegative = ''
761
- if (partValue.startsWith('-')) {
762
- partNegative = '-'
763
- partValue = partValue.slice(1)
764
- }
765
-
766
- // Handle important modifier: p[4!] -> !p-4
767
- let important = ''
768
- if (partValue.endsWith('!')) {
769
- important = '!'
770
- partValue = partValue.slice(0, -1)
771
- }
772
-
773
- const expanded = mapping.expand(partValue, aliases)
774
- if (expanded) {
775
- const neg = isNegative ? '-' : ''
776
- results.push(`${variantPrefix}${innerVariant}${important}${neg}${partNegative}${expanded}`)
777
- }
778
- }
779
-
780
- bracketExpansionCache.set(cacheKey, results)
781
- return results
782
- }
783
-
784
- /**
785
- * Extract classes from attributify syntax
786
- * e.g., <div hw-flex hw-items-center hw-bg="blue-500" hw-p="4">
787
- * Returns classes like: flex, items-center, bg-blue-500, p-4
788
- *
789
- * Also supports variant attributes:
790
- * e.g., <div hw-hover:bg="blue-600" hw-dark:text="white">
791
- * Returns classes like: hover:bg-blue-600, dark:text-white
792
- */
793
- export function extractAttributifyClasses(content: string, config?: AttributifyConfig): Set<string> {
794
- const classes = new Set<string>()
795
-
796
- if (!config?.enabled) {
797
- return classes
798
- }
799
-
800
- const prefix = config.prefix ?? 'hw-'
801
- const defaultIgnoreList = [
802
- 'class',
803
- 'className',
804
- 'style',
805
- 'id',
806
- 'name',
807
- 'type',
808
- 'value',
809
- 'href',
810
- 'src',
811
- 'alt',
812
- 'title',
813
- 'role',
814
- 'for',
815
- 'action',
816
- 'method',
817
- 'target',
818
- 'rel',
819
- 'placeholder',
820
- 'disabled',
821
- 'checked',
822
- 'selected',
823
- 'readonly',
824
- 'required',
825
- 'maxlength',
826
- 'minlength',
827
- 'pattern',
828
- 'autocomplete',
829
- 'autofocus',
830
- 'tabindex',
831
- 'contenteditable',
832
- 'draggable',
833
- 'spellcheck',
834
- 'lang',
835
- 'dir',
836
- 'hidden',
837
- 'slot',
838
- 'part',
839
- 'is',
840
- 'key',
841
- 'ref',
842
- ]
843
- const ignoreList = config.ignoreAttributes || defaultIgnoreList
844
-
845
- const shouldIgnore = (attr: string): boolean => {
846
- // Remove any variant prefix for ignore check
847
- const baseAttr = attr.includes(':') ? attr.split(':').pop()! : attr
848
-
849
- // Check ignore list
850
- for (const pattern of ignoreList) {
851
- if (pattern.endsWith('*')) {
852
- if (baseAttr.startsWith(pattern.slice(0, -1))) return true
853
- } else if (baseAttr === pattern) {
854
- return true
855
- }
856
- }
857
- // Also ignore attributes starting with on (event handlers)
858
- if (baseAttr.startsWith('on')) return true
859
- // Ignore data-* and aria-*
860
- if (baseAttr.startsWith('data-') || baseAttr.startsWith('aria-')) return true
861
- return false
862
- }
863
-
864
- // Extract all attributes from all tags
865
- // Match each tag separately
866
- const tagPattern = /<([a-z][a-z0-9-]*)\s([^>]*)>/gi
867
- let tagMatch
868
-
869
- // eslint-disable-next-line no-cond-assign
870
- while ((tagMatch = tagPattern.exec(content)) !== null) {
871
- const attributesStr = tagMatch[2]
872
-
873
- // Parse attributes from this tag
874
- // Match both value attributes and boolean attributes
875
- // Updated pattern to support colons in attribute names for variants: hw-hover:bg="blue-500"
876
- // Use greedy match for attribute name to capture full dark:hover:bg style names
877
- const attrPattern = /([a-z][a-z0-9-:]*)(?:=["']([^"']*)["'])?(?=\s|$|\/?>)/gi
878
- let attrMatch
879
-
880
- // eslint-disable-next-line no-cond-assign
881
- while ((attrMatch = attrPattern.exec(attributesStr)) !== null) {
882
- let attrName = attrMatch[1]
883
- const attrValue = attrMatch[2]
884
-
885
- // Handle prefix
886
- if (prefix) {
887
- if (!attrName.startsWith(prefix)) continue
888
- attrName = attrName.slice(prefix.length)
889
- }
890
-
891
- // Skip ignored attributes
892
- if (shouldIgnore(attrName)) continue
893
-
894
- // Check for variant prefix in attribute name: hover:bg, dark:text, etc.
895
- let variantPrefix = ''
896
- let utilityName = attrName
897
-
898
- // Extract variant(s) if present
899
- const variantMatch = attrName.match(/^((?:[a-z@][a-z0-9-]*:)+)([a-z][a-z0-9-]*)$/i)
900
- if (variantMatch) {
901
- const potentialVariants = variantMatch[1].slice(0, -1).split(':')
902
- // Verify all are valid variants
903
- const allVariants = potentialVariants.every(v => variantPrefixes.has(v))
904
- if (allVariants) {
905
- variantPrefix = variantMatch[1]
906
- utilityName = variantMatch[2]
907
- }
908
- }
909
-
910
- if (attrValue !== undefined) {
911
- // Value attribute: bg="blue-500" -> bg-blue-500
912
- // Or with variant: hover:bg="blue-600" -> hover:bg-blue-600
913
- const values = attrValue.split(/\s+/).filter(Boolean)
914
- for (const v of values) {
915
- const className = `${variantPrefix}${utilityName}-${v}`
916
- // Strip all variant prefixes for validation (e.g., dark:hover:bg-gray-800 -> bg-gray-800)
917
- if (isValidUtilityName(className.replace(/^(?:[a-z@][a-z0-9-]*:)+/gi, ''))) {
918
- classes.add(className)
919
- }
920
- }
921
- } else {
922
- // Boolean attribute: flex -> flex
923
- // Or with variant: hover:underline -> hover:underline
924
- const className = `${variantPrefix}${utilityName}`
925
- if (isValidUtilityName(utilityName)) {
926
- classes.add(className)
927
- }
928
- }
929
- }
930
- }
931
-
932
- return classes
933
- }
934
-
935
- /**
936
- * Check if a string looks like a valid utility name (for attributify)
937
- * More permissive than isValidClassName since we need to match potential utilities
938
- */
939
- function isValidUtilityName(name: string): boolean {
940
- if (!name || name.length === 0) return false
941
- // Must start with letter, can contain letters, numbers, dashes, slashes (for fractions)
942
- // Can have arbitrary values in brackets
943
- return /^[a-z][a-z0-9-/]*(?:-\[[^\]]+\])?$/i.test(name)
944
- }
945
-
946
- /**
947
- * Parses a utility class string into its components
948
- * Examples: "p-4" -> {raw: "p-4", variants: [], utility: "p", value: "4", important: false, arbitrary: false}
949
- * "hover:bg-blue-500" -> {raw: "hover:bg-blue-500", variants: ["hover"], utility: "bg", value: "blue-500", important: false, arbitrary: false}
950
- * "!p-4" -> {raw: "!p-4", variants: [], utility: "p", value: "4", important: true, arbitrary: false}
951
- * "w-[100px]" -> {raw: "w-[100px]", variants: [], utility: "w", value: "100px", important: false, arbitrary: true}
952
- */
953
- export function parseClass(className: string): ParsedClass {
954
- // Check cache first
955
- const cached = parseCache.get(className)
956
- if (cached) {
957
- return cached
958
- }
959
-
960
- const result = parseClassImpl(className)
961
- parseCache.set(className, result)
962
- return result
963
- }
964
-
965
- /**
966
- * Internal implementation of parseClass
967
- */
968
- function parseClassImpl(className: string): ParsedClass {
969
- // Check for important modifier
970
- let important = false
971
- let cleanClassName = className
972
- if (className.startsWith('!')) {
973
- important = true
974
- cleanClassName = className.slice(1)
975
- }
976
-
977
- // Check for arbitrary properties BEFORE splitting on colons: [color:red], [mask-type:luminance]
978
- const arbitraryPropMatch = cleanClassName.match(/^\[([a-z-]+):(.+)\]$/)
979
- if (arbitraryPropMatch) {
980
- return {
981
- raw: className,
982
- variants: [],
983
- utility: arbitraryPropMatch[1],
984
- value: arbitraryPropMatch[2],
985
- important,
986
- arbitrary: true,
987
- }
988
- }
989
-
990
- // Check for arbitrary values with brackets BEFORE splitting on colons
991
- // This handles cases like bg-[url(https://...)] where the URL contains colons
992
- // Also handles type hints like text-[color:var(--muted)]
993
- const preArbitraryMatch = cleanClassName.match(/^((?:[a-z-]+:)*)([a-z-]+?)-\[(.+)\]$/)
994
- if (preArbitraryMatch) {
995
- const variantPart = preArbitraryMatch[1]
996
- const variants = variantPart ? variantPart.split(':').filter(Boolean) : []
997
- let value = preArbitraryMatch[3]
998
- let typeHint: string | undefined
999
-
1000
- // Check for type hint in arbitrary value: text-[color:var(--muted)]
1001
- // Type hints are: color, length, url, number, percentage, position, etc.
1002
- // Don't match if it looks like a CSS variable var(--...) or CSS function
1003
- const typeHintMatch = value.match(/^(color|length|url|number|percentage|position|line-width|absolute-size|relative-size|image|angle|time|flex|string|family-name):(.*)/i)
1004
- if (typeHintMatch) {
1005
- typeHint = typeHintMatch[1].toLowerCase()
1006
- value = typeHintMatch[2]
1007
- }
1008
-
1009
- return {
1010
- raw: className,
1011
- variants,
1012
- utility: preArbitraryMatch[2],
1013
- value,
1014
- important,
1015
- arbitrary: true,
1016
- typeHint,
1017
- }
1018
- }
1019
-
1020
- const parts = cleanClassName.split(':')
1021
- const utility = parts[parts.length - 1]
1022
- const variants = parts.slice(0, -1)
1023
-
1024
- // Check for full utility names that should not be split
1025
- const fullUtilityNames = [
1026
- // Display utilities
1027
- 'inline-block',
1028
- 'inline-flex',
1029
- 'inline-grid',
1030
- // Flex utilities without values
1031
- 'flex-row',
1032
- 'flex-row-reverse',
1033
- 'flex-col',
1034
- 'flex-col-reverse',
1035
- 'flex-wrap',
1036
- 'flex-wrap-reverse',
1037
- 'flex-nowrap',
1038
- 'flex-1',
1039
- 'flex-auto',
1040
- 'flex-initial',
1041
- 'flex-none',
1042
- 'flex-grow',
1043
- 'flex-shrink',
1044
- ]
1045
- if (fullUtilityNames.includes(utility)) {
1046
- return {
1047
- raw: className,
1048
- variants,
1049
- utility,
1050
- value: undefined,
1051
- important,
1052
- arbitrary: false,
1053
- }
1054
- }
1055
-
1056
- // Check for arbitrary values: w-[100px] or bg-[#ff0000] or text-[color:var(--muted)]
1057
- const arbitraryMatch = utility.match(/^([a-z-]+?)-\[(.+?)\]$/)
1058
- if (arbitraryMatch) {
1059
- let value = arbitraryMatch[2]
1060
- let typeHint: string | undefined
1061
-
1062
- // Check for type hint in arbitrary value: text-[color:var(--muted)]
1063
- // Type hints are: color, length, url, number, percentage, position, etc.
1064
- const typeHintMatch = value.match(/^(color|length|url|number|percentage|position|line-width|absolute-size|relative-size|image|angle|time|flex|string|family-name):(.*)/i)
1065
- if (typeHintMatch) {
1066
- typeHint = typeHintMatch[1].toLowerCase()
1067
- value = typeHintMatch[2]
1068
- }
1069
-
1070
- return {
1071
- raw: className,
1072
- variants,
1073
- utility: arbitraryMatch[1],
1074
- value,
1075
- important,
1076
- arbitrary: true,
1077
- typeHint,
1078
- }
1079
- }
1080
-
1081
- // Handle compound utilities with specific prefixes
1082
- // grid-cols-3, grid-rows-2, translate-x-4, etc.
1083
- const compoundPrefixes = [
1084
- // Border side utilities (border-t-0, border-r-2, etc.)
1085
- 'border-t',
1086
- 'border-r',
1087
- 'border-b',
1088
- 'border-l',
1089
- 'border-x',
1090
- 'border-y',
1091
- // Logical border utilities (for RTL support)
1092
- 'border-s',
1093
- 'border-e',
1094
- 'grid-cols',
1095
- 'grid-rows',
1096
- 'grid-flow',
1097
- 'auto-cols',
1098
- 'auto-rows',
1099
- 'col-span',
1100
- 'row-span',
1101
- 'col-start',
1102
- 'col-end',
1103
- 'row-start',
1104
- 'row-end',
1105
- 'translate-x',
1106
- 'translate-y',
1107
- 'translate-z',
1108
- 'scale-x',
1109
- 'scale-y',
1110
- 'scale-z',
1111
- 'rotate-x',
1112
- 'rotate-y',
1113
- 'rotate-z',
1114
- 'skew-x',
1115
- 'skew-y',
1116
- 'scroll-m',
1117
- 'scroll-mx',
1118
- 'scroll-my',
1119
- 'scroll-mt',
1120
- 'scroll-mr',
1121
- 'scroll-mb',
1122
- 'scroll-ml',
1123
- 'scroll-p',
1124
- 'scroll-px',
1125
- 'scroll-py',
1126
- 'scroll-pt',
1127
- 'scroll-pr',
1128
- 'scroll-pb',
1129
- 'scroll-pl',
1130
- 'gap-x',
1131
- 'gap-y',
1132
- 'overflow-x',
1133
- 'overflow-y',
1134
- 'min-w',
1135
- 'max-w',
1136
- 'min-h',
1137
- 'max-h',
1138
- 'space-x',
1139
- 'space-y',
1140
- 'ring-offset',
1141
- 'underline-offset',
1142
- 'outline-offset',
1143
- 'backdrop-blur',
1144
- 'backdrop-brightness',
1145
- 'backdrop-contrast',
1146
- 'backdrop-grayscale',
1147
- 'backdrop-invert',
1148
- 'backdrop-saturate',
1149
- 'backdrop-sepia',
1150
- 'hue-rotate',
1151
- 'drop-shadow',
1152
- 'mask-clip',
1153
- 'flex-grow',
1154
- 'flex-shrink',
1155
- 'mask-composite',
1156
- 'mask-image',
1157
- 'mask-mode',
1158
- 'mask-origin',
1159
- 'mask-position',
1160
- 'mask-repeat',
1161
- 'mask-size',
1162
- 'mask-type',
1163
- 'perspective-origin',
1164
- 'justify-self',
1165
- 'form-input',
1166
- 'form-textarea',
1167
- 'form-select',
1168
- 'form-multiselect',
1169
- 'form-checkbox',
1170
- 'form-radio',
1171
- 'mix-blend',
1172
- 'bg-blend',
1173
- 'line-clamp',
1174
- 'border-spacing',
1175
- 'border-spacing-x',
1176
- 'border-spacing-y',
1177
- 'rounded-s',
1178
- 'rounded-e',
1179
- 'rounded-ss',
1180
- 'rounded-se',
1181
- 'rounded-es',
1182
- 'rounded-ee',
1183
- 'border-opacity',
1184
- 'ring-opacity',
1185
- 'stroke-dasharray',
1186
- 'stroke-dashoffset',
1187
- 'animate-iteration',
1188
- 'animate-duration',
1189
- 'animate-delay',
1190
- 'text-emphasis',
1191
- 'text-emphasis-color',
1192
- 'word-spacing',
1193
- 'column-gap',
1194
- 'column-rule',
1195
- ]
1196
-
1197
- // Special case for divide-x and divide-y (without values, they should be treated as compound)
1198
- // divide-x -> utility: "divide-x", value: undefined
1199
- // divide-x-2 -> utility: "divide-x", value: "2"
1200
- if (utility === 'divide-x' || utility === 'divide-y') {
1201
- return {
1202
- raw: className,
1203
- variants,
1204
- utility,
1205
- value: undefined,
1206
- important,
1207
- arbitrary: false,
1208
- }
1209
- }
1210
-
1211
- // Check for divide-x-{width} and divide-y-{width}
1212
- const divideMatch = utility.match(/^(divide-[xy])-(.+)$/)
1213
- if (divideMatch) {
1214
- return {
1215
- raw: className,
1216
- variants,
1217
- utility: divideMatch[1],
1218
- value: divideMatch[2],
1219
- important,
1220
- arbitrary: false,
1221
- }
1222
- }
1223
-
1224
- for (const prefix of compoundPrefixes) {
1225
- if (utility.startsWith(`${prefix}-`)) {
1226
- return {
1227
- raw: className,
1228
- variants,
1229
- utility: prefix,
1230
- value: utility.slice(prefix.length + 1),
1231
- important,
1232
- arbitrary: false,
1233
- }
1234
- }
1235
- }
1236
-
1237
- // Check for negative values: -m-4, -top-4, -translate-x-4
1238
- if (utility.startsWith('-')) {
1239
- const positiveUtility = utility.slice(1)
1240
-
1241
- // Try compound prefixes first
1242
- for (const prefix of compoundPrefixes) {
1243
- if (positiveUtility.startsWith(`${prefix}-`)) {
1244
- return {
1245
- raw: className,
1246
- variants,
1247
- utility: prefix,
1248
- value: `-${positiveUtility.slice(prefix.length + 1)}`,
1249
- important,
1250
- arbitrary: false,
1251
- }
1252
- }
1253
- }
1254
-
1255
- // Regular negative value
1256
- const match = positiveUtility.match(/^([a-z]+(?:-[a-z]+)*?)-(.+)$/)
1257
- if (match) {
1258
- return {
1259
- raw: className,
1260
- variants,
1261
- utility: match[1],
1262
- value: `-${match[2]}`,
1263
- important,
1264
- arbitrary: false,
1265
- }
1266
- }
1267
- // If no match, it's a standalone utility with just a negative sign (e.g., -flex doesn't make sense)
1268
- // Return as-is
1269
- return {
1270
- raw: className,
1271
- variants,
1272
- utility: positiveUtility,
1273
- value: undefined,
1274
- important,
1275
- arbitrary: false,
1276
- }
1277
- }
1278
-
1279
- // Check for color opacity modifiers: bg-blue-500/50, text-red-500/75
1280
- // Must come before fractional values to avoid conflict
1281
- const opacityMatch = utility.match(/^([a-z]+(?:-[a-z]+)*?)-(.+?)\/(\d+)$/)
1282
- if (opacityMatch && ['bg', 'text', 'border', 'ring', 'placeholder', 'divide'].includes(opacityMatch[1])) {
1283
- return {
1284
- raw: className,
1285
- variants,
1286
- utility: opacityMatch[1],
1287
- value: `${opacityMatch[2]}/${opacityMatch[3]}`,
1288
- important,
1289
- arbitrary: false,
1290
- }
1291
- }
1292
-
1293
- // Check for fractional values: w-1/2, h-3/4
1294
- const fractionMatch = utility.match(/^([a-z]+(?:-[a-z]+)*?)-(\d+)\/(\d+)$/)
1295
- if (fractionMatch) {
1296
- return {
1297
- raw: className,
1298
- variants,
1299
- utility: fractionMatch[1],
1300
- value: `${fractionMatch[2]}/${fractionMatch[3]}`,
1301
- important,
1302
- arbitrary: false,
1303
- }
1304
- }
1305
-
1306
- // Regular parsing - split on last dash
1307
- // First try: utility-value pattern (e.g., text-current, p-4)
1308
- const matchWithValue = utility.match(/^([a-z]+(?:-[a-z]+)*?)-(.+)$/)
1309
- if (matchWithValue) {
1310
- return {
1311
- raw: className,
1312
- variants,
1313
- utility: matchWithValue[1],
1314
- value: matchWithValue[2],
1315
- important,
1316
- arbitrary: false,
1317
- }
1318
- }
1319
-
1320
- // If no dash, treat entire string as utility with no value (e.g., flex, block)
1321
- return {
1322
- raw: className,
1323
- variants,
1324
- utility,
1325
- value: undefined,
1326
- important,
1327
- arbitrary: false,
1328
- }
1329
- }
1330
-
1331
-
1332
- /**
1333
- * Validates if a string is a valid Tailwind utility class name
1334
- * Supports arbitrary values like z-[1], w-[100px], bg-[#ff0000]
1335
- * Supports variants like hover:, focus:, sm:, md:
1336
- * Supports important modifier !
1337
- * Supports negative values like -m-4
1338
- * Supports bracket syntax like flex[col jc-center] when enabled
1339
- * Supports colon syntax like bg:black when enabled
1340
- */
1341
- function isValidClassName(name: string, bracketConfig?: BracketSyntaxConfig): boolean {
1342
- // Quick check for obviously invalid names
1343
- if (!name || name.length === 0) return false
1344
-
1345
- // Arbitrary properties like [color:red], [mask-type:luminance]
1346
- if (/^\[[a-z-]+:[^\]]+\]$/i.test(name)) return true
1347
-
1348
- // Bracket syntax: flex[col jc-center ai-center]
1349
- if (bracketConfig?.enabled && /^[a-z]+\[[^\]]+\]$/i.test(name)) return true
1350
-
1351
- // Colon syntax: bg:black, w:100%
1352
- if (bracketConfig?.colonSyntax && /^[a-z]+:[^[\]:]+$/i.test(name)) return true
1353
-
1354
- // Standard utility classes with optional:
1355
- // - ! prefix (important)
1356
- // - - prefix (negative values)
1357
- // - Arbitrary values in brackets like -[100px] or -[#ff0000]
1358
- // - Variant prefixes with colons like hover:, sm:, focus:
1359
- // - Decimal values like py-2.5, gap-0.5
1360
- return /^!?-?[a-z][a-z0-9.-]*(?:-\[[^\]]+\])?(?::!?-?[a-z][a-z0-9.-]*(?:-\[[^\]]+\])?)*$/i.test(name)
1361
- }
1362
-
1363
- /**
1364
- * Split class string preserving bracket groups
1365
- * e.g., "flex[col jc-center] bg:black p-4" -> ["flex[col jc-center]", "bg:black", "p-4"]
1366
- */
1367
- function splitClassString(classStr: string): string[] {
1368
- const classes: string[] = []
1369
- let current = ''
1370
- let bracketDepth = 0
1371
-
1372
- for (const char of classStr) {
1373
- if (char === '[') {
1374
- bracketDepth++
1375
- current += char
1376
- } else if (char === ']') {
1377
- bracketDepth--
1378
- current += char
1379
- } else if (/\s/.test(char) && bracketDepth === 0) {
1380
- if (current) {
1381
- classes.push(current)
1382
- current = ''
1383
- }
1384
- } else {
1385
- current += char
1386
- }
1387
- }
1388
-
1389
- if (current) {
1390
- classes.push(current)
1391
- }
1392
-
1393
- return classes
1394
- }
1395
-
1396
- /**
1397
- * Extracts all utility classes from content
1398
- * Matches class patterns in HTML/JSX attributes and template strings
1399
- * Supports bracket syntax (e.g., flex[col jc-center]) and attributify mode
1400
- */
1401
- export function extractClasses(content: string, options?: ExtractClassesOptions): Set<string> {
1402
- const classes = new Set<string>()
1403
-
1404
- // Match class="..." and className="..." and className={...}
1405
- const patterns = [
1406
- /class(?:Name)?=["']([^"']+)["']/g,
1407
- /class(?:Name)?=\{["']([^"']+)["']\}/g,
1408
- /class(?:Name)?=\{`([^`]+)`\}/g,
1409
- ]
1410
-
1411
- for (const pattern of patterns) {
1412
- let match
1413
- // eslint-disable-next-line no-cond-assign
1414
- while ((match = pattern.exec(content)) !== null) {
1415
- const classStr = match[1]
1416
- // Extract all quoted strings from the class string (handles template literals with expressions)
1417
- const quotedStrings = classStr.match(/["']([^"']+)["']/g)
1418
- if (quotedStrings) {
1419
- for (const quoted of quotedStrings) {
1420
- const cleaned = quoted.replace(/["']/g, '')
1421
- const classNames = splitClassString(cleaned)
1422
- for (const className of classNames) {
1423
- addClassWithExpansion(classes, className, options)
1424
- }
1425
- }
1426
- }
1427
-
1428
- // Also extract classes not in quotes (for simple cases)
1429
- const cleanedStr = classStr
1430
- .replace(/["'`]/g, ' ')
1431
- .replace(/\$\{[^}]+\}/g, ' ')
1432
-
1433
- const classNames = splitClassString(cleanedStr)
1434
- .filter(name => isValidClassName(name, options?.bracketSyntax))
1435
-
1436
- for (const className of classNames) {
1437
- addClassWithExpansion(classes, className, options)
1438
- }
1439
- }
1440
- }
1441
-
1442
- // Extract attributify classes if enabled
1443
- if (options?.attributify?.enabled) {
1444
- const attributifyClasses = extractAttributifyClasses(content, options.attributify)
1445
- for (const cls of attributifyClasses) {
1446
- classes.add(cls)
1447
- }
1448
- }
1449
-
1450
- return classes
1451
- }
1452
-
1453
- /**
1454
- * Helper to add a class with potential bracket syntax expansion
1455
- */
1456
- function addClassWithExpansion(classes: Set<string>, className: string, options?: ExtractClassesOptions): void {
1457
- if (options?.bracketSyntax?.enabled) {
1458
- // Check if this is bracket or colon syntax
1459
- const hasBracket = /^[a-z]+\[[^\]]+\]$/i.test(className)
1460
- const hasColon = options.bracketSyntax.colonSyntax && /^[a-z]+:[^[\]]+$/i.test(className)
1461
-
1462
- if (hasBracket || hasColon) {
1463
- const expanded = expandBracketSyntax(className, options.bracketSyntax)
1464
- for (const cls of expanded) {
1465
- classes.add(cls)
1466
- }
1467
- return
1468
- }
1469
- }
1470
- classes.add(className)
1471
- }