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