@cwcss/crosswind 0.1.4 → 0.1.6
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/LICENSE.md +21 -0
- package/README.md +390 -0
- package/dist/build.d.ts +24 -0
- package/dist/config.d.ts +5 -0
- package/dist/generator.d.ts +31 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +12798 -0
- package/dist/parser.d.ts +42 -0
- package/dist/plugin.d.ts +22 -0
- package/dist/preflight-forms.d.ts +5 -0
- package/dist/preflight.d.ts +2 -0
- package/dist/rules-advanced.d.ts +27 -0
- package/dist/rules-effects.d.ts +25 -0
- package/dist/rules-forms.d.ts +7 -0
- package/dist/rules-grid.d.ts +13 -0
- package/dist/rules-interactivity.d.ts +41 -0
- package/dist/rules-layout.d.ts +26 -0
- package/dist/rules-transforms.d.ts +33 -0
- package/dist/rules-typography.d.ts +41 -0
- package/dist/rules.d.ts +39 -0
- package/dist/scanner.d.ts +18 -0
- package/dist/transformer-compile-class.d.ts +37 -0
- package/{src/types.ts → dist/types.d.ts} +17 -86
- package/package.json +1 -1
- package/PLUGIN.md +0 -235
- package/benchmark/framework-comparison.bench.ts +0 -850
- package/bin/cli.ts +0 -365
- package/bin/crosswind +0 -0
- package/bin/headwind +0 -0
- package/build.ts +0 -8
- package/crosswind.config.ts +0 -9
- package/example/comprehensive.html +0 -70
- package/example/index.html +0 -21
- package/example/output.css +0 -236
- package/examples/plugin/README.md +0 -112
- package/examples/plugin/build.ts +0 -32
- package/examples/plugin/src/index.html +0 -34
- package/examples/plugin/src/index.ts +0 -7
- package/headwind +0 -2
- package/src/build.ts +0 -101
- package/src/config.ts +0 -529
- package/src/generator.ts +0 -2173
- package/src/index.ts +0 -10
- package/src/parser.ts +0 -1471
- package/src/plugin.ts +0 -118
- package/src/preflight-forms.ts +0 -229
- package/src/preflight.ts +0 -388
- package/src/rules-advanced.ts +0 -477
- package/src/rules-effects.ts +0 -457
- package/src/rules-forms.ts +0 -103
- package/src/rules-grid.ts +0 -241
- package/src/rules-interactivity.ts +0 -525
- package/src/rules-layout.ts +0 -385
- package/src/rules-transforms.ts +0 -412
- package/src/rules-typography.ts +0 -486
- package/src/rules.ts +0 -805
- package/src/scanner.ts +0 -84
- package/src/transformer-compile-class.ts +0 -275
- package/test/advanced-features.test.ts +0 -911
- package/test/arbitrary.test.ts +0 -396
- package/test/attributify.test.ts +0 -592
- package/test/bracket-syntax.test.ts +0 -1133
- package/test/build.test.ts +0 -99
- package/test/colors.test.ts +0 -934
- package/test/flexbox.test.ts +0 -669
- package/test/generator.test.ts +0 -597
- package/test/grid.test.ts +0 -584
- package/test/layout.test.ts +0 -404
- package/test/modifiers.test.ts +0 -417
- package/test/parser.test.ts +0 -564
- package/test/performance-regression.test.ts +0 -376
- package/test/performance.test.ts +0 -568
- package/test/plugin.test.ts +0 -160
- package/test/scanner.test.ts +0 -94
- package/test/sizing.test.ts +0 -481
- package/test/spacing.test.ts +0 -394
- package/test/transformer-compile-class.test.ts +0 -287
- package/test/transforms.test.ts +0 -448
- package/test/typography.test.ts +0 -632
- package/test/variants-form-states.test.ts +0 -225
- package/test/variants-group-peer.test.ts +0 -66
- package/test/variants-media.test.ts +0 -213
- package/test/variants-positional.test.ts +0 -58
- package/test/variants-pseudo-elements.test.ts +0 -47
- package/test/variants-state.test.ts +0 -62
- 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
|
-
}
|