@clarity-contrib/tailwindcss-mcp-server 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.
- package/LICENSE +21 -0
- package/README.md +470 -0
- package/build/index.js +546 -0
- package/build/services/base.js +92 -0
- package/build/services/conversion-service.js +516 -0
- package/build/services/documentation-scraper.js +407 -0
- package/build/services/index.js +40 -0
- package/build/services/installation-service.js +248 -0
- package/build/services/template-service.js +637 -0
- package/build/services/utility-mapper.js +333 -0
- package/build/types/index.js +5 -0
- package/build/utils/index.js +112 -0
- package/package.json +67 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversion Service for TailwindCSS MCP Server
|
|
3
|
+
* Converts traditional CSS to TailwindCSS utilities using CSS parsing
|
|
4
|
+
*/
|
|
5
|
+
import { ServiceError } from './base.js';
|
|
6
|
+
import * as csstree from 'css-tree';
|
|
7
|
+
export class ConversionService {
|
|
8
|
+
propertyMap = new Map();
|
|
9
|
+
tailwindUtilities = new Set();
|
|
10
|
+
async initialize() {
|
|
11
|
+
this.setupPropertyMappings();
|
|
12
|
+
this.setupTailwindUtilities();
|
|
13
|
+
}
|
|
14
|
+
async cleanup() {
|
|
15
|
+
this.propertyMap.clear();
|
|
16
|
+
this.tailwindUtilities.clear();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Convert CSS to TailwindCSS utilities
|
|
20
|
+
*/
|
|
21
|
+
async convertCSS(params) {
|
|
22
|
+
try {
|
|
23
|
+
const { css, mode = 'classes' } = params;
|
|
24
|
+
if (!css.trim()) {
|
|
25
|
+
return {
|
|
26
|
+
tailwindClasses: '',
|
|
27
|
+
unsupportedStyles: [],
|
|
28
|
+
suggestions: ['Provide some CSS to convert']
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// Check for obviously malformed CSS before parsing
|
|
32
|
+
if (this.isMalformedCSS(css)) {
|
|
33
|
+
throw new ServiceError('Invalid CSS syntax', 'ConversionService', 'convertCSS');
|
|
34
|
+
}
|
|
35
|
+
const ast = this.parseCSS(css);
|
|
36
|
+
const conversions = this.extractStylesFromAST(ast);
|
|
37
|
+
return this.formatResult(conversions, mode);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof ServiceError) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
throw new ServiceError('Failed to convert CSS to TailwindCSS', 'ConversionService', 'convertCSS', error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse CSS string into AST
|
|
48
|
+
*/
|
|
49
|
+
parseCSS(css) {
|
|
50
|
+
try {
|
|
51
|
+
return csstree.parse(css);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// Check for common invalid CSS patterns that should throw errors
|
|
55
|
+
if (css.includes('{') && !css.includes('}')) {
|
|
56
|
+
throw new ServiceError('Invalid CSS syntax', 'ConversionService', 'parseCSS', error);
|
|
57
|
+
}
|
|
58
|
+
// For other parse errors, also throw
|
|
59
|
+
throw new ServiceError('Invalid CSS syntax', 'ConversionService', 'parseCSS', error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Extract styles from CSS AST
|
|
64
|
+
*/
|
|
65
|
+
extractStylesFromAST(ast) {
|
|
66
|
+
const result = {
|
|
67
|
+
utilities: [],
|
|
68
|
+
unsupported: [],
|
|
69
|
+
custom: []
|
|
70
|
+
};
|
|
71
|
+
csstree.walk(ast, (node, item, list) => {
|
|
72
|
+
if (node.type === 'Rule') {
|
|
73
|
+
this.processRule(node, result);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Process a CSS rule
|
|
80
|
+
*/
|
|
81
|
+
processRule(rule, result) {
|
|
82
|
+
if (!rule.block || rule.block.type !== 'Block')
|
|
83
|
+
return;
|
|
84
|
+
const declarations = [];
|
|
85
|
+
csstree.walk(rule.block, (node) => {
|
|
86
|
+
if (node.type === 'Declaration') {
|
|
87
|
+
const declaration = node;
|
|
88
|
+
declarations.push({
|
|
89
|
+
property: declaration.property,
|
|
90
|
+
value: csstree.generate(declaration.value)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
declarations.forEach(({ property, value }) => {
|
|
95
|
+
this.processDeclaration(property, value, result);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Process a CSS declaration
|
|
100
|
+
*/
|
|
101
|
+
processDeclaration(property, value, result) {
|
|
102
|
+
const mapping = this.propertyMap.get(property);
|
|
103
|
+
if (!mapping) {
|
|
104
|
+
result.unsupported.push(`${property}: ${value}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const tailwindClass = this.convertDeclaration(property, value, mapping);
|
|
108
|
+
if (tailwindClass) {
|
|
109
|
+
// Check if it's a space-separated list of utilities (like "py-4 px-8")
|
|
110
|
+
if (tailwindClass.includes(' ')) {
|
|
111
|
+
const utilities = tailwindClass.split(' ');
|
|
112
|
+
const validUtilities = utilities.filter(util => this.tailwindUtilities.has(util));
|
|
113
|
+
if (validUtilities.length === utilities.length) {
|
|
114
|
+
result.utilities.push(...validUtilities);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
result.custom.push(`${property}: ${value} → Consider creating custom utility: ${tailwindClass}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (this.tailwindUtilities.has(tailwindClass)) {
|
|
121
|
+
result.utilities.push(tailwindClass);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
result.custom.push(`${property}: ${value} → Consider creating custom utility: ${tailwindClass}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
result.unsupported.push(`${property}: ${value}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Convert a CSS declaration to TailwindCSS class
|
|
133
|
+
*/
|
|
134
|
+
convertDeclaration(property, value, mapping) {
|
|
135
|
+
const cleanValue = value.trim();
|
|
136
|
+
// Handle specific value mappings
|
|
137
|
+
if (mapping.valueMap && mapping.valueMap.has(cleanValue)) {
|
|
138
|
+
return mapping.valueMap.get(cleanValue);
|
|
139
|
+
}
|
|
140
|
+
// Handle pattern-based mappings
|
|
141
|
+
if (mapping.pattern) {
|
|
142
|
+
return this.applyPattern(cleanValue, mapping.pattern, mapping.prefix);
|
|
143
|
+
}
|
|
144
|
+
// Handle unit-based mappings
|
|
145
|
+
if (mapping.unitMapping) {
|
|
146
|
+
return this.convertWithUnits(cleanValue, mapping.prefix);
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Apply pattern-based conversion
|
|
152
|
+
*/
|
|
153
|
+
applyPattern(value, pattern, prefix) {
|
|
154
|
+
const match = value.match(pattern);
|
|
155
|
+
if (match) {
|
|
156
|
+
return `${prefix}-${match[1] || value}`;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Convert values with unit handling
|
|
162
|
+
*/
|
|
163
|
+
convertWithUnits(value, prefix) {
|
|
164
|
+
// Handle shorthand values like "1rem 2rem" for padding/margin
|
|
165
|
+
if (value.includes(' ')) {
|
|
166
|
+
const values = value.split(/\s+/);
|
|
167
|
+
if (values.length === 2) {
|
|
168
|
+
if (values[0] === values[1]) {
|
|
169
|
+
// Same value for all sides (e.g., "1rem 1rem")
|
|
170
|
+
return this.convertSingleValue(values[0], prefix);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// Different vertical and horizontal (e.g., "1rem 2rem")
|
|
174
|
+
const vertical = this.convertSingleValue(values[0], prefix.charAt(0) + 'y');
|
|
175
|
+
const horizontal = this.convertSingleValue(values[1], prefix.charAt(0) + 'x');
|
|
176
|
+
return [vertical, horizontal].filter(Boolean).join(' ');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else if (values.length === 4) {
|
|
180
|
+
// All sides specified
|
|
181
|
+
const top = this.convertSingleValue(values[0], prefix.charAt(0) + 't');
|
|
182
|
+
const right = this.convertSingleValue(values[1], prefix.charAt(0) + 'r');
|
|
183
|
+
const bottom = this.convertSingleValue(values[2], prefix.charAt(0) + 'b');
|
|
184
|
+
const left = this.convertSingleValue(values[3], prefix.charAt(0) + 'l');
|
|
185
|
+
return [top, right, bottom, left].filter(Boolean).join(' ');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return this.convertSingleValue(value, prefix);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Convert a single value with unit handling
|
|
192
|
+
*/
|
|
193
|
+
convertSingleValue(value, prefix) {
|
|
194
|
+
// Handle numeric values with units
|
|
195
|
+
const numericMatch = value.match(/^(-?\d*\.?\d+)(px|rem|em|%|vh|vw)?$/);
|
|
196
|
+
if (numericMatch) {
|
|
197
|
+
const [, num, unit] = numericMatch;
|
|
198
|
+
const numValue = parseFloat(num);
|
|
199
|
+
// Convert to Tailwind scale
|
|
200
|
+
if (unit === 'px') {
|
|
201
|
+
const remValue = numValue / 16; // Assuming 16px = 1rem
|
|
202
|
+
const tailwindValue = this.findClosestTailwindValue(remValue);
|
|
203
|
+
return `${prefix}-${tailwindValue}`;
|
|
204
|
+
}
|
|
205
|
+
if (unit === 'rem' || !unit) {
|
|
206
|
+
const tailwindValue = this.findClosestTailwindValue(numValue);
|
|
207
|
+
return `${prefix}-${tailwindValue}`;
|
|
208
|
+
}
|
|
209
|
+
if (unit === '%') {
|
|
210
|
+
const percentValue = this.convertPercentToTailwind(numValue);
|
|
211
|
+
return percentValue ? `${prefix}-${percentValue}` : null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Find closest Tailwind spacing value
|
|
218
|
+
*/
|
|
219
|
+
findClosestTailwindValue(remValue) {
|
|
220
|
+
const spacingScale = [
|
|
221
|
+
{ value: 0, class: '0' },
|
|
222
|
+
{ value: 0.125, class: '0.5' },
|
|
223
|
+
{ value: 0.25, class: '1' },
|
|
224
|
+
{ value: 0.375, class: '1.5' },
|
|
225
|
+
{ value: 0.5, class: '2' },
|
|
226
|
+
{ value: 0.625, class: '2.5' },
|
|
227
|
+
{ value: 0.75, class: '3' },
|
|
228
|
+
{ value: 0.875, class: '3.5' },
|
|
229
|
+
{ value: 1, class: '4' },
|
|
230
|
+
{ value: 1.25, class: '5' },
|
|
231
|
+
{ value: 1.5, class: '6' },
|
|
232
|
+
{ value: 1.75, class: '7' },
|
|
233
|
+
{ value: 2, class: '8' },
|
|
234
|
+
{ value: 2.25, class: '9' },
|
|
235
|
+
{ value: 2.5, class: '10' },
|
|
236
|
+
{ value: 2.75, class: '11' },
|
|
237
|
+
{ value: 3, class: '12' },
|
|
238
|
+
{ value: 3.5, class: '14' },
|
|
239
|
+
{ value: 4, class: '16' },
|
|
240
|
+
{ value: 5, class: '20' },
|
|
241
|
+
{ value: 6, class: '24' }
|
|
242
|
+
];
|
|
243
|
+
let closest = spacingScale[0];
|
|
244
|
+
let minDiff = Math.abs(remValue - closest.value);
|
|
245
|
+
for (const scale of spacingScale) {
|
|
246
|
+
const diff = Math.abs(remValue - scale.value);
|
|
247
|
+
if (diff < minDiff) {
|
|
248
|
+
minDiff = diff;
|
|
249
|
+
closest = scale;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return closest.class;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Convert percentage to Tailwind fraction
|
|
256
|
+
*/
|
|
257
|
+
convertPercentToTailwind(percent) {
|
|
258
|
+
const fractions = [
|
|
259
|
+
{ percent: 8.333333, class: '1/12' },
|
|
260
|
+
{ percent: 16.666667, class: '1/6' },
|
|
261
|
+
{ percent: 20, class: '1/5' },
|
|
262
|
+
{ percent: 25, class: '1/4' },
|
|
263
|
+
{ percent: 33.333333, class: '1/3' },
|
|
264
|
+
{ percent: 41.666667, class: '5/12' },
|
|
265
|
+
{ percent: 50, class: '1/2' },
|
|
266
|
+
{ percent: 58.333333, class: '7/12' },
|
|
267
|
+
{ percent: 66.666667, class: '2/3' },
|
|
268
|
+
{ percent: 75, class: '3/4' },
|
|
269
|
+
{ percent: 83.333333, class: '5/6' },
|
|
270
|
+
{ percent: 91.666667, class: '11/12' },
|
|
271
|
+
{ percent: 100, class: 'full' }
|
|
272
|
+
];
|
|
273
|
+
let closest = fractions[0];
|
|
274
|
+
let minDiff = Math.abs(percent - closest.percent);
|
|
275
|
+
for (const fraction of fractions) {
|
|
276
|
+
const diff = Math.abs(percent - fraction.percent);
|
|
277
|
+
if (diff < minDiff) {
|
|
278
|
+
minDiff = diff;
|
|
279
|
+
closest = fraction;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return minDiff < 2 ? closest.class : null; // Only return if close enough
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Format the conversion result
|
|
286
|
+
*/
|
|
287
|
+
formatResult(conversions, mode) {
|
|
288
|
+
const result = {
|
|
289
|
+
tailwindClasses: '',
|
|
290
|
+
unsupportedStyles: conversions.unsupported,
|
|
291
|
+
suggestions: [],
|
|
292
|
+
customUtilities: conversions.custom
|
|
293
|
+
};
|
|
294
|
+
// Add specific suggestions first
|
|
295
|
+
if (conversions.unsupported.length > 0) {
|
|
296
|
+
if (!result.suggestions)
|
|
297
|
+
result.suggestions = [];
|
|
298
|
+
result.suggestions.push("Some CSS properties don't have direct TailwindCSS equivalents. Consider using arbitrary values like [property:value]");
|
|
299
|
+
}
|
|
300
|
+
if (conversions.custom.length > 0) {
|
|
301
|
+
if (!result.suggestions)
|
|
302
|
+
result.suggestions = [];
|
|
303
|
+
result.suggestions.push("Some values are outside Tailwind's default scale. Consider extending your Tailwind config or using arbitrary values");
|
|
304
|
+
}
|
|
305
|
+
if (conversions.utilities.length === 0) {
|
|
306
|
+
// Only add the generic message if no specific suggestions were added
|
|
307
|
+
if (!result.suggestions || result.suggestions.length === 0) {
|
|
308
|
+
if (!result.suggestions)
|
|
309
|
+
result.suggestions = [];
|
|
310
|
+
result.suggestions.push('No direct TailwindCSS equivalents found for the provided CSS');
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
switch (mode) {
|
|
315
|
+
case 'inline':
|
|
316
|
+
result.tailwindClasses = `class="${conversions.utilities.join(' ')}"`;
|
|
317
|
+
break;
|
|
318
|
+
case 'component':
|
|
319
|
+
result.tailwindClasses = `.component {\n @apply ${conversions.utilities.join(' ')};\n}`;
|
|
320
|
+
break;
|
|
321
|
+
default: // 'classes'
|
|
322
|
+
result.tailwindClasses = conversions.utilities.join(' ');
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Setup property mappings from CSS to TailwindCSS
|
|
328
|
+
*/
|
|
329
|
+
setupPropertyMappings() {
|
|
330
|
+
// Display properties
|
|
331
|
+
this.propertyMap.set('display', {
|
|
332
|
+
prefix: '',
|
|
333
|
+
valueMap: new Map([
|
|
334
|
+
['block', 'block'],
|
|
335
|
+
['inline-block', 'inline-block'],
|
|
336
|
+
['inline', 'inline'],
|
|
337
|
+
['flex', 'flex'],
|
|
338
|
+
['inline-flex', 'inline-flex'],
|
|
339
|
+
['table', 'table'],
|
|
340
|
+
['inline-table', 'inline-table'],
|
|
341
|
+
['table-caption', 'table-caption'],
|
|
342
|
+
['table-cell', 'table-cell'],
|
|
343
|
+
['table-column', 'table-column'],
|
|
344
|
+
['table-column-group', 'table-column-group'],
|
|
345
|
+
['table-footer-group', 'table-footer-group'],
|
|
346
|
+
['table-header-group', 'table-header-group'],
|
|
347
|
+
['table-row-group', 'table-row-group'],
|
|
348
|
+
['table-row', 'table-row'],
|
|
349
|
+
['flow-root', 'flow-root'],
|
|
350
|
+
['grid', 'grid'],
|
|
351
|
+
['inline-grid', 'inline-grid'],
|
|
352
|
+
['contents', 'contents'],
|
|
353
|
+
['list-item', 'list-item'],
|
|
354
|
+
['hidden', 'hidden'],
|
|
355
|
+
['none', 'hidden']
|
|
356
|
+
])
|
|
357
|
+
});
|
|
358
|
+
// Position properties
|
|
359
|
+
this.propertyMap.set('position', {
|
|
360
|
+
prefix: '',
|
|
361
|
+
valueMap: new Map([
|
|
362
|
+
['static', 'static'],
|
|
363
|
+
['fixed', 'fixed'],
|
|
364
|
+
['absolute', 'absolute'],
|
|
365
|
+
['relative', 'relative'],
|
|
366
|
+
['sticky', 'sticky']
|
|
367
|
+
])
|
|
368
|
+
});
|
|
369
|
+
// Spacing properties
|
|
370
|
+
this.propertyMap.set('margin', { prefix: 'm', unitMapping: true });
|
|
371
|
+
this.propertyMap.set('margin-top', { prefix: 'mt', unitMapping: true });
|
|
372
|
+
this.propertyMap.set('margin-right', { prefix: 'mr', unitMapping: true });
|
|
373
|
+
this.propertyMap.set('margin-bottom', { prefix: 'mb', unitMapping: true });
|
|
374
|
+
this.propertyMap.set('margin-left', { prefix: 'ml', unitMapping: true });
|
|
375
|
+
this.propertyMap.set('padding', { prefix: 'p', unitMapping: true });
|
|
376
|
+
this.propertyMap.set('padding-top', { prefix: 'pt', unitMapping: true });
|
|
377
|
+
this.propertyMap.set('padding-right', { prefix: 'pr', unitMapping: true });
|
|
378
|
+
this.propertyMap.set('padding-bottom', { prefix: 'pb', unitMapping: true });
|
|
379
|
+
this.propertyMap.set('padding-left', { prefix: 'pl', unitMapping: true });
|
|
380
|
+
// Width and Height
|
|
381
|
+
this.propertyMap.set('width', { prefix: 'w', unitMapping: true });
|
|
382
|
+
this.propertyMap.set('height', { prefix: 'h', unitMapping: true });
|
|
383
|
+
this.propertyMap.set('min-width', { prefix: 'min-w', unitMapping: true });
|
|
384
|
+
this.propertyMap.set('max-width', { prefix: 'max-w', unitMapping: true });
|
|
385
|
+
this.propertyMap.set('min-height', { prefix: 'min-h', unitMapping: true });
|
|
386
|
+
this.propertyMap.set('max-height', { prefix: 'max-h', unitMapping: true });
|
|
387
|
+
// Typography
|
|
388
|
+
this.propertyMap.set('font-size', { prefix: 'text', unitMapping: true });
|
|
389
|
+
this.propertyMap.set('font-weight', {
|
|
390
|
+
prefix: 'font',
|
|
391
|
+
valueMap: new Map([
|
|
392
|
+
['100', 'font-thin'],
|
|
393
|
+
['200', 'font-extralight'],
|
|
394
|
+
['300', 'font-light'],
|
|
395
|
+
['400', 'font-normal'],
|
|
396
|
+
['500', 'font-medium'],
|
|
397
|
+
['600', 'font-semibold'],
|
|
398
|
+
['700', 'font-bold'],
|
|
399
|
+
['800', 'font-extrabold'],
|
|
400
|
+
['900', 'font-black'],
|
|
401
|
+
['normal', 'font-normal'],
|
|
402
|
+
['bold', 'font-bold']
|
|
403
|
+
])
|
|
404
|
+
});
|
|
405
|
+
this.propertyMap.set('text-align', {
|
|
406
|
+
prefix: 'text',
|
|
407
|
+
valueMap: new Map([
|
|
408
|
+
['left', 'text-left'],
|
|
409
|
+
['center', 'text-center'],
|
|
410
|
+
['right', 'text-right'],
|
|
411
|
+
['justify', 'text-justify']
|
|
412
|
+
])
|
|
413
|
+
});
|
|
414
|
+
// Colors
|
|
415
|
+
this.propertyMap.set('color', { prefix: 'text', pattern: /#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/ });
|
|
416
|
+
this.propertyMap.set('background-color', { prefix: 'bg', pattern: /#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/ });
|
|
417
|
+
this.propertyMap.set('border-color', { prefix: 'border', pattern: /#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/ });
|
|
418
|
+
// Flexbox
|
|
419
|
+
this.propertyMap.set('flex-direction', {
|
|
420
|
+
prefix: 'flex',
|
|
421
|
+
valueMap: new Map([
|
|
422
|
+
['row', 'flex-row'],
|
|
423
|
+
['row-reverse', 'flex-row-reverse'],
|
|
424
|
+
['column', 'flex-col'],
|
|
425
|
+
['column-reverse', 'flex-col-reverse']
|
|
426
|
+
])
|
|
427
|
+
});
|
|
428
|
+
this.propertyMap.set('justify-content', {
|
|
429
|
+
prefix: 'justify',
|
|
430
|
+
valueMap: new Map([
|
|
431
|
+
['flex-start', 'justify-start'],
|
|
432
|
+
['flex-end', 'justify-end'],
|
|
433
|
+
['center', 'justify-center'],
|
|
434
|
+
['space-between', 'justify-between'],
|
|
435
|
+
['space-around', 'justify-around'],
|
|
436
|
+
['space-evenly', 'justify-evenly']
|
|
437
|
+
])
|
|
438
|
+
});
|
|
439
|
+
this.propertyMap.set('align-items', {
|
|
440
|
+
prefix: 'items',
|
|
441
|
+
valueMap: new Map([
|
|
442
|
+
['flex-start', 'items-start'],
|
|
443
|
+
['flex-end', 'items-end'],
|
|
444
|
+
['center', 'items-center'],
|
|
445
|
+
['baseline', 'items-baseline'],
|
|
446
|
+
['stretch', 'items-stretch']
|
|
447
|
+
])
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Setup common TailwindCSS utilities for validation
|
|
452
|
+
*/
|
|
453
|
+
setupTailwindUtilities() {
|
|
454
|
+
const utilities = [
|
|
455
|
+
// Display
|
|
456
|
+
'block', 'inline-block', 'inline', 'flex', 'inline-flex', 'grid', 'inline-grid', 'hidden',
|
|
457
|
+
// Position
|
|
458
|
+
'static', 'fixed', 'absolute', 'relative', 'sticky',
|
|
459
|
+
// Spacing (sample)
|
|
460
|
+
'm-0', 'm-1', 'm-2', 'm-3', 'm-4', 'm-5', 'm-6', 'm-8', 'm-10', 'm-12', 'mb-8',
|
|
461
|
+
'mt-0', 'mt-1', 'mt-2', 'mt-3', 'mt-4', 'mt-5', 'mt-6', 'mt-8', 'mt-10', 'mt-12',
|
|
462
|
+
'mr-0', 'mr-1', 'mr-2', 'mr-3', 'mr-4', 'mr-5', 'mr-6', 'mr-8', 'mr-10', 'mr-12',
|
|
463
|
+
'mb-0', 'mb-1', 'mb-2', 'mb-3', 'mb-4', 'mb-5', 'mb-6', 'mb-8', 'mb-10', 'mb-12',
|
|
464
|
+
'ml-0', 'ml-1', 'ml-2', 'ml-3', 'ml-4', 'ml-5', 'ml-6', 'ml-8', 'ml-10', 'ml-12',
|
|
465
|
+
'p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-5', 'p-6', 'p-8', 'p-10', 'p-12',
|
|
466
|
+
'px-0', 'px-1', 'px-2', 'px-3', 'px-4', 'px-5', 'px-6', 'px-8', 'px-10', 'px-12',
|
|
467
|
+
'py-0', 'py-1', 'py-2', 'py-3', 'py-4', 'py-5', 'py-6', 'py-8', 'py-10', 'py-12',
|
|
468
|
+
'pt-0', 'pt-1', 'pt-2', 'pt-3', 'pt-4', 'pt-5', 'pt-6', 'pt-8', 'pt-10', 'pt-12',
|
|
469
|
+
'pr-0', 'pr-1', 'pr-2', 'pr-3', 'pr-4', 'pr-5', 'pr-6', 'pr-8', 'pr-10', 'pr-12',
|
|
470
|
+
'pb-0', 'pb-1', 'pb-2', 'pb-3', 'pb-4', 'pb-5', 'pb-6', 'pb-8', 'pb-10', 'pb-12',
|
|
471
|
+
'pl-0', 'pl-1', 'pl-2', 'pl-3', 'pl-4', 'pl-5', 'pl-6', 'pl-8', 'pl-10', 'pl-12',
|
|
472
|
+
// Width/Height (sample)
|
|
473
|
+
'w-0', 'w-1', 'w-2', 'w-3', 'w-4', 'w-5', 'w-6', 'w-8', 'w-10', 'w-12', 'w-16', 'w-full', 'w-1/2', 'w-1/3', 'w-2/3', 'w-1/4', 'w-3/4',
|
|
474
|
+
'h-0', 'h-1', 'h-2', 'h-3', 'h-4', 'h-5', 'h-6', 'h-8', 'h-10', 'h-12', 'h-16', 'h-full', 'h-screen',
|
|
475
|
+
// Typography
|
|
476
|
+
'font-thin', 'font-light', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'font-extrabold', 'font-black',
|
|
477
|
+
'text-left', 'text-center', 'text-right', 'text-justify',
|
|
478
|
+
'text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl', 'text-5xl', 'text-6xl',
|
|
479
|
+
// Flexbox
|
|
480
|
+
'flex-row', 'flex-row-reverse', 'flex-col', 'flex-col-reverse',
|
|
481
|
+
'justify-start', 'justify-end', 'justify-center', 'justify-between', 'justify-around', 'justify-evenly',
|
|
482
|
+
'items-start', 'items-end', 'items-center', 'items-baseline', 'items-stretch'
|
|
483
|
+
];
|
|
484
|
+
utilities.forEach(utility => this.tailwindUtilities.add(utility));
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Check if CSS is malformed
|
|
488
|
+
*/
|
|
489
|
+
isMalformedCSS(css) {
|
|
490
|
+
// Check for unbalanced braces
|
|
491
|
+
const openBraces = (css.match(/{/g) || []).length;
|
|
492
|
+
const closeBraces = (css.match(/}/g) || []).length;
|
|
493
|
+
if (openBraces !== closeBraces) {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
// Skip @media and @keyframes rules which are valid but complex
|
|
497
|
+
if (css.includes('@media') || css.includes('@keyframes') || css.includes('@supports')) {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
// Check for selectors with opening brace but no closing brace on the same "rule"
|
|
501
|
+
const rules = css.split('}');
|
|
502
|
+
for (const rule of rules) {
|
|
503
|
+
const trimmedRule = rule.trim();
|
|
504
|
+
if (trimmedRule && trimmedRule.includes('{')) {
|
|
505
|
+
// This rule has an opening brace, check if it looks malformed
|
|
506
|
+
const afterBrace = trimmedRule.split('{')[1];
|
|
507
|
+
if (afterBrace && afterBrace.trim() && !afterBrace.includes(':') && !afterBrace.includes('@')) {
|
|
508
|
+
// Has content after brace but no colon (likely malformed property)
|
|
509
|
+
// But skip @ rules which are valid
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
}
|