@productivemark/snipcss 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.claude-plugin/plugin.json +10 -0
  3. package/.mcp.json +8 -0
  4. package/dist/auth/config-manager.d.ts +13 -0
  5. package/dist/auth/config-manager.d.ts.map +1 -0
  6. package/dist/auth/config-manager.js +48 -0
  7. package/dist/auth/config-manager.js.map +1 -0
  8. package/dist/auth/usage-gate.d.ts +13 -0
  9. package/dist/auth/usage-gate.d.ts.map +1 -0
  10. package/dist/auth/usage-gate.js +69 -0
  11. package/dist/auth/usage-gate.js.map +1 -0
  12. package/dist/browser/browser-manager.d.ts +15 -0
  13. package/dist/browser/browser-manager.d.ts.map +1 -0
  14. package/dist/browser/browser-manager.js +61 -0
  15. package/dist/browser/browser-manager.js.map +1 -0
  16. package/dist/browser/viewport-manager.d.ts +8 -0
  17. package/dist/browser/viewport-manager.d.ts.map +1 -0
  18. package/dist/browser/viewport-manager.js +50 -0
  19. package/dist/browser/viewport-manager.js.map +1 -0
  20. package/dist/extraction/css-variable-resolver.d.ts +27 -0
  21. package/dist/extraction/css-variable-resolver.d.ts.map +1 -0
  22. package/dist/extraction/css-variable-resolver.js +105 -0
  23. package/dist/extraction/css-variable-resolver.js.map +1 -0
  24. package/dist/extraction/dom-labeler.d.ts +26 -0
  25. package/dist/extraction/dom-labeler.d.ts.map +1 -0
  26. package/dist/extraction/dom-labeler.js +124 -0
  27. package/dist/extraction/dom-labeler.js.map +1 -0
  28. package/dist/extraction/element-discovery.d.ts +59 -0
  29. package/dist/extraction/element-discovery.d.ts.map +1 -0
  30. package/dist/extraction/element-discovery.js +525 -0
  31. package/dist/extraction/element-discovery.js.map +1 -0
  32. package/dist/extraction/extraction-pipeline.d.ts +26 -0
  33. package/dist/extraction/extraction-pipeline.d.ts.map +1 -0
  34. package/dist/extraction/extraction-pipeline.js +200 -0
  35. package/dist/extraction/extraction-pipeline.js.map +1 -0
  36. package/dist/extraction/font-collector.d.ts +26 -0
  37. package/dist/extraction/font-collector.d.ts.map +1 -0
  38. package/dist/extraction/font-collector.js +160 -0
  39. package/dist/extraction/font-collector.js.map +1 -0
  40. package/dist/extraction/html-cleaner.d.ts +16 -0
  41. package/dist/extraction/html-cleaner.d.ts.map +1 -0
  42. package/dist/extraction/html-cleaner.js +149 -0
  43. package/dist/extraction/html-cleaner.js.map +1 -0
  44. package/dist/extraction/keyframe-collector.d.ts +16 -0
  45. package/dist/extraction/keyframe-collector.d.ts.map +1 -0
  46. package/dist/extraction/keyframe-collector.js +62 -0
  47. package/dist/extraction/keyframe-collector.js.map +1 -0
  48. package/dist/extraction/pseudo-state-handler.d.ts +36 -0
  49. package/dist/extraction/pseudo-state-handler.d.ts.map +1 -0
  50. package/dist/extraction/pseudo-state-handler.js +210 -0
  51. package/dist/extraction/pseudo-state-handler.js.map +1 -0
  52. package/dist/extraction/result-builder.d.ts +25 -0
  53. package/dist/extraction/result-builder.d.ts.map +1 -0
  54. package/dist/extraction/result-builder.js +136 -0
  55. package/dist/extraction/result-builder.js.map +1 -0
  56. package/dist/extraction/rule-deduplicator.d.ts +39 -0
  57. package/dist/extraction/rule-deduplicator.d.ts.map +1 -0
  58. package/dist/extraction/rule-deduplicator.js +107 -0
  59. package/dist/extraction/rule-deduplicator.js.map +1 -0
  60. package/dist/extraction/selector-fixer.d.ts +25 -0
  61. package/dist/extraction/selector-fixer.d.ts.map +1 -0
  62. package/dist/extraction/selector-fixer.js +111 -0
  63. package/dist/extraction/selector-fixer.js.map +1 -0
  64. package/dist/extraction/specificity.d.ts +17 -0
  65. package/dist/extraction/specificity.d.ts.map +1 -0
  66. package/dist/extraction/specificity.js +88 -0
  67. package/dist/extraction/specificity.js.map +1 -0
  68. package/dist/extraction/style-matcher.d.ts +33 -0
  69. package/dist/extraction/style-matcher.d.ts.map +1 -0
  70. package/dist/extraction/style-matcher.js +199 -0
  71. package/dist/extraction/style-matcher.js.map +1 -0
  72. package/dist/extraction/stylesheet-collector.d.ts +33 -0
  73. package/dist/extraction/stylesheet-collector.d.ts.map +1 -0
  74. package/dist/extraction/stylesheet-collector.js +71 -0
  75. package/dist/extraction/stylesheet-collector.js.map +1 -0
  76. package/dist/index.d.ts +3 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +235 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/mcp-server.d.ts +3 -0
  81. package/dist/mcp-server.d.ts.map +1 -0
  82. package/dist/mcp-server.js +349 -0
  83. package/dist/mcp-server.js.map +1 -0
  84. package/dist/tailwind/css-to-tailwind.d.ts +17 -0
  85. package/dist/tailwind/css-to-tailwind.d.ts.map +1 -0
  86. package/dist/tailwind/css-to-tailwind.js +1583 -0
  87. package/dist/tailwind/css-to-tailwind.js.map +1 -0
  88. package/dist/tailwind/shorthand-expander.d.ts +27 -0
  89. package/dist/tailwind/shorthand-expander.d.ts.map +1 -0
  90. package/dist/tailwind/shorthand-expander.js +812 -0
  91. package/dist/tailwind/shorthand-expander.js.map +1 -0
  92. package/dist/tailwind/tailwind-converter.d.ts +35 -0
  93. package/dist/tailwind/tailwind-converter.d.ts.map +1 -0
  94. package/dist/tailwind/tailwind-converter.js +1223 -0
  95. package/dist/tailwind/tailwind-converter.js.map +1 -0
  96. package/dist/tailwind/tailwind-helpers.d.ts +95 -0
  97. package/dist/tailwind/tailwind-helpers.d.ts.map +1 -0
  98. package/dist/tailwind/tailwind-helpers.js +593 -0
  99. package/dist/tailwind/tailwind-helpers.js.map +1 -0
  100. package/dist/tailwind/tailwind-reducer.d.ts +36 -0
  101. package/dist/tailwind/tailwind-reducer.d.ts.map +1 -0
  102. package/dist/tailwind/tailwind-reducer.js +189 -0
  103. package/dist/tailwind/tailwind-reducer.js.map +1 -0
  104. package/dist/types/index.d.ts +239 -0
  105. package/dist/types/index.d.ts.map +1 -0
  106. package/dist/types/index.js +94 -0
  107. package/dist/types/index.js.map +1 -0
  108. package/dist/utils/helpers.d.ts +34 -0
  109. package/dist/utils/helpers.d.ts.map +1 -0
  110. package/dist/utils/helpers.js +120 -0
  111. package/dist/utils/helpers.js.map +1 -0
  112. package/dist/utils/parsel.d.ts +41 -0
  113. package/dist/utils/parsel.d.ts.map +1 -0
  114. package/dist/utils/parsel.js +314 -0
  115. package/dist/utils/parsel.js.map +1 -0
  116. package/package.json +41 -0
  117. package/skills/workflow/SKILL.md +95 -0
@@ -0,0 +1,1223 @@
1
+ /**
2
+ * Tailwind conversion orchestrator.
3
+ * Port of tailwind_main.js (1,681 lines)
4
+ *
5
+ * Main entry points:
6
+ * - getTailwindHtml() converts labelled HTML + CSS into Tailwind-classed HTML
7
+ * - getTailwindBodyClasses() extracts body-level Tailwind classes from snipped rules
8
+ */
9
+ import * as cheerio from 'cheerio';
10
+ import { cssToTailwind, getTransformClasses, getFilterClasses, getFontClasses, getBestTailwindClasses, } from './css-to-tailwind.js';
11
+ import { getPropertyType, expandShorthandProperty, } from './shorthand-expander.js';
12
+ import { mergeRanges, getMediaPrefix, parseMediaQueryToTailwind, resolveCssVariableValue, getPseudos, parseStyleAttribute, } from './tailwind-helpers.js';
13
+ import { reduceTailwindClasses, updatePropSpecifityWithMergedProperties, } from './tailwind-reducer.js';
14
+ // ============================================================
15
+ // Cheerio options (matches the original extension's CHEERIO_OPTIONS)
16
+ // ============================================================
17
+ /**
18
+ * Cheerio load options.
19
+ * The original extension used htmlparser2-specific keys (decodeEntities,
20
+ * normalizeWhitespace, recognizeSelfClosing, _useHtmlParser2).
21
+ * In cheerio 1.x these are passed through the `xml` key.
22
+ */
23
+ const CHEERIO_OPTIONS = {
24
+ xml: {
25
+ xmlMode: false,
26
+ decodeEntities: false,
27
+ normalizeWhitespace: false,
28
+ recognizeSelfClosing: true,
29
+ },
30
+ };
31
+ // ============================================================
32
+ // Icon Font Class Preservation for Tailwind Conversion
33
+ // ============================================================
34
+ /** Known icon font class name patterns (fast regex check) */
35
+ const ICON_CLASS_PATTERNS = [
36
+ /^fa[srldb]?$/, // Font Awesome base classes (fa, fas, far, fal, fad, fab)
37
+ /^fa-/, // Font Awesome icons (fa-chevron-right, etc.)
38
+ /^ti$/, // Tabler Icons base
39
+ /^ti-/, // Tabler Icons
40
+ /^bi$/, // Bootstrap Icons base
41
+ /^bi-/, // Bootstrap Icons
42
+ /^material-icons/, // Material Icons
43
+ /^glyphicon/, // Glyphicons
44
+ /^icon-/, // Generic icon prefix
45
+ /^icofont-/, // IcoFont
46
+ /^ri-/, // Remix Icons
47
+ /^bx-?/, // BoxIcons
48
+ /^la-?/, // Line Awesome
49
+ ];
50
+ /** Known icon font-family names (for CSS-based detection fallback) */
51
+ const ICON_FONT_FAMILIES = [
52
+ 'font awesome',
53
+ 'fontawesome',
54
+ 'tabler',
55
+ 'bootstrap-icons',
56
+ 'material icons',
57
+ 'glyphicons',
58
+ 'icomoon',
59
+ 'icofont',
60
+ 'remixicon',
61
+ 'boxicons',
62
+ 'line awesome',
63
+ ];
64
+ // ============================================================
65
+ // Tailwind spacing scale for arbitrary-value replacement
66
+ // ============================================================
67
+ const TAILWIND_SPACING_SCALE = {
68
+ 0: '0',
69
+ 2: '0.5',
70
+ 4: '1',
71
+ 6: '1.5',
72
+ 8: '2',
73
+ 10: '2.5',
74
+ 12: '3',
75
+ 14: '3.5',
76
+ 16: '4',
77
+ 20: '5',
78
+ 24: '6',
79
+ 28: '7',
80
+ 32: '8',
81
+ 36: '9',
82
+ 40: '10',
83
+ 44: '11',
84
+ 48: '12',
85
+ 56: '14',
86
+ 64: '16',
87
+ 80: '20',
88
+ 96: '24',
89
+ 112: '28',
90
+ 128: '32',
91
+ 144: '36',
92
+ 160: '40',
93
+ 176: '44',
94
+ 192: '48',
95
+ 208: '52',
96
+ 224: '56',
97
+ 240: '60',
98
+ 256: '64',
99
+ 288: '72',
100
+ 320: '80',
101
+ 384: '96',
102
+ };
103
+ const SPACING_PIXEL_VALUES = Object.keys(TAILWIND_SPACING_SCALE).map(Number);
104
+ // ============================================================
105
+ // Utility helpers
106
+ // ============================================================
107
+ /** Check if a class name matches known icon font patterns */
108
+ function isIconClassByName(className) {
109
+ return ICON_CLASS_PATTERNS.some(pattern => pattern.test(className));
110
+ }
111
+ /** Check if any CSS rule for this class sets an icon font-family */
112
+ function isIconClassByCSS(className, tSnippedArr) {
113
+ if (!tSnippedArr)
114
+ return false;
115
+ for (const rule of tSnippedArr) {
116
+ if (!rule.selector || !rule.selector.includes('.' + className))
117
+ continue;
118
+ if (!rule.body)
119
+ continue;
120
+ const fontMatch = rule.body.match(/font-family\s*:\s*([^;]+)/i);
121
+ if (fontMatch) {
122
+ const fontValue = fontMatch[1].toLowerCase();
123
+ if (ICON_FONT_FAMILIES.some(font => fontValue.includes(font))) {
124
+ return true;
125
+ }
126
+ }
127
+ }
128
+ return false;
129
+ }
130
+ /** Get all icon classes from an element's class attribute */
131
+ function getIconClasses(classAttr, tSnippedArr) {
132
+ if (!classAttr)
133
+ return [];
134
+ const classes = classAttr.split(/\s+/).filter(c => c.trim());
135
+ return classes.filter(cls => !cls.startsWith('snipcss') &&
136
+ (isIconClassByName(cls) || isIconClassByCSS(cls, tSnippedArr)));
137
+ }
138
+ /**
139
+ * Split a string by commas, respecting balanced parentheses.
140
+ * Port of splitNoParen() from snipbackground.js.
141
+ */
142
+ function splitNoParen(s) {
143
+ const results = [];
144
+ let str = '';
145
+ let left = 0;
146
+ let right = 0;
147
+ for (let i = 0; i < s.length; i++) {
148
+ switch (s[i]) {
149
+ case ',':
150
+ if (left === right) {
151
+ results.push(str);
152
+ str = '';
153
+ left = right = 0;
154
+ }
155
+ else {
156
+ str += s[i];
157
+ }
158
+ break;
159
+ case '(':
160
+ left++;
161
+ str += s[i];
162
+ break;
163
+ case ')':
164
+ right++;
165
+ str += s[i];
166
+ break;
167
+ default:
168
+ str += s[i];
169
+ }
170
+ }
171
+ results.push(str);
172
+ return results;
173
+ }
174
+ /**
175
+ * Get override classes for special shorthand properties
176
+ * (margin, padding, transform, filter, backdrop-filter, font).
177
+ */
178
+ function getOverrideClasses(oProp, oVal) {
179
+ let overrideClasses = null;
180
+ const hasImportant = oVal.includes('!important');
181
+ const cleanValue = oVal.replace(/\s*!important\s*/g, '').trim();
182
+ if (oProp === 'margin' || oProp === 'padding') {
183
+ const expanded = expandShorthandProperty(oProp, cleanValue);
184
+ if (expanded) {
185
+ const iOverwritten = expanded['overwritten_properties'];
186
+ overrideClasses = getBestTailwindClasses(oProp, iOverwritten);
187
+ }
188
+ }
189
+ else if (oProp === 'transform') {
190
+ overrideClasses = getTransformClasses(cleanValue);
191
+ }
192
+ else if (oProp === 'filter') {
193
+ overrideClasses = getFilterClasses(cleanValue);
194
+ }
195
+ else if (oProp === 'backdrop-filter') {
196
+ overrideClasses = getFilterClasses(cleanValue, true);
197
+ }
198
+ else if (oProp === 'font') {
199
+ overrideClasses = getFontClasses(cleanValue);
200
+ }
201
+ // Add '!' prefix to each class if !important was present
202
+ if (hasImportant && Array.isArray(overrideClasses)) {
203
+ overrideClasses = overrideClasses.map(className => `!${className}`);
204
+ }
205
+ return overrideClasses;
206
+ }
207
+ /**
208
+ * Gets the ancestry chain of an element for CSS variable resolution.
209
+ * Parses snip class names to build ancestry (snip classes encode hierarchy).
210
+ *
211
+ * Format: snipcss{device}-{level}-{parentId}-{currId}
212
+ */
213
+ export function getElementAncestryChain(elemClass, allClassnamesArr) {
214
+ const chain = [];
215
+ let current = elemClass;
216
+ while (current) {
217
+ chain.unshift(current); // parent first
218
+ const parts = current.split('-');
219
+ if (parts.length < 4)
220
+ break;
221
+ const device = parts[0]; // e.g. 'snipcss0'
222
+ const level = parseInt(parts[1], 10);
223
+ const parentId = parseInt(parts[2], 10);
224
+ if (level === 0 || parentId === 0)
225
+ break;
226
+ // Find parent: same device, level-1, currId === our parentId
227
+ const parentClass = allClassnamesArr.find((c) => {
228
+ const pParts = c.split('-');
229
+ if (pParts.length >= 4) {
230
+ return (pParts[0] === device &&
231
+ parseInt(pParts[1], 10) === level - 1 &&
232
+ parseInt(pParts[3], 10) === parentId);
233
+ }
234
+ return false;
235
+ });
236
+ current = parentClass;
237
+ }
238
+ return chain;
239
+ }
240
+ /**
241
+ * Calculate a rough CSS specificity score for a single selector part.
242
+ * This is a simplified fallback; the original uses parsel with a calculateSingle
243
+ * backup.
244
+ */
245
+ function calculateSpecificity(selectorPart) {
246
+ let a = 0; // ID selectors
247
+ let b = 0; // class selectors, attribute selectors, pseudo-classes
248
+ let c = 0; // type selectors, pseudo-elements
249
+ // Remove :not() content but count its internals
250
+ const withoutNot = selectorPart.replace(/:not\(([^)]*)\)/g, (_m, inner) => {
251
+ // Count specificity of :not() argument
252
+ const innerScore = calculateSpecificity(inner);
253
+ a += Math.floor(innerScore / 100);
254
+ b += Math.floor((innerScore % 100) / 10);
255
+ c += innerScore % 10;
256
+ return '';
257
+ });
258
+ // IDs
259
+ const ids = withoutNot.match(/#[a-zA-Z_][\w-]*/g);
260
+ if (ids)
261
+ a += ids.length;
262
+ // Classes, attribute selectors, pseudo-classes
263
+ const classes = withoutNot.match(/\.[a-zA-Z_][\w-]*/g);
264
+ if (classes)
265
+ b += classes.length;
266
+ const attrs = withoutNot.match(/\[[^\]]+\]/g);
267
+ if (attrs)
268
+ b += attrs.length;
269
+ // Pseudo-classes (single colon, not pseudo-elements with double colon)
270
+ const pseudoClasses = withoutNot.match(/(?<!:):[a-zA-Z][\w-]*/g);
271
+ if (pseudoClasses)
272
+ b += pseudoClasses.length;
273
+ // Pseudo-elements (double colon)
274
+ const pseudoElements = withoutNot.match(/::[a-zA-Z][\w-]*/g);
275
+ if (pseudoElements)
276
+ c += pseudoElements.length;
277
+ // Type selectors (element names) - simplified
278
+ const typeSelectors = withoutNot.replace(/#[a-zA-Z_][\w-]*/g, '')
279
+ .replace(/\.[a-zA-Z_][\w-]*/g, '')
280
+ .replace(/\[[^\]]+\]/g, '')
281
+ .replace(/::?[a-zA-Z][\w-]*/g, '')
282
+ .replace(/[>+~*\s]/g, ' ')
283
+ .trim()
284
+ .split(/\s+/)
285
+ .filter(t => t && /^[a-zA-Z]/.test(t));
286
+ c += typeSelectors.length;
287
+ return a * 100 + b * 10 + c;
288
+ }
289
+ /**
290
+ * Parse a var() function call with proper parenthesis matching.
291
+ * (Local copy used by resolveAllVariables)
292
+ */
293
+ function parseVarFunction(str, startIndex) {
294
+ if (!str.substring(startIndex).startsWith('var('))
295
+ return null;
296
+ let i = startIndex + 4;
297
+ let parenCount = 1;
298
+ let varName = '';
299
+ let defaultValue = null;
300
+ let inVarName = true;
301
+ let buffer = '';
302
+ while (i < str.length && parenCount > 0) {
303
+ const char = str[i];
304
+ if (char === '(') {
305
+ parenCount++;
306
+ buffer += char;
307
+ }
308
+ else if (char === ')') {
309
+ parenCount--;
310
+ if (parenCount > 0)
311
+ buffer += char;
312
+ }
313
+ else if (char === ',' && parenCount === 1 && inVarName) {
314
+ varName = buffer.trim();
315
+ buffer = '';
316
+ inVarName = false;
317
+ }
318
+ else {
319
+ buffer += char;
320
+ }
321
+ i++;
322
+ }
323
+ if (parenCount === 0) {
324
+ if (inVarName)
325
+ varName = buffer.trim();
326
+ else
327
+ defaultValue = buffer.trim();
328
+ if (!varName.startsWith('--'))
329
+ return null;
330
+ return {
331
+ varName,
332
+ defaultValue,
333
+ endIndex: i,
334
+ fullMatch: str.substring(startIndex, i),
335
+ };
336
+ }
337
+ return null;
338
+ }
339
+ /**
340
+ * Resolves all CSS var() references in a value string, using context variables.
341
+ */
342
+ function resolveAllVariables(value, elementLabels, cssvarDefinedArr, maxDepth = 10, currentDepth = 0) {
343
+ if (currentDepth >= maxDepth) {
344
+ return value;
345
+ }
346
+ let resolvedValue = value;
347
+ const cssVarMap = {};
348
+ let i = 0;
349
+ const replacements = [];
350
+ while (i < resolvedValue.length) {
351
+ const varIndex = resolvedValue.indexOf('var(', i);
352
+ if (varIndex === -1)
353
+ break;
354
+ const parsed = parseVarFunction(resolvedValue, varIndex);
355
+ if (!parsed) {
356
+ i = varIndex + 4;
357
+ continue;
358
+ }
359
+ const { varName, defaultValue, endIndex } = parsed;
360
+ const resolvedRanges = resolveCssVariableValue(varName, elementLabels, cssvarDefinedArr, cssVarMap);
361
+ let concreteValue;
362
+ if (resolvedRanges && resolvedRanges.length > 0) {
363
+ concreteValue = resolvedRanges[0].concrete_value;
364
+ }
365
+ else if (defaultValue !== null) {
366
+ concreteValue = defaultValue;
367
+ if (concreteValue.includes('var(')) {
368
+ concreteValue = resolveAllVariables(concreteValue, elementLabels, cssvarDefinedArr, maxDepth, currentDepth + 1);
369
+ }
370
+ }
371
+ else {
372
+ // Can't resolve this variable — preserve the original var() reference
373
+ // instead of producing empty values (e.g., rgba(,_1) from unresolved Bootstrap vars)
374
+ concreteValue = `var(${varName})`;
375
+ }
376
+ // Legacy linear-gradient spacing hack
377
+ if (value.indexOf('linear-gradient') >= 0 && concreteValue) {
378
+ concreteValue = concreteValue + ' ';
379
+ }
380
+ replacements.push({ start: varIndex, end: endIndex, replacement: concreteValue });
381
+ i = endIndex;
382
+ }
383
+ // Apply replacements in reverse order to keep indices valid
384
+ for (let j = replacements.length - 1; j >= 0; j--) {
385
+ const { start, end, replacement } = replacements[j];
386
+ resolvedValue = resolvedValue.substring(0, start) + replacement + resolvedValue.substring(end);
387
+ }
388
+ // Recursively resolve any newly introduced var() references
389
+ if (resolvedValue.includes('var(') && currentDepth < maxDepth - 1) {
390
+ resolvedValue = resolveAllVariables(resolvedValue, elementLabels, cssvarDefinedArr, maxDepth, currentDepth + 1);
391
+ }
392
+ return resolvedValue;
393
+ }
394
+ // ============================================================
395
+ // Arbitrary-value post-processing
396
+ // ============================================================
397
+ /** Convert an arbitrary CSS value to a pixel amount (or null if not convertible). */
398
+ function getArbitraryPxValue(value) {
399
+ const v = value.trim().toLowerCase();
400
+ const numValue = parseFloat(v);
401
+ if (isNaN(numValue))
402
+ return null;
403
+ if (v.endsWith('rem'))
404
+ return numValue * 16;
405
+ if (v.endsWith('px'))
406
+ return numValue;
407
+ if (v.endsWith('em'))
408
+ return numValue * 16;
409
+ if (v.endsWith('%') || v.endsWith('vh') || v.endsWith('vw'))
410
+ return null;
411
+ return numValue; // assume px
412
+ }
413
+ /** Find the closest Tailwind spacing token for a pixel value (within 0.5px). */
414
+ function getClosestTailwindSpacingClass(pxValue) {
415
+ let closestValue = null;
416
+ let smallestDifference = Infinity;
417
+ for (const val of SPACING_PIXEL_VALUES) {
418
+ const diff = Math.abs(pxValue - val);
419
+ if (diff < smallestDifference) {
420
+ smallestDifference = diff;
421
+ closestValue = val;
422
+ }
423
+ }
424
+ if (closestValue !== null && smallestDifference <= 0.5) {
425
+ return TAILWIND_SPACING_SCALE[closestValue];
426
+ }
427
+ return null;
428
+ }
429
+ /** Replace arbitrary-value Tailwind classes (e.g. `px-[2rem]`) with standard tokens. */
430
+ function replaceArbitraryValueWithTailwindClass(tailClass) {
431
+ const classParts = tailClass.split(':');
432
+ const baseClass = classParts.pop();
433
+ const prefixesToCheck = [
434
+ 'm-', 'mt-', 'mb-', 'ml-', 'mr-', 'mx-', 'my-', 'ms-', 'me-',
435
+ 'p-', 'pt-', 'pb-', 'pl-', 'pr-', 'px-', 'py-', 'ps-', 'pe-',
436
+ 'space-x-', 'space-y-',
437
+ 'gap-', 'gap-x-', 'gap-y-',
438
+ 'inset-', 'top-', 'right-', 'bottom-', 'left-', 'start-', 'end-',
439
+ 'w-', 'h-',
440
+ ];
441
+ const arbitraryValueRegex = new RegExp(`^(${prefixesToCheck.join('|').replace(/-/g, '\\-')})\\[(.+)\\]$`);
442
+ const match = baseClass.match(arbitraryValueRegex);
443
+ if (match) {
444
+ const fullPrefix = match[1];
445
+ const value = match[2];
446
+ const pxValue = getArbitraryPxValue(value);
447
+ if (pxValue !== null) {
448
+ const closestTailwindClass = getClosestTailwindSpacingClass(pxValue);
449
+ if (closestTailwindClass) {
450
+ const newBaseClass = fullPrefix + closestTailwindClass;
451
+ return [...classParts, newBaseClass].join(':');
452
+ }
453
+ }
454
+ }
455
+ return tailClass;
456
+ }
457
+ // ============================================================
458
+ // Selectors to skip during conversion
459
+ // ============================================================
460
+ const SKIP_SELECTORS = [
461
+ '*', 'body', 'html', ':root',
462
+ '*,:before,:after', ':backdrop', ':-webkit-scrollbar',
463
+ ':before', ':after', ':selection', '::selection',
464
+ ':after,:before', ':-webkit-scrollbar', ':-webkit-scrollbar-thumb',
465
+ ':-webkit-scrollbar-track', ':-webkit-scrollbar:hover', ':-webkit-scrollbar-track:hover',
466
+ '::-webkit-scrollbar-thumb:hover', ':-webkit-scrollbar-thumb:hover',
467
+ '::placeholder', '::marker', '::spelling-error', '::grammar-error',
468
+ '::-webkit-file-upload-button', '::-webkit-inner-spin-button',
469
+ '::-webkit-outer-spin-button', '::-webkit-resizer',
470
+ '::-webkit-calendar-picker-indicator', '::-webkit-details-marker',
471
+ '::-webkit-scrollbar-corner', '::cue', ':-webkit-autofill', ':-webkit-full-screen',
472
+ ];
473
+ // ============================================================
474
+ // Helper: process pseudo-elements for a property (shared logic)
475
+ // ============================================================
476
+ /**
477
+ * Builds propKey and pseudoPrefix from allPseudo, then merges
478
+ * tailwind ranges into propSpecifityWithMediaVals.
479
+ *
480
+ * This is a helper that encapsulates the repeated pseudo-handling
481
+ * pattern used for both shorthand expanded properties and regular
482
+ * properties.
483
+ */
484
+ function processPseudoRanges(allPseudo, basePropKey, currMediaRanges, modifiedScore, ruleIndex, tailClassFn, overrideClassesFn, propSpecifityWithMediaVals) {
485
+ const hasBefore = allPseudo.includes('before');
486
+ const hasAfter = allPseudo.includes('after');
487
+ const pseudoElementsToProcess = [];
488
+ const otherPseudos = [];
489
+ for (const p of allPseudo) {
490
+ if (p === 'before' || p === 'after') {
491
+ pseudoElementsToProcess.push(p);
492
+ }
493
+ else {
494
+ otherPseudos.push(p);
495
+ }
496
+ }
497
+ if (hasBefore && hasAfter) {
498
+ // Process each pseudo-element separately
499
+ for (const pseudoEl of pseudoElementsToProcess) {
500
+ let propKey = basePropKey;
501
+ let pseudoPrefix = '';
502
+ for (const op of otherPseudos) {
503
+ propKey = op + ':' + propKey;
504
+ pseudoPrefix += op + ':';
505
+ }
506
+ propKey = pseudoEl + ':' + propKey;
507
+ pseudoPrefix += pseudoEl + ':';
508
+ if (!(propKey in propSpecifityWithMediaVals)) {
509
+ propSpecifityWithMediaVals[propKey] = [];
510
+ }
511
+ for (let c = 0; c < currMediaRanges.length; c++) {
512
+ const currMediaRange = JSON.parse(JSON.stringify(currMediaRanges[c]));
513
+ currMediaRange['prop'] = propKey;
514
+ currMediaRange['score'] = modifiedScore;
515
+ currMediaRange['ruleIndex'] = ruleIndex;
516
+ if (overrideClassesFn === null) {
517
+ const tailClass = tailClassFn(pseudoPrefix);
518
+ if (!tailClass)
519
+ continue;
520
+ currMediaRange['tailwind_classes'] = [tailClass];
521
+ }
522
+ else {
523
+ currMediaRange['tailwind_classes'] = [];
524
+ const oc = overrideClassesFn(pseudoPrefix);
525
+ for (const tc of oc) {
526
+ currMediaRange['tailwind_classes'].push(tc);
527
+ }
528
+ }
529
+ propSpecifityWithMediaVals[propKey] = mergeRanges(propSpecifityWithMediaVals[propKey], currMediaRange);
530
+ }
531
+ }
532
+ }
533
+ else {
534
+ // Original chaining behavior
535
+ let propKey = basePropKey;
536
+ let pseudoPrefix = '';
537
+ for (const pe of allPseudo) {
538
+ propKey = pe + ':' + propKey;
539
+ pseudoPrefix += pe + ':';
540
+ }
541
+ if (!(propKey in propSpecifityWithMediaVals)) {
542
+ propSpecifityWithMediaVals[propKey] = [];
543
+ }
544
+ for (let c = 0; c < currMediaRanges.length; c++) {
545
+ const currMediaRange = currMediaRanges[c];
546
+ currMediaRange['prop'] = propKey;
547
+ currMediaRange['score'] = modifiedScore;
548
+ currMediaRange['ruleIndex'] = ruleIndex;
549
+ if (overrideClassesFn === null) {
550
+ const tailClass = tailClassFn(pseudoPrefix);
551
+ if (!tailClass)
552
+ continue;
553
+ currMediaRange['tailwind_classes'] = [
554
+ ...(currMediaRange['tailwind_classes'] || []),
555
+ tailClass,
556
+ ];
557
+ }
558
+ else {
559
+ const oc = overrideClassesFn(pseudoPrefix);
560
+ for (const tc of oc) {
561
+ currMediaRange['tailwind_classes'] = [
562
+ ...(currMediaRange['tailwind_classes'] || []),
563
+ tc,
564
+ ];
565
+ }
566
+ }
567
+ propSpecifityWithMediaVals[propKey] = mergeRanges(propSpecifityWithMediaVals[propKey], currMediaRange);
568
+ }
569
+ }
570
+ }
571
+ // ============================================================
572
+ // Main exports
573
+ // ============================================================
574
+ /**
575
+ * Convert labelled HTML and CSS into Tailwind-classed HTML.
576
+ *
577
+ * For every element identified by a snipcss class, the function:
578
+ * 1. Processes inline style attributes
579
+ * 2. Iterates over matching CSS rules (from ctx.matchingFinalRules)
580
+ * 3. Expands shorthands and resolves variables
581
+ * 4. Converts each property to Tailwind classes
582
+ * 5. Handles pseudo-classes/elements and media queries
583
+ * 6. Merges, reduces and applies the resulting classes via cheerio
584
+ */
585
+ export function getTailwindHtml(labelHtml, allCss, tSnippedArr, forceBreakpoints, resolveVariables, ctx) {
586
+ const $editHtml = cheerio.load(labelHtml, CHEERIO_OPTIONS, false);
587
+ for (let x = 0; x < ctx.allClassnamesArr.length; x++) {
588
+ try {
589
+ const currClass = ctx.allClassnamesArr[x];
590
+ const theElem = $editHtml.root().find('.' + currClass).get(0);
591
+ if (!theElem)
592
+ continue;
593
+ // Build ancestry chain for CSS variable resolution
594
+ const ancestryChain = getElementAncestryChain(currClass, ctx.allClassnamesArr);
595
+ const propSpecifityWithMediaVals = {};
596
+ // -----------------------------------------------------------
597
+ // 1) Process style attribute declarations
598
+ // -----------------------------------------------------------
599
+ let theStyleAttr = $editHtml(theElem).attr('style');
600
+ if (theStyleAttr) {
601
+ // Decode HTML entities in style values (Cheerio encodes " as &quot; in inline styles)
602
+ theStyleAttr = theStyleAttr
603
+ .replace(/&quot;/g, '"')
604
+ .replace(/&amp;/g, '&')
605
+ .replace(/&lt;/g, '<')
606
+ .replace(/&gt;/g, '>')
607
+ .replace(/&#39;/g, "'");
608
+ const declarations = parseStyleAttribute(theStyleAttr);
609
+ // First pass: register CSS variable definitions from inline styles
610
+ declarations.forEach(({ property: inProp, value: inVal }) => {
611
+ if (resolveVariables && inProp.startsWith('--')) {
612
+ const mediaSelector = '';
613
+ const selText = '.' + currClass;
614
+ if (!ctx.cssvarDefinedArr.hasOwnProperty(inProp)) {
615
+ ctx.cssvarDefinedArr[inProp] = [];
616
+ }
617
+ const newKey = mediaSelector + selText + inProp;
618
+ const exists = ctx.cssvarDefinedArr[inProp].some(item => item.key === newKey);
619
+ if (!exists) {
620
+ ctx.cssvarDefinedArr[inProp].push({
621
+ key: newKey,
622
+ label: currClass,
623
+ value: inVal,
624
+ media: mediaSelector,
625
+ selector: selText,
626
+ source: 'inline',
627
+ });
628
+ }
629
+ }
630
+ });
631
+ // Second pass: convert style attribute properties to Tailwind
632
+ declarations.forEach(({ property: inProp, value: inVal }) => {
633
+ if (resolveVariables && inProp.startsWith('--'))
634
+ return;
635
+ const shortlongType = getPropertyType(inProp);
636
+ if (shortlongType.type === 'short') {
637
+ const expanded = expandShorthandProperty(inProp, inVal);
638
+ if (!expanded)
639
+ return;
640
+ const pOverwritten = expanded['overwritten_properties'];
641
+ for (const longProp in pOverwritten) {
642
+ const longVal = pOverwritten[longProp];
643
+ const tailClass = cssToTailwind(longProp, longVal);
644
+ if (!tailClass)
645
+ continue;
646
+ propSpecifityWithMediaVals[longProp] = [{
647
+ media_min: -1,
648
+ media_max: 9999,
649
+ score: 110,
650
+ tailwind_classes: [tailClass],
651
+ ruleIndex: 1,
652
+ prop: longProp,
653
+ }];
654
+ }
655
+ }
656
+ else {
657
+ let val = inVal;
658
+ if (resolveVariables && val.indexOf('var') >= 0) {
659
+ val = resolveAllVariables(val, ancestryChain, ctx.cssvarDefinedArr);
660
+ }
661
+ const overrideClasses = getOverrideClasses(inProp, val);
662
+ if (overrideClasses == null) {
663
+ const tailClass = cssToTailwind(inProp, val);
664
+ if (!tailClass)
665
+ return;
666
+ propSpecifityWithMediaVals[inProp] = [{
667
+ media_min: -1,
668
+ media_max: 9999,
669
+ score: 110,
670
+ tailwind_classes: [tailClass],
671
+ ruleIndex: 1,
672
+ prop: inProp,
673
+ }];
674
+ }
675
+ else {
676
+ if (!(inProp in propSpecifityWithMediaVals)) {
677
+ propSpecifityWithMediaVals[inProp] = [];
678
+ }
679
+ const currMediaRange = {
680
+ media_min: -1,
681
+ media_max: 9999,
682
+ score: 110,
683
+ tailwind_classes: [],
684
+ ruleIndex: 1,
685
+ prop: inProp,
686
+ };
687
+ for (const tailClass of overrideClasses) {
688
+ if (!tailClass)
689
+ continue;
690
+ currMediaRange.tailwind_classes.push(tailClass);
691
+ }
692
+ propSpecifityWithMediaVals[inProp] = mergeRanges(propSpecifityWithMediaVals[inProp], currMediaRange);
693
+ }
694
+ }
695
+ });
696
+ // Remove the style attribute after processing
697
+ $editHtml(theElem).removeAttr('style');
698
+ }
699
+ // -----------------------------------------------------------
700
+ // 2) Process matching CSS rules
701
+ // -----------------------------------------------------------
702
+ const matchData = ctx.matchingFinalRules[currClass];
703
+ if (!matchData)
704
+ continue;
705
+ const allSelectors = matchData.selectors;
706
+ const allMedia = matchData.media_queries;
707
+ const allMatchingIndices = matchData.indices;
708
+ const allInheritedTypes = matchData.inherited_type;
709
+ const allInvalidPseudos = matchData.invalid_pseudos;
710
+ const allBodies = matchData.bodies;
711
+ const matchingParts = matchData.matching_parts;
712
+ for (let s = 0; s < allSelectors.length; s++) {
713
+ const aSelector = allSelectors[s];
714
+ const aBody = allBodies[s];
715
+ const aMediaTarget = allMedia[s];
716
+ const myMatchingParts = matchingParts[s];
717
+ const ruleIndex = allMatchingIndices[s];
718
+ const inheritedType = allInheritedTypes[s];
719
+ const invalidPseudo = allInvalidPseudos[s];
720
+ // Skip universal/body/html selectors and pseudo-element-only selectors
721
+ const normalizedSel = aSelector.replace(/\s+/g, '').replace(/::/g, ':');
722
+ if (SKIP_SELECTORS.includes(aSelector) || SKIP_SELECTORS.includes(normalizedSel))
723
+ continue;
724
+ // Also skip selectors that are just * with optional pseudo-elements
725
+ if (/^\*[\s,]/.test(aSelector) || aSelector === '*')
726
+ continue;
727
+ if (invalidPseudo)
728
+ continue;
729
+ if (inheritedType === 'other_inherited' || inheritedType === 'inherited')
730
+ continue;
731
+ // Calculate specificity and collect pseudos for all matching parts
732
+ let highestScore = 0;
733
+ let allPseudo = [];
734
+ const hasPseudoElement = aSelector.indexOf(':before') >= 0 || aSelector.indexOf(':after') >= 0 ||
735
+ aSelector.indexOf('::before') >= 0 || aSelector.indexOf('::after') >= 0;
736
+ for (let m = 0; m < myMatchingParts.length; m++) {
737
+ const mPart = myMatchingParts[m];
738
+ if (mPart.indexOf(':') >= 0) {
739
+ const pseudoParts = getPseudos(mPart);
740
+ // Check for sibling pseudo-elements (before/after) in the full selector
741
+ if (pseudoParts.length > 0) {
742
+ const baseSelector = mPart.replace(/::?(before|after)/g, '');
743
+ for (const pseudo of ['before', 'after']) {
744
+ if (!pseudoParts.includes(pseudo)) {
745
+ const sib1 = baseSelector + ':' + pseudo;
746
+ const sib2 = baseSelector + '::' + pseudo;
747
+ if (aSelector.indexOf(sib1) >= 0 || aSelector.indexOf(sib2) >= 0) {
748
+ pseudoParts.push(pseudo);
749
+ }
750
+ }
751
+ }
752
+ }
753
+ allPseudo = allPseudo.concat(pseudoParts);
754
+ }
755
+ let myScore;
756
+ if (mPart in ctx.selectorSpecifityScore) {
757
+ myScore = ctx.selectorSpecifityScore[mPart];
758
+ }
759
+ else {
760
+ myScore = calculateSpecificity(mPart);
761
+ }
762
+ if (myScore > highestScore) {
763
+ highestScore = myScore;
764
+ }
765
+ }
766
+ // Match against tSnippedArr to find the rule body
767
+ for (let xi = 0; xi < tSnippedArr.length; xi++) {
768
+ const mySelector = tSnippedArr[xi].selector;
769
+ const myBody = tSnippedArr[xi].body;
770
+ const myMedia = tSnippedArr[xi].media;
771
+ if (mySelector.trim() !== aSelector.trim() || aMediaTarget.trim() !== myMedia.trim()) {
772
+ continue;
773
+ }
774
+ const bodyUsedProps = {};
775
+ const theBodySplit = myBody.split(/\r?\n/);
776
+ for (let bb = 0; bb < theBodySplit.length; bb++) {
777
+ const line = theBodySplit[bb].trim();
778
+ if (line === '' || line.startsWith('/*'))
779
+ continue;
780
+ if (line.indexOf(':') < 0)
781
+ continue;
782
+ const tSplit = line.split(/:(.+)/);
783
+ if (tSplit.length < 2)
784
+ continue;
785
+ const tProp = tSplit[0].trim();
786
+ if (tProp.startsWith('--'))
787
+ continue;
788
+ let modifiedScore = highestScore;
789
+ if (tProp in bodyUsedProps) {
790
+ bodyUsedProps[tProp] += 1;
791
+ modifiedScore += bodyUsedProps[tProp];
792
+ }
793
+ else {
794
+ bodyUsedProps[tProp] = 0;
795
+ }
796
+ let tVal = tSplit[1].trim().replace(/;$/, '');
797
+ tVal = tVal.replace(/\/\*[\s\S]*?\*\//g, '').trim();
798
+ const shortlongType = getPropertyType(tProp);
799
+ if (shortlongType.type === 'short') {
800
+ // ------ Shorthand property ------
801
+ const expanded = expandShorthandProperty(tProp, tVal);
802
+ if (!expanded)
803
+ continue;
804
+ const pOverwritten = expanded['overwritten_properties'];
805
+ for (const longProp in pOverwritten) {
806
+ let currMediaRanges = [];
807
+ let longVal = pOverwritten[longProp];
808
+ const individualMediaQueries = splitNoParen(myMedia);
809
+ individualMediaQueries.forEach(individualMedia => {
810
+ const ranges = parseMediaQueryToTailwind(individualMedia, forceBreakpoints);
811
+ currMediaRanges = currMediaRanges.concat(ranges);
812
+ });
813
+ if (allPseudo.length <= 0) {
814
+ // No pseudo-classes
815
+ const propKey = longProp;
816
+ if (!(propKey in propSpecifityWithMediaVals)) {
817
+ propSpecifityWithMediaVals[propKey] = [];
818
+ }
819
+ for (let c = 0; c < currMediaRanges.length; c++) {
820
+ const currMediaRange = currMediaRanges[c];
821
+ currMediaRange['prop'] = propKey;
822
+ currMediaRange['score'] = modifiedScore;
823
+ currMediaRange['ruleIndex'] = ruleIndex;
824
+ if (resolveVariables && longVal.indexOf('var') >= 0) {
825
+ longVal = resolveAllVariables(longVal, ancestryChain, ctx.cssvarDefinedArr);
826
+ }
827
+ const tailClass = cssToTailwind(longProp, longVal);
828
+ if (!tailClass)
829
+ continue;
830
+ currMediaRange['tailwind_classes'] = [
831
+ ...(currMediaRange['tailwind_classes'] || []),
832
+ tailClass,
833
+ ];
834
+ propSpecifityWithMediaVals[propKey] = mergeRanges(propSpecifityWithMediaVals[propKey], currMediaRange);
835
+ }
836
+ }
837
+ else {
838
+ // With pseudo-classes
839
+ const resolvedLongVal = (resolveVariables && longVal.indexOf('var') >= 0)
840
+ ? resolveAllVariables(longVal, ancestryChain, ctx.cssvarDefinedArr)
841
+ : longVal;
842
+ processPseudoRanges(allPseudo, longProp, currMediaRanges, modifiedScore, ruleIndex, (prefix) => {
843
+ const tc = cssToTailwind(longProp, resolvedLongVal);
844
+ return tc ? prefix + tc : null;
845
+ }, null, propSpecifityWithMediaVals);
846
+ }
847
+ }
848
+ }
849
+ else {
850
+ // ------ Not a shorthand property ------
851
+ let currMediaRanges = [];
852
+ const individualMediaQueries = splitNoParen(myMedia);
853
+ individualMediaQueries.forEach(individualMedia => {
854
+ const ranges = parseMediaQueryToTailwind(individualMedia, forceBreakpoints);
855
+ currMediaRanges = currMediaRanges.concat(ranges);
856
+ });
857
+ if (resolveVariables && tVal.indexOf('var') >= 0) {
858
+ tVal = resolveAllVariables(tVal, ancestryChain, ctx.cssvarDefinedArr);
859
+ }
860
+ const overrideClasses = getOverrideClasses(tProp, tVal);
861
+ if (allPseudo.length <= 0) {
862
+ // No pseudo-classes
863
+ const propKey = tProp;
864
+ if (!(propKey in propSpecifityWithMediaVals)) {
865
+ propSpecifityWithMediaVals[propKey] = [];
866
+ }
867
+ for (let c = 0; c < currMediaRanges.length; c++) {
868
+ const currMediaRange = currMediaRanges[c];
869
+ currMediaRange['prop'] = propKey;
870
+ currMediaRange['score'] = modifiedScore;
871
+ currMediaRange['ruleIndex'] = ruleIndex;
872
+ if (overrideClasses == null) {
873
+ const tailClass = cssToTailwind(tProp, tVal);
874
+ if (!tailClass)
875
+ continue;
876
+ currMediaRange['tailwind_classes'] = [
877
+ ...(currMediaRange['tailwind_classes'] || []),
878
+ tailClass,
879
+ ];
880
+ }
881
+ else {
882
+ for (const tailClass of overrideClasses) {
883
+ currMediaRange['tailwind_classes'] = [
884
+ ...(currMediaRange['tailwind_classes'] || []),
885
+ tailClass,
886
+ ];
887
+ }
888
+ }
889
+ propSpecifityWithMediaVals[propKey] = mergeRanges(propSpecifityWithMediaVals[propKey], currMediaRange);
890
+ }
891
+ }
892
+ else {
893
+ // With pseudo-classes
894
+ processPseudoRanges(allPseudo, tProp, currMediaRanges, modifiedScore, ruleIndex, (prefix) => {
895
+ if (overrideClasses == null) {
896
+ const tc = cssToTailwind(tProp, tVal);
897
+ return tc ? prefix + tc : null;
898
+ }
899
+ return null; // handled by overrideClassesFn
900
+ }, overrideClasses != null
901
+ ? (prefix) => overrideClasses.map(oc => prefix + oc)
902
+ : null, propSpecifityWithMediaVals);
903
+ }
904
+ }
905
+ }
906
+ }
907
+ }
908
+ // -----------------------------------------------------------
909
+ // 3) Preserve icon font classes, then remove existing classes
910
+ // -----------------------------------------------------------
911
+ const iconClasses = getIconClasses($editHtml(theElem).attr('class'), tSnippedArr);
912
+ $editHtml(theElem).removeAttr('class');
913
+ // -----------------------------------------------------------
914
+ // 4) Merge padding/margin shorthand properties
915
+ // -----------------------------------------------------------
916
+ const mergedPropMap = updatePropSpecifityWithMergedProperties(propSpecifityWithMediaVals);
917
+ // -----------------------------------------------------------
918
+ // 5) Apply Tailwind classes to the element
919
+ // -----------------------------------------------------------
920
+ for (const aProp in mergedPropMap) {
921
+ const ranges = mergedPropMap[aProp];
922
+ let hasImportantClass = false;
923
+ let noMediaAtEdge = true;
924
+ let hasMediaMax = false;
925
+ for (const range of ranges) {
926
+ for (const tailClass of range.tailwind_classes) {
927
+ if (tailClass.startsWith('!')) {
928
+ hasImportantClass = true;
929
+ break;
930
+ }
931
+ }
932
+ if (hasImportantClass)
933
+ break;
934
+ if (range.media_min === -1 || range.media_max === 9999) {
935
+ noMediaAtEdge = false;
936
+ }
937
+ if (range.media_max === 9999) {
938
+ hasMediaMax = true;
939
+ }
940
+ }
941
+ if (hasImportantClass || noMediaAtEdge || !hasMediaMax) {
942
+ // Add classes with media prefix directly
943
+ for (const range of ranges) {
944
+ let mediaPrefix = '';
945
+ if (range.media_min > -1 || range.media_max < 9999) {
946
+ mediaPrefix = getMediaPrefix(range.media_min, range.media_max);
947
+ }
948
+ for (const rawTailClass of range.tailwind_classes) {
949
+ if (!rawTailClass)
950
+ continue;
951
+ let tailClass = mediaPrefix ? mediaPrefix + ':' + rawTailClass : rawTailClass;
952
+ const replaced = replaceArbitraryValueWithTailwindClass(tailClass);
953
+ if (!replaced)
954
+ continue;
955
+ tailClass = replaced;
956
+ $editHtml(theElem).addClass(tailClass);
957
+ }
958
+ }
959
+ }
960
+ else {
961
+ // Reduce the Tailwind classes for this property
962
+ const reducedClasses = reduceTailwindClasses(ranges);
963
+ for (const rawClass of reducedClasses) {
964
+ const replaced = replaceArbitraryValueWithTailwindClass(rawClass);
965
+ if (!replaced)
966
+ continue;
967
+ $editHtml(theElem).addClass(replaced);
968
+ }
969
+ }
970
+ }
971
+ // Restore preserved icon font classes
972
+ if (iconClasses && iconClasses.length > 0) {
973
+ iconClasses.forEach(cls => $editHtml(theElem).addClass(cls));
974
+ }
975
+ }
976
+ catch (error) {
977
+ // Log but do not abort the whole conversion
978
+ console.error('Error processing Tailwind element:', error);
979
+ }
980
+ }
981
+ const retHtml = $editHtml.root().html() ?? '';
982
+ return retHtml;
983
+ }
984
+ // ============================================================
985
+ // Body-level Tailwind class extraction
986
+ // ============================================================
987
+ /**
988
+ * Extract Tailwind utility classes that should be applied to the <body>
989
+ * element (or an equivalent wrapper) based on CSS rules targeting body,
990
+ * html, :root, or * selectors.
991
+ */
992
+ export function getTailwindBodyClasses(tSnippedArr, forceBreakpoints, resolveVariables, tailwindUltimateArr, ctx) {
993
+ let propSpecifityWithMediaVals = {};
994
+ const bodyClasses = [];
995
+ // -----------------------------------------------------------
996
+ // 1) Process snipped rules in reverse order (later rules win)
997
+ // -----------------------------------------------------------
998
+ for (let p = tSnippedArr.length - 1; p >= 0; p--) {
999
+ try {
1000
+ const tSelector = tSnippedArr[p].selector;
1001
+ const tBody = tSnippedArr[p].body;
1002
+ const tMedia = tSnippedArr[p].media;
1003
+ const tSelIndex = tSnippedArr.length - 1 - p;
1004
+ const allSelectors = tSelector.split(',');
1005
+ let processProperties = false;
1006
+ for (const oneSelector of allSelectors) {
1007
+ const trimSelector = oneSelector.trim();
1008
+ if (trimSelector === 'body' || trimSelector === '*' || trimSelector === ':root') {
1009
+ processProperties = true;
1010
+ }
1011
+ }
1012
+ if (!processProperties)
1013
+ continue;
1014
+ const highestScore = 100;
1015
+ const bodyUsedProps = {};
1016
+ const theBodySplit = tBody.split(/\r?\n/);
1017
+ for (let bb = 0; bb < theBodySplit.length; bb++) {
1018
+ const line = theBodySplit[bb].trim();
1019
+ if (line === '' || line.startsWith('/*'))
1020
+ continue;
1021
+ if (line.indexOf(':') < 0)
1022
+ continue;
1023
+ const tSplit = line.split(/:(.+)/);
1024
+ if (tSplit.length < 2)
1025
+ continue;
1026
+ const tProp = tSplit[0].trim();
1027
+ if (tProp.startsWith('--'))
1028
+ continue;
1029
+ let modifiedScore = highestScore;
1030
+ if (tProp in bodyUsedProps) {
1031
+ bodyUsedProps[tProp] += 1;
1032
+ modifiedScore += bodyUsedProps[tProp];
1033
+ }
1034
+ else {
1035
+ bodyUsedProps[tProp] = 0;
1036
+ }
1037
+ let tVal = tSplit[1].trim().replace(/;$/, '');
1038
+ tVal = tVal.replace(/\/\*[\s\S]*?\*\//g, '').trim();
1039
+ const shortlongType = getPropertyType(tProp);
1040
+ if (shortlongType.type === 'short') {
1041
+ // Shorthand property
1042
+ const expanded = expandShorthandProperty(tProp, tVal);
1043
+ if (!expanded)
1044
+ continue;
1045
+ const pOverwritten = expanded['overwritten_properties'];
1046
+ for (const longProp in pOverwritten) {
1047
+ let currMediaRanges = [];
1048
+ const longVal = pOverwritten[longProp];
1049
+ const individualMediaQueries = splitNoParen(tMedia);
1050
+ individualMediaQueries.forEach(individualMedia => {
1051
+ const ranges = parseMediaQueryToTailwind(individualMedia, forceBreakpoints);
1052
+ currMediaRanges = currMediaRanges.concat(ranges);
1053
+ });
1054
+ const propKey = longProp;
1055
+ if (!(propKey in propSpecifityWithMediaVals)) {
1056
+ propSpecifityWithMediaVals[propKey] = [];
1057
+ }
1058
+ for (let c = 0; c < currMediaRanges.length; c++) {
1059
+ const currMediaRange = currMediaRanges[c];
1060
+ currMediaRange['prop'] = propKey;
1061
+ currMediaRange['score'] = modifiedScore;
1062
+ currMediaRange['ruleIndex'] = tSelIndex;
1063
+ let currLongVal = longVal;
1064
+ if (resolveVariables && currLongVal.indexOf('var') >= 0) {
1065
+ currLongVal = resolveAllVariables(currLongVal, ['html', 'body'], ctx.cssvarDefinedArr);
1066
+ }
1067
+ const tailClass = cssToTailwind(longProp, currLongVal);
1068
+ if (!tailClass)
1069
+ continue;
1070
+ currMediaRange['tailwind_classes'] = [
1071
+ ...(currMediaRange['tailwind_classes'] || []),
1072
+ tailClass,
1073
+ ];
1074
+ propSpecifityWithMediaVals[propKey] = mergeRanges(propSpecifityWithMediaVals[propKey], currMediaRange);
1075
+ }
1076
+ }
1077
+ }
1078
+ else {
1079
+ // Non-shorthand property
1080
+ let currMediaRanges = [];
1081
+ const individualMediaQueries = splitNoParen(tMedia);
1082
+ individualMediaQueries.forEach(individualMedia => {
1083
+ const ranges = parseMediaQueryToTailwind(individualMedia, forceBreakpoints);
1084
+ currMediaRanges = currMediaRanges.concat(ranges);
1085
+ });
1086
+ if (resolveVariables && tVal.indexOf('var') >= 0) {
1087
+ tVal = resolveAllVariables(tVal, ['html', 'body'], ctx.cssvarDefinedArr);
1088
+ }
1089
+ const overrideClasses = getOverrideClasses(tProp, tVal);
1090
+ const propKey = tProp;
1091
+ if (!(propKey in propSpecifityWithMediaVals)) {
1092
+ propSpecifityWithMediaVals[propKey] = [];
1093
+ }
1094
+ for (let c = 0; c < currMediaRanges.length; c++) {
1095
+ const currMediaRange = currMediaRanges[c];
1096
+ currMediaRange['prop'] = propKey;
1097
+ currMediaRange['score'] = modifiedScore;
1098
+ currMediaRange['ruleIndex'] = tSelIndex;
1099
+ let currTVal = tVal;
1100
+ if (resolveVariables && currTVal.indexOf('var') >= 0) {
1101
+ currTVal = resolveAllVariables(currTVal, ['html', 'body'], ctx.cssvarDefinedArr);
1102
+ }
1103
+ if (overrideClasses == null) {
1104
+ const tailClass = cssToTailwind(tProp, currTVal);
1105
+ if (!tailClass)
1106
+ continue;
1107
+ currMediaRange['tailwind_classes'] = [
1108
+ ...(currMediaRange['tailwind_classes'] || []),
1109
+ tailClass,
1110
+ ];
1111
+ }
1112
+ else {
1113
+ for (const tailClass of overrideClasses) {
1114
+ currMediaRange['tailwind_classes'] = [
1115
+ ...(currMediaRange['tailwind_classes'] || []),
1116
+ tailClass,
1117
+ ];
1118
+ }
1119
+ }
1120
+ propSpecifityWithMediaVals[propKey] = mergeRanges(propSpecifityWithMediaVals[propKey], currMediaRange);
1121
+ }
1122
+ }
1123
+ }
1124
+ }
1125
+ catch (ex) {
1126
+ console.error('Error processing body classes:', ex);
1127
+ }
1128
+ }
1129
+ // -----------------------------------------------------------
1130
+ // 2) Process the "ultimate" rules (higher-precedence body rules)
1131
+ // -----------------------------------------------------------
1132
+ const ALLOWED_INHERITED_PROPS = [
1133
+ 'background-color',
1134
+ 'background',
1135
+ 'font-size',
1136
+ 'font',
1137
+ 'font-family',
1138
+ 'color',
1139
+ ];
1140
+ const ultimateSpecificity = 200;
1141
+ const ultimateRuleIndex = 9999;
1142
+ for (let i = 0; i < tailwindUltimateArr.length; i++) {
1143
+ const rule = tailwindUltimateArr[i];
1144
+ const theSelector = rule.selector.trim();
1145
+ const theBodyLines = rule.body.split(/\r?\n/);
1146
+ const theMedia = rule.media || '';
1147
+ if (theSelector !== 'body')
1148
+ continue;
1149
+ for (let line of theBodyLines) {
1150
+ line = line.trim();
1151
+ if (!line || line.startsWith('/*'))
1152
+ continue;
1153
+ if (line.indexOf(':') < 0)
1154
+ continue;
1155
+ const splitResult = line.split(/:(.+)/);
1156
+ if (!splitResult[1])
1157
+ continue;
1158
+ const prop = splitResult[0].trim();
1159
+ const val = splitResult[1].trim().replace(/;$/, '');
1160
+ if (!ALLOWED_INHERITED_PROPS.includes(prop))
1161
+ continue;
1162
+ let resolvedVal = val;
1163
+ if (resolveVariables && resolvedVal.indexOf('var') >= 0) {
1164
+ resolvedVal = resolveAllVariables(resolvedVal, ['body'], ctx.cssvarDefinedArr);
1165
+ }
1166
+ // Try shorthand override
1167
+ const oc = getOverrideClasses(prop, resolvedVal);
1168
+ let tailClasses = [];
1169
+ if (!oc) {
1170
+ const twClass = cssToTailwind(prop, resolvedVal);
1171
+ if (twClass)
1172
+ tailClasses.push(twClass);
1173
+ }
1174
+ else {
1175
+ tailClasses = oc;
1176
+ }
1177
+ if (tailClasses.length === 0)
1178
+ continue;
1179
+ // Parse media
1180
+ let mediaRanges = parseMediaQueryToTailwind(theMedia, forceBreakpoints);
1181
+ if (mediaRanges.length === 0) {
1182
+ mediaRanges = [{ media_min: -1, media_max: 9999, tailwind_classes: [] }];
1183
+ }
1184
+ if (!propSpecifityWithMediaVals[prop]) {
1185
+ propSpecifityWithMediaVals[prop] = [];
1186
+ }
1187
+ mediaRanges.forEach(mr => {
1188
+ const newObj = {
1189
+ media_min: mr.media_min,
1190
+ media_max: mr.media_max,
1191
+ tailwind_classes: [...tailClasses],
1192
+ score: ultimateSpecificity,
1193
+ ruleIndex: ultimateRuleIndex,
1194
+ prop,
1195
+ };
1196
+ propSpecifityWithMediaVals[prop] = mergeRanges(propSpecifityWithMediaVals[prop], newObj);
1197
+ });
1198
+ }
1199
+ }
1200
+ // -----------------------------------------------------------
1201
+ // 3) Merge padding/margin properties and emit final classes
1202
+ // -----------------------------------------------------------
1203
+ propSpecifityWithMediaVals = updatePropSpecifityWithMergedProperties(propSpecifityWithMediaVals);
1204
+ for (const aProp in propSpecifityWithMediaVals) {
1205
+ const ranges = propSpecifityWithMediaVals[aProp];
1206
+ for (const range of ranges) {
1207
+ let mediaPrefix = '';
1208
+ if (range.media_min > -1 || range.media_max < 9999) {
1209
+ mediaPrefix = getMediaPrefix(range.media_min, range.media_max);
1210
+ }
1211
+ for (let tailClass of range.tailwind_classes) {
1212
+ if (mediaPrefix) {
1213
+ tailClass = mediaPrefix + ':' + tailClass;
1214
+ }
1215
+ if (!bodyClasses.includes(tailClass)) {
1216
+ bodyClasses.push(tailClass);
1217
+ }
1218
+ }
1219
+ }
1220
+ }
1221
+ return bodyClasses.join(' ');
1222
+ }
1223
+ //# sourceMappingURL=tailwind-converter.js.map