mnfst 0.5.199-next.4 → 0.5.199
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/lib/manifest.appwrite.data.js +1 -1
- package/lib/manifest.data.js +1 -1
- package/lib/manifest.integrity.json +6 -6
- package/lib/manifest.js +11 -1
- package/lib/manifest.localization.js +11 -7
- package/lib/manifest.sw.js +1 -1
- package/lib/manifest.utilities.js +204 -7
- package/lib/manifest.utilities.node.mjs +2870 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2870 @@
|
|
|
1
|
+
/* manifest.utilities.node.mjs — built from scripts/utilities/ (DOM-free) */
|
|
2
|
+
|
|
3
|
+
// Utility generators
|
|
4
|
+
// Functions that generate CSS utilities from CSS variable suffixes
|
|
5
|
+
|
|
6
|
+
function createUtilityGenerators() {
|
|
7
|
+
return {
|
|
8
|
+
'color-': (suffix, value) => {
|
|
9
|
+
const utilities = [];
|
|
10
|
+
const addUtility = (prefix, property, baseValue) => {
|
|
11
|
+
utilities.push([`${prefix}-${suffix}`, `${property}: ${baseValue}`]);
|
|
12
|
+
};
|
|
13
|
+
addUtility('text', 'color', value);
|
|
14
|
+
addUtility('bg', 'background-color', value);
|
|
15
|
+
addUtility('border', 'border-color', value);
|
|
16
|
+
addUtility('outline', 'outline-color', value);
|
|
17
|
+
addUtility('ring', 'box-shadow', `0 0 0 1px ${value}`);
|
|
18
|
+
addUtility('fill', 'fill', value);
|
|
19
|
+
addUtility('stroke', 'stroke', value);
|
|
20
|
+
addUtility('decoration', 'text-decoration-color', value);
|
|
21
|
+
addUtility('accent', 'accent-color', value);
|
|
22
|
+
addUtility('caret', 'caret-color', value);
|
|
23
|
+
utilities.push([`from-${suffix}`, `--tw-gradient-from: ${value}; --tw-gradient-stops: var(--tw-gradient-via-stops)`]);
|
|
24
|
+
utilities.push([`via-${suffix}`, `--tw-gradient-via: ${value}; --tw-gradient-stops: var(--tw-gradient-via-stops)`]);
|
|
25
|
+
utilities.push([`to-${suffix}`, `--tw-gradient-to: ${value}; --tw-gradient-stops: var(--tw-gradient-via-stops)`]);
|
|
26
|
+
return utilities;
|
|
27
|
+
},
|
|
28
|
+
'font-': (suffix, value) => [
|
|
29
|
+
[`font-${suffix}`, `font-family: ${value}`]
|
|
30
|
+
],
|
|
31
|
+
'text-': (suffix, value) => [
|
|
32
|
+
[`text-${suffix}`, `font-size: ${value}`]
|
|
33
|
+
],
|
|
34
|
+
'font-weight-': (suffix, value) => [
|
|
35
|
+
[`font-${suffix}`, `font-weight: ${value}`]
|
|
36
|
+
],
|
|
37
|
+
'tracking-': (suffix, value) => [
|
|
38
|
+
[`tracking-${suffix}`, `letter-spacing: ${value}`]
|
|
39
|
+
],
|
|
40
|
+
'leading-': (suffix, value) => [
|
|
41
|
+
[`leading-${suffix}`, `line-height: ${value}`]
|
|
42
|
+
],
|
|
43
|
+
'breakpoint-': (suffix, value) => [
|
|
44
|
+
[`@${suffix}`, `@media (min-width: ${value})`]
|
|
45
|
+
],
|
|
46
|
+
'container-': (suffix, value) => [
|
|
47
|
+
[`container-${suffix}`, `max-width: ${value}`],
|
|
48
|
+
[`@container-${suffix}`, `@container (min-width: ${value})`]
|
|
49
|
+
],
|
|
50
|
+
'spacing-': (suffix, value) => [
|
|
51
|
+
[`gap-${suffix}`, `gap: ${value}`],
|
|
52
|
+
[`p-${suffix}`, `padding: ${value}`],
|
|
53
|
+
[`px-${suffix}`, `padding-left: ${value}; padding-right: ${value}`],
|
|
54
|
+
[`py-${suffix}`, `padding-top: ${value}; padding-bottom: ${value}`],
|
|
55
|
+
[`m-${suffix}`, `margin: ${value}`],
|
|
56
|
+
[`mx-${suffix}`, `margin-left: ${value}; margin-right: ${value}`],
|
|
57
|
+
[`my-${suffix}`, `margin-top: ${value}; margin-bottom: ${value}`],
|
|
58
|
+
[`space-x-${suffix}`, `> * + * { margin-left: ${value}; }`],
|
|
59
|
+
[`space-y-${suffix}`, `> * + * { margin-top: ${value}; }`],
|
|
60
|
+
[`max-w-${suffix}`, `max-width: ${value}`],
|
|
61
|
+
[`max-h-${suffix}`, `max-height: ${value}`],
|
|
62
|
+
[`min-w-${suffix}`, `min-width: ${value}`],
|
|
63
|
+
[`min-h-${suffix}`, `min-height: ${value}`],
|
|
64
|
+
[`w-${suffix}`, `width: ${value}`],
|
|
65
|
+
[`h-${suffix}`, `height: ${value}`]
|
|
66
|
+
],
|
|
67
|
+
'radius-': (suffix, value) => [
|
|
68
|
+
[`rounded-${suffix}`, `border-radius: ${value}`]
|
|
69
|
+
],
|
|
70
|
+
'shadow-': (suffix, value) => [
|
|
71
|
+
[`shadow-${suffix}`, `box-shadow: ${value}`]
|
|
72
|
+
],
|
|
73
|
+
'inset-shadow-': (suffix, value) => [
|
|
74
|
+
[`inset-shadow-${suffix}`, `box-shadow: inset ${value}`]
|
|
75
|
+
],
|
|
76
|
+
'drop-shadow-': (suffix, value) => [
|
|
77
|
+
[`drop-shadow-${suffix}`, `filter: drop-shadow(${value})`]
|
|
78
|
+
],
|
|
79
|
+
'blur-': (suffix, value) => [
|
|
80
|
+
[`blur-${suffix}`, `filter: blur(${value})`]
|
|
81
|
+
],
|
|
82
|
+
'perspective-': (suffix, value) => [
|
|
83
|
+
[`perspective-${suffix}`, `perspective: ${value}`]
|
|
84
|
+
],
|
|
85
|
+
'aspect-': (suffix, value) => [
|
|
86
|
+
[`aspect-${suffix}`, `aspect-ratio: ${value}`]
|
|
87
|
+
],
|
|
88
|
+
'ease-': (suffix, value) => [
|
|
89
|
+
[`ease-${suffix}`, `transition-timing-function: ${value}`]
|
|
90
|
+
],
|
|
91
|
+
'animate-': (suffix, value) => [
|
|
92
|
+
[`animate-${suffix}`, `animation: ${value}`]
|
|
93
|
+
],
|
|
94
|
+
'border-width-': (suffix, value) => [
|
|
95
|
+
[`border-${suffix}`, `border-width: ${value}`]
|
|
96
|
+
],
|
|
97
|
+
'border-style-': (suffix, value) => [
|
|
98
|
+
[`border-${suffix}`, `border-style: ${value}`]
|
|
99
|
+
],
|
|
100
|
+
'outline-': (suffix, value) => [
|
|
101
|
+
[`outline-${suffix}`, `outline-color: ${value}`]
|
|
102
|
+
],
|
|
103
|
+
'outline-width-': (suffix, value) => [
|
|
104
|
+
[`outline-${suffix}`, `outline-width: ${value}`]
|
|
105
|
+
],
|
|
106
|
+
'outline-style-': (suffix, value) => [
|
|
107
|
+
[`outline-${suffix}`, `outline-style: ${value}`]
|
|
108
|
+
],
|
|
109
|
+
'ring-': (suffix, value) => [
|
|
110
|
+
[`ring-${suffix}`, `box-shadow: 0 0 0 ${value} var(--color-ring)`]
|
|
111
|
+
],
|
|
112
|
+
'ring-offset-': (suffix, value) => [
|
|
113
|
+
[`ring-offset-${suffix}`, `--tw-ring-offset-width: ${value}`]
|
|
114
|
+
],
|
|
115
|
+
'divide-': (suffix, value) => [
|
|
116
|
+
[`divide-${suffix}`, `border-color: ${value}`]
|
|
117
|
+
],
|
|
118
|
+
'accent-': (suffix, value) => [
|
|
119
|
+
[`accent-${suffix}`, `accent-color: ${value}`]
|
|
120
|
+
],
|
|
121
|
+
'caret-': (suffix, value) => [
|
|
122
|
+
[`caret-${suffix}`, `caret-color: ${value}`]
|
|
123
|
+
],
|
|
124
|
+
'decoration-': (suffix, value) => [
|
|
125
|
+
[`decoration-${suffix}`, `text-decoration-color: ${value}`]
|
|
126
|
+
],
|
|
127
|
+
'placeholder-': (suffix, value) => [
|
|
128
|
+
[`placeholder-${suffix}`, `&::placeholder { color: ${value} }`]
|
|
129
|
+
],
|
|
130
|
+
'selection-': (suffix, value) => [
|
|
131
|
+
[`selection-${suffix}`, `&::selection { background-color: ${value} }`]
|
|
132
|
+
],
|
|
133
|
+
'scrollbar-': (suffix, value) => [
|
|
134
|
+
[`scrollbar-${suffix}`, `scrollbar-color: ${value}`]
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
// Variants and variant groups
|
|
142
|
+
// CSS selector mappings for Tailwind variants
|
|
143
|
+
|
|
144
|
+
function createVariants() {
|
|
145
|
+
return {
|
|
146
|
+
// State variants
|
|
147
|
+
'hover': ':hover',
|
|
148
|
+
'focus': ':focus',
|
|
149
|
+
'focus-visible': ':focus-visible',
|
|
150
|
+
'focus-within': ':focus-within',
|
|
151
|
+
'active': ':active',
|
|
152
|
+
'visited': ':visited',
|
|
153
|
+
'target': ':target',
|
|
154
|
+
'first': ':first-child',
|
|
155
|
+
'last': ':last-child',
|
|
156
|
+
'only': ':only-child',
|
|
157
|
+
'odd': ':nth-child(odd)',
|
|
158
|
+
'even': ':nth-child(even)',
|
|
159
|
+
'first-of-type': ':first-of-type',
|
|
160
|
+
'last-of-type': ':last-of-type',
|
|
161
|
+
'only-of-type': ':only-of-type',
|
|
162
|
+
'empty': ':empty',
|
|
163
|
+
'disabled': ':disabled',
|
|
164
|
+
'enabled': ':enabled',
|
|
165
|
+
'checked': ':checked',
|
|
166
|
+
'indeterminate': ':indeterminate',
|
|
167
|
+
'default': ':default',
|
|
168
|
+
'required': ':required',
|
|
169
|
+
'valid': ':valid',
|
|
170
|
+
'invalid': ':invalid',
|
|
171
|
+
'in-range': ':in-range',
|
|
172
|
+
'out-of-range': ':out-of-range',
|
|
173
|
+
'placeholder-shown': ':placeholder-shown',
|
|
174
|
+
'autofill': ':autofill',
|
|
175
|
+
'read-only': ':read-only',
|
|
176
|
+
'read-write': ':read-write',
|
|
177
|
+
'optional': ':optional',
|
|
178
|
+
'user-valid': ':user-valid',
|
|
179
|
+
'user-invalid': ':user-invalid',
|
|
180
|
+
'inert': ':inert',
|
|
181
|
+
'open': ':is([open], :popover-open, :open) &',
|
|
182
|
+
'closed': ':not(:is([open], :popover-open, :open)) &',
|
|
183
|
+
'paused': '[data-state="paused"] &',
|
|
184
|
+
'playing': '[data-state="playing"] &',
|
|
185
|
+
'muted': '[data-state="muted"] &',
|
|
186
|
+
'unmuted': '[data-state="unmuted"] &',
|
|
187
|
+
'collapsed': '[data-state="collapsed"] &',
|
|
188
|
+
'expanded': '[data-state="expanded"] &',
|
|
189
|
+
'unchecked': ':not(:checked)',
|
|
190
|
+
'selected': '[data-state="selected"] &',
|
|
191
|
+
'unselected': '[data-state="unselected"] &',
|
|
192
|
+
'details-content': '::details-content',
|
|
193
|
+
'nth': ':nth-child',
|
|
194
|
+
'nth-last': ':nth-last-child',
|
|
195
|
+
'nth-of-type': ':nth-of-type',
|
|
196
|
+
'nth-last-of-type': ':nth-last-of-type',
|
|
197
|
+
'has': ':has',
|
|
198
|
+
'not': ':not',
|
|
199
|
+
|
|
200
|
+
// Pseudo-elements
|
|
201
|
+
'before': '::before',
|
|
202
|
+
'after': '::after',
|
|
203
|
+
'first-letter': '::first-letter',
|
|
204
|
+
'first-line': '::first-line',
|
|
205
|
+
'marker': '::marker',
|
|
206
|
+
'selection': '::selection',
|
|
207
|
+
'file': '::file-selector-button',
|
|
208
|
+
'backdrop': '::backdrop',
|
|
209
|
+
'placeholder': '::placeholder',
|
|
210
|
+
'target-text': '::target-text',
|
|
211
|
+
'spelling-error': '::spelling-error',
|
|
212
|
+
'grammar-error': '::grammar-error',
|
|
213
|
+
|
|
214
|
+
// Media queries
|
|
215
|
+
'dark': '.dark &',
|
|
216
|
+
'light': '.light &',
|
|
217
|
+
|
|
218
|
+
// Group variants
|
|
219
|
+
'group': '.group &',
|
|
220
|
+
'group-hover': '.group:hover &',
|
|
221
|
+
'group-focus': '.group:focus &',
|
|
222
|
+
'group-focus-within': '.group:focus-within &',
|
|
223
|
+
'group-active': '.group:active &',
|
|
224
|
+
'group-disabled': '.group:disabled &',
|
|
225
|
+
'group-visited': '.group:visited &',
|
|
226
|
+
'group-checked': '.group:checked &',
|
|
227
|
+
'group-required': '.group:required &',
|
|
228
|
+
'group-valid': '.group:valid &',
|
|
229
|
+
'group-invalid': '.group:invalid &',
|
|
230
|
+
'group-in-range': '.group:in-range &',
|
|
231
|
+
'group-out-of-range': '.group:out-of-range &',
|
|
232
|
+
'group-placeholder-shown': '.group:placeholder-shown &',
|
|
233
|
+
'group-autofill': '.group:autofill &',
|
|
234
|
+
'group-read-only': '.group:read-only &',
|
|
235
|
+
'group-read-write': '.group:read-write &',
|
|
236
|
+
'group-optional': '.group:optional &',
|
|
237
|
+
'group-user-valid': '.group:user-valid &',
|
|
238
|
+
'group-user-invalid': '.group:user-invalid &',
|
|
239
|
+
|
|
240
|
+
// Peer variants
|
|
241
|
+
'peer': '.peer ~ &',
|
|
242
|
+
'peer-hover': '.peer:hover ~ &',
|
|
243
|
+
'peer-focus': '.peer:focus ~ &',
|
|
244
|
+
'peer-focus-within': '.peer:focus-within ~ &',
|
|
245
|
+
'peer-active': '.peer:active ~ &',
|
|
246
|
+
'peer-disabled': '.peer:disabled ~ &',
|
|
247
|
+
'peer-visited': '.peer:visited ~ &',
|
|
248
|
+
'peer-checked': '.peer:checked ~ &',
|
|
249
|
+
'peer-required': '.peer:required ~ &',
|
|
250
|
+
'peer-valid': '.peer:valid ~ &',
|
|
251
|
+
'peer-invalid': '.peer:invalid ~ &',
|
|
252
|
+
'peer-in-range': '.peer:in-range ~ &',
|
|
253
|
+
'peer-out-of-range': '.peer:out-of-range ~ &',
|
|
254
|
+
'peer-placeholder-shown': '.peer:placeholder-shown ~ &',
|
|
255
|
+
'peer-autofill': '.peer:autofill ~ &',
|
|
256
|
+
'peer-read-only': '.peer:read-only ~ &',
|
|
257
|
+
'peer-read-write': '.peer:read-write ~ &',
|
|
258
|
+
'peer-optional': '.peer:optional ~ &',
|
|
259
|
+
'peer-user-valid': '.peer:user-valid ~ &',
|
|
260
|
+
'peer-user-invalid': '.peer:user-invalid &',
|
|
261
|
+
|
|
262
|
+
'motion-safe': '@media (prefers-reduced-motion: no-preference)',
|
|
263
|
+
'motion-reduce': '@media (prefers-reduced-motion: reduce)',
|
|
264
|
+
'print': '@media print',
|
|
265
|
+
'portrait': '@media (orientation: portrait)',
|
|
266
|
+
'landscape': '@media (orientation: landscape)',
|
|
267
|
+
'contrast-more': '@media (prefers-contrast: more)',
|
|
268
|
+
'contrast-less': '@media (prefers-contrast: less)',
|
|
269
|
+
'forced-colors': '@media (forced-colors: active)',
|
|
270
|
+
'rtl': '&:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *)',
|
|
271
|
+
'ltr': '&:where(:dir(ltr), [dir="ltr"], [dir="ltr"] *)',
|
|
272
|
+
'[dir=rtl]': '[dir="rtl"] &',
|
|
273
|
+
'[dir=ltr]': '[dir="ltr"] &',
|
|
274
|
+
'pointer-fine': '@media (pointer: fine)',
|
|
275
|
+
'pointer-coarse': '@media (pointer: coarse)',
|
|
276
|
+
'pointer-none': '@media (pointer: none)',
|
|
277
|
+
'any-pointer-fine': '@media (any-pointer: fine)',
|
|
278
|
+
'any-pointer-coarse': '@media (any-pointer: coarse)',
|
|
279
|
+
'any-pointer-none': '@media (any-pointer: none)',
|
|
280
|
+
'scripting-enabled': '@media (scripting: enabled)',
|
|
281
|
+
'can-hover': '@media (hover: hover)',
|
|
282
|
+
'can-not-hover': '@media (hover: none)',
|
|
283
|
+
'any-hover': '@media (any-hover: hover)',
|
|
284
|
+
'any-hover-none': '@media (any-hover: none)',
|
|
285
|
+
'any-pointer': '@media (any-pointer: fine)',
|
|
286
|
+
'any-pointer-coarse': '@media (any-pointer: coarse)',
|
|
287
|
+
'any-pointer-none': '@media (any-pointer: none)',
|
|
288
|
+
'color': '@media (color)',
|
|
289
|
+
'color-gamut': '@media (color-gamut: srgb)',
|
|
290
|
+
'color-gamut-p3': '@media (color-gamut: p3)',
|
|
291
|
+
'color-gamut-rec2020': '@media (color-gamut: rec2020)',
|
|
292
|
+
'monochrome': '@media (monochrome)',
|
|
293
|
+
'monochrome-color': '@media (monochrome: 0)',
|
|
294
|
+
'monochrome-grayscale': '@media (monochrome: 1)',
|
|
295
|
+
'inverted-colors': '@media (inverted-colors: inverted)',
|
|
296
|
+
'inverted-colors-none': '@media (inverted-colors: none)',
|
|
297
|
+
'update': '@media (update: fast)',
|
|
298
|
+
'update-slow': '@media (update: slow)',
|
|
299
|
+
'update-none': '@media (update: none)',
|
|
300
|
+
'overflow-block': '@media (overflow-block: scroll)',
|
|
301
|
+
'overflow-block-paged': '@media (overflow-block: paged)',
|
|
302
|
+
'overflow-inline': '@media (overflow-inline: scroll)',
|
|
303
|
+
'overflow-inline-auto': '@media (overflow-inline: auto)',
|
|
304
|
+
'prefers-color-scheme': '@media (prefers-color-scheme: dark)',
|
|
305
|
+
'prefers-color-scheme-light': '@media (prefers-color-scheme: light)',
|
|
306
|
+
'prefers-contrast': '@media (prefers-contrast: more)',
|
|
307
|
+
'prefers-contrast-less': '@media (prefers-contrast: less)',
|
|
308
|
+
'prefers-contrast-no-preference': '@media (prefers-contrast: no-preference)',
|
|
309
|
+
'prefers-reduced-motion': '@media (prefers-reduced-motion: reduce)',
|
|
310
|
+
'prefers-reduced-motion-no-preference': '@media (prefers-reduced-motion: no-preference)',
|
|
311
|
+
'prefers-reduced-transparency': '@media (prefers-reduced-transparency: reduce)',
|
|
312
|
+
'prefers-reduced-transparency-no-preference': '@media (prefers-reduced-transparency: no-preference)',
|
|
313
|
+
'resolution': '@media (resolution: 1dppx)',
|
|
314
|
+
'resolution-low': '@media (resolution: 1dppx)',
|
|
315
|
+
'resolution-high': '@media (resolution: 2dppx)',
|
|
316
|
+
'scan': '@media (scan: progressive)',
|
|
317
|
+
'scan-interlace': '@media (scan: interlace)',
|
|
318
|
+
'scripting': '@media (scripting: enabled)',
|
|
319
|
+
'scripting-none': '@media (scripting: none)',
|
|
320
|
+
'scripting-initial-only': '@media (scripting: initial-only)',
|
|
321
|
+
|
|
322
|
+
// Container queries
|
|
323
|
+
'container': '@container',
|
|
324
|
+
'container-name': '@container',
|
|
325
|
+
|
|
326
|
+
// Important modifier
|
|
327
|
+
'!': '!important',
|
|
328
|
+
|
|
329
|
+
// Responsive breakpoints
|
|
330
|
+
'sm': '@media (min-width: 640px)',
|
|
331
|
+
'md': '@media (min-width: 768px)',
|
|
332
|
+
'lg': '@media (min-width: 1024px)',
|
|
333
|
+
'xl': '@media (min-width: 1280px)',
|
|
334
|
+
'2xl': '@media (min-width: 1536px)',
|
|
335
|
+
|
|
336
|
+
// Supports queries
|
|
337
|
+
'supports': '@supports',
|
|
338
|
+
|
|
339
|
+
// Starting style
|
|
340
|
+
'starting': '@starting-style',
|
|
341
|
+
|
|
342
|
+
// Device / input modality (breakpoint-independent, pure media queries)
|
|
343
|
+
'touch': '@media (pointer: coarse)',
|
|
344
|
+
'cursor': '@media (pointer: fine) and (hover: hover)',
|
|
345
|
+
'pointer': '@media (any-pointer: fine)',
|
|
346
|
+
|
|
347
|
+
// Operating system (html[data-os] is set by the utilities plugin)
|
|
348
|
+
'mac': 'html[data-os="macos"] &',
|
|
349
|
+
'windows': 'html[data-os="windows"] &',
|
|
350
|
+
'linux': 'html[data-os="linux"] &',
|
|
351
|
+
'ios': 'html[data-os="ios"] &',
|
|
352
|
+
'android': 'html[data-os="android"] &',
|
|
353
|
+
'apple': 'html[data-os="macos"] &, html[data-os="ios"] &',
|
|
354
|
+
|
|
355
|
+
// Data attribute variants (common patterns)
|
|
356
|
+
'data-open': '[data-state="open"] &',
|
|
357
|
+
'data-closed': '[data-state="closed"] &',
|
|
358
|
+
'data-checked': '[data-state="checked"] &',
|
|
359
|
+
'data-unchecked': '[data-state="unchecked"] &',
|
|
360
|
+
'data-on': '[data-state="on"] &',
|
|
361
|
+
'data-off': '[data-state="off"] &',
|
|
362
|
+
'data-visible': '[data-state="visible"] &',
|
|
363
|
+
'data-hidden': '[data-state="hidden"] &',
|
|
364
|
+
'data-disabled': '[data-disabled] &',
|
|
365
|
+
'data-loading': '[data-loading] &',
|
|
366
|
+
'data-error': '[data-error] &',
|
|
367
|
+
'data-success': '[data-success] &',
|
|
368
|
+
'data-warning': '[data-warning] &',
|
|
369
|
+
'data-selected': '[data-selected] &',
|
|
370
|
+
'data-highlighted': '[data-highlighted] &',
|
|
371
|
+
'data-pressed': '[data-pressed] &',
|
|
372
|
+
'data-expanded': '[data-expanded] &',
|
|
373
|
+
'data-collapsed': '[data-collapsed] &',
|
|
374
|
+
'data-active': '[data-active] &',
|
|
375
|
+
'data-inactive': '[data-inactive] &',
|
|
376
|
+
'data-valid': '[data-valid] &',
|
|
377
|
+
'data-invalid': '[data-invalid] &',
|
|
378
|
+
'data-required': '[data-required] &',
|
|
379
|
+
'data-optional': '[data-optional] &',
|
|
380
|
+
'data-readonly': '[data-readonly] &',
|
|
381
|
+
'data-write': '[data-write] &',
|
|
382
|
+
|
|
383
|
+
// Aria attribute variants (common patterns)
|
|
384
|
+
'aria-expanded': '[aria-expanded="true"] &',
|
|
385
|
+
'aria-collapsed': '[aria-expanded="false"] &',
|
|
386
|
+
'aria-pressed': '[aria-pressed="true"] &',
|
|
387
|
+
'aria-unpressed': '[aria-pressed="false"] &',
|
|
388
|
+
'aria-checked': '[aria-checked="true"] &',
|
|
389
|
+
'aria-unchecked': '[aria-checked="false"] &',
|
|
390
|
+
'aria-selected': '[aria-selected="true"] &',
|
|
391
|
+
'aria-unselected': '[aria-selected="false"] &',
|
|
392
|
+
'aria-invalid': '[aria-invalid="true"] &',
|
|
393
|
+
'aria-valid': '[aria-invalid="false"] &',
|
|
394
|
+
'aria-required': '[aria-required="true"] &',
|
|
395
|
+
'aria-optional': '[aria-required="false"] &',
|
|
396
|
+
'aria-disabled': '[aria-disabled="true"] &',
|
|
397
|
+
'aria-enabled': '[aria-disabled="false"] &',
|
|
398
|
+
'aria-hidden': '[aria-hidden="true"] &',
|
|
399
|
+
'aria-visible': '[aria-hidden="false"] &',
|
|
400
|
+
'aria-busy': '[aria-busy="true"] &',
|
|
401
|
+
'aria-available': '[aria-busy="false"] &',
|
|
402
|
+
'aria-current': '[aria-current="true"] &',
|
|
403
|
+
'aria-not-current': '[aria-current="false"] &',
|
|
404
|
+
'aria-live': '[aria-live="polite"] &, [aria-live="assertive"] &',
|
|
405
|
+
'aria-atomic': '[aria-atomic="true"] &',
|
|
406
|
+
'aria-relevant': '[aria-relevant="additions"] &, [aria-relevant="removals"] &, [aria-relevant="text"] &, [aria-relevant="all"] &'
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function createVariantGroups() {
|
|
411
|
+
return {
|
|
412
|
+
'state': ['hover', 'focus', 'active', 'visited', 'target', 'open', 'closed', 'paused', 'playing', 'muted', 'unmuted', 'collapsed', 'expanded', 'unchecked', 'selected', 'unselected'],
|
|
413
|
+
'child': ['first', 'last', 'only', 'odd', 'even'],
|
|
414
|
+
'form': ['disabled', 'enabled', 'checked', 'indeterminate', 'required', 'valid', 'invalid'],
|
|
415
|
+
'pseudo': ['before', 'after', 'first-letter', 'first-line', 'marker', 'selection', 'file', 'backdrop'],
|
|
416
|
+
'media': ['dark', 'light', 'motion-safe', 'motion-reduce', 'print', 'portrait', 'landscape', 'rtl', 'ltr', 'can-hover', 'can-not-hover', 'any-hover', 'any-hover-none', 'color', 'monochrome', 'inverted-colors', 'inverted-colors-none', 'update', 'update-slow', 'update-none', 'overflow-block', 'overflow-block-paged', 'overflow-inline', 'overflow-inline-auto', 'prefers-color-scheme', 'prefers-color-scheme-light', 'prefers-contrast', 'prefers-contrast-less', 'prefers-contrast-no-preference', 'prefers-reduced-motion', 'prefers-reduced-motion-no-preference', 'prefers-reduced-transparency', 'prefers-reduced-transparency-no-preference', 'resolution', 'resolution-low', 'resolution-high', 'scan', 'scan-interlace', 'scripting', 'scripting-none', 'scripting-initial-only', 'forced-colors', 'contrast-more', 'contrast-less', 'pointer-fine', 'pointer-coarse', 'pointer-none', 'any-pointer-fine', 'any-pointer-coarse', 'any-pointer-none', 'scripting-enabled'],
|
|
417
|
+
'responsive': ['sm', 'md', 'lg', 'xl', '2xl'],
|
|
418
|
+
'device': ['touch', 'cursor', 'pointer'],
|
|
419
|
+
'os': ['mac', 'windows', 'linux', 'ios', 'android', 'apple'],
|
|
420
|
+
'group': ['group', 'group-hover', 'group-focus', 'group-active', 'group-disabled', 'group-checked', 'group-required', 'group-valid', 'group-invalid'],
|
|
421
|
+
'peer': ['peer', 'peer-hover', 'peer-focus', 'peer-active', 'peer-disabled', 'peer-checked', 'peer-required', 'peer-valid', 'peer-invalid'],
|
|
422
|
+
'data': ['data-open', 'data-closed', 'data-checked', 'data-unchecked', 'data-visible', 'data-hidden', 'data-disabled', 'data-loading', 'data-error', 'data-success', 'data-warning', 'data-selected', 'data-highlighted', 'data-pressed', 'data-expanded', 'data-collapsed', 'data-active', 'data-inactive', 'data-valid', 'data-invalid', 'data-required', 'data-optional', 'data-readonly', 'data-write'],
|
|
423
|
+
'aria': ['aria-expanded', 'aria-collapsed', 'aria-pressed', 'aria-unpressed', 'aria-checked', 'aria-unchecked', 'aria-selected', 'aria-unselected', 'aria-invalid', 'aria-valid', 'aria-required', 'aria-optional', 'aria-disabled', 'aria-enabled', 'aria-hidden', 'aria-visible', 'aria-busy', 'aria-available', 'aria-current', 'aria-not-current', 'aria-live', 'aria-atomic', 'aria-relevant']
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
/* Manifest Utilities */
|
|
430
|
+
|
|
431
|
+
/** True when prerender wrote utilities to prerender.utilities.css — skip runtime #manifest-styles generation. */
|
|
432
|
+
function manifestPageUsesStaticPrerenderUtilities() {
|
|
433
|
+
if (typeof document === 'undefined') return false;
|
|
434
|
+
try {
|
|
435
|
+
if (!document.querySelector('meta[name="manifest:prerendered"][content="1"]')) return false;
|
|
436
|
+
for (const link of document.querySelectorAll('link[rel="stylesheet"][href]')) {
|
|
437
|
+
if ((link.getAttribute('href') || '').toLowerCase().includes('prerender.utilities.css')) {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} catch (e) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Browser runtime compiler
|
|
448
|
+
class TailwindCompiler {
|
|
449
|
+
constructor(options = {}) {
|
|
450
|
+
this.usesStaticPrerenderUtilities = manifestPageUsesStaticPrerenderUtilities();
|
|
451
|
+
|
|
452
|
+
// Prerender already emitted utility CSS; do not inject duplicate #manifest-styles / observers.
|
|
453
|
+
if (this.usesStaticPrerenderUtilities) {
|
|
454
|
+
this.debug = options.debug === true;
|
|
455
|
+
this.startTime = performance.now();
|
|
456
|
+
this.options = {
|
|
457
|
+
rootSelector: options.rootSelector || ':root',
|
|
458
|
+
themeSelector: options.themeSelector || '@theme',
|
|
459
|
+
debounceTime: options.debounceTime || 50,
|
|
460
|
+
maxCacheAge: options.maxCacheAge || 24 * 60 * 60 * 1000,
|
|
461
|
+
debug: options.debug !== false,
|
|
462
|
+
...options
|
|
463
|
+
};
|
|
464
|
+
this.criticalStyleElement = null;
|
|
465
|
+
this.styleElement = null;
|
|
466
|
+
this.tailwindLink = null;
|
|
467
|
+
this.observer = null;
|
|
468
|
+
this.isCompiling = false;
|
|
469
|
+
this.compileTimeout = null;
|
|
470
|
+
this.cache = new Map();
|
|
471
|
+
this.hasInitialized = true;
|
|
472
|
+
this.staticUtilitiesCoveredClasses = null;
|
|
473
|
+
// manifest.code.js (and others) may still register ignore rules; mirror full constructor defaults.
|
|
474
|
+
this.ignoredClassPatterns = [
|
|
475
|
+
/^hljs/, /^language-/, /^copy$/, /^copied$/, /^lines$/, /^selected$/
|
|
476
|
+
];
|
|
477
|
+
this.ignoredElementSelectors = [
|
|
478
|
+
'pre', 'code', 'x-code', 'x-code-group'
|
|
479
|
+
];
|
|
480
|
+
this.significantChangeSelectors = [
|
|
481
|
+
'[data-component]', '[x-data]'
|
|
482
|
+
];
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.debug = options.debug === true;
|
|
487
|
+
this.startTime = performance.now();
|
|
488
|
+
|
|
489
|
+
// Create critical style element FIRST - must be before any rendering
|
|
490
|
+
const criticalStyleStart = performance.now();
|
|
491
|
+
this.criticalStyleElement = document.createElement('style');
|
|
492
|
+
this.criticalStyleElement.id = 'manifest-styles-critical';
|
|
493
|
+
// Insert at the very beginning of head
|
|
494
|
+
if (document.head) {
|
|
495
|
+
if (document.head.firstChild) {
|
|
496
|
+
document.head.insertBefore(this.criticalStyleElement, document.head.firstChild);
|
|
497
|
+
} else {
|
|
498
|
+
document.head.appendChild(this.criticalStyleElement);
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
// If head doesn't exist yet, wait for it (shouldn't happen, but safety check)
|
|
502
|
+
const checkHead = setInterval(() => {
|
|
503
|
+
if (document.head) {
|
|
504
|
+
clearInterval(checkHead);
|
|
505
|
+
if (document.head.firstChild) {
|
|
506
|
+
document.head.insertBefore(this.criticalStyleElement, document.head.firstChild);
|
|
507
|
+
} else {
|
|
508
|
+
document.head.appendChild(this.criticalStyleElement);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}, 1);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Initialize options first (needed for regex patterns)
|
|
515
|
+
this.options = {
|
|
516
|
+
rootSelector: options.rootSelector || ':root',
|
|
517
|
+
themeSelector: options.themeSelector || '@theme',
|
|
518
|
+
debounceTime: options.debounceTime || 50,
|
|
519
|
+
maxCacheAge: options.maxCacheAge || 24 * 60 * 60 * 1000,
|
|
520
|
+
debug: options.debug !== false,
|
|
521
|
+
...options
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// Initialize regex patterns (needed for utility generation)
|
|
525
|
+
this.regexPatterns = {
|
|
526
|
+
root: new RegExp(`${this.options.rootSelector}\\s*{([^}]*)}`, 'g'),
|
|
527
|
+
theme: new RegExp(`${this.options.themeSelector}\\s*{([^}]*)}`, 'g'),
|
|
528
|
+
variable: /--([\w-]+):\s*([^;]+);/g,
|
|
529
|
+
tailwindPrefix: /^(color|font|text|font-weight|tracking|leading|breakpoint|container|spacing|radius|shadow|inset-shadow|drop-shadow|blur|perspective|aspect|ease|animate|border-width|border-style|outline|outline-width|outline-style|ring|ring-offset|divide|accent|caret|decoration|placeholder|selection|scrollbar)-/
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// Initialize utility generators from component
|
|
533
|
+
const generators = createUtilityGenerators();
|
|
534
|
+
// Start with minimal generators needed for synchronous generation
|
|
535
|
+
this.utilityGenerators = {
|
|
536
|
+
'color-': generators['color-'],
|
|
537
|
+
'font-': generators['font-'],
|
|
538
|
+
'text-': generators['text-'],
|
|
539
|
+
'font-weight-': generators['font-weight-'],
|
|
540
|
+
'tracking-': generators['tracking-'],
|
|
541
|
+
'leading-': generators['leading-'],
|
|
542
|
+
'spacing-': generators['spacing-'],
|
|
543
|
+
'radius-': generators['radius-'],
|
|
544
|
+
'shadow-': generators['shadow-'],
|
|
545
|
+
'blur-': generators['blur-']
|
|
546
|
+
};
|
|
547
|
+
// Add remaining generators
|
|
548
|
+
Object.assign(this.utilityGenerators, generators);
|
|
549
|
+
|
|
550
|
+
// Initialize variants from component (needed for parseClassName during sync generation)
|
|
551
|
+
this.variants = createVariants();
|
|
552
|
+
this.variantGroups = createVariantGroups();
|
|
553
|
+
|
|
554
|
+
// Cache for parsed class names (must be before addCriticalBlockingStylesSync)
|
|
555
|
+
this.classCache = new Map();
|
|
556
|
+
|
|
557
|
+
// Read any publish/render-provided static utilities sheet before the
|
|
558
|
+
// first compile — its classes are skipped rather than regenerated.
|
|
559
|
+
this.staticUtilitiesCoveredClasses = null;
|
|
560
|
+
this.detectStaticUtilitiesSheet();
|
|
561
|
+
|
|
562
|
+
// Add critical styles IMMEDIATELY - don't wait for anything
|
|
563
|
+
this.addCriticalBlockingStylesSync();
|
|
564
|
+
|
|
565
|
+
// Create main style element for generated utilities
|
|
566
|
+
this.styleElement = document.createElement('style');
|
|
567
|
+
this.styleElement.id = 'manifest-styles';
|
|
568
|
+
document.head.appendChild(this.styleElement);
|
|
569
|
+
this.setupUtilityStylesOrderObserver();
|
|
570
|
+
|
|
571
|
+
// Initialize properties
|
|
572
|
+
this.tailwindLink = null;
|
|
573
|
+
this.observer = null;
|
|
574
|
+
this.isCompiling = false;
|
|
575
|
+
this.compileTimeout = null;
|
|
576
|
+
this.cache = new Map();
|
|
577
|
+
this.lastThemeHash = null;
|
|
578
|
+
this.processedElements = new WeakSet();
|
|
579
|
+
this.activeBreakpoints = new Set();
|
|
580
|
+
this.activeModifiers = new Set();
|
|
581
|
+
this.cssFiles = new Set();
|
|
582
|
+
this.pendingStyles = new Map();
|
|
583
|
+
this.currentThemeVars = new Map();
|
|
584
|
+
this.hasInitialized = false;
|
|
585
|
+
this.lastCompileTime = 0;
|
|
586
|
+
this.minCompileInterval = 100; // Minimum time between compilations in ms
|
|
587
|
+
this.cssContentCache = new Map(); // Cache CSS file contents with timestamps
|
|
588
|
+
this.lastClassesHash = ''; // Track changes in used classes
|
|
589
|
+
this.staticClassCache = new Set(); // Cache classes found in static HTML/components
|
|
590
|
+
this.dynamicClassCache = new Set(); // Cache classes that appear dynamically
|
|
591
|
+
this.hasScannedStatic = false; // Track if we've done initial static scan
|
|
592
|
+
this.staticScanPromise = null; // Promise for initial static scan
|
|
593
|
+
this.ignoredClassPatterns = [ // Patterns for classes to ignore
|
|
594
|
+
/^hljs/, /^language-/, /^copy$/, /^copied$/, /^lines$/, /^selected$/
|
|
595
|
+
];
|
|
596
|
+
this.ignoredElementSelectors = [ // Elements to ignore for DOM mutations
|
|
597
|
+
'pre', 'code', 'x-code', 'x-code-group'
|
|
598
|
+
];
|
|
599
|
+
this.ignoredAttributes = [ // Attribute changes to ignore (non-visual/utility changes)
|
|
600
|
+
'id', 'data-order', 'data-component-id', 'data-highlighted', 'data-processed',
|
|
601
|
+
'x-intersect', 'x-intersect:leave', 'x-show', 'x-hide', 'x-transition',
|
|
602
|
+
'aria-expanded', 'aria-selected', 'aria-current', 'aria-hidden', 'aria-label',
|
|
603
|
+
'tabindex', 'role', 'title', 'alt', 'data-state', 'data-value'
|
|
604
|
+
];
|
|
605
|
+
this.significantChangeSelectors = [ // Only these DOM additions trigger recompilation
|
|
606
|
+
'[data-component]', '[x-data]' // Components and Alpine elements
|
|
607
|
+
];
|
|
608
|
+
|
|
609
|
+
// Pre-define pseudo classes
|
|
610
|
+
this.pseudoClasses = ['hover', 'focus', 'active', 'disabled', 'dark'];
|
|
611
|
+
|
|
612
|
+
// Cache for discovered custom utility classes
|
|
613
|
+
this.customUtilities = new Map();
|
|
614
|
+
|
|
615
|
+
// Variants and classCache already initialized above (before addCriticalBlockingStylesSync)
|
|
616
|
+
|
|
617
|
+
// Load cache and start processing
|
|
618
|
+
this.loadAndApplyCache();
|
|
619
|
+
|
|
620
|
+
// If cache loaded utilities, they'll be in the main style element
|
|
621
|
+
// The critical style element will be cleared when full utilities are ready
|
|
622
|
+
|
|
623
|
+
// Try to generate minimal utilities synchronously from inline styles
|
|
624
|
+
this.generateSynchronousUtilities();
|
|
625
|
+
|
|
626
|
+
// Listen for component loads
|
|
627
|
+
this.setupComponentLoadListener();
|
|
628
|
+
|
|
629
|
+
this.waitForTailwind().then(() => {
|
|
630
|
+
this.startProcessing();
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Public API for other plugins to configure behavior
|
|
635
|
+
addIgnoredClassPattern(pattern) {
|
|
636
|
+
if (!Array.isArray(this.ignoredClassPatterns)) {
|
|
637
|
+
this.ignoredClassPatterns = [];
|
|
638
|
+
}
|
|
639
|
+
if (pattern instanceof RegExp) {
|
|
640
|
+
this.ignoredClassPatterns.push(pattern);
|
|
641
|
+
} else if (typeof pattern === 'string') {
|
|
642
|
+
this.ignoredClassPatterns.push(new RegExp(pattern));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
addIgnoredElementSelector(selector) {
|
|
647
|
+
if (!Array.isArray(this.ignoredElementSelectors)) {
|
|
648
|
+
this.ignoredElementSelectors = [];
|
|
649
|
+
}
|
|
650
|
+
if (typeof selector === 'string') {
|
|
651
|
+
this.ignoredElementSelectors.push(selector);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
addSignificantChangeSelector(selector) {
|
|
656
|
+
if (!Array.isArray(this.significantChangeSelectors)) {
|
|
657
|
+
this.significantChangeSelectors = [];
|
|
658
|
+
}
|
|
659
|
+
if (typeof selector === 'string') {
|
|
660
|
+
this.significantChangeSelectors.push(selector);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Allow plugins to trigger recompilation when needed
|
|
665
|
+
triggerRecompilation(reason = 'manual') {
|
|
666
|
+
this.compile();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Debounce utility
|
|
670
|
+
debounce(func, wait) {
|
|
671
|
+
let timeout;
|
|
672
|
+
return function executedFunction(...args) {
|
|
673
|
+
const later = () => {
|
|
674
|
+
clearTimeout(timeout);
|
|
675
|
+
func(...args);
|
|
676
|
+
};
|
|
677
|
+
clearTimeout(timeout);
|
|
678
|
+
timeout = setTimeout(later, wait);
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Wait for Tailwind to be available
|
|
683
|
+
async waitForTailwind() {
|
|
684
|
+
return new Promise((resolve) => {
|
|
685
|
+
if (this.isTailwindAvailable()) {
|
|
686
|
+
resolve();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const checkInterval = setInterval(() => {
|
|
691
|
+
if (this.isTailwindAvailable()) {
|
|
692
|
+
clearInterval(checkInterval);
|
|
693
|
+
resolve();
|
|
694
|
+
}
|
|
695
|
+
}, 100);
|
|
696
|
+
|
|
697
|
+
// Also check on DOMContentLoaded
|
|
698
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
699
|
+
if (this.isTailwindAvailable()) {
|
|
700
|
+
clearInterval(checkInterval);
|
|
701
|
+
resolve();
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// Set a timeout to prevent infinite waiting
|
|
706
|
+
setTimeout(() => {
|
|
707
|
+
clearInterval(checkInterval);
|
|
708
|
+
resolve();
|
|
709
|
+
}, 5000);
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Check if Tailwind is available
|
|
714
|
+
isTailwindAvailable() {
|
|
715
|
+
// Check for Tailwind in various ways
|
|
716
|
+
return (
|
|
717
|
+
// Check for Tailwind CSS file
|
|
718
|
+
Array.from(document.styleSheets).some(sheet =>
|
|
719
|
+
sheet.href && (
|
|
720
|
+
sheet.href.includes('tailwind') ||
|
|
721
|
+
sheet.href.includes('tailwindcss') ||
|
|
722
|
+
sheet.href.includes('manifest')
|
|
723
|
+
)
|
|
724
|
+
) ||
|
|
725
|
+
// Check for Tailwind classes in document
|
|
726
|
+
document.querySelector('[class*="tailwind"]') ||
|
|
727
|
+
// Check for Tailwind in window object
|
|
728
|
+
window.tailwind ||
|
|
729
|
+
// Check for Tailwind in document head
|
|
730
|
+
document.head.innerHTML.includes('tailwind') ||
|
|
731
|
+
// Check for Manifest CSS files
|
|
732
|
+
document.head.innerHTML.includes('manifest')
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
// Helper methods
|
|
740
|
+
// Utility functions for extracting, parsing, and processing CSS and classes
|
|
741
|
+
|
|
742
|
+
// Ensure #manifest-styles is last in head so our responsive/variant rules win over Tailwind and any later-injected styles
|
|
743
|
+
TailwindCompiler.prototype.ensureUtilityStylesLast = function () {
|
|
744
|
+
if (this.styleElement && this.styleElement.parentNode && document.head.lastElementChild !== this.styleElement) {
|
|
745
|
+
document.head.appendChild(this.styleElement);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
// When any element is added to head after ours, move #manifest-styles to end. Handles CDN load order (e.g. Tailwind injecting after we run).
|
|
750
|
+
TailwindCompiler.prototype.setupUtilityStylesOrderObserver = function () {
|
|
751
|
+
if (!document.head || !this.styleElement) return;
|
|
752
|
+
const self = this;
|
|
753
|
+
const observer = new MutationObserver((mutations) => {
|
|
754
|
+
for (const mutation of mutations) {
|
|
755
|
+
if (mutation.type !== 'childList' || !mutation.addedNodes.length) continue;
|
|
756
|
+
for (const node of mutation.addedNodes) {
|
|
757
|
+
if (node.nodeType !== Node.ELEMENT_NODE || node === self.styleElement) continue;
|
|
758
|
+
self.ensureUtilityStylesLast();
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
observer.observe(document.head, { childList: true, subtree: false });
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// Schedule ensureUtilityStylesLast at 0ms, 100ms, 500ms so we win when Tailwind (or other scripts) inject styles later (e.g. CDN).
|
|
767
|
+
TailwindCompiler.prototype.scheduleEnsureUtilityStylesLast = function () {
|
|
768
|
+
const self = this;
|
|
769
|
+
[0, 100, 500].forEach((ms) => {
|
|
770
|
+
setTimeout(() => self.ensureUtilityStylesLast(), ms);
|
|
771
|
+
});
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// Discover CSS files from stylesheets and imports
|
|
775
|
+
TailwindCompiler.prototype.discoverCssFiles = function () {
|
|
776
|
+
try {
|
|
777
|
+
// Get all stylesheets from the document
|
|
778
|
+
const stylesheets = Array.from(document.styleSheets);
|
|
779
|
+
|
|
780
|
+
// Process each stylesheet
|
|
781
|
+
for (const sheet of stylesheets) {
|
|
782
|
+
try {
|
|
783
|
+
// Process local files and framework CDN files (manifestjs, mnfst)
|
|
784
|
+
const isFrameworkCdn = (href) =>
|
|
785
|
+
href.includes('manifestjs') || href.includes('mnfst') ||
|
|
786
|
+
(href.includes('jsdelivr') && (href.includes('manifestjs') || href.includes('mnfst'))) ||
|
|
787
|
+
(href.includes('unpkg') && (href.includes('manifestjs') || href.includes('mnfst')));
|
|
788
|
+
if (sheet.href && (
|
|
789
|
+
sheet.href.startsWith(window.location.origin) ||
|
|
790
|
+
isFrameworkCdn(sheet.href)
|
|
791
|
+
)) {
|
|
792
|
+
this.cssFiles.add(sheet.href);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Get all @import rules (local and framework CDN)
|
|
796
|
+
const rules = Array.from(sheet.cssRules || []);
|
|
797
|
+
for (const rule of rules) {
|
|
798
|
+
if (rule.type === CSSRule.IMPORT_RULE && rule.href && (
|
|
799
|
+
rule.href.startsWith(window.location.origin) ||
|
|
800
|
+
isFrameworkCdn(rule.href)
|
|
801
|
+
)) {
|
|
802
|
+
this.cssFiles.add(rule.href);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
} catch (e) {
|
|
806
|
+
// Skip stylesheets that can't be accessed (external CDN files, CORS, etc.)
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Add any inline styles (exclude generated styles)
|
|
811
|
+
const styleElements = document.querySelectorAll('style:not(#manifest-styles)');
|
|
812
|
+
for (const style of styleElements) {
|
|
813
|
+
if (style.textContent && style.textContent.trim()) {
|
|
814
|
+
const id = style.id || `inline-style-${Array.from(styleElements).indexOf(style)}`;
|
|
815
|
+
this.cssFiles.add('inline:' + id);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
} catch (error) {
|
|
819
|
+
console.warn('Error discovering CSS files:', error);
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// Scan static HTML files and components for classes
|
|
824
|
+
TailwindCompiler.prototype.scanStaticClasses = async function () {
|
|
825
|
+
if (this.staticScanPromise) {
|
|
826
|
+
return this.staticScanPromise;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
this.staticScanPromise = (async () => {
|
|
830
|
+
try {
|
|
831
|
+
const staticClasses = new Set();
|
|
832
|
+
|
|
833
|
+
// 1. Scan index.html content
|
|
834
|
+
const htmlContent = document.documentElement.outerHTML;
|
|
835
|
+
this.extractClassesFromHTML(htmlContent, staticClasses);
|
|
836
|
+
|
|
837
|
+
// 2. Scan component files from manifest
|
|
838
|
+
// Wait for components registry to be ready (with timeout)
|
|
839
|
+
let registry = window.ManifestComponentsRegistry;
|
|
840
|
+
const maxWaitTime = 2000; // Wait up to 2 seconds for registry
|
|
841
|
+
const checkInterval = 50;
|
|
842
|
+
let waited = 0;
|
|
843
|
+
|
|
844
|
+
while (!registry && waited < maxWaitTime) {
|
|
845
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
846
|
+
registry = window.ManifestComponentsRegistry;
|
|
847
|
+
waited += checkInterval;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const componentUrls = [];
|
|
851
|
+
|
|
852
|
+
if (registry && registry.manifest) {
|
|
853
|
+
// Get all component paths from manifest
|
|
854
|
+
const allComponents = [
|
|
855
|
+
...(registry.manifest.preloadedComponents || []),
|
|
856
|
+
...(registry.manifest.components || [])
|
|
857
|
+
];
|
|
858
|
+
componentUrls.push(...allComponents);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const componentPromises = componentUrls.map(async (url) => {
|
|
862
|
+
try {
|
|
863
|
+
// Use root-relative URL first to avoid 404s on doc routes (e.g. /getting-started/...)
|
|
864
|
+
const rootRelative = url.startsWith('/') ? url : '/' + url;
|
|
865
|
+
const pathsToTry = [rootRelative, url];
|
|
866
|
+
for (const path of pathsToTry) {
|
|
867
|
+
try {
|
|
868
|
+
const response = await fetch(path);
|
|
869
|
+
if (response.ok) {
|
|
870
|
+
const html = await response.text();
|
|
871
|
+
this.extractClassesFromHTML(html, staticClasses);
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
} catch (e) {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
} catch (error) {
|
|
879
|
+
// Silently ignore missing components
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
await Promise.all(componentPromises);
|
|
884
|
+
|
|
885
|
+
// Cache static classes
|
|
886
|
+
for (const cls of staticClasses) {
|
|
887
|
+
this.staticClassCache.add(cls);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
this.hasScannedStatic = true;
|
|
891
|
+
|
|
892
|
+
return staticClasses;
|
|
893
|
+
} catch (error) {
|
|
894
|
+
console.warn('[TailwindCompiler] Error scanning static classes:', error);
|
|
895
|
+
this.hasScannedStatic = true;
|
|
896
|
+
return new Set();
|
|
897
|
+
}
|
|
898
|
+
})();
|
|
899
|
+
|
|
900
|
+
return this.staticScanPromise;
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
// Extract classes from HTML content
|
|
904
|
+
TailwindCompiler.prototype.extractClassesFromHTML = function (html, classSet) {
|
|
905
|
+
// Plain class attributes only — the leading boundary excludes Alpine's
|
|
906
|
+
// :class / x-bind:class bindings (JS expressions, not token lists)
|
|
907
|
+
const classRegex = /(?:^|[\s"'`>/])(?:[a-zA-Z][\w.-]*-)?class=["']([^"']+)["']/g;
|
|
908
|
+
let match;
|
|
909
|
+
|
|
910
|
+
while ((match = classRegex.exec(html)) !== null) {
|
|
911
|
+
const classString = match[1];
|
|
912
|
+
const classes = classString.split(/\s+/).filter(Boolean);
|
|
913
|
+
for (const cls of classes) {
|
|
914
|
+
if (cls && !cls.startsWith('x-') && !cls.startsWith('$')) {
|
|
915
|
+
classSet.add(cls);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Also check for x-data and other Alpine directives that might contain classes
|
|
921
|
+
const alpineRegex = /x-(?:data|bind:class|class)=["']([^"']+)["']/g;
|
|
922
|
+
while ((match = alpineRegex.exec(html)) !== null) {
|
|
923
|
+
// Simple extraction - could be enhanced for complex Alpine expressions
|
|
924
|
+
const content = match[1];
|
|
925
|
+
const classMatches = content.match(/['"`]([^'"`\s]+)['"`]/g);
|
|
926
|
+
if (classMatches) {
|
|
927
|
+
for (const classMatch of classMatches) {
|
|
928
|
+
const cls = classMatch.replace(/['"`]/g, '');
|
|
929
|
+
if (cls && !cls.startsWith('$') && !cls.includes('(')) {
|
|
930
|
+
classSet.add(cls);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
// Get all used classes from static and dynamic sources
|
|
938
|
+
TailwindCompiler.prototype.getUsedClasses = function () {
|
|
939
|
+
try {
|
|
940
|
+
const allClasses = new Set();
|
|
941
|
+
const usedVariableSuffixes = new Set();
|
|
942
|
+
|
|
943
|
+
// Add static classes (pre-scanned)
|
|
944
|
+
for (const cls of this.staticClassCache) {
|
|
945
|
+
allClasses.add(cls);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Scan current DOM for dynamic classes only
|
|
949
|
+
const elements = document.getElementsByTagName('*');
|
|
950
|
+
for (const element of elements) {
|
|
951
|
+
let classes = [];
|
|
952
|
+
if (typeof element.className === 'string') {
|
|
953
|
+
classes = element.className.split(/\s+/).filter(Boolean);
|
|
954
|
+
} else if (element.classList) {
|
|
955
|
+
classes = Array.from(element.classList);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
for (const cls of classes) {
|
|
959
|
+
if (!cls) continue;
|
|
960
|
+
|
|
961
|
+
// Skip classes using configurable patterns
|
|
962
|
+
const isIgnoredClass = this.ignoredClassPatterns.some(pattern =>
|
|
963
|
+
pattern.test(cls)
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
if (isIgnoredClass) {
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Add all classes (static + dynamic)
|
|
971
|
+
allClasses.add(cls);
|
|
972
|
+
|
|
973
|
+
// Track dynamic classes separately
|
|
974
|
+
if (!this.staticClassCache.has(cls)) {
|
|
975
|
+
this.dynamicClassCache.add(cls);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Process all classes for variable suffixes
|
|
981
|
+
for (const cls of allClasses) {
|
|
982
|
+
// Extract base class and variants
|
|
983
|
+
const parts = cls.split(':');
|
|
984
|
+
const baseClass = parts[parts.length - 1];
|
|
985
|
+
|
|
986
|
+
// Extract suffix for variable matching
|
|
987
|
+
const classParts = baseClass.split('-');
|
|
988
|
+
if (classParts.length > 1) {
|
|
989
|
+
let suffix = classParts.slice(1).join('-');
|
|
990
|
+
|
|
991
|
+
// Handle opacity modifiers (like /90, /50)
|
|
992
|
+
let baseSuffix = suffix;
|
|
993
|
+
if (suffix.includes('/')) {
|
|
994
|
+
const parts = suffix.split('/');
|
|
995
|
+
baseSuffix = parts[0];
|
|
996
|
+
const opacity = parts[1];
|
|
997
|
+
|
|
998
|
+
// Add both the base suffix and the full suffix with opacity
|
|
999
|
+
usedVariableSuffixes.add(baseSuffix);
|
|
1000
|
+
usedVariableSuffixes.add(suffix); // Keep the full suffix with opacity
|
|
1001
|
+
} else {
|
|
1002
|
+
usedVariableSuffixes.add(suffix);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// For compound classes like text-content-subtle, also add the full suffix
|
|
1006
|
+
if (classParts.length > 2) {
|
|
1007
|
+
const fullSuffix = classParts.slice(1).join('-');
|
|
1008
|
+
if (fullSuffix.includes('/')) {
|
|
1009
|
+
usedVariableSuffixes.add(fullSuffix.split('/')[0]);
|
|
1010
|
+
} else {
|
|
1011
|
+
usedVariableSuffixes.add(fullSuffix);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const result = {
|
|
1018
|
+
classes: this.filterStaticallyCoveredClasses(Array.from(allClasses)),
|
|
1019
|
+
variableSuffixes: Array.from(usedVariableSuffixes)
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
return result;
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
console.error('Error getting used classes:', error);
|
|
1025
|
+
return { classes: [], variableSuffixes: [] };
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
// Fetch theme content from CSS files
|
|
1030
|
+
TailwindCompiler.prototype.fetchThemeContent = async function () {
|
|
1031
|
+
const themeContents = new Set();
|
|
1032
|
+
const fetchPromises = [];
|
|
1033
|
+
|
|
1034
|
+
// If we haven't discovered CSS files yet, do it now
|
|
1035
|
+
if (this.cssFiles.size === 0) {
|
|
1036
|
+
this.discoverCssFiles();
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Process all files concurrently
|
|
1040
|
+
for (const source of this.cssFiles) {
|
|
1041
|
+
const fetchPromise = (async () => {
|
|
1042
|
+
try {
|
|
1043
|
+
let content = '';
|
|
1044
|
+
let needsFetch = true;
|
|
1045
|
+
|
|
1046
|
+
if (source.startsWith('inline:')) {
|
|
1047
|
+
const styleId = source.replace('inline:', '');
|
|
1048
|
+
const styleElement = styleId ?
|
|
1049
|
+
document.getElementById(styleId) :
|
|
1050
|
+
document.querySelector('style');
|
|
1051
|
+
if (styleElement) {
|
|
1052
|
+
content = styleElement.textContent;
|
|
1053
|
+
}
|
|
1054
|
+
needsFetch = false;
|
|
1055
|
+
} else {
|
|
1056
|
+
// Smart caching: use session storage + timestamp approach
|
|
1057
|
+
const cacheKey = source;
|
|
1058
|
+
const cached = this.cssContentCache.get(cacheKey);
|
|
1059
|
+
const now = Date.now();
|
|
1060
|
+
|
|
1061
|
+
// Different cache times based on file source
|
|
1062
|
+
let cacheTime;
|
|
1063
|
+
if (source.includes('manifestjs') || source.includes('mnfst') || source.includes('jsdelivr') || source.includes('unpkg')) {
|
|
1064
|
+
// CDN files: cache longer (5 minutes for static, 1 minute for dynamic)
|
|
1065
|
+
cacheTime = this.hasScannedStatic ? 60000 : 300000;
|
|
1066
|
+
} else {
|
|
1067
|
+
// Local files: shorter cache (5 seconds for dynamic, 30 seconds for static)
|
|
1068
|
+
cacheTime = this.hasScannedStatic ? 5000 : 30000;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (cached && (now - cached.timestamp) < cacheTime) {
|
|
1072
|
+
content = cached.content;
|
|
1073
|
+
needsFetch = false;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (needsFetch) {
|
|
1077
|
+
// Add timestamp for development cache busting, but keep it minimal
|
|
1078
|
+
const timestamp = Math.floor(now / 1000); // Only changes every second
|
|
1079
|
+
const url = `${source}${source.includes('?') ? '&' : '?'}t=${timestamp}`;
|
|
1080
|
+
|
|
1081
|
+
const response = await fetch(url);
|
|
1082
|
+
|
|
1083
|
+
if (!response.ok) {
|
|
1084
|
+
console.warn('Failed to fetch stylesheet:', url);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
content = await response.text();
|
|
1089
|
+
|
|
1090
|
+
// Cache the content with timestamp
|
|
1091
|
+
this.cssContentCache.set(cacheKey, {
|
|
1092
|
+
content: content,
|
|
1093
|
+
timestamp: now
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (content) {
|
|
1099
|
+
themeContents.add(content);
|
|
1100
|
+
}
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
console.warn(`Error fetching CSS from ${source}:`, error);
|
|
1103
|
+
}
|
|
1104
|
+
})();
|
|
1105
|
+
fetchPromises.push(fetchPromise);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Wait for all fetches to complete
|
|
1109
|
+
await Promise.all(fetchPromises);
|
|
1110
|
+
|
|
1111
|
+
return Array.from(themeContents).join('\n');
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
// Extract CSS variables from CSS text
|
|
1115
|
+
TailwindCompiler.prototype.extractThemeVariables = function (cssText) {
|
|
1116
|
+
const variables = new Map();
|
|
1117
|
+
|
|
1118
|
+
// Extract ALL CSS custom properties from ANY declaration block
|
|
1119
|
+
const varRegex = /--([\w-]+):\s*([^;]+);/g;
|
|
1120
|
+
|
|
1121
|
+
let varMatch;
|
|
1122
|
+
while ((varMatch = varRegex.exec(cssText)) !== null) {
|
|
1123
|
+
const name = varMatch[1];
|
|
1124
|
+
const value = varMatch[2].trim();
|
|
1125
|
+
variables.set(name, value);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return variables;
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
// Extract custom utilities from CSS text
|
|
1132
|
+
TailwindCompiler.prototype.extractCustomUtilities = function (cssText) {
|
|
1133
|
+
const utilities = new Map();
|
|
1134
|
+
|
|
1135
|
+
// Helper to ensure CSS is always a string
|
|
1136
|
+
const ensureCssString = (css) => {
|
|
1137
|
+
if (typeof css === 'string') {
|
|
1138
|
+
// Clean up any [object Object] that might have snuck in
|
|
1139
|
+
return css.replace(/\[object Object\](;?\s*)/g, '').trim();
|
|
1140
|
+
}
|
|
1141
|
+
if (css && typeof css === 'object' && css.css) {
|
|
1142
|
+
return ensureCssString(css.css);
|
|
1143
|
+
}
|
|
1144
|
+
const stringified = String(css);
|
|
1145
|
+
// Return empty string instead of [object Object]
|
|
1146
|
+
if (stringified === '[object Object]') {
|
|
1147
|
+
return '';
|
|
1148
|
+
}
|
|
1149
|
+
return stringified;
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
// Extract custom utility classes from CSS
|
|
1153
|
+
// Match: .classname or .!classname (where classname can contain word chars and hyphens)
|
|
1154
|
+
const utilityRegex = /(?:@layer\s+utilities\s*{[^}]*}|^)(?:[^{}]*?)(?:^|\s)(\.!?[\w-]+)\s*{([^}]+)}/gm;
|
|
1155
|
+
|
|
1156
|
+
let match;
|
|
1157
|
+
while ((match = utilityRegex.exec(cssText)) !== null) {
|
|
1158
|
+
const className = match[1].substring(1); // Remove the leading dot
|
|
1159
|
+
const cssRules = match[2].trim();
|
|
1160
|
+
|
|
1161
|
+
// Skip if it's a Tailwind-generated class (check base name without !)
|
|
1162
|
+
const baseClassName = className.startsWith('!') ? className.slice(1) : className;
|
|
1163
|
+
if (this.isTailwindGeneratedClass(baseClassName)) {
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Check if CSS rules contain !important
|
|
1168
|
+
const hasImportant = /\s!important/.test(cssRules);
|
|
1169
|
+
const classHasImportantPrefix = className.startsWith('!');
|
|
1170
|
+
|
|
1171
|
+
// Determine final CSS rules
|
|
1172
|
+
let finalCssRules = cssRules;
|
|
1173
|
+
if (classHasImportantPrefix) {
|
|
1174
|
+
// Class has ! prefix, ensure CSS has !important
|
|
1175
|
+
if (!hasImportant) {
|
|
1176
|
+
finalCssRules = cssRules.includes(';') ?
|
|
1177
|
+
cssRules.replace(/;/g, ' !important;') :
|
|
1178
|
+
cssRules + ' !important';
|
|
1179
|
+
}
|
|
1180
|
+
} else {
|
|
1181
|
+
// Class doesn't have ! prefix, remove !important if present
|
|
1182
|
+
if (hasImportant) {
|
|
1183
|
+
finalCssRules = cssRules.replace(/\s!important/g, '');
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Store the utility class with its full name (including ! prefix) as the key
|
|
1188
|
+
// This ensures !col is stored separately from col
|
|
1189
|
+
// Ensure CSS is always a string
|
|
1190
|
+
const cssString = ensureCssString(finalCssRules);
|
|
1191
|
+
if (utilities.has(className)) {
|
|
1192
|
+
const existingRules = utilities.get(className);
|
|
1193
|
+
const existingCssString = ensureCssString(existingRules);
|
|
1194
|
+
utilities.set(className, `${existingCssString}; ${cssString}`);
|
|
1195
|
+
} else {
|
|
1196
|
+
utilities.set(className, cssString);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Also look for :where() selectors which are common in Manifest utilities
|
|
1201
|
+
// Handle both single class and multiple class selectors
|
|
1202
|
+
// Use a function to properly match braces for nested rules
|
|
1203
|
+
const extractWhereSelectors = (text) => {
|
|
1204
|
+
const matches = [];
|
|
1205
|
+
let i = 0;
|
|
1206
|
+
while (i < text.length) {
|
|
1207
|
+
// Find :where(
|
|
1208
|
+
const whereStart = text.indexOf(':where(', i);
|
|
1209
|
+
if (whereStart === -1) break;
|
|
1210
|
+
|
|
1211
|
+
// Find matching closing paren for :where(
|
|
1212
|
+
let parenDepth = 1;
|
|
1213
|
+
let j = whereStart + 7; // Skip ':where('
|
|
1214
|
+
while (j < text.length && parenDepth > 0) {
|
|
1215
|
+
if (text[j] === '(') parenDepth++;
|
|
1216
|
+
else if (text[j] === ')') parenDepth--;
|
|
1217
|
+
j++;
|
|
1218
|
+
}
|
|
1219
|
+
if (parenDepth > 0) {
|
|
1220
|
+
i = whereStart + 1;
|
|
1221
|
+
continue; // Malformed, skip
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const selectorContent = text.slice(whereStart + 7, j - 1);
|
|
1225
|
+
|
|
1226
|
+
// Skip whitespace and find opening brace
|
|
1227
|
+
while (j < text.length && /\s/.test(text[j])) j++;
|
|
1228
|
+
if (j >= text.length || text[j] !== '{') {
|
|
1229
|
+
i = whereStart + 1;
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Find matching closing brace (handle nested braces)
|
|
1234
|
+
let braceDepth = 1;
|
|
1235
|
+
let blockStart = j + 1;
|
|
1236
|
+
j++;
|
|
1237
|
+
while (j < text.length && braceDepth > 0) {
|
|
1238
|
+
if (text[j] === '{') braceDepth++;
|
|
1239
|
+
else if (text[j] === '}') braceDepth--;
|
|
1240
|
+
j++;
|
|
1241
|
+
}
|
|
1242
|
+
if (braceDepth > 0) {
|
|
1243
|
+
i = whereStart + 1;
|
|
1244
|
+
continue; // Malformed, skip
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const cssRules = text.slice(blockStart, j - 1).trim();
|
|
1248
|
+
matches.push({ selectorContent, cssRules, fullMatch: text.slice(whereStart, j) });
|
|
1249
|
+
i = j;
|
|
1250
|
+
}
|
|
1251
|
+
return matches;
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
const whereMatches = extractWhereSelectors(cssText);
|
|
1255
|
+
for (const match of whereMatches) {
|
|
1256
|
+
const selectorContent = match.selectorContent;
|
|
1257
|
+
const cssRules = match.cssRules;
|
|
1258
|
+
|
|
1259
|
+
// Check if CSS rules contain !important
|
|
1260
|
+
const hasImportant = /\s!important/.test(cssRules);
|
|
1261
|
+
|
|
1262
|
+
// Extract individual class names from the selector, including those with ! prefix
|
|
1263
|
+
// Match: .classname or .!classname (where classname can contain word chars and hyphens)
|
|
1264
|
+
const classMatches = selectorContent.match(/\.(!?[\w-]+)/g);
|
|
1265
|
+
if (classMatches) {
|
|
1266
|
+
for (const classMatch of classMatches) {
|
|
1267
|
+
const className = classMatch.substring(1); // Remove the leading dot
|
|
1268
|
+
|
|
1269
|
+
// Skip if it's a Tailwind-generated class (but check base name without !)
|
|
1270
|
+
const baseClassName = className.startsWith('!') ? className.slice(1) : className;
|
|
1271
|
+
if (this.isTailwindGeneratedClass(baseClassName)) {
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// Determine if this class should have !important
|
|
1276
|
+
// Only apply !important if the class name itself starts with ! (e.g., !col)
|
|
1277
|
+
const classHasImportantPrefix = className.startsWith('!');
|
|
1278
|
+
let finalCssRules = cssRules;
|
|
1279
|
+
|
|
1280
|
+
if (classHasImportantPrefix) {
|
|
1281
|
+
// Class has ! prefix, ensure CSS has !important
|
|
1282
|
+
if (!hasImportant) {
|
|
1283
|
+
finalCssRules = cssRules.includes(';') ?
|
|
1284
|
+
cssRules.replace(/;/g, ' !important;') :
|
|
1285
|
+
cssRules + ' !important';
|
|
1286
|
+
}
|
|
1287
|
+
} else {
|
|
1288
|
+
// Class doesn't have ! prefix, remove !important if present
|
|
1289
|
+
if (hasImportant) {
|
|
1290
|
+
finalCssRules = cssRules.replace(/\s!important/g, '');
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// Store the class with its full name (including ! prefix) as the key
|
|
1295
|
+
// This ensures !col is stored separately from col
|
|
1296
|
+
// Store as selector-aware object to preserve :where() context for variants
|
|
1297
|
+
// Mark as fullBlock since it contains nested rules
|
|
1298
|
+
const storageKey = className;
|
|
1299
|
+
const originalSelector = `:where(${selectorContent})`;
|
|
1300
|
+
// Ensure CSS is always a string
|
|
1301
|
+
const cssString = ensureCssString(finalCssRules);
|
|
1302
|
+
const value = {
|
|
1303
|
+
selector: originalSelector,
|
|
1304
|
+
css: cssString,
|
|
1305
|
+
fullBlock: true // :where() blocks often contain nested rules
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
// Combine CSS rules if the class already exists
|
|
1309
|
+
if (utilities.has(storageKey)) {
|
|
1310
|
+
const existing = utilities.get(storageKey);
|
|
1311
|
+
// If existing is a string, convert to array format
|
|
1312
|
+
if (typeof existing === 'string') {
|
|
1313
|
+
const existingCssString = ensureCssString(existing);
|
|
1314
|
+
utilities.set(storageKey, [
|
|
1315
|
+
{ selector: `.${className}`, css: existingCssString },
|
|
1316
|
+
value
|
|
1317
|
+
]);
|
|
1318
|
+
} else if (Array.isArray(existing)) {
|
|
1319
|
+
// Check if this selector already exists in the array
|
|
1320
|
+
const found = existing.find(e => e.selector === value.selector);
|
|
1321
|
+
if (found) {
|
|
1322
|
+
// Ensure both are strings before concatenating
|
|
1323
|
+
const foundCss = ensureCssString(found.css);
|
|
1324
|
+
const valueCss = ensureCssString(value.css);
|
|
1325
|
+
found.css = `${foundCss}; ${valueCss}`;
|
|
1326
|
+
} else {
|
|
1327
|
+
existing.push(value);
|
|
1328
|
+
}
|
|
1329
|
+
} else if (existing && existing.selector) {
|
|
1330
|
+
// Convert single object to array
|
|
1331
|
+
if (existing.selector === value.selector) {
|
|
1332
|
+
// Ensure both are strings before concatenating
|
|
1333
|
+
const existingCss = ensureCssString(existing.css);
|
|
1334
|
+
const valueCss = ensureCssString(value.css);
|
|
1335
|
+
existing.css = `${existingCss}; ${valueCss}`;
|
|
1336
|
+
utilities.set(storageKey, [existing]);
|
|
1337
|
+
} else {
|
|
1338
|
+
utilities.set(storageKey, [existing, value]);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
} else {
|
|
1342
|
+
utilities.set(storageKey, [value]);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Fallback: detect classes inside compound selectors (e.g., aside[popover].appear-start { ... })
|
|
1349
|
+
// Use brace matching to handle nested rules properly
|
|
1350
|
+
try {
|
|
1351
|
+
let i = 0;
|
|
1352
|
+
while (i < cssText.length) {
|
|
1353
|
+
// Skip whitespace
|
|
1354
|
+
while (i < cssText.length && /\s/.test(cssText[i])) i++;
|
|
1355
|
+
if (i >= cssText.length) break;
|
|
1356
|
+
|
|
1357
|
+
// Skip at-rules
|
|
1358
|
+
if (cssText[i] === '@') {
|
|
1359
|
+
// Skip to next line or closing brace
|
|
1360
|
+
while (i < cssText.length && cssText[i] !== '\n' && cssText[i] !== '}') i++;
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Capture selector up to '{'
|
|
1365
|
+
let selStart = i;
|
|
1366
|
+
while (i < cssText.length && cssText[i] !== '{') i++;
|
|
1367
|
+
if (i >= cssText.length) break;
|
|
1368
|
+
const selector = cssText.slice(selStart, i).trim();
|
|
1369
|
+
|
|
1370
|
+
// Find matching '}' with brace depth (handles nested braces)
|
|
1371
|
+
i++; // skip '{'
|
|
1372
|
+
let depth = 1;
|
|
1373
|
+
let blockStart = i;
|
|
1374
|
+
while (i < cssText.length && depth > 0) {
|
|
1375
|
+
if (cssText[i] === '{') depth++;
|
|
1376
|
+
else if (cssText[i] === '}') depth--;
|
|
1377
|
+
i++;
|
|
1378
|
+
}
|
|
1379
|
+
if (depth > 0) {
|
|
1380
|
+
// Malformed, skip
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
const cssRules = cssText.slice(blockStart, i - 1).trim();
|
|
1384
|
+
|
|
1385
|
+
// Skip at-rules and keyframes/selectors without classes
|
|
1386
|
+
if (selector.startsWith('@')) continue;
|
|
1387
|
+
|
|
1388
|
+
// Match: .classname or .!classname
|
|
1389
|
+
const classMatches = selector.match(/\.!?[A-Za-z0-9_-]+/g);
|
|
1390
|
+
if (!classMatches) continue;
|
|
1391
|
+
|
|
1392
|
+
// Check if CSS rules contain !important
|
|
1393
|
+
const hasImportant = /\s!important/.test(cssRules);
|
|
1394
|
+
|
|
1395
|
+
for (const classToken of classMatches) {
|
|
1396
|
+
const className = classToken.substring(1); // Remove the leading dot
|
|
1397
|
+
|
|
1398
|
+
// Skip Tailwind-generated or already captured classes (check base name without !)
|
|
1399
|
+
const baseClassName = className.startsWith('!') ? className.slice(1) : className;
|
|
1400
|
+
if (this.isTailwindGeneratedClass(baseClassName)) continue;
|
|
1401
|
+
|
|
1402
|
+
// Determine if this class should have !important
|
|
1403
|
+
const classHasImportantPrefix = className.startsWith('!');
|
|
1404
|
+
let finalCssRules = cssRules;
|
|
1405
|
+
|
|
1406
|
+
if (classHasImportantPrefix) {
|
|
1407
|
+
// Class has ! prefix, ensure CSS has !important
|
|
1408
|
+
if (!hasImportant) {
|
|
1409
|
+
finalCssRules = cssRules.includes(';') ?
|
|
1410
|
+
cssRules.replace(/;/g, ' !important;') :
|
|
1411
|
+
cssRules + ' !important';
|
|
1412
|
+
}
|
|
1413
|
+
} else {
|
|
1414
|
+
// Class doesn't have ! prefix, remove !important if present
|
|
1415
|
+
if (hasImportant) {
|
|
1416
|
+
finalCssRules = cssRules.replace(/\s!important/g, '');
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Store the class with its full name (including ! prefix) as the key
|
|
1421
|
+
// Ensure CSS is always a string
|
|
1422
|
+
const cssString = ensureCssString(finalCssRules);
|
|
1423
|
+
if (utilities.has(className)) {
|
|
1424
|
+
const existingRules = utilities.get(className);
|
|
1425
|
+
|
|
1426
|
+
// Handle arrays properly - don't stringify them
|
|
1427
|
+
if (Array.isArray(existingRules)) {
|
|
1428
|
+
// If existing is an array, add this as a new entry
|
|
1429
|
+
// Create a simple selector entry for the compound selector
|
|
1430
|
+
utilities.set(className, [...existingRules, {
|
|
1431
|
+
selector: `.${className}`,
|
|
1432
|
+
css: cssString,
|
|
1433
|
+
fullBlock: true // Compound selectors often have nested rules
|
|
1434
|
+
}]);
|
|
1435
|
+
} else {
|
|
1436
|
+
// Existing is a string or object - convert to array format
|
|
1437
|
+
const existingCssString = ensureCssString(existingRules);
|
|
1438
|
+
utilities.set(className, [
|
|
1439
|
+
{ selector: `.${className}`, css: existingCssString },
|
|
1440
|
+
{ selector: `.${className}`, css: cssString, fullBlock: true }
|
|
1441
|
+
]);
|
|
1442
|
+
}
|
|
1443
|
+
} else {
|
|
1444
|
+
utilities.set(className, cssString);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
} catch (e) {
|
|
1449
|
+
// Be tolerant: this is a best-effort extractor
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Universal fallback with basic nesting resolution for selectors using '&'
|
|
1453
|
+
// Captures context like :where(aside[popover]) &.appear-start &:not(:popover-open)
|
|
1454
|
+
try {
|
|
1455
|
+
const rules = [];
|
|
1456
|
+
|
|
1457
|
+
// Minimal nested CSS resolver: scans and builds combined selectors
|
|
1458
|
+
// Only stores top-level rules with their full block content (including nested blocks)
|
|
1459
|
+
const resolveNested = (text, parentSelector = '', isTopLevel = true) => {
|
|
1460
|
+
let i = 0;
|
|
1461
|
+
while (i < text.length) {
|
|
1462
|
+
// Skip whitespace
|
|
1463
|
+
while (i < text.length && /\s/.test(text[i])) i++;
|
|
1464
|
+
if (i >= text.length) break;
|
|
1465
|
+
|
|
1466
|
+
// Capture selector up to '{'
|
|
1467
|
+
let selStart = i;
|
|
1468
|
+
while (i < text.length && text[i] !== '{') i++;
|
|
1469
|
+
if (i >= text.length) break;
|
|
1470
|
+
const rawSelector = text.slice(selStart, i).trim();
|
|
1471
|
+
|
|
1472
|
+
// Find matching '}' with brace depth
|
|
1473
|
+
i++; // skip '{'
|
|
1474
|
+
let depth = 1;
|
|
1475
|
+
let blockStart = i;
|
|
1476
|
+
while (i < text.length && depth > 0) {
|
|
1477
|
+
if (text[i] === '{') depth++;
|
|
1478
|
+
else if (text[i] === '}') depth--;
|
|
1479
|
+
i++;
|
|
1480
|
+
}
|
|
1481
|
+
const block = text.slice(blockStart, i - 1);
|
|
1482
|
+
|
|
1483
|
+
// Build combined selector by replacing '&' with parentSelector
|
|
1484
|
+
const combinedSelector = parentSelector
|
|
1485
|
+
? rawSelector.replace(/&/g, parentSelector).trim()
|
|
1486
|
+
: rawSelector.trim();
|
|
1487
|
+
|
|
1488
|
+
// Only store top-level rules (not nested blocks) with their full content
|
|
1489
|
+
// This preserves @starting-style and other nested at-rules in the CSS
|
|
1490
|
+
if (isTopLevel) {
|
|
1491
|
+
const fullBlock = block.trim();
|
|
1492
|
+
if (fullBlock) {
|
|
1493
|
+
rules.push({ selector: combinedSelector, css: fullBlock, fullBlock: true });
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// Recurse into nested blocks but don't store them separately
|
|
1498
|
+
// They're already included in the parent block's CSS
|
|
1499
|
+
resolveNested(block, combinedSelector, false);
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1503
|
+
resolveNested(cssText, '');
|
|
1504
|
+
|
|
1505
|
+
// Map resolved rules to utilities by class token presence
|
|
1506
|
+
for (const rule of rules) {
|
|
1507
|
+
// Clean selector: strip comments and normalize whitespace
|
|
1508
|
+
let cleanedSelector = rule.selector.replace(/\/\*[^]*?\*\//g, '').replace(/\s+/g, ' ').trim();
|
|
1509
|
+
// Match: .classname or .!classname
|
|
1510
|
+
const classTokens = cleanedSelector.match(/\.!?[A-Za-z0-9_-]+/g);
|
|
1511
|
+
if (!classTokens) continue;
|
|
1512
|
+
|
|
1513
|
+
// Check if CSS rules contain !important
|
|
1514
|
+
const hasImportant = /\s!important/.test(rule.css);
|
|
1515
|
+
|
|
1516
|
+
for (const token of classTokens) {
|
|
1517
|
+
const className = token.slice(1); // Remove the leading dot
|
|
1518
|
+
|
|
1519
|
+
// Skip Tailwind-generated classes (check base name without !)
|
|
1520
|
+
const baseClassName = className.startsWith('!') ? className.slice(1) : className;
|
|
1521
|
+
if (this.isTailwindGeneratedClass(baseClassName)) continue;
|
|
1522
|
+
|
|
1523
|
+
// Determine if this class should have !important
|
|
1524
|
+
const classHasImportantPrefix = className.startsWith('!');
|
|
1525
|
+
let finalCss = rule.css;
|
|
1526
|
+
|
|
1527
|
+
if (classHasImportantPrefix) {
|
|
1528
|
+
// Class has ! prefix, ensure CSS has !important
|
|
1529
|
+
if (!hasImportant) {
|
|
1530
|
+
finalCss = rule.css.includes(';') ?
|
|
1531
|
+
rule.css.replace(/;/g, ' !important;') :
|
|
1532
|
+
rule.css + ' !important';
|
|
1533
|
+
}
|
|
1534
|
+
} else {
|
|
1535
|
+
// Class doesn't have ! prefix, remove !important if present
|
|
1536
|
+
if (hasImportant) {
|
|
1537
|
+
finalCss = rule.css.replace(/\s!important/g, '');
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Store selector-aware utility so variants preserve context and pseudos
|
|
1542
|
+
// Use full className (including !) as the key
|
|
1543
|
+
// Preserve fullBlock flag if present
|
|
1544
|
+
// Ensure CSS is always a string
|
|
1545
|
+
const cssString = ensureCssString(finalCss);
|
|
1546
|
+
const value = {
|
|
1547
|
+
selector: cleanedSelector,
|
|
1548
|
+
css: cssString,
|
|
1549
|
+
fullBlock: rule.fullBlock || false
|
|
1550
|
+
};
|
|
1551
|
+
if (utilities.has(className)) {
|
|
1552
|
+
const existing = utilities.get(className);
|
|
1553
|
+
if (typeof existing === 'string') {
|
|
1554
|
+
const existingCssString = ensureCssString(existing);
|
|
1555
|
+
utilities.set(className, [{ selector: `.${className}`, css: existingCssString }, value]);
|
|
1556
|
+
} else if (Array.isArray(existing)) {
|
|
1557
|
+
const found = existing.find(e => e.selector === value.selector);
|
|
1558
|
+
if (found) {
|
|
1559
|
+
// Ensure both are strings before concatenating
|
|
1560
|
+
const foundCss = ensureCssString(found.css);
|
|
1561
|
+
const valueCss = ensureCssString(value.css);
|
|
1562
|
+
found.css = `${foundCss}; ${valueCss}`;
|
|
1563
|
+
} else {
|
|
1564
|
+
existing.push(value);
|
|
1565
|
+
}
|
|
1566
|
+
} else if (existing && existing.selector) {
|
|
1567
|
+
if (existing.selector === value.selector) {
|
|
1568
|
+
// Ensure both are strings before concatenating
|
|
1569
|
+
const existingCss = ensureCssString(existing.css);
|
|
1570
|
+
const valueCss = ensureCssString(value.css);
|
|
1571
|
+
existing.css = `${existingCss}; ${valueCss}`;
|
|
1572
|
+
utilities.set(className, [existing]);
|
|
1573
|
+
} else {
|
|
1574
|
+
utilities.set(className, [existing, value]);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
} else {
|
|
1578
|
+
utilities.set(className, [value]);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
} catch (e) {
|
|
1583
|
+
// Tolerate parsing errors; this is best-effort
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Dedupe per class: the four parser passes can capture the same rule with
|
|
1587
|
+
// differing whitespace/selector order; normalize before comparing.
|
|
1588
|
+
for (const [className, value] of utilities.entries()) {
|
|
1589
|
+
if (!Array.isArray(value)) continue;
|
|
1590
|
+
const seen = new Set();
|
|
1591
|
+
const deduped = [];
|
|
1592
|
+
for (const entry of value) {
|
|
1593
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
1594
|
+
const sel = String(entry.selector || '').replace(/\s+/g, ' ').trim();
|
|
1595
|
+
const css = String(entry.css || '').replace(/\s+/g, ' ').trim();
|
|
1596
|
+
if (!css) continue;
|
|
1597
|
+
const key = `${sel}${css}`;
|
|
1598
|
+
if (seen.has(key)) continue;
|
|
1599
|
+
seen.add(key);
|
|
1600
|
+
deduped.push(entry);
|
|
1601
|
+
}
|
|
1602
|
+
if (deduped.length > 0) {
|
|
1603
|
+
utilities.set(className, deduped);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
return utilities;
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
// Check if a class name looks like a Tailwind-generated class
|
|
1611
|
+
TailwindCompiler.prototype.isTailwindGeneratedClass = function (className) {
|
|
1612
|
+
// Check if this looks like a Tailwind-generated class
|
|
1613
|
+
const tailwindPatterns = [
|
|
1614
|
+
/^[a-z]+-\d+$/, // spacing, sizing classes like p-4, w-10
|
|
1615
|
+
/^[a-z]+-\[/, // arbitrary values like w-[100px]
|
|
1616
|
+
/^(text|bg|border|ring|shadow|opacity|scale|rotate|translate|skew|origin|transform|transition|duration|delay|ease|animate|backdrop|blur|brightness|contrast|drop-shadow|grayscale|hue-rotate|invert|saturate|sepia|filter|backdrop-)/, // common Tailwind prefixes
|
|
1617
|
+
/^(sm|md|lg|xl|2xl):/, // responsive prefixes
|
|
1618
|
+
/^(hover|focus|active|disabled|group-hover|group-focus|peer-hover|peer-focus):/, // state prefixes
|
|
1619
|
+
/^(dark|light):/, // theme prefixes
|
|
1620
|
+
/^!/, // important modifier
|
|
1621
|
+
/^\[/, // arbitrary selectors
|
|
1622
|
+
];
|
|
1623
|
+
|
|
1624
|
+
return tailwindPatterns.some(pattern => pattern.test(className));
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
// Parse a class name into its components (variants, base class, important)
|
|
1628
|
+
TailwindCompiler.prototype.parseClassName = function (className) {
|
|
1629
|
+
// Check cache first
|
|
1630
|
+
if (this.classCache.has(className)) {
|
|
1631
|
+
return this.classCache.get(className);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const result = {
|
|
1635
|
+
important: className.startsWith('!'),
|
|
1636
|
+
variants: [],
|
|
1637
|
+
baseClass: className
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
// Remove important modifier if present
|
|
1641
|
+
if (result.important) {
|
|
1642
|
+
className = className.slice(1);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// Split by variant separator, but preserve content within brackets
|
|
1646
|
+
const parts = [];
|
|
1647
|
+
let current = '';
|
|
1648
|
+
let bracketDepth = 0;
|
|
1649
|
+
|
|
1650
|
+
for (let i = 0; i < className.length; i++) {
|
|
1651
|
+
const char = className[i];
|
|
1652
|
+
|
|
1653
|
+
if (char === '[') {
|
|
1654
|
+
bracketDepth++;
|
|
1655
|
+
} else if (char === ']') {
|
|
1656
|
+
bracketDepth--;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (char === ':' && bracketDepth === 0) {
|
|
1660
|
+
parts.push(current);
|
|
1661
|
+
current = '';
|
|
1662
|
+
} else {
|
|
1663
|
+
current += char;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
parts.push(current); // Add the last part
|
|
1667
|
+
|
|
1668
|
+
result.baseClass = parts.pop(); // Last part is always the base class
|
|
1669
|
+
|
|
1670
|
+
// Process variants in order (left to right)
|
|
1671
|
+
result.variants = parts.map(variant => {
|
|
1672
|
+
// FIRST: Check for exact variant matches (most common case)
|
|
1673
|
+
const exactSelector = this.variants[variant];
|
|
1674
|
+
if (exactSelector) {
|
|
1675
|
+
return {
|
|
1676
|
+
name: variant,
|
|
1677
|
+
selector: exactSelector,
|
|
1678
|
+
isArbitrary: false
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// SECOND: Check for arbitrary selector variants [&_selector]
|
|
1683
|
+
if (variant.startsWith('[') && variant.endsWith(']')) {
|
|
1684
|
+
const arbitrarySelector = variant.slice(1, -1); // Remove brackets
|
|
1685
|
+
if (arbitrarySelector.startsWith('&')) {
|
|
1686
|
+
return {
|
|
1687
|
+
name: variant,
|
|
1688
|
+
selector: arbitrarySelector,
|
|
1689
|
+
isArbitrary: true
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
// Handle arbitrary data variants like [data-state=active]
|
|
1693
|
+
if (arbitrarySelector.startsWith('data-')) {
|
|
1694
|
+
return {
|
|
1695
|
+
name: variant,
|
|
1696
|
+
selector: `[${arbitrarySelector}] &`,
|
|
1697
|
+
isArbitrary: true
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// THIRD: Handle parameterized variants (only if exact match not found)
|
|
1703
|
+
|
|
1704
|
+
// Handle data-[...] variants like data-[state=active] (not starting with bracket)
|
|
1705
|
+
// These should be regular attribute selectors, not arbitrary
|
|
1706
|
+
const dataMatch = variant.match(/^data-\[(.+)\]$/);
|
|
1707
|
+
if (dataMatch) {
|
|
1708
|
+
const attrValue = dataMatch[1];
|
|
1709
|
+
// Add quotes around the value if it contains = (e.g., state=active -> state="active")
|
|
1710
|
+
const quotedValue = attrValue.includes('=')
|
|
1711
|
+
? attrValue.replace(/^([^=]+)=(.+)$/, '$1="$2"')
|
|
1712
|
+
: attrValue;
|
|
1713
|
+
return {
|
|
1714
|
+
name: variant,
|
|
1715
|
+
selector: `[data-${quotedValue}] &`,
|
|
1716
|
+
isArbitrary: false
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// Handle parameterized nth variants: nth-3, nth-last-2, nth-of-type-2, nth-last-of-type-2
|
|
1721
|
+
const nthMatch = variant.match(/^(nth|nth-last|nth-of-type|nth-last-of-type)-(\d+)$/);
|
|
1722
|
+
if (nthMatch) {
|
|
1723
|
+
const baseVariant = nthMatch[1];
|
|
1724
|
+
const param = nthMatch[2];
|
|
1725
|
+
const baseSelector = this.variants[baseVariant];
|
|
1726
|
+
if (baseSelector) {
|
|
1727
|
+
return {
|
|
1728
|
+
name: variant,
|
|
1729
|
+
selector: `${baseSelector}(${param})`,
|
|
1730
|
+
isArbitrary: false
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// Handle arbitrary nth variants: nth-[4n+1], nth-[2n], nth-last-[n+3], nth-of-type-[odd], etc.
|
|
1736
|
+
const nthArbitraryMatch = variant.match(/^(nth|nth-last|nth-of-type|nth-last-of-type)-\[(.+)\]$/);
|
|
1737
|
+
if (nthArbitraryMatch) {
|
|
1738
|
+
const baseVariant = nthArbitraryMatch[1];
|
|
1739
|
+
const param = nthArbitraryMatch[2];
|
|
1740
|
+
const baseSelector = this.variants[baseVariant];
|
|
1741
|
+
if (baseSelector) {
|
|
1742
|
+
return {
|
|
1743
|
+
name: variant,
|
|
1744
|
+
selector: `${baseSelector}(${param})`,
|
|
1745
|
+
isArbitrary: false
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// Handle parameterized has/not variants: has-[>p], not-\[hidden\]
|
|
1751
|
+
const hasMatch = variant.match(/^has-\[(.+)\]$/);
|
|
1752
|
+
if (hasMatch) {
|
|
1753
|
+
const param = hasMatch[1];
|
|
1754
|
+
const baseSelector = this.variants['has'];
|
|
1755
|
+
if (baseSelector) {
|
|
1756
|
+
return {
|
|
1757
|
+
name: variant,
|
|
1758
|
+
selector: `${baseSelector}(${param})`,
|
|
1759
|
+
isArbitrary: false
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// Handle not-\[...\] variants like not-\[hidden\]
|
|
1765
|
+
// The backslashes escape the brackets in the class name
|
|
1766
|
+
// Match: not-\[content\] where content may have escaped brackets
|
|
1767
|
+
const notMatch = variant.match(/^not-\\\[(.+?)\\\]$/);
|
|
1768
|
+
if (notMatch) {
|
|
1769
|
+
const param = notMatch[1];
|
|
1770
|
+
// The parameter is already clean (backslashes were just for escaping brackets in class name)
|
|
1771
|
+
const baseSelector = this.variants['not'];
|
|
1772
|
+
if (baseSelector) {
|
|
1773
|
+
return {
|
|
1774
|
+
name: variant,
|
|
1775
|
+
selector: `${baseSelector}([${param}])`,
|
|
1776
|
+
isArbitrary: false
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// Container query variants (Tailwind v4): @[size], @min-[size], @max-[size],
|
|
1782
|
+
// named @sm/@md/… and @max-sm/…, each optionally scoped to a /name container.
|
|
1783
|
+
// Tailwind emits these for its own utilities; recognising them here keeps them
|
|
1784
|
+
// off the "Unknown variant" path and lets the Manifest compiler apply them to
|
|
1785
|
+
// theme/semantic utilities too. The @-prefixed selector is wrapped as an
|
|
1786
|
+
// at-rule by the generator, producing `@container (width …) { … }`.
|
|
1787
|
+
if (variant.startsWith('@')) {
|
|
1788
|
+
const containerSizes = {
|
|
1789
|
+
'3xs': '16rem', '2xs': '18rem', 'xs': '20rem', 'sm': '24rem',
|
|
1790
|
+
'md': '28rem', 'lg': '32rem', 'xl': '36rem', '2xl': '42rem',
|
|
1791
|
+
'3xl': '48rem', '4xl': '56rem', '5xl': '64rem', '6xl': '72rem', '7xl': '80rem'
|
|
1792
|
+
};
|
|
1793
|
+
const slash = variant.indexOf('/');
|
|
1794
|
+
const name = slash !== -1 ? variant.slice(slash + 1) : '';
|
|
1795
|
+
const ctx = name ? `@container ${name}` : '@container';
|
|
1796
|
+
const body = (slash !== -1 ? variant.slice(0, slash) : variant).slice(1); // strip '@'
|
|
1797
|
+
let m;
|
|
1798
|
+
if (body === '' || body === 'container') {
|
|
1799
|
+
return { name: variant, selector: ctx, isArbitrary: false };
|
|
1800
|
+
}
|
|
1801
|
+
if ((m = body.match(/^(?:min-)?\[(.+)\]$/))) {
|
|
1802
|
+
return { name: variant, selector: `${ctx} (width >= ${m[1]})`, isArbitrary: false };
|
|
1803
|
+
}
|
|
1804
|
+
if ((m = body.match(/^max-\[(.+)\]$/))) {
|
|
1805
|
+
return { name: variant, selector: `${ctx} (width < ${m[1]})`, isArbitrary: false };
|
|
1806
|
+
}
|
|
1807
|
+
if ((m = body.match(/^max-(.+)$/))) {
|
|
1808
|
+
if (containerSizes[m[1]]) {
|
|
1809
|
+
return { name: variant, selector: `${ctx} (width < ${containerSizes[m[1]]})`, isArbitrary: false };
|
|
1810
|
+
}
|
|
1811
|
+
} else if (containerSizes[body]) {
|
|
1812
|
+
return { name: variant, selector: `${ctx} (width >= ${containerSizes[body]})`, isArbitrary: false };
|
|
1813
|
+
}
|
|
1814
|
+
// Unrecognised @-form falls through to the warning below.
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// If no match found, warn once per unique token and return null.
|
|
1818
|
+
// Deduped so unsupported variants (e.g. container queries like
|
|
1819
|
+
// `@[10rem]:`) don't flood the console on every compile pass.
|
|
1820
|
+
if (!this._warnedVariants) this._warnedVariants = new Set();
|
|
1821
|
+
if (!this._warnedVariants.has(variant)) {
|
|
1822
|
+
this._warnedVariants.add(variant);
|
|
1823
|
+
console.warn(`Unknown variant: ${variant}`);
|
|
1824
|
+
}
|
|
1825
|
+
return null;
|
|
1826
|
+
}).filter(Boolean);
|
|
1827
|
+
|
|
1828
|
+
// Cache the result
|
|
1829
|
+
this.classCache.set(className, result);
|
|
1830
|
+
return result;
|
|
1831
|
+
};
|
|
1832
|
+
|
|
1833
|
+
|
|
1834
|
+
|
|
1835
|
+
// Compilation logic and utility generation
|
|
1836
|
+
|
|
1837
|
+
// Generate utilities from CSS variables
|
|
1838
|
+
TailwindCompiler.prototype.generateUtilitiesFromVars = function (cssText, usedData) {
|
|
1839
|
+
try {
|
|
1840
|
+
const utilities = [];
|
|
1841
|
+
const generatedRules = new Set(); // Track generated rules to prevent duplicates
|
|
1842
|
+
const variables = this.extractThemeVariables(cssText);
|
|
1843
|
+
const { classes: usedClasses, variableSuffixes } = usedData;
|
|
1844
|
+
|
|
1845
|
+
if (variables.size === 0) {
|
|
1846
|
+
return '';
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// Helper to escape special characters in class names
|
|
1850
|
+
const escapeClassName = (className) => {
|
|
1851
|
+
return className.replace(/[^a-zA-Z0-9-]/g, '\\$&');
|
|
1852
|
+
};
|
|
1853
|
+
|
|
1854
|
+
// Helper to generate a single utility with its variants
|
|
1855
|
+
const generateUtility = (baseClass, css) => {
|
|
1856
|
+
// Find all variants of this base class that are actually used
|
|
1857
|
+
const usedVariants = usedClasses
|
|
1858
|
+
.filter(cls => {
|
|
1859
|
+
const parts = cls.split(':');
|
|
1860
|
+
const basePart = parts[parts.length - 1];
|
|
1861
|
+
return basePart === baseClass || (basePart.startsWith('!') && basePart.slice(1) === baseClass);
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
// Generate base utility if it's used directly
|
|
1865
|
+
if (usedClasses.includes(baseClass)) {
|
|
1866
|
+
const rule = `.${escapeClassName(baseClass)} { ${css} }`;
|
|
1867
|
+
if (!generatedRules.has(rule)) {
|
|
1868
|
+
utilities.push(rule);
|
|
1869
|
+
generatedRules.add(rule);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
// Generate important version if used
|
|
1873
|
+
if (usedClasses.includes('!' + baseClass)) {
|
|
1874
|
+
const importantCss = css.includes(';') ?
|
|
1875
|
+
css.replace(/;/g, ' !important;') :
|
|
1876
|
+
css + ' !important';
|
|
1877
|
+
const rule = `.${escapeClassName('!' + baseClass)} { ${importantCss} }`;
|
|
1878
|
+
if (!generatedRules.has(rule)) {
|
|
1879
|
+
utilities.push(rule);
|
|
1880
|
+
generatedRules.add(rule);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// Generate each variant as a separate class
|
|
1885
|
+
for (const variantClass of usedVariants) {
|
|
1886
|
+
if (variantClass === baseClass) continue;
|
|
1887
|
+
|
|
1888
|
+
const parsed = this.parseClassName(variantClass);
|
|
1889
|
+
|
|
1890
|
+
// Check if this is an important variant
|
|
1891
|
+
const isImportant = parsed.important;
|
|
1892
|
+
// Ensure css is a string (handle cases where it might be an object)
|
|
1893
|
+
const cssString = typeof css === 'string' ? css : (css && typeof css === 'object' && css.css ? css.css : String(css));
|
|
1894
|
+
const cssContent = isImportant ?
|
|
1895
|
+
(cssString.includes(';') ? cssString.replace(/;/g, ' !important;') : cssString + ' !important') :
|
|
1896
|
+
cssString;
|
|
1897
|
+
|
|
1898
|
+
// Build selector by applying variants
|
|
1899
|
+
let selector = `.${escapeClassName(variantClass)}`;
|
|
1900
|
+
let hasMediaQuery = false;
|
|
1901
|
+
let mediaQueryRule = '';
|
|
1902
|
+
|
|
1903
|
+
for (const variant of parsed.variants) {
|
|
1904
|
+
if (variant.isArbitrary) {
|
|
1905
|
+
// Handle arbitrary selectors like [&_figure] or [&_fieldset:has(legend):not(.whatever)]
|
|
1906
|
+
// For selectors starting with &, replace & with the base class and use as regular selector
|
|
1907
|
+
let arbitrarySelector = variant.selector;
|
|
1908
|
+
|
|
1909
|
+
if (arbitrarySelector.startsWith('&')) {
|
|
1910
|
+
// Replace & with the base class selector and convert _ to spaces
|
|
1911
|
+
arbitrarySelector = arbitrarySelector.replace(/_/g, ' ').replace(/&/g, selector);
|
|
1912
|
+
selector = arbitrarySelector;
|
|
1913
|
+
} else {
|
|
1914
|
+
// For other arbitrary selectors (like data attributes), use nested CSS
|
|
1915
|
+
arbitrarySelector = arbitrarySelector.replace(/_/g, ' ');
|
|
1916
|
+
selector = { baseClass: selector, arbitrarySelector };
|
|
1917
|
+
}
|
|
1918
|
+
} else if (variant.selector.includes('&')) {
|
|
1919
|
+
// Substitute & with the current selector. This produces flat CSS that
|
|
1920
|
+
// works for both ancestor patterns (`.dark &` → `.dark .X`) and
|
|
1921
|
+
// self-extending patterns (`& > p` → `.X > p`).
|
|
1922
|
+
selector = variant.selector.replace(/&/g, selector);
|
|
1923
|
+
} else if (variant.selector.startsWith(':')) {
|
|
1924
|
+
// For pseudo-classes, append to selector
|
|
1925
|
+
selector = `${selector}${variant.selector}`;
|
|
1926
|
+
} else if (variant.selector.startsWith('@')) {
|
|
1927
|
+
// For media queries, wrap the whole rule
|
|
1928
|
+
hasMediaQuery = true;
|
|
1929
|
+
mediaQueryRule = variant.selector;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// Generate the final rule
|
|
1934
|
+
let rule;
|
|
1935
|
+
if (typeof selector === 'object' && selector.arbitrarySelector) {
|
|
1936
|
+
// Handle arbitrary selectors with nested CSS (for non-& selectors)
|
|
1937
|
+
rule = `${selector.baseClass} {\n ${selector.arbitrarySelector} {\n ${cssContent}\n }\n}`;
|
|
1938
|
+
} else {
|
|
1939
|
+
// Regular selector
|
|
1940
|
+
rule = `${selector} { ${cssContent} }`;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
const finalRule = hasMediaQuery ?
|
|
1944
|
+
`${mediaQueryRule} { ${rule} }` :
|
|
1945
|
+
rule;
|
|
1946
|
+
|
|
1947
|
+
if (!generatedRules.has(finalRule)) {
|
|
1948
|
+
utilities.push(finalRule);
|
|
1949
|
+
generatedRules.add(finalRule);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
|
|
1954
|
+
// Generate utilities based on variable prefix
|
|
1955
|
+
for (const [varName, varValue] of variables.entries()) {
|
|
1956
|
+
if (!varName.match(this.regexPatterns.tailwindPrefix)) {
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
const suffix = varName.split('-').slice(1).join('-');
|
|
1961
|
+
const value = `var(--${varName})`;
|
|
1962
|
+
const prefix = varName.split('-')[0] + '-';
|
|
1963
|
+
const generator = this.utilityGenerators[prefix];
|
|
1964
|
+
|
|
1965
|
+
if (generator) {
|
|
1966
|
+
const utilityPairs = generator(suffix, value);
|
|
1967
|
+
for (const [className, css] of utilityPairs) {
|
|
1968
|
+
// Check if this specific utility class is actually used (including variants and important)
|
|
1969
|
+
const isUsed = usedClasses.some(cls => {
|
|
1970
|
+
// Parse the class to extract the base utility name
|
|
1971
|
+
const parsed = this.parseClassName(cls);
|
|
1972
|
+
const baseClass = parsed.baseClass;
|
|
1973
|
+
|
|
1974
|
+
// Check both normal and important versions
|
|
1975
|
+
return baseClass === className ||
|
|
1976
|
+
baseClass === '!' + className ||
|
|
1977
|
+
(baseClass.startsWith('!') && baseClass.slice(1) === className);
|
|
1978
|
+
});
|
|
1979
|
+
if (isUsed) {
|
|
1980
|
+
generateUtility(className, css);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// Opacity variants: collect the bare base (e.g. `bg-black/10`),
|
|
1984
|
+
// not the prefixed class — generateUtility discovers variant
|
|
1985
|
+
// prefixes itself, so passing the bare base emits the correct
|
|
1986
|
+
// selector (`.backdrop\:bg-black\/10::backdrop`).
|
|
1987
|
+
const opacityBaseClasses = new Set();
|
|
1988
|
+
for (const cls of usedClasses) {
|
|
1989
|
+
// Parse the class to extract the base utility name
|
|
1990
|
+
const parsed = this.parseClassName(cls);
|
|
1991
|
+
const baseClass = parsed.baseClass;
|
|
1992
|
+
|
|
1993
|
+
// Check if this class has an opacity modifier and matches our base class
|
|
1994
|
+
if (baseClass.includes('/')) {
|
|
1995
|
+
const baseWithoutOpacity = baseClass.split('/')[0];
|
|
1996
|
+
if (baseWithoutOpacity === className) {
|
|
1997
|
+
const opacity = baseClass.split('/')[1];
|
|
1998
|
+
// Validate that the opacity is a number between 0-100
|
|
1999
|
+
if (!isNaN(opacity) && opacity >= 0 && opacity <= 100) {
|
|
2000
|
+
opacityBaseClasses.add(baseClass);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// Generate opacity utilities for each opacity value found
|
|
2007
|
+
for (const opacityBaseClass of opacityBaseClasses) {
|
|
2008
|
+
const opacity = opacityBaseClass.split('/')[1];
|
|
2009
|
+
const opacityValue = `color-mix(in oklch, ${value} ${opacity}%, transparent)`;
|
|
2010
|
+
const opacityCss = css.replace(value, opacityValue);
|
|
2011
|
+
generateUtility(opacityBaseClass, opacityCss);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
return utilities.join('\n');
|
|
2018
|
+
} catch (error) {
|
|
2019
|
+
console.error('Error generating utilities:', error);
|
|
2020
|
+
return '';
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
|
|
2024
|
+
// Generate custom utilities from discovered custom utility classes
|
|
2025
|
+
TailwindCompiler.prototype.generateCustomUtilities = function (usedData) {
|
|
2026
|
+
try {
|
|
2027
|
+
const utilities = [];
|
|
2028
|
+
const generatedRules = new Set();
|
|
2029
|
+
const { classes: usedClasses } = usedData;
|
|
2030
|
+
|
|
2031
|
+
// Helper to clean up [object Object] from CSS strings
|
|
2032
|
+
const cleanCssString = (css) => {
|
|
2033
|
+
if (typeof css !== 'string') return css;
|
|
2034
|
+
return css.replace(/\[object Object\](;?\s*)/g, '').trim();
|
|
2035
|
+
};
|
|
2036
|
+
|
|
2037
|
+
if (this.customUtilities.size === 0) {
|
|
2038
|
+
return '';
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Helper to escape special characters in class names
|
|
2042
|
+
const escapeClassName = (className) => {
|
|
2043
|
+
return className.replace(/[^a-zA-Z0-9-]/g, '\\$&');
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
// Replace & in selectors — legacy/flattened CSS only; nested CSS keeps &
|
|
2047
|
+
// as-is so native nesting works.
|
|
2048
|
+
const replaceAmpersandInSelectors = (cssText, replacement) => {
|
|
2049
|
+
const hasNestedSelectors =
|
|
2050
|
+
/&\s*[:\.\[{]/.test(cssText) || // &:not(), &::before, &[attr], & {
|
|
2051
|
+
/&\s*\n\s*[:\.\[{]/.test(cssText) || // & on new line followed by selector
|
|
2052
|
+
/\]\s*&/.test(cssText) || // [dir=rtl] & pattern
|
|
2053
|
+
/&\s*$/.test(cssText.split('\n').find(line => line.trim().startsWith('&')) || ''); // & on its own line
|
|
2054
|
+
|
|
2055
|
+
if (hasNestedSelectors) {
|
|
2056
|
+
// This is nested CSS - don't replace &, preserve it as-is
|
|
2057
|
+
return cssText;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// Legacy behavior: replace & for flattened CSS (shouldn't be needed for nested CSS)
|
|
2061
|
+
let result = '';
|
|
2062
|
+
let i = 0;
|
|
2063
|
+
let inString = false;
|
|
2064
|
+
let stringChar = '';
|
|
2065
|
+
let inComment = false;
|
|
2066
|
+
|
|
2067
|
+
while (i < cssText.length) {
|
|
2068
|
+
const char = cssText[i];
|
|
2069
|
+
const nextChar = i + 1 < cssText.length ? cssText[i + 1] : '';
|
|
2070
|
+
const prevChar = i > 0 ? cssText[i - 1] : '';
|
|
2071
|
+
|
|
2072
|
+
// Handle strings
|
|
2073
|
+
if ((char === '"' || char === "'") && !inComment) {
|
|
2074
|
+
if (!inString) {
|
|
2075
|
+
inString = true;
|
|
2076
|
+
stringChar = char;
|
|
2077
|
+
} else if (char === stringChar && prevChar !== '\\') {
|
|
2078
|
+
inString = false;
|
|
2079
|
+
stringChar = '';
|
|
2080
|
+
}
|
|
2081
|
+
result += char;
|
|
2082
|
+
i++;
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
if (inString) {
|
|
2087
|
+
result += char;
|
|
2088
|
+
i++;
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// Handle comments
|
|
2093
|
+
if (char === '/' && nextChar === '*') {
|
|
2094
|
+
inComment = true;
|
|
2095
|
+
result += char;
|
|
2096
|
+
i++;
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
if (inComment) {
|
|
2101
|
+
if (char === '*' && nextChar === '/') {
|
|
2102
|
+
inComment = false;
|
|
2103
|
+
result += char;
|
|
2104
|
+
i++;
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
result += char;
|
|
2108
|
+
i++;
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// Replace & when it's in a selector context (only for legacy/flattened CSS)
|
|
2113
|
+
if (char === '&') {
|
|
2114
|
+
const lookAhead = cssText.slice(i + 1, Math.min(i + 10, cssText.length));
|
|
2115
|
+
const isSelector =
|
|
2116
|
+
lookAhead.match(/^[:\.\[\+\>~,)]/) ||
|
|
2117
|
+
lookAhead.match(/^\s+[:\.\[\+\>~,{]/) ||
|
|
2118
|
+
lookAhead === '' ||
|
|
2119
|
+
lookAhead[0] === '\n' ||
|
|
2120
|
+
(prevChar === '\n' && (lookAhead[0] === ':' || lookAhead[0] === '.' || lookAhead[0] === '[' || lookAhead[0] === ' '));
|
|
2121
|
+
|
|
2122
|
+
if (isSelector) {
|
|
2123
|
+
result += replacement;
|
|
2124
|
+
i++;
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
result += char;
|
|
2130
|
+
i++;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
return result;
|
|
2134
|
+
};
|
|
2135
|
+
|
|
2136
|
+
// Helper to generate a single utility with its variants
|
|
2137
|
+
const generateUtility = (baseClass, css, selectorInfo) => {
|
|
2138
|
+
// Ensure css is a string at the start
|
|
2139
|
+
if (typeof css !== 'string') {
|
|
2140
|
+
css = typeof css === 'object' && css && css.css ? css.css : String(css);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// Find all variants of this base class that are actually used
|
|
2144
|
+
const usedVariants = usedClasses
|
|
2145
|
+
.filter(cls => {
|
|
2146
|
+
const parts = cls.split(':');
|
|
2147
|
+
const basePart = parts[parts.length - 1];
|
|
2148
|
+
const isMatch = basePart === baseClass || (basePart.startsWith('!') && basePart.slice(1) === baseClass);
|
|
2149
|
+
return isMatch;
|
|
2150
|
+
});
|
|
2151
|
+
|
|
2152
|
+
// Skip generating base utility - it already exists in the CSS
|
|
2153
|
+
// Only generate variants and important versions
|
|
2154
|
+
|
|
2155
|
+
// Generate important version if used
|
|
2156
|
+
if (usedClasses.includes('!' + baseClass)) {
|
|
2157
|
+
// Check if CSS already has !important to avoid double !important
|
|
2158
|
+
const alreadyHasImportant = /\s!important/.test(css);
|
|
2159
|
+
const importantCss = alreadyHasImportant ? css :
|
|
2160
|
+
(css.includes(';') ?
|
|
2161
|
+
css.replace(/;/g, ' !important;') :
|
|
2162
|
+
css + ' !important');
|
|
2163
|
+
let rule;
|
|
2164
|
+
if (selectorInfo && selectorInfo.selector) {
|
|
2165
|
+
// If the original selector contains :where(), don't try to modify it
|
|
2166
|
+
// Generate a separate rule for the important version
|
|
2167
|
+
// This prevents :where(.row, .col) from becoming :where(.row, .!col) with !important on all
|
|
2168
|
+
if (selectorInfo.selector.includes(':where(')) {
|
|
2169
|
+
// For :where() selectors, generate individual class selector for important version
|
|
2170
|
+
rule = `.${escapeClassName('!' + baseClass)} { ${importantCss} }`;
|
|
2171
|
+
} else {
|
|
2172
|
+
// For other contextual selectors, do the replacement
|
|
2173
|
+
const variantSel = `.${escapeClassName('!' + baseClass)}`;
|
|
2174
|
+
let contextual = selectorInfo.selector.replace(new RegExp(`\\.${baseClass}(?=[^a-zA-Z0-9_-]|$)`), variantSel);
|
|
2175
|
+
if (contextual === selectorInfo.selector) {
|
|
2176
|
+
// Fallback: append class to the end if base token not found
|
|
2177
|
+
contextual = `${selectorInfo.selector}${variantSel}`;
|
|
2178
|
+
}
|
|
2179
|
+
rule = `${contextual} { ${importantCss} }`;
|
|
2180
|
+
}
|
|
2181
|
+
} else {
|
|
2182
|
+
rule = `.${escapeClassName('!' + baseClass)} { ${importantCss} }`;
|
|
2183
|
+
}
|
|
2184
|
+
if (!generatedRules.has(rule)) {
|
|
2185
|
+
utilities.push(rule);
|
|
2186
|
+
generatedRules.add(rule);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// Generate each variant as a separate class
|
|
2191
|
+
for (const variantClass of usedVariants) {
|
|
2192
|
+
if (variantClass === baseClass) continue;
|
|
2193
|
+
|
|
2194
|
+
const parsed = this.parseClassName(variantClass);
|
|
2195
|
+
|
|
2196
|
+
// Check if this is an important variant
|
|
2197
|
+
const isImportant = parsed.important;
|
|
2198
|
+
// Ensure css is a string (handle cases where it might be an object)
|
|
2199
|
+
const cssString = typeof css === 'string' ? css : (css && typeof css === 'object' && css.css ? css.css : String(css));
|
|
2200
|
+
const cssContent = isImportant ?
|
|
2201
|
+
(cssString.includes(';') ? cssString.replace(/;/g, ' !important;') : cssString + ' !important') :
|
|
2202
|
+
cssString;
|
|
2203
|
+
|
|
2204
|
+
// Build selector by applying variants
|
|
2205
|
+
let selector = `.${escapeClassName(variantClass)}`;
|
|
2206
|
+
let hasMediaQuery = false;
|
|
2207
|
+
let mediaQueryRule = '';
|
|
2208
|
+
let nestedSelector = null;
|
|
2209
|
+
|
|
2210
|
+
for (const variant of parsed.variants) {
|
|
2211
|
+
if (variant.isArbitrary) {
|
|
2212
|
+
// Handle arbitrary selectors like [&_figure] or [&_fieldset:has(legend):not(.whatever)]
|
|
2213
|
+
// For selectors starting with &, replace & with the base class and use as regular selector
|
|
2214
|
+
let arbitrarySelector = variant.selector;
|
|
2215
|
+
|
|
2216
|
+
if (arbitrarySelector.startsWith('&')) {
|
|
2217
|
+
// Replace & with the base class selector and convert _ to spaces
|
|
2218
|
+
arbitrarySelector = arbitrarySelector.replace(/_/g, ' ').replace(/&/g, selector);
|
|
2219
|
+
selector = arbitrarySelector;
|
|
2220
|
+
} else {
|
|
2221
|
+
// For other arbitrary selectors (like data attributes), use nested CSS
|
|
2222
|
+
arbitrarySelector = arbitrarySelector.replace(/_/g, ' ');
|
|
2223
|
+
selector = { baseClass: selector, arbitrarySelector };
|
|
2224
|
+
}
|
|
2225
|
+
} else if (variant.selector.includes('&')) {
|
|
2226
|
+
// Substitute & with the current selector. This produces flat CSS that
|
|
2227
|
+
// works for both ancestor patterns (`.dark &` → `.dark .X`) and
|
|
2228
|
+
// self-extending patterns (`& > p` → `.X > p`).
|
|
2229
|
+
selector = variant.selector.replace(/&/g, selector);
|
|
2230
|
+
} else if (variant.selector.startsWith(':')) {
|
|
2231
|
+
// For pseudo-classes, append to selector
|
|
2232
|
+
selector = `${selector}${variant.selector}`;
|
|
2233
|
+
} else if (variant.selector.startsWith('@')) {
|
|
2234
|
+
// For media queries, wrap the whole rule
|
|
2235
|
+
hasMediaQuery = true;
|
|
2236
|
+
mediaQueryRule = variant.selector;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// Generate the final rule
|
|
2241
|
+
let rule;
|
|
2242
|
+
// Ensure cssContent is a string before using it anywhere
|
|
2243
|
+
let cssContentStr = typeof cssContent === 'string' ? cssContent : String(cssContent);
|
|
2244
|
+
// Clean up any [object Object] that might have snuck in
|
|
2245
|
+
cssContentStr = cssContentStr.replace(/\[object Object\](;?\s*)/g, '').trim();
|
|
2246
|
+
|
|
2247
|
+
if (typeof selector === 'object' && selector.arbitrarySelector) {
|
|
2248
|
+
// Handle arbitrary selectors with nested CSS (for non-& selectors)
|
|
2249
|
+
rule = `${selector.baseClass} {\n ${selector.arbitrarySelector} {\n ${cssContentStr}\n }\n}`;
|
|
2250
|
+
} else if (nestedSelector) {
|
|
2251
|
+
// Handle nested selectors (variants ending with &)
|
|
2252
|
+
// Check if CSS is a full block (contains nested blocks like @starting-style)
|
|
2253
|
+
const isFullBlock = selectorInfo && selectorInfo.fullBlock !== undefined ? selectorInfo.fullBlock :
|
|
2254
|
+
(cssContentStr.includes('@starting-style') ||
|
|
2255
|
+
cssContentStr.includes('@media') ||
|
|
2256
|
+
cssContentStr.includes('@supports') ||
|
|
2257
|
+
(cssContentStr.includes('{') && cssContentStr.includes('}')));
|
|
2258
|
+
|
|
2259
|
+
// Regular selector or contextual replacement using original selector info
|
|
2260
|
+
if (selectorInfo && selectorInfo.selector) {
|
|
2261
|
+
// If the original selector contains :where(), don't try to modify it
|
|
2262
|
+
if (selectorInfo.selector.includes(':where(')) {
|
|
2263
|
+
// For :where() selectors, generate individual class selector
|
|
2264
|
+
if (isFullBlock) {
|
|
2265
|
+
const resolvedCss = replaceAmpersandInSelectors(cssContentStr, selector);
|
|
2266
|
+
rule = `${selector} {\n ${nestedSelector} {\n${resolvedCss}\n }\n}`;
|
|
2267
|
+
} else {
|
|
2268
|
+
rule = `${selector} {\n ${nestedSelector} {\n ${cssContentStr}\n }\n}`;
|
|
2269
|
+
}
|
|
2270
|
+
} else {
|
|
2271
|
+
// For other contextual selectors, do the replacement
|
|
2272
|
+
const contextualRe = new RegExp(`\\.${baseClass}(?=[^a-zA-Z0-9_-]|$)`);
|
|
2273
|
+
let contextual = selectorInfo.selector.replace(contextualRe, selector);
|
|
2274
|
+
if (contextual === selectorInfo.selector) {
|
|
2275
|
+
// Fallback when base token not directly present
|
|
2276
|
+
contextual = `${selectorInfo.selector}${selector}`;
|
|
2277
|
+
}
|
|
2278
|
+
if (isFullBlock) {
|
|
2279
|
+
const resolvedCss = replaceAmpersandInSelectors(cssContentStr, contextual);
|
|
2280
|
+
rule = `${contextual} {\n ${nestedSelector} {\n${resolvedCss}\n }\n}`;
|
|
2281
|
+
} else {
|
|
2282
|
+
rule = `${contextual} {\n ${nestedSelector} {\n ${cssContentStr}\n }\n}`;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
} else {
|
|
2286
|
+
if (isFullBlock) {
|
|
2287
|
+
const resolvedCss = replaceAmpersandInSelectors(cssContentStr, selector);
|
|
2288
|
+
rule = `${selector} {\n ${nestedSelector} {\n${resolvedCss}\n }\n}`;
|
|
2289
|
+
} else {
|
|
2290
|
+
rule = `${selector} {\n ${nestedSelector} {\n ${cssContentStr}\n }\n}`;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
} else {
|
|
2294
|
+
// Check if CSS is a full block (contains nested blocks like @starting-style)
|
|
2295
|
+
// Use selectorInfo.fullBlock if available, otherwise check CSS content
|
|
2296
|
+
const isFullBlock = selectorInfo && selectorInfo.fullBlock !== undefined ? selectorInfo.fullBlock :
|
|
2297
|
+
(cssContentStr.includes('@starting-style') ||
|
|
2298
|
+
cssContentStr.includes('@media') ||
|
|
2299
|
+
cssContentStr.includes('@supports') ||
|
|
2300
|
+
(cssContentStr.includes('{') && cssContentStr.includes('}')));
|
|
2301
|
+
|
|
2302
|
+
// Regular selector or contextual replacement using original selector info
|
|
2303
|
+
if (selectorInfo && selectorInfo.selector) {
|
|
2304
|
+
// If the original selector contains :where(), don't try to modify it
|
|
2305
|
+
// Instead, generate a separate rule for this specific class
|
|
2306
|
+
// This prevents issues where :where(.row, .col) would become :where(.row, .!col)
|
|
2307
|
+
// with !important applied to all classes
|
|
2308
|
+
if (selectorInfo.selector.includes(':where(')) {
|
|
2309
|
+
// For :where() selectors, generate individual class selector
|
|
2310
|
+
// This ensures !important is only applied to the specific class
|
|
2311
|
+
if (isFullBlock) {
|
|
2312
|
+
// Full block CSS includes nested content with & references
|
|
2313
|
+
// Replace & in selectors with the actual selector (handles all selector contexts)
|
|
2314
|
+
const resolvedCss = replaceAmpersandInSelectors(cssContentStr, selector);
|
|
2315
|
+
rule = `${selector} {\n${resolvedCss}\n}`;
|
|
2316
|
+
} else {
|
|
2317
|
+
rule = `${selector} { ${cssContentStr} }`;
|
|
2318
|
+
}
|
|
2319
|
+
} else {
|
|
2320
|
+
// For other contextual selectors, do the replacement
|
|
2321
|
+
const contextualRe = new RegExp(`\\.${baseClass}(?=[^a-zA-Z0-9_-]|$)`);
|
|
2322
|
+
let contextual = selectorInfo.selector.replace(contextualRe, selector);
|
|
2323
|
+
if (contextual === selectorInfo.selector) {
|
|
2324
|
+
// Fallback when base token not directly present
|
|
2325
|
+
contextual = `${selectorInfo.selector}${selector}`;
|
|
2326
|
+
}
|
|
2327
|
+
if (isFullBlock) {
|
|
2328
|
+
// Replace & in selectors with the contextual selector (handles all selector contexts)
|
|
2329
|
+
const resolvedCss = replaceAmpersandInSelectors(cssContentStr, contextual);
|
|
2330
|
+
rule = `${contextual} {\n${resolvedCss}\n}`;
|
|
2331
|
+
} else {
|
|
2332
|
+
rule = `${contextual} { ${cssContentStr} }`;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
} else {
|
|
2336
|
+
if (isFullBlock) {
|
|
2337
|
+
// Replace & in selectors with the actual selector (handles all selector contexts)
|
|
2338
|
+
const resolvedCss = replaceAmpersandInSelectors(cssContentStr, selector);
|
|
2339
|
+
rule = `${selector} {\n${resolvedCss}\n}`;
|
|
2340
|
+
} else {
|
|
2341
|
+
rule = `${selector} { ${cssContentStr} }`;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
let finalRule;
|
|
2347
|
+
if (hasMediaQuery) {
|
|
2348
|
+
// Wrap once for responsive variants unless the rule already contains @media
|
|
2349
|
+
if (typeof rule === 'string' && rule.trim().startsWith('@media')) {
|
|
2350
|
+
finalRule = rule;
|
|
2351
|
+
} else {
|
|
2352
|
+
finalRule = `${mediaQueryRule} { ${rule} }`;
|
|
2353
|
+
}
|
|
2354
|
+
} else {
|
|
2355
|
+
finalRule = rule;
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
if (!generatedRules.has(finalRule)) {
|
|
2359
|
+
utilities.push(finalRule);
|
|
2360
|
+
generatedRules.add(finalRule);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
|
|
2365
|
+
// Generate utilities for each custom class that's actually used
|
|
2366
|
+
for (const [className, cssOrSelector] of this.customUtilities.entries()) {
|
|
2367
|
+
// Normalize class name: if it starts with !, extract the base name
|
|
2368
|
+
const hasImportantPrefix = className.startsWith('!');
|
|
2369
|
+
const baseClassName = hasImportantPrefix ? className.slice(1) : className;
|
|
2370
|
+
|
|
2371
|
+
// Check if this specific utility class is actually used (including variants and important)
|
|
2372
|
+
const isUsed = usedClasses.some(cls => {
|
|
2373
|
+
// Parse the class to extract the base utility name
|
|
2374
|
+
const parsed = this.parseClassName(cls);
|
|
2375
|
+
const baseClass = parsed.baseClass;
|
|
2376
|
+
|
|
2377
|
+
// Check both normal and important versions
|
|
2378
|
+
return baseClass === className ||
|
|
2379
|
+
baseClass === baseClassName ||
|
|
2380
|
+
baseClass === '!' + baseClassName ||
|
|
2381
|
+
(baseClass.startsWith('!') && baseClass.slice(1) === baseClassName);
|
|
2382
|
+
});
|
|
2383
|
+
|
|
2384
|
+
if (isUsed) {
|
|
2385
|
+
// Normalize CSS: if className has ! prefix, the CSS should already have !important
|
|
2386
|
+
// But we need to pass the base class name to generateUtility
|
|
2387
|
+
let normalizedCss = cssOrSelector;
|
|
2388
|
+
if (typeof cssOrSelector === 'string') {
|
|
2389
|
+
normalizedCss = cleanCssString(cssOrSelector);
|
|
2390
|
+
} else if (Array.isArray(cssOrSelector)) {
|
|
2391
|
+
// For arrays, we'll handle each entry separately below
|
|
2392
|
+
} else if (cssOrSelector && cssOrSelector.css) {
|
|
2393
|
+
// Ensure we extract the CSS string, not an object
|
|
2394
|
+
const extracted = typeof cssOrSelector.css === 'string' ? cssOrSelector.css :
|
|
2395
|
+
(cssOrSelector.css && typeof cssOrSelector.css === 'object' && cssOrSelector.css.css ? cssOrSelector.css.css :
|
|
2396
|
+
String(cssOrSelector.css));
|
|
2397
|
+
normalizedCss = cleanCssString(extracted);
|
|
2398
|
+
} else {
|
|
2399
|
+
// Fallback: convert to string
|
|
2400
|
+
normalizedCss = cleanCssString(String(cssOrSelector));
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// Generate utility with base class name (without !)
|
|
2404
|
+
// The CSS already has !important if className started with !
|
|
2405
|
+
if (typeof cssOrSelector === 'string') {
|
|
2406
|
+
generateUtility(baseClassName, normalizedCss, null);
|
|
2407
|
+
} else if (Array.isArray(cssOrSelector)) {
|
|
2408
|
+
for (const entry of cssOrSelector) {
|
|
2409
|
+
if (entry && entry.css && entry.selector) {
|
|
2410
|
+
// Ensure entry.css is a string (not an object)
|
|
2411
|
+
const extracted = typeof entry.css === 'string' ? entry.css :
|
|
2412
|
+
(entry.css && typeof entry.css === 'object' && entry.css.css ? entry.css.css :
|
|
2413
|
+
String(entry.css));
|
|
2414
|
+
const entryCss = cleanCssString(extracted);
|
|
2415
|
+
|
|
2416
|
+
generateUtility(baseClassName, entryCss, {
|
|
2417
|
+
selector: entry.selector,
|
|
2418
|
+
fullBlock: entry.fullBlock || false
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
} else if (cssOrSelector && cssOrSelector.css && cssOrSelector.selector) {
|
|
2423
|
+
// Ensure cssOrSelector.css is a string (not an object)
|
|
2424
|
+
const extracted = typeof cssOrSelector.css === 'string' ? cssOrSelector.css :
|
|
2425
|
+
(cssOrSelector.css && typeof cssOrSelector.css === 'object' && cssOrSelector.css.css ? cssOrSelector.css.css :
|
|
2426
|
+
String(cssOrSelector.css));
|
|
2427
|
+
const selectorCss = cleanCssString(extracted);
|
|
2428
|
+
|
|
2429
|
+
generateUtility(baseClassName, selectorCss, {
|
|
2430
|
+
selector: cssOrSelector.selector,
|
|
2431
|
+
fullBlock: cssOrSelector.fullBlock || false
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
return utilities.join('\n');
|
|
2438
|
+
} catch (error) {
|
|
2439
|
+
console.error('Error generating custom utilities:', error);
|
|
2440
|
+
return '';
|
|
2441
|
+
}
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
// Helper function to sort utilities so base classes come before variants
|
|
2445
|
+
TailwindCompiler.prototype.sortUtilities = function(utilitiesText) {
|
|
2446
|
+
if (!utilitiesText) return '';
|
|
2447
|
+
|
|
2448
|
+
// Split into individual rules (each rule starts with . or @media)
|
|
2449
|
+
const rules = [];
|
|
2450
|
+
let currentRule = '';
|
|
2451
|
+
const lines = utilitiesText.split('\n');
|
|
2452
|
+
|
|
2453
|
+
for (const line of lines) {
|
|
2454
|
+
// Check if this line starts a new rule
|
|
2455
|
+
if (line.trim().match(/^(\.|@media|@layer|@supports)/)) {
|
|
2456
|
+
// Save previous rule if exists
|
|
2457
|
+
if (currentRule.trim()) {
|
|
2458
|
+
rules.push(currentRule.trim());
|
|
2459
|
+
}
|
|
2460
|
+
currentRule = line;
|
|
2461
|
+
} else {
|
|
2462
|
+
// Continue current rule
|
|
2463
|
+
currentRule += '\n' + line;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
// Add last rule
|
|
2467
|
+
if (currentRule.trim()) {
|
|
2468
|
+
rules.push(currentRule.trim());
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// Sort: base utilities (no @media) come before variants (with @media)
|
|
2472
|
+
rules.sort((a, b) => {
|
|
2473
|
+
const aHasMedia = a.startsWith('@media');
|
|
2474
|
+
const bHasMedia = b.startsWith('@media');
|
|
2475
|
+
|
|
2476
|
+
// Base utilities come before variants
|
|
2477
|
+
if (!aHasMedia && bHasMedia) return -1;
|
|
2478
|
+
if (aHasMedia && !bHasMedia) return 1;
|
|
2479
|
+
|
|
2480
|
+
// Same type, maintain order
|
|
2481
|
+
return 0;
|
|
2482
|
+
});
|
|
2483
|
+
|
|
2484
|
+
return rules.join('\n\n');
|
|
2485
|
+
};
|
|
2486
|
+
|
|
2487
|
+
// Helper function to filter critical utilities that are already in layer utilities
|
|
2488
|
+
TailwindCompiler.prototype.filterCriticalUtilities = function(criticalText, layerUtilities) {
|
|
2489
|
+
if (!criticalText || !layerUtilities) return '';
|
|
2490
|
+
|
|
2491
|
+
// Extract base class names from layer utilities (including variants)
|
|
2492
|
+
const layerClassMatches = layerUtilities.match(/\.([a-zA-Z0-9_:!\\-]+)\s*{/g) || [];
|
|
2493
|
+
const layerClasses = new Set();
|
|
2494
|
+
|
|
2495
|
+
for (const match of layerClassMatches) {
|
|
2496
|
+
// Remove escaping and extract class name
|
|
2497
|
+
const className = match.replace(/^\./, '').replace(/\s*{.*$/, '').replace(/\\/g, '');
|
|
2498
|
+
// Extract base class name (last part after colons for variants)
|
|
2499
|
+
const parts = className.split(':');
|
|
2500
|
+
const baseClass = parts.length > 1 ? parts[parts.length - 1] : className;
|
|
2501
|
+
// Remove ! prefix if present
|
|
2502
|
+
const cleanBase = baseClass.startsWith('!') ? baseClass.slice(1) : baseClass;
|
|
2503
|
+
layerClasses.add(cleanBase);
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// Filter critical CSS to exclude utilities already in layer
|
|
2507
|
+
const criticalLines = criticalText.split('\n');
|
|
2508
|
+
const filteredCritical = [];
|
|
2509
|
+
let inRule = false;
|
|
2510
|
+
let currentRule = '';
|
|
2511
|
+
let ruleClass = '';
|
|
2512
|
+
|
|
2513
|
+
for (const line of criticalLines) {
|
|
2514
|
+
// Check if line starts a new rule
|
|
2515
|
+
const ruleMatch = line.match(/^\.([a-zA-Z0-9_:!\\-]+)\s*{/);
|
|
2516
|
+
if (ruleMatch) {
|
|
2517
|
+
// Save previous rule if it wasn't filtered
|
|
2518
|
+
if (currentRule) {
|
|
2519
|
+
const prevBaseClass = ruleClass.replace(/\\/g, '').split(':').pop().replace(/^!/, '');
|
|
2520
|
+
if (!layerClasses.has(prevBaseClass)) {
|
|
2521
|
+
filteredCritical.push(currentRule);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
// Start new rule
|
|
2525
|
+
ruleClass = ruleMatch[1];
|
|
2526
|
+
currentRule = line;
|
|
2527
|
+
inRule = true;
|
|
2528
|
+
} else if (inRule) {
|
|
2529
|
+
currentRule += '\n' + line;
|
|
2530
|
+
if (line.trim() === '}') {
|
|
2531
|
+
// End of rule - check if we should keep it
|
|
2532
|
+
const baseClass = ruleClass.replace(/\\/g, '').split(':').pop().replace(/^!/, '');
|
|
2533
|
+
if (!layerClasses.has(baseClass)) {
|
|
2534
|
+
filteredCritical.push(currentRule);
|
|
2535
|
+
}
|
|
2536
|
+
currentRule = '';
|
|
2537
|
+
ruleClass = '';
|
|
2538
|
+
inRule = false;
|
|
2539
|
+
}
|
|
2540
|
+
} else {
|
|
2541
|
+
// Not in a rule, keep as-is (comments, etc.)
|
|
2542
|
+
filteredCritical.push(line);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// Add any remaining rule
|
|
2547
|
+
if (currentRule) {
|
|
2548
|
+
const baseClass = ruleClass.replace(/\\/g, '').split(':').pop().replace(/^!/, '');
|
|
2549
|
+
if (!layerClasses.has(baseClass)) {
|
|
2550
|
+
filteredCritical.push(currentRule);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
return filteredCritical.join('\n').trim();
|
|
2555
|
+
};
|
|
2556
|
+
|
|
2557
|
+
// Main compilation method
|
|
2558
|
+
TailwindCompiler.prototype.compile = async function () {
|
|
2559
|
+
if (this.usesStaticPrerenderUtilities) {
|
|
2560
|
+
// Static utilities shipped with the page — nothing to compile.
|
|
2561
|
+
if (!window.__manifestUtilitiesReady) {
|
|
2562
|
+
window.__manifestUtilitiesReady = true;
|
|
2563
|
+
window.dispatchEvent(new CustomEvent('manifest:utilities-ready'));
|
|
2564
|
+
}
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
const compileStart = performance.now();
|
|
2569
|
+
|
|
2570
|
+
try {
|
|
2571
|
+
// Throttled or busy: don't DROP the request — queue exactly one retry
|
|
2572
|
+
// so late-discovered classes (md: variants on swapped-in components)
|
|
2573
|
+
// always get compiled. The pending counter lets manifest:ready and the
|
|
2574
|
+
// prerenderer's settle hold until utilities are actually current.
|
|
2575
|
+
const now = Date.now();
|
|
2576
|
+
if (now - this.lastCompileTime < this.minCompileInterval || this.isCompiling) {
|
|
2577
|
+
if (!this._retryQueued) {
|
|
2578
|
+
this._retryQueued = true;
|
|
2579
|
+
window.__manifestUtilitiesPending = (window.__manifestUtilitiesPending || 0) + 1;
|
|
2580
|
+
setTimeout(() => {
|
|
2581
|
+
this._retryQueued = false;
|
|
2582
|
+
window.__manifestUtilitiesPending = Math.max(0, (window.__manifestUtilitiesPending || 1) - 1);
|
|
2583
|
+
this.compile();
|
|
2584
|
+
}, this.minCompileInterval + 50);
|
|
2585
|
+
}
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
this.lastCompileTime = now;
|
|
2589
|
+
this.isCompiling = true;
|
|
2590
|
+
window.__manifestUtilitiesPending = (window.__manifestUtilitiesPending || 0) + 1;
|
|
2591
|
+
this._compileCounted = true;
|
|
2592
|
+
|
|
2593
|
+
// On first run, scan static classes and CSS variables
|
|
2594
|
+
if (!this.hasScannedStatic) {
|
|
2595
|
+
await this.scanStaticClasses();
|
|
2596
|
+
|
|
2597
|
+
// Fetch CSS content once for initial compilation
|
|
2598
|
+
const themeCss = await this.fetchThemeContent();
|
|
2599
|
+
if (themeCss) {
|
|
2600
|
+
// Extract custom utilities. Framework CSS is scanned too so the
|
|
2601
|
+
// generator can emit variants of semantic classes (md:row,
|
|
2602
|
+
// hover:brand); base forms are suppressed in generateCustomUtilities.
|
|
2603
|
+
const discoveredCustomUtilities = this.extractCustomUtilities(themeCss);
|
|
2604
|
+
for (const [name, value] of discoveredCustomUtilities.entries()) {
|
|
2605
|
+
this.customUtilities.set(name, value);
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
const variables = this.extractThemeVariables(themeCss);
|
|
2609
|
+
for (const [name, value] of variables.entries()) {
|
|
2610
|
+
this.currentThemeVars.set(name, value);
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// Generate utilities for all static classes (minus anything the
|
|
2614
|
+
// static utilities sheet already covers).
|
|
2615
|
+
const staticUsedData = {
|
|
2616
|
+
classes: this.filterStaticallyCoveredClasses(Array.from(this.staticClassCache)),
|
|
2617
|
+
variableSuffixes: []
|
|
2618
|
+
};
|
|
2619
|
+
// Process static classes for variable suffixes
|
|
2620
|
+
for (const cls of this.staticClassCache) {
|
|
2621
|
+
const parts = cls.split(':');
|
|
2622
|
+
const baseClass = parts[parts.length - 1];
|
|
2623
|
+
const classParts = baseClass.split('-');
|
|
2624
|
+
if (classParts.length > 1) {
|
|
2625
|
+
staticUsedData.variableSuffixes.push(classParts.slice(1).join('-'));
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
// Generate both variable-based and custom utilities
|
|
2630
|
+
const varUtilities = this.generateUtilitiesFromVars(themeCss, staticUsedData);
|
|
2631
|
+
const customUtilitiesGenerated = this.generateCustomUtilities(staticUsedData);
|
|
2632
|
+
|
|
2633
|
+
let allUtilities = [varUtilities, customUtilitiesGenerated].filter(Boolean).join('\n\n');
|
|
2634
|
+
// Sort utilities so base classes come before variants
|
|
2635
|
+
allUtilities = this.sortUtilities(allUtilities);
|
|
2636
|
+
|
|
2637
|
+
if (allUtilities) {
|
|
2638
|
+
const finalCss = `@layer utilities {\n${allUtilities}\n}`;
|
|
2639
|
+
|
|
2640
|
+
this.styleElement.textContent = finalCss;
|
|
2641
|
+
this.ensureUtilityStylesLast();
|
|
2642
|
+
this.scheduleEnsureUtilityStylesLast();
|
|
2643
|
+
|
|
2644
|
+
// Remove critical style element entirely after compilation
|
|
2645
|
+
// Use requestAnimationFrame to ensure styles are painted before removing
|
|
2646
|
+
requestAnimationFrame(() => {
|
|
2647
|
+
requestAnimationFrame(() => {
|
|
2648
|
+
// Double RAF ensures paint has occurred
|
|
2649
|
+
if (this.criticalStyleElement && this.criticalStyleElement.parentNode) {
|
|
2650
|
+
this.criticalStyleElement.parentNode.removeChild(this.criticalStyleElement);
|
|
2651
|
+
this.criticalStyleElement = null;
|
|
2652
|
+
}
|
|
2653
|
+
this.ensureUtilityStylesLast();
|
|
2654
|
+
});
|
|
2655
|
+
});
|
|
2656
|
+
this.lastClassesHash = staticUsedData.classes.sort().join(',');
|
|
2657
|
+
|
|
2658
|
+
// Save to cache for next page load
|
|
2659
|
+
const themeHash = this.generateThemeHash(themeCss);
|
|
2660
|
+
const cacheKey = `${this.lastClassesHash}-${themeHash}`;
|
|
2661
|
+
this.cache.set(cacheKey, {
|
|
2662
|
+
css: finalCss,
|
|
2663
|
+
timestamp: Date.now(),
|
|
2664
|
+
themeHash: themeHash
|
|
2665
|
+
});
|
|
2666
|
+
this.savePersistentCache();
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
this.hasInitialized = true;
|
|
2671
|
+
this.isCompiling = false;
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// For subsequent compilations, check for new dynamic classes
|
|
2676
|
+
const usedData = this.getUsedClasses();
|
|
2677
|
+
const dynamicClasses = Array.from(this.dynamicClassCache);
|
|
2678
|
+
|
|
2679
|
+
// Create a hash of current dynamic classes to detect changes
|
|
2680
|
+
const dynamicClassesHash = dynamicClasses.sort().join(',');
|
|
2681
|
+
|
|
2682
|
+
// Check if dynamic classes have actually changed
|
|
2683
|
+
if (dynamicClassesHash !== this.lastClassesHash || !this.hasInitialized) {
|
|
2684
|
+
// Fetch CSS content for dynamic compilation
|
|
2685
|
+
const themeCss = await this.fetchThemeContent();
|
|
2686
|
+
if (!themeCss) {
|
|
2687
|
+
this.isCompiling = false;
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
// Update custom utilities cache if needed
|
|
2692
|
+
const discoveredCustomUtilities = this.extractCustomUtilities(themeCss);
|
|
2693
|
+
for (const [name, value] of discoveredCustomUtilities.entries()) {
|
|
2694
|
+
this.customUtilities.set(name, value);
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
// Check for variable changes
|
|
2698
|
+
const variables = this.extractThemeVariables(themeCss);
|
|
2699
|
+
let hasVariableChanges = false;
|
|
2700
|
+
for (const [name, value] of variables.entries()) {
|
|
2701
|
+
const currentValue = this.currentThemeVars.get(name);
|
|
2702
|
+
if (currentValue !== value) {
|
|
2703
|
+
hasVariableChanges = true;
|
|
2704
|
+
this.currentThemeVars.set(name, value);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// Generate utilities for all classes (static + dynamic) if needed
|
|
2709
|
+
if (hasVariableChanges || dynamicClassesHash !== this.lastClassesHash) {
|
|
2710
|
+
|
|
2711
|
+
// Generate both variable-based and custom utilities
|
|
2712
|
+
const varUtilities = this.generateUtilitiesFromVars(themeCss, usedData);
|
|
2713
|
+
const customUtilitiesGenerated = this.generateCustomUtilities(usedData);
|
|
2714
|
+
|
|
2715
|
+
let allUtilities = [varUtilities, customUtilitiesGenerated].filter(Boolean).join('\n\n');
|
|
2716
|
+
// Sort utilities so base classes come before variants
|
|
2717
|
+
allUtilities = this.sortUtilities(allUtilities);
|
|
2718
|
+
|
|
2719
|
+
if (allUtilities) {
|
|
2720
|
+
const finalCss = `@layer utilities {\n${allUtilities}\n}`;
|
|
2721
|
+
|
|
2722
|
+
this.styleElement.textContent = finalCss;
|
|
2723
|
+
this.ensureUtilityStylesLast();
|
|
2724
|
+
this.scheduleEnsureUtilityStylesLast();
|
|
2725
|
+
|
|
2726
|
+
// Remove critical style element entirely after compilation
|
|
2727
|
+
// Use requestAnimationFrame to ensure styles are painted before removing
|
|
2728
|
+
requestAnimationFrame(() => {
|
|
2729
|
+
requestAnimationFrame(() => {
|
|
2730
|
+
// Double RAF ensures paint has occurred
|
|
2731
|
+
if (this.criticalStyleElement && this.criticalStyleElement.parentNode) {
|
|
2732
|
+
this.criticalStyleElement.parentNode.removeChild(this.criticalStyleElement);
|
|
2733
|
+
this.criticalStyleElement = null;
|
|
2734
|
+
}
|
|
2735
|
+
this.ensureUtilityStylesLast();
|
|
2736
|
+
});
|
|
2737
|
+
});
|
|
2738
|
+
this.lastClassesHash = dynamicClassesHash;
|
|
2739
|
+
|
|
2740
|
+
// Save to cache for next page load
|
|
2741
|
+
const themeHash = this.generateThemeHash(themeCss);
|
|
2742
|
+
const cacheKey = `${this.lastClassesHash}-${themeHash}`;
|
|
2743
|
+
this.cache.set(cacheKey, {
|
|
2744
|
+
css: finalCss,
|
|
2745
|
+
timestamp: Date.now(),
|
|
2746
|
+
themeHash: themeHash
|
|
2747
|
+
});
|
|
2748
|
+
this.savePersistentCache();
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
} catch (error) {
|
|
2754
|
+
console.error('[Manifest Utilities] Error compiling Tailwind CSS:', error);
|
|
2755
|
+
} finally {
|
|
2756
|
+
this.isCompiling = false;
|
|
2757
|
+
if (this._compileCounted) {
|
|
2758
|
+
this._compileCounted = false;
|
|
2759
|
+
window.__manifestUtilitiesPending = Math.max(0, (window.__manifestUtilitiesPending || 1) - 1);
|
|
2760
|
+
if (!window.__manifestUtilitiesPending) {
|
|
2761
|
+
window.dispatchEvent(new CustomEvent('manifest:utilities-idle'));
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
// First-compile settle signal for the manifest:ready coordinator.
|
|
2765
|
+
if (!window.__manifestUtilitiesReady) {
|
|
2766
|
+
window.__manifestUtilitiesReady = true;
|
|
2767
|
+
window.dispatchEvent(new CustomEvent('manifest:utilities-ready'));
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
};
|
|
2771
|
+
|
|
2772
|
+
|
|
2773
|
+
|
|
2774
|
+
// Node/Workers wrapper — combined at build time (see build.mjs
|
|
2775
|
+
// buildUtilitiesNodeModule) with the DOM-free subset of the browser plugin's
|
|
2776
|
+
// subscripts (generators, variants, main, helpers, compile) so this exports
|
|
2777
|
+
// the exact same generation logic the browser JIT runs, with no document.
|
|
2778
|
+
|
|
2779
|
+
/** A bare TailwindCompiler instance carrying only what the pure generation
|
|
2780
|
+
* methods (generateUtilitiesFromVars, generateCustomUtilities, parseClassName,
|
|
2781
|
+
* ...) read from `this` — mirrors the constructor's generator/variant setup
|
|
2782
|
+
* (manifest.utilities.main.js) without any of its DOM work, which this never
|
|
2783
|
+
* runs: `new TailwindCompiler()` is never called here. */
|
|
2784
|
+
function createNodeCompiler() {
|
|
2785
|
+
const instance = Object.create(TailwindCompiler.prototype);
|
|
2786
|
+
instance.options = { rootSelector: ':root', themeSelector: '@theme' };
|
|
2787
|
+
instance.regexPatterns = {
|
|
2788
|
+
root: new RegExp(`${instance.options.rootSelector}\\s*{([^}]*)}`, 'g'),
|
|
2789
|
+
theme: new RegExp(`${instance.options.themeSelector}\\s*{([^}]*)}`, 'g'),
|
|
2790
|
+
variable: /--([\w-]+):\s*([^;]+);/g,
|
|
2791
|
+
tailwindPrefix: /^(color|font|text|font-weight|tracking|leading|breakpoint|container|spacing|radius|shadow|inset-shadow|drop-shadow|blur|perspective|aspect|ease|animate|border-width|border-style|outline|outline-width|outline-style|ring|ring-offset|divide|accent|caret|decoration|placeholder|selection|scrollbar)-/
|
|
2792
|
+
};
|
|
2793
|
+
instance.utilityGenerators = createUtilityGenerators();
|
|
2794
|
+
instance.variants = createVariants();
|
|
2795
|
+
instance.variantGroups = createVariantGroups();
|
|
2796
|
+
instance.classCache = new Map();
|
|
2797
|
+
instance.customUtilities = new Map();
|
|
2798
|
+
return instance;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
/**
|
|
2802
|
+
* Compile the utility CSS the browser plugin would produce for exactly
|
|
2803
|
+
* `classes` — variants, theme-variable-driven utilities (generateUtilitiesFromVars)
|
|
2804
|
+
* and custom utilities (generateCustomUtilities) discovered in `themeCss`/`baseCss`.
|
|
2805
|
+
* Deterministic for a given (classes, themeCss, baseCss) triple.
|
|
2806
|
+
*
|
|
2807
|
+
* @param {{classes?: string[], themeCss?: string, baseCss?: string}} options
|
|
2808
|
+
* @returns {Promise<string>}
|
|
2809
|
+
*/
|
|
2810
|
+
export async function compileUtilities({ classes = [], themeCss = '', baseCss = '' } = {}) {
|
|
2811
|
+
const compiler = createNodeCompiler();
|
|
2812
|
+
const cssText = [baseCss, themeCss].filter(Boolean).join('\n');
|
|
2813
|
+
|
|
2814
|
+
const discovered = compiler.extractCustomUtilities(cssText);
|
|
2815
|
+
for (const [name, value] of discovered) compiler.customUtilities.set(name, value);
|
|
2816
|
+
|
|
2817
|
+
const usedData = { classes: Array.from(new Set(classes)), variableSuffixes: [] };
|
|
2818
|
+
|
|
2819
|
+
const varUtilities = compiler.generateUtilitiesFromVars(cssText, usedData);
|
|
2820
|
+
const customUtilities = compiler.generateCustomUtilities(usedData);
|
|
2821
|
+
|
|
2822
|
+
let allUtilities = [varUtilities, customUtilities].filter(Boolean).join('\n\n');
|
|
2823
|
+
allUtilities = compiler.sortUtilities(allUtilities);
|
|
2824
|
+
|
|
2825
|
+
return allUtilities ? `@layer utilities {\n${allUtilities}\n}` : '';
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
// `class:token=` (per-class conditional binding) and the `:class="..."`
|
|
2829
|
+
// shorthand aren't matched by extractClassesFromHTML's `x-(data|bind:class|class)=`
|
|
2830
|
+
// regex (no `x-` prefix), so pick those up here too — cheap regex pass, not a
|
|
2831
|
+
// JS/expression parser, so only literal-looking quoted tokens are kept.
|
|
2832
|
+
const CLASS_TOKEN_BINDING_RE = /\bclass:([a-zA-Z0-9_-]+(?:\\.[a-zA-Z0-9_-]+)*)\s*=/g;
|
|
2833
|
+
// Backreference to the opening quote so a nested quote char (`:class="'a b'"`)
|
|
2834
|
+
// doesn't truncate the match early.
|
|
2835
|
+
const SHORTHAND_CLASS_BINDING_RE = /\B:class=(["'])((?:(?!\1)[\s\S])*)\1/g;
|
|
2836
|
+
|
|
2837
|
+
/**
|
|
2838
|
+
* Scan HTML for utility class tokens using the same extraction the browser
|
|
2839
|
+
* plugin uses (extractClassesFromHTML), plus `class:`/`:class` static tokens.
|
|
2840
|
+
* Skips `x-`/`$` tokens. Returns a sorted, deduplicated array.
|
|
2841
|
+
*
|
|
2842
|
+
* @param {string} html
|
|
2843
|
+
* @returns {string[]}
|
|
2844
|
+
*/
|
|
2845
|
+
export function scanClasses(html) {
|
|
2846
|
+
const compiler = createNodeCompiler();
|
|
2847
|
+
const set = new Set();
|
|
2848
|
+
compiler.extractClassesFromHTML(html, set);
|
|
2849
|
+
|
|
2850
|
+
let m;
|
|
2851
|
+
CLASS_TOKEN_BINDING_RE.lastIndex = 0;
|
|
2852
|
+
while ((m = CLASS_TOKEN_BINDING_RE.exec(html)) !== null) {
|
|
2853
|
+
const cls = m[1].replace(/\\(.)/g, '$1');
|
|
2854
|
+
if (cls && !cls.startsWith('x-') && !cls.startsWith('$')) set.add(cls);
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
SHORTHAND_CLASS_BINDING_RE.lastIndex = 0;
|
|
2858
|
+
while ((m = SHORTHAND_CLASS_BINDING_RE.exec(html)) !== null) {
|
|
2859
|
+
const content = m[2];
|
|
2860
|
+
const quoted = content.match(/['"`]([^'"`\s]+)['"`]/g);
|
|
2861
|
+
if (quoted) {
|
|
2862
|
+
for (const q of quoted) {
|
|
2863
|
+
const cls = q.replace(/['"`]/g, '');
|
|
2864
|
+
if (cls && !cls.startsWith('$') && !cls.startsWith('x-') && !cls.includes('(')) set.add(cls);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
return Array.from(set).sort();
|
|
2870
|
+
}
|