tailwind-canonical 0.7.1 → 0.9.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/README.md +131 -11
- package/dist/cli/cli.js +47 -5
- package/dist/cli/index.js +1 -1
- package/dist/cli/watch.d.ts +12 -2
- package/dist/cli/watch.js +144 -24
- package/dist/core/class-strings.js +547 -49
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +4 -1
- package/dist/core/consistency.d.ts +5 -0
- package/dist/core/consistency.js +22 -15
- package/dist/core/deduplicator.js +4 -0
- package/dist/core/merger.d.ts +1 -0
- package/dist/core/merger.js +5 -2
- package/dist/core/rules.d.ts +7 -0
- package/dist/core/rules.js +444 -9
- package/dist/core/scanner.d.ts +0 -1
- package/dist/core/scanner.js +1 -50
- package/dist/eslint/plugin.d.ts +1 -1
- package/dist/eslint/plugin.js +105 -28
- package/dist/io/config.d.ts +4 -1
- package/dist/io/config.js +45 -4
- package/dist/io/consistency.js +4 -6
- package/dist/io/scanner.js +26 -10
- package/package.json +8 -6
package/dist/core/rules.js
CHANGED
|
@@ -13,15 +13,32 @@ const TEXT_SIZE_MAP = {
|
|
|
13
13
|
96: '8xl',
|
|
14
14
|
128: '9xl',
|
|
15
15
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Radius px → token suffix. v4 renamed the small end (`rounded-sm` → `-xs`,
|
|
18
|
+
* bare `rounded` → `-sm`) and added `4xl`; `''` is the bare v3 `rounded`.
|
|
19
|
+
*/
|
|
20
|
+
const ROUNDED_MAP_V4 = {
|
|
21
|
+
0: 'none',
|
|
22
|
+
2: 'xs',
|
|
19
23
|
4: 'sm',
|
|
20
24
|
6: 'md',
|
|
21
25
|
8: 'lg',
|
|
22
26
|
12: 'xl',
|
|
23
27
|
16: '2xl',
|
|
24
28
|
24: '3xl',
|
|
29
|
+
32: '4xl',
|
|
30
|
+
9999: 'full',
|
|
31
|
+
};
|
|
32
|
+
const ROUNDED_MAP_V3 = {
|
|
33
|
+
0: 'none',
|
|
34
|
+
2: 'sm',
|
|
35
|
+
4: '',
|
|
36
|
+
6: 'md',
|
|
37
|
+
8: 'lg',
|
|
38
|
+
12: 'xl',
|
|
39
|
+
16: '2xl',
|
|
40
|
+
24: '3xl',
|
|
41
|
+
9999: 'full',
|
|
25
42
|
};
|
|
26
43
|
const FRACTION_MAP = {
|
|
27
44
|
'50': '1/2',
|
|
@@ -58,6 +75,7 @@ const FRACTION_PREFIXES = [
|
|
|
58
75
|
'bottom',
|
|
59
76
|
'translate-x',
|
|
60
77
|
'translate-y',
|
|
78
|
+
'basis',
|
|
61
79
|
];
|
|
62
80
|
const SPACING_PREFIXES = [
|
|
63
81
|
'h',
|
|
@@ -93,10 +111,49 @@ const SPACING_PREFIXES = [
|
|
|
93
111
|
'translate-y',
|
|
94
112
|
'space-x',
|
|
95
113
|
'space-y',
|
|
114
|
+
'leading',
|
|
96
115
|
];
|
|
97
116
|
const SPACING_PX_RE = new RegExp(`^(${SPACING_PREFIXES.join('|')})-\\[(\\d+)px\\]$`);
|
|
98
117
|
const SPACING_REM_RE = new RegExp(`^(${SPACING_PREFIXES.join('|')})-\\[(\\d+(?:\\.\\d+)?)rem\\]$`);
|
|
99
118
|
const FRACTION_RE = new RegExp(`^(${FRACTION_PREFIXES.join('|')})-\\[(\\d+(?:\\.\\d+)?)%\\]$`);
|
|
119
|
+
/** Pixel widths shipped by `border`, `divide`, `ring` and `outline` in both majors. */
|
|
120
|
+
const WIDTH_SCALE = new Set([0, 1, 2, 4, 8]);
|
|
121
|
+
/** Bare form is 1px in both majors. */
|
|
122
|
+
const BARE_WIDTH_RE = /^(border(?:-[xytrblse])?|divide-[xy])-\[(\d+)px\]$/;
|
|
123
|
+
/** No bare form: `ring` is 3px in v3, `outline` is a style in v3. */
|
|
124
|
+
const NUMBERED_WIDTH_RE = /^(ring|outline)-\[(\d+)px\]$/;
|
|
125
|
+
const LEADING_KEYWORDS = {
|
|
126
|
+
'1': 'none',
|
|
127
|
+
'1.25': 'tight',
|
|
128
|
+
'1.375': 'snug',
|
|
129
|
+
'1.5': 'normal',
|
|
130
|
+
'1.625': 'relaxed',
|
|
131
|
+
'2': 'loose',
|
|
132
|
+
};
|
|
133
|
+
const FONT_WEIGHT_MAP = {
|
|
134
|
+
100: 'thin',
|
|
135
|
+
200: 'extralight',
|
|
136
|
+
300: 'light',
|
|
137
|
+
400: 'normal',
|
|
138
|
+
500: 'medium',
|
|
139
|
+
600: 'semibold',
|
|
140
|
+
700: 'bold',
|
|
141
|
+
800: 'extrabold',
|
|
142
|
+
900: 'black',
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Unitless integer utilities. v4 derives each from the bare number, so any
|
|
146
|
+
* integer at or above `min` works; v3 only ships the listed steps.
|
|
147
|
+
*/
|
|
148
|
+
const INTEGER_SCALES = {
|
|
149
|
+
z: { min: 0, v3: new Set([0, 10, 20, 30, 40, 50]) },
|
|
150
|
+
order: { min: 1, v3: new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) },
|
|
151
|
+
'line-clamp': { min: 1, v3: new Set([1, 2, 3, 4, 5, 6]) },
|
|
152
|
+
columns: { min: 1, v3: new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) },
|
|
153
|
+
};
|
|
154
|
+
const INTEGER_SCALE_RE = new RegExp(`^(${Object.keys(INTEGER_SCALES).join('|')})-\\[(\\d+)\\]$`);
|
|
155
|
+
/** Leading steps v3 ships; v4 derives any integer from the spacing scale. */
|
|
156
|
+
const V3_LEADING_STEPS = new Set([3, 4, 5, 6, 7, 8, 9, 10]);
|
|
100
157
|
const OPACITY_SCALE = new Set([
|
|
101
158
|
0, 5, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 95, 100,
|
|
102
159
|
]);
|
|
@@ -140,6 +197,106 @@ const V3_FRACTIONS_BY_PREFIX = {
|
|
|
140
197
|
bottom: new Set(V3_INSET_FRACTIONS),
|
|
141
198
|
'translate-x': new Set(V3_INSET_FRACTIONS),
|
|
142
199
|
'translate-y': new Set(V3_INSET_FRACTIONS),
|
|
200
|
+
basis: new Set([...V3_HEIGHT_FRACTIONS, '1/12', '11/12']),
|
|
201
|
+
};
|
|
202
|
+
const SIZING_PREFIXES = ['w', 'h', 'min-w', 'max-w', 'min-h', 'max-h'];
|
|
203
|
+
const INSET_PREFIXES = ['inset', 'top', 'left', 'right', 'bottom'];
|
|
204
|
+
const MARGIN_PREFIXES = ['m', 'mx', 'my', 'mt', 'mb', 'ml', 'mr'];
|
|
205
|
+
const fullViewport = (keyword, prefixes) => ({
|
|
206
|
+
keyword,
|
|
207
|
+
v4: prefixes,
|
|
208
|
+
v3: [],
|
|
209
|
+
});
|
|
210
|
+
/**
|
|
211
|
+
* Arbitrary value → named keyword, with the prefixes shipping it per major.
|
|
212
|
+
* v3 lists what tailwindcss@3.3.7 generates: no `size-*`, no dynamic
|
|
213
|
+
* viewport units, and `screen` only on the axis it measures.
|
|
214
|
+
*/
|
|
215
|
+
const KEYWORD_VALUES = {
|
|
216
|
+
'100%': {
|
|
217
|
+
keyword: 'full',
|
|
218
|
+
v4: [
|
|
219
|
+
...SIZING_PREFIXES,
|
|
220
|
+
'size',
|
|
221
|
+
...INSET_PREFIXES,
|
|
222
|
+
'translate-x',
|
|
223
|
+
'translate-y',
|
|
224
|
+
'basis',
|
|
225
|
+
],
|
|
226
|
+
v3: [
|
|
227
|
+
...SIZING_PREFIXES,
|
|
228
|
+
...INSET_PREFIXES,
|
|
229
|
+
'translate-x',
|
|
230
|
+
'translate-y',
|
|
231
|
+
'basis',
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
'100vw': { keyword: 'screen', v4: ['w', 'min-w', 'max-w'], v3: ['w'] },
|
|
235
|
+
'100vh': {
|
|
236
|
+
keyword: 'screen',
|
|
237
|
+
v4: ['h', 'min-h', 'max-h'],
|
|
238
|
+
v3: ['h', 'min-h', 'max-h'],
|
|
239
|
+
},
|
|
240
|
+
'100dvh': fullViewport('dvh', ['h', 'min-h', 'max-h']),
|
|
241
|
+
'100svh': fullViewport('svh', ['h', 'min-h', 'max-h']),
|
|
242
|
+
'100lvh': fullViewport('lvh', ['h', 'min-h', 'max-h']),
|
|
243
|
+
'100dvw': fullViewport('dvw', ['w']),
|
|
244
|
+
'100svw': fullViewport('svw', ['w']),
|
|
245
|
+
'100lvw': fullViewport('lvw', ['w']),
|
|
246
|
+
auto: {
|
|
247
|
+
keyword: 'auto',
|
|
248
|
+
v4: [
|
|
249
|
+
'w',
|
|
250
|
+
'h',
|
|
251
|
+
'size',
|
|
252
|
+
...MARGIN_PREFIXES,
|
|
253
|
+
...INSET_PREFIXES,
|
|
254
|
+
'basis',
|
|
255
|
+
'aspect',
|
|
256
|
+
],
|
|
257
|
+
v3: ['w', 'h', ...MARGIN_PREFIXES, ...INSET_PREFIXES, 'basis', 'aspect'],
|
|
258
|
+
},
|
|
259
|
+
'fit-content': {
|
|
260
|
+
keyword: 'fit',
|
|
261
|
+
v4: [...SIZING_PREFIXES, 'size'],
|
|
262
|
+
v3: SIZING_PREFIXES,
|
|
263
|
+
},
|
|
264
|
+
'min-content': {
|
|
265
|
+
keyword: 'min',
|
|
266
|
+
v4: [...SIZING_PREFIXES, 'size'],
|
|
267
|
+
v3: SIZING_PREFIXES,
|
|
268
|
+
},
|
|
269
|
+
'max-content': {
|
|
270
|
+
keyword: 'max',
|
|
271
|
+
v4: [...SIZING_PREFIXES, 'size'],
|
|
272
|
+
v3: SIZING_PREFIXES,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
const KEYWORD_RE = /^([a-z]+(?:-[a-z]+)*)-\[([a-z0-9%-]+)\]$/;
|
|
276
|
+
const SPACING_ZERO_RE = new RegExp(`^(${SPACING_PREFIXES.join('|')})-\\[0\\]$`);
|
|
277
|
+
const ASPECT_RATIO_RE = /^aspect-\[(\d+)_?\/_?(\d+)\]$/;
|
|
278
|
+
/** Discrete v3 scales; v4 accepts any integer for these utilities. */
|
|
279
|
+
const V3_TIME_STEPS = new Set([0, 75, 100, 150, 200, 300, 500, 700, 1000]);
|
|
280
|
+
const V3_SCALE_STEPS = new Set([0, 50, 75, 90, 95, 100, 105, 110, 125, 150]);
|
|
281
|
+
const V3_ROTATE_STEPS = new Set([0, 1, 2, 3, 6, 12, 45, 90, 180]);
|
|
282
|
+
/** px → blur suffix; `''` is the bare `blur` class. */
|
|
283
|
+
const BLUR_MAP_V4 = {
|
|
284
|
+
4: '-xs',
|
|
285
|
+
8: '-sm',
|
|
286
|
+
12: '-md',
|
|
287
|
+
16: '-lg',
|
|
288
|
+
24: '-xl',
|
|
289
|
+
40: '-2xl',
|
|
290
|
+
64: '-3xl',
|
|
291
|
+
};
|
|
292
|
+
const BLUR_MAP_V3 = {
|
|
293
|
+
4: '-sm',
|
|
294
|
+
8: '',
|
|
295
|
+
12: '-md',
|
|
296
|
+
16: '-lg',
|
|
297
|
+
24: '-xl',
|
|
298
|
+
40: '-2xl',
|
|
299
|
+
64: '-3xl',
|
|
143
300
|
};
|
|
144
301
|
function remToPx(rem) {
|
|
145
302
|
const px = rem * 16;
|
|
@@ -153,8 +310,25 @@ function isIgnored(cls, patterns) {
|
|
|
153
310
|
return re.test(cls);
|
|
154
311
|
});
|
|
155
312
|
}
|
|
313
|
+
/** `value × factor` when it lands on an integer, else null (float-safe). */
|
|
314
|
+
function toInteger(value, factor) {
|
|
315
|
+
const scaled = value * factor;
|
|
316
|
+
const rounded = Math.round(scaled);
|
|
317
|
+
return Math.abs(scaled - rounded) <= 1e-9 ? rounded : null;
|
|
318
|
+
}
|
|
319
|
+
function scaleStepSuggestion(cls, prefix, step, v3Steps) {
|
|
320
|
+
if (step === null || (v3Steps && !v3Steps.has(step)))
|
|
321
|
+
return null;
|
|
322
|
+
return {
|
|
323
|
+
original: cls,
|
|
324
|
+
canonical: `${prefix}-${step}`,
|
|
325
|
+
isCustomToken: false,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
156
328
|
/** True when the ÷4 result would be a class v3 never generates. */
|
|
157
329
|
function isMissingInV3(prefix, step) {
|
|
330
|
+
if (prefix === 'leading')
|
|
331
|
+
return !V3_LEADING_STEPS.has(step);
|
|
158
332
|
if (V3_UNSCALED_SPACING_PREFIXES.has(prefix))
|
|
159
333
|
return true;
|
|
160
334
|
if (V3_ZERO_ONLY_SPACING_PREFIXES.has(prefix))
|
|
@@ -168,7 +342,7 @@ function textSuggestion(cls, px, textTokens) {
|
|
|
168
342
|
return {
|
|
169
343
|
original: cls,
|
|
170
344
|
canonical: `text-${token}`,
|
|
171
|
-
isCustomToken:
|
|
345
|
+
isCustomToken: textTokens[px] !== TEXT_SIZE_MAP[px],
|
|
172
346
|
};
|
|
173
347
|
}
|
|
174
348
|
function spacingSuggestion(cls, prefix, px, spacingTokens, v3) {
|
|
@@ -179,6 +353,14 @@ function spacingSuggestion(cls, prefix, px, spacingTokens, v3) {
|
|
|
179
353
|
isCustomToken: true,
|
|
180
354
|
};
|
|
181
355
|
}
|
|
356
|
+
if (px === 1) {
|
|
357
|
+
if (v3 &&
|
|
358
|
+
(V3_UNSCALED_SPACING_PREFIXES.has(prefix) ||
|
|
359
|
+
V3_ZERO_ONLY_SPACING_PREFIXES.has(prefix))) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
return { original: cls, canonical: `${prefix}-px`, isCustomToken: false };
|
|
363
|
+
}
|
|
182
364
|
if (px % 4 !== 0)
|
|
183
365
|
return null;
|
|
184
366
|
if (v3 && isMissingInV3(prefix, px / 4))
|
|
@@ -189,9 +371,123 @@ function spacingSuggestion(cls, prefix, px, spacingTokens, v3) {
|
|
|
189
371
|
isCustomToken: false,
|
|
190
372
|
};
|
|
191
373
|
}
|
|
374
|
+
/** Utilities whose arbitrary value may be negated (`-mt-2`, `-translate-x-1/2`). */
|
|
375
|
+
const NEGATABLE_PREFIXES = [
|
|
376
|
+
'm',
|
|
377
|
+
'mx',
|
|
378
|
+
'my',
|
|
379
|
+
'mt',
|
|
380
|
+
'mb',
|
|
381
|
+
'ml',
|
|
382
|
+
'mr',
|
|
383
|
+
'inset',
|
|
384
|
+
'top',
|
|
385
|
+
'left',
|
|
386
|
+
'right',
|
|
387
|
+
'bottom',
|
|
388
|
+
'translate-x',
|
|
389
|
+
'translate-y',
|
|
390
|
+
'space-x',
|
|
391
|
+
'space-y',
|
|
392
|
+
'scale',
|
|
393
|
+
'scale-x',
|
|
394
|
+
'scale-y',
|
|
395
|
+
'rotate',
|
|
396
|
+
'z',
|
|
397
|
+
'order',
|
|
398
|
+
];
|
|
399
|
+
/** Index just past the last top-level `:` — brackets/parens may hold colons. */
|
|
400
|
+
function variantsEnd(cls) {
|
|
401
|
+
let depth = 0;
|
|
402
|
+
let end = 0;
|
|
403
|
+
for (let i = 0; i < cls.length; i++) {
|
|
404
|
+
const ch = cls[i];
|
|
405
|
+
if (ch === '[' || ch === '(')
|
|
406
|
+
depth++;
|
|
407
|
+
else if ((ch === ']' || ch === ')') && depth > 0)
|
|
408
|
+
depth--;
|
|
409
|
+
else if (ch === ':' && depth === 0)
|
|
410
|
+
end = i + 1;
|
|
411
|
+
}
|
|
412
|
+
return end;
|
|
413
|
+
}
|
|
414
|
+
function parseClass(cls) {
|
|
415
|
+
const end = variantsEnd(cls);
|
|
416
|
+
let utility = cls.slice(end);
|
|
417
|
+
const leadingImportant = utility.startsWith('!');
|
|
418
|
+
if (leadingImportant)
|
|
419
|
+
utility = utility.slice(1);
|
|
420
|
+
const trailingImportant = !leadingImportant && utility.endsWith('!');
|
|
421
|
+
if (trailingImportant)
|
|
422
|
+
utility = utility.slice(0, -1);
|
|
423
|
+
let negative = utility.startsWith('-');
|
|
424
|
+
if (negative)
|
|
425
|
+
utility = utility.slice(1);
|
|
426
|
+
const innerNegative = utility.match(/^([a-z-]+)-\[-([^\]]+)\]$/);
|
|
427
|
+
if (innerNegative) {
|
|
428
|
+
negative = !negative;
|
|
429
|
+
utility = `${innerNegative[1]}-[${innerNegative[2]}]`;
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
variants: cls.slice(0, end),
|
|
433
|
+
leadingImportant,
|
|
434
|
+
trailingImportant,
|
|
435
|
+
negative,
|
|
436
|
+
utility,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Suggests the canonical form of a class, preserving its variants
|
|
441
|
+
* (`hover:`, `md:`, `[&:hover]:`), important modifier (`!p-4` / `p-4!`)
|
|
442
|
+
* and sign (`-mt-[8px]` / `mt-[-8px]` → `-mt-2`).
|
|
443
|
+
*/
|
|
192
444
|
export function suggestCanonical(cls, config = {}) {
|
|
193
445
|
if (isIgnored(cls, config.ignorePatterns))
|
|
194
446
|
return null;
|
|
447
|
+
const parsed = parseClass(cls);
|
|
448
|
+
if (parsed.utility !== cls &&
|
|
449
|
+
isIgnored(parsed.utility, config.ignorePatterns))
|
|
450
|
+
return null;
|
|
451
|
+
const base = suggestUtility(parsed.utility, config);
|
|
452
|
+
if (!base)
|
|
453
|
+
return null;
|
|
454
|
+
if (parsed.negative &&
|
|
455
|
+
!NEGATABLE_PREFIXES.some((p) => parsed.utility.startsWith(`${p}-[`))) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
if (parsed.negative && base.canonical.endsWith('-auto'))
|
|
459
|
+
return null;
|
|
460
|
+
const sign = parsed.negative && !/-(0|none)$/.test(base.canonical) ? '-' : '';
|
|
461
|
+
return {
|
|
462
|
+
original: cls,
|
|
463
|
+
canonical: `${parsed.variants}${parsed.leadingImportant ? '!' : ''}${sign}${base.canonical}${parsed.trailingImportant ? '!' : ''}`,
|
|
464
|
+
isCustomToken: base.isCustomToken,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function suggestUtility(cls, config) {
|
|
468
|
+
if (config.useDefaultTheme === false) {
|
|
469
|
+
const text = cls.match(/^text-\[(\d+)px\]$/);
|
|
470
|
+
if (text) {
|
|
471
|
+
const token = config.customTextTokens?.[Number(text[1])];
|
|
472
|
+
return token
|
|
473
|
+
? { original: cls, canonical: `text-${token}`, isCustomToken: true }
|
|
474
|
+
: null;
|
|
475
|
+
}
|
|
476
|
+
const spacing = cls.match(SPACING_PX_RE);
|
|
477
|
+
if (spacing) {
|
|
478
|
+
const token = config.customSpacingTokens?.[Number(spacing[2])];
|
|
479
|
+
return token
|
|
480
|
+
? {
|
|
481
|
+
original: cls,
|
|
482
|
+
canonical: `${spacing[1]}-${token}`,
|
|
483
|
+
isCustomToken: true,
|
|
484
|
+
}
|
|
485
|
+
: null;
|
|
486
|
+
}
|
|
487
|
+
// Even rem-to-px equivalences depend on the consumer's root font size.
|
|
488
|
+
// A custom theme must opt in through explicit mappings, never inference.
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
195
491
|
const textTokens = { ...TEXT_SIZE_MAP, ...config.customTextTokens };
|
|
196
492
|
const spacingTokens = config.customSpacingTokens ?? {};
|
|
197
493
|
const v3 = config.tailwindVersion === 3;
|
|
@@ -206,6 +502,39 @@ export function suggestCanonical(cls, config = {}) {
|
|
|
206
502
|
const px = remToPx(parseFloat(textRemMatch[1]));
|
|
207
503
|
return px === null ? null : textSuggestion(cls, px, textTokens);
|
|
208
504
|
}
|
|
505
|
+
// w-[100%] → w-full, h-[100vh] → h-screen, mx-[auto] → mx-auto
|
|
506
|
+
const keywordMatch = cls.match(KEYWORD_RE);
|
|
507
|
+
const keywordRule = keywordMatch && KEYWORD_VALUES[keywordMatch[2]];
|
|
508
|
+
if (keywordMatch && keywordRule) {
|
|
509
|
+
const prefix = keywordMatch[1];
|
|
510
|
+
if (!(v3 ? keywordRule.v3 : keywordRule.v4).includes(prefix))
|
|
511
|
+
return null;
|
|
512
|
+
return {
|
|
513
|
+
original: cls,
|
|
514
|
+
canonical: `${prefix}-${keywordRule.keyword}`,
|
|
515
|
+
isCustomToken: false,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
// p-[0] → p-0 (unitless zero)
|
|
519
|
+
const spacingZeroMatch = cls.match(SPACING_ZERO_RE);
|
|
520
|
+
if (spacingZeroMatch) {
|
|
521
|
+
return spacingSuggestion(cls, spacingZeroMatch[1], 0, spacingTokens, v3);
|
|
522
|
+
}
|
|
523
|
+
// aspect-[16/9] → aspect-video; other ratios are v4-only
|
|
524
|
+
const aspectMatch = cls.match(ASPECT_RATIO_RE);
|
|
525
|
+
if (aspectMatch) {
|
|
526
|
+
if (/^0+$/.test(aspectMatch[1]) || /^0+$/.test(aspectMatch[2]))
|
|
527
|
+
return null;
|
|
528
|
+
const ratio = `${aspectMatch[1]}/${aspectMatch[2]}`;
|
|
529
|
+
const keyword = { '16/9': 'video', '1/1': 'square' }[ratio];
|
|
530
|
+
if (!keyword && v3)
|
|
531
|
+
return null;
|
|
532
|
+
return {
|
|
533
|
+
original: cls,
|
|
534
|
+
canonical: `aspect-${keyword ?? ratio}`,
|
|
535
|
+
isCustomToken: false,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
209
538
|
// spacing-[Npx]
|
|
210
539
|
const spacingPxMatch = cls.match(SPACING_PX_RE);
|
|
211
540
|
if (spacingPxMatch) {
|
|
@@ -236,8 +565,60 @@ export function suggestCanonical(cls, config = {}) {
|
|
|
236
565
|
isCustomToken: false,
|
|
237
566
|
};
|
|
238
567
|
}
|
|
239
|
-
//
|
|
240
|
-
const
|
|
568
|
+
// leading-[1.5] → leading-normal
|
|
569
|
+
const leadingUnitlessMatch = cls.match(/^leading-\[(\d+(?:\.\d+)?|\.\d+)\]$/);
|
|
570
|
+
if (leadingUnitlessMatch) {
|
|
571
|
+
const keyword = LEADING_KEYWORDS[String(parseFloat(leadingUnitlessMatch[1]))];
|
|
572
|
+
if (!keyword)
|
|
573
|
+
return null;
|
|
574
|
+
return {
|
|
575
|
+
original: cls,
|
|
576
|
+
canonical: `leading-${keyword}`,
|
|
577
|
+
isCustomToken: false,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
// font-[700] → font-bold
|
|
581
|
+
const fontWeightMatch = cls.match(/^font-\[(\d+)\]$/);
|
|
582
|
+
if (fontWeightMatch) {
|
|
583
|
+
const weight = FONT_WEIGHT_MAP[parseInt(fontWeightMatch[1], 10)];
|
|
584
|
+
if (!weight)
|
|
585
|
+
return null;
|
|
586
|
+
return { original: cls, canonical: `font-${weight}`, isCustomToken: false };
|
|
587
|
+
}
|
|
588
|
+
// z/order/line-clamp/columns-[N]
|
|
589
|
+
const integerMatch = cls.match(INTEGER_SCALE_RE);
|
|
590
|
+
if (integerMatch) {
|
|
591
|
+
const prefix = integerMatch[1];
|
|
592
|
+
const value = parseInt(integerMatch[2], 10);
|
|
593
|
+
if (prefix === 'order' && value === 0) {
|
|
594
|
+
return { original: cls, canonical: 'order-none', isCustomToken: false };
|
|
595
|
+
}
|
|
596
|
+
const scale = INTEGER_SCALES[prefix];
|
|
597
|
+
if (value < scale.min)
|
|
598
|
+
return null;
|
|
599
|
+
if (v3 && !scale.v3.has(value))
|
|
600
|
+
return null;
|
|
601
|
+
return {
|
|
602
|
+
original: cls,
|
|
603
|
+
canonical: `${prefix}-${value}`,
|
|
604
|
+
isCustomToken: false,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
// grow-[N] / shrink-[N]
|
|
608
|
+
const flexFactorMatch = cls.match(/^(grow|shrink)-\[(\d+)\]$/);
|
|
609
|
+
if (flexFactorMatch) {
|
|
610
|
+
const prefix = flexFactorMatch[1];
|
|
611
|
+
const value = parseInt(flexFactorMatch[2], 10);
|
|
612
|
+
if (value > 1 && v3)
|
|
613
|
+
return null;
|
|
614
|
+
return {
|
|
615
|
+
original: cls,
|
|
616
|
+
canonical: value === 1 ? prefix : `${prefix}-${value}`,
|
|
617
|
+
isCustomToken: false,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
// opacity-[N.N], opacity-[0.N] or opacity-[.N]
|
|
621
|
+
const opacityMatch = cls.match(/^opacity-\[(\d+(?:\.\d+)?|\.\d+)\]$/);
|
|
241
622
|
if (opacityMatch) {
|
|
242
623
|
const raw = parseFloat(opacityMatch[1]);
|
|
243
624
|
// CSS opacity is a 0..1 fraction; a bare value > 1 clamps to 1 and is not
|
|
@@ -256,19 +637,73 @@ export function suggestCanonical(cls, config = {}) {
|
|
|
256
637
|
isCustomToken: false,
|
|
257
638
|
};
|
|
258
639
|
}
|
|
640
|
+
// border/divide-[Npx]
|
|
641
|
+
const bareWidthMatch = cls.match(BARE_WIDTH_RE);
|
|
642
|
+
if (bareWidthMatch) {
|
|
643
|
+
const px = parseInt(bareWidthMatch[2], 10);
|
|
644
|
+
if (!WIDTH_SCALE.has(px))
|
|
645
|
+
return null;
|
|
646
|
+
return {
|
|
647
|
+
original: cls,
|
|
648
|
+
canonical: px === 1 ? bareWidthMatch[1] : `${bareWidthMatch[1]}-${px}`,
|
|
649
|
+
isCustomToken: false,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
// ring/outline-[Npx]
|
|
653
|
+
const numberedWidthMatch = cls.match(NUMBERED_WIDTH_RE);
|
|
654
|
+
if (numberedWidthMatch) {
|
|
655
|
+
const px = parseInt(numberedWidthMatch[2], 10);
|
|
656
|
+
if (!WIDTH_SCALE.has(px))
|
|
657
|
+
return null;
|
|
658
|
+
return {
|
|
659
|
+
original: cls,
|
|
660
|
+
canonical: `${numberedWidthMatch[1]}-${px}`,
|
|
661
|
+
isCustomToken: false,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
259
664
|
// rounded-[Npx]
|
|
260
665
|
const roundedMatch = cls.match(/^(rounded|rounded-[a-z]+)-\[(\d+)px\]$/);
|
|
261
666
|
if (roundedMatch) {
|
|
262
667
|
const prefix = roundedMatch[1];
|
|
263
668
|
const px = parseInt(roundedMatch[2], 10);
|
|
264
|
-
const token =
|
|
265
|
-
if (
|
|
669
|
+
const token = (v3 ? ROUNDED_MAP_V3 : ROUNDED_MAP_V4)[px];
|
|
670
|
+
if (token === undefined)
|
|
266
671
|
return null;
|
|
267
672
|
return {
|
|
268
673
|
original: cls,
|
|
269
|
-
canonical: `${prefix}-${token}
|
|
674
|
+
canonical: token ? `${prefix}-${token}` : prefix,
|
|
270
675
|
isCustomToken: false,
|
|
271
676
|
};
|
|
272
677
|
}
|
|
678
|
+
// duration-[200ms] / delay-[0.2s]
|
|
679
|
+
const timeMatch = cls.match(/^(duration|delay)-\[(\d+(?:\.\d+)?)(ms|s)\]$/);
|
|
680
|
+
if (timeMatch) {
|
|
681
|
+
const ms = toInteger(parseFloat(timeMatch[2]), timeMatch[3] === 's' ? 1000 : 1);
|
|
682
|
+
return scaleStepSuggestion(cls, timeMatch[1], ms, v3 ? V3_TIME_STEPS : null);
|
|
683
|
+
}
|
|
684
|
+
// scale-[1.05] / scale-x-[105%]
|
|
685
|
+
const scaleMatch = cls.match(/^(scale(?:-[xy])?)-\[(\d+(?:\.\d+)?)(%?)\]$/);
|
|
686
|
+
if (scaleMatch) {
|
|
687
|
+
const pct = toInteger(parseFloat(scaleMatch[2]), scaleMatch[3] ? 1 : 100);
|
|
688
|
+
return scaleStepSuggestion(cls, scaleMatch[1], pct, v3 ? V3_SCALE_STEPS : null);
|
|
689
|
+
}
|
|
690
|
+
// rotate-[45deg]
|
|
691
|
+
const rotateMatch = cls.match(/^rotate-\[(\d+(?:\.\d+)?)deg\]$/);
|
|
692
|
+
if (rotateMatch) {
|
|
693
|
+
const deg = toInteger(parseFloat(rotateMatch[1]), 1);
|
|
694
|
+
return scaleStepSuggestion(cls, 'rotate', deg, v3 ? V3_ROTATE_STEPS : null);
|
|
695
|
+
}
|
|
696
|
+
// blur-[8px] / blur-[0.5rem]
|
|
697
|
+
const blurMatch = cls.match(/^blur-\[(\d+(?:\.\d+)?)(px|rem)\]$/);
|
|
698
|
+
if (blurMatch) {
|
|
699
|
+
const raw = parseFloat(blurMatch[1]);
|
|
700
|
+
const px = blurMatch[2] === 'rem' ? remToPx(raw) : toInteger(raw, 1);
|
|
701
|
+
if (px === null)
|
|
702
|
+
return null;
|
|
703
|
+
const suffix = (v3 ? BLUR_MAP_V3 : BLUR_MAP_V4)[px];
|
|
704
|
+
if (suffix === undefined)
|
|
705
|
+
return null;
|
|
706
|
+
return { original: cls, canonical: `blur${suffix}`, isCustomToken: false };
|
|
707
|
+
}
|
|
273
708
|
return null;
|
|
274
709
|
}
|
package/dist/core/scanner.d.ts
CHANGED
package/dist/core/scanner.js
CHANGED
|
@@ -15,55 +15,6 @@ export const DEFAULT_IGNORE = [
|
|
|
15
15
|
'coverage',
|
|
16
16
|
];
|
|
17
17
|
export function isGlob(target) {
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
function escapeRegex(text) {
|
|
21
|
-
return text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
22
|
-
}
|
|
23
|
-
function wildcardToRegex(text) {
|
|
24
|
-
return text
|
|
25
|
-
.split('*')
|
|
26
|
-
.map((part) => part.split('?').map(escapeRegex).join('[^/]'))
|
|
27
|
-
.join('[^/]*');
|
|
28
|
-
}
|
|
29
|
-
export function globToRegex(pattern) {
|
|
30
|
-
let i = 0;
|
|
31
|
-
const anchored = pattern.startsWith('/') || /^[A-Za-z]:/.test(pattern);
|
|
32
|
-
let re = anchored ? '^' : '(?:^|/)';
|
|
33
|
-
while (i < pattern.length) {
|
|
34
|
-
const c = pattern[i];
|
|
35
|
-
if (c === '*' && pattern[i + 1] === '*') {
|
|
36
|
-
i += 2;
|
|
37
|
-
if (pattern[i] === '/') {
|
|
38
|
-
re += '(?:[^/]+/)*';
|
|
39
|
-
i++;
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
re += '.*';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
else if (c === '*') {
|
|
46
|
-
re += '[^/]*';
|
|
47
|
-
i++;
|
|
48
|
-
}
|
|
49
|
-
else if (c === '?') {
|
|
50
|
-
re += '[^/]';
|
|
51
|
-
i++;
|
|
52
|
-
}
|
|
53
|
-
else if (c === '{' && pattern.indexOf('}', i) !== -1) {
|
|
54
|
-
const close = pattern.indexOf('}', i);
|
|
55
|
-
const alts = pattern
|
|
56
|
-
.slice(i + 1, close)
|
|
57
|
-
.split(',')
|
|
58
|
-
.map(wildcardToRegex);
|
|
59
|
-
re += `(?:${alts.join('|')})`;
|
|
60
|
-
i = close + 1;
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
re += escapeRegex(c);
|
|
64
|
-
i++;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return new RegExp(`${re}$`);
|
|
18
|
+
return /[*?{[(]/.test(target);
|
|
68
19
|
}
|
|
69
20
|
export { resolveTargets, scanFiles } from '../io/scanner.js';
|