tailwind-canonical 0.7.1 → 0.8.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 +51 -3
- package/dist/core/rules.d.ts +5 -0
- package/dist/core/rules.js +420 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -352,10 +352,36 @@ left-2 right-2 → inset-x-2
|
|
|
352
352
|
| `text-[12px]` | `text-xs` | Built-in |
|
|
353
353
|
| `text-[0.75rem]` | `text-xs` | rem values |
|
|
354
354
|
| `h-[64px]` | `h-16` | Spacing scale ÷4 |
|
|
355
|
+
| `p-[1px]` / `gap-[1px]` | `p-px` / `gap-px` | 1px spacing step |
|
|
355
356
|
| `w-[50%]` | `w-1/2` | Percentage fractions |
|
|
356
|
-
| `
|
|
357
|
+
| `w-[100%]` / `basis-[100%]` | `w-full` / `basis-full` | Sizing, inset, translate, basis |
|
|
358
|
+
| `h-[100vh]` / `w-[100vw]` | `h-screen` / `w-screen` | Axis-checked; `min-w-screen` is v4-only |
|
|
359
|
+
| `h-[100dvh]` / `svh` / `lvh` | `h-dvh` / `h-svh` / `h-lvh` | v4 only |
|
|
360
|
+
| `w-[auto]` / `mx-[auto]` | `w-auto` / `mx-auto` | Never negated |
|
|
361
|
+
| `w-[fit-content]` / `min-content` / `max-content` | `w-fit` / `w-min` / `w-max` | |
|
|
362
|
+
| `p-[0]` / `top-[0]` | `p-0` / `top-0` | Unitless zero |
|
|
363
|
+
| `aspect-[16/9]` / `aspect-[1/1]` | `aspect-video` / `aspect-square` | Other ratios (`aspect-4/3`) v4 only |
|
|
364
|
+
| `rounded-[9999px]` | `rounded-full` | |
|
|
365
|
+
| `opacity-[0.5]` / `opacity-[.5]` | `opacity-50` | Opacity scale |
|
|
366
|
+
| `border-[1px]` / `border-t-[1px]` | `border` / `border-t` | Bare = 1px; `[0/2/4/8px]` → `border-0/2/4/8` |
|
|
367
|
+
| `divide-x-[1px]` | `divide-x` | Same scale as `border` |
|
|
368
|
+
| `ring-[1px]` / `outline-[2px]` | `ring-1` / `outline-2` | Never bare: `ring` is 3px on v3 |
|
|
369
|
+
| `duration-[200ms]` / `delay-[0.15s]` | `duration-200` / `delay-150` | ms or s |
|
|
370
|
+
| `scale-[1.05]` / `scale-x-[95%]` | `scale-105` / `scale-x-95` | Factor or percentage |
|
|
371
|
+
| `rotate-[45deg]` | `rotate-45` | Integer degrees |
|
|
372
|
+
| `blur-[4px]` | `blur-xs` | `blur-sm` with `tailwindVersion: 3` |
|
|
373
|
+
| `z-[10]` | `z-10` | Any integer on v4 |
|
|
374
|
+
| `font-[700]` | `font-bold` | Weights 100–900 |
|
|
375
|
+
| `leading-[24px]` / `leading-[1.5rem]` | `leading-6` | Spacing scale ÷4 |
|
|
376
|
+
| `leading-[1.5]` | `leading-normal` | `none`, `tight`, `snug`, `normal`, `relaxed`, `loose` |
|
|
377
|
+
| `order-[1]` / `order-[0]` | `order-1` / `order-none` | Any integer on v4 |
|
|
378
|
+
| `grow-[1]` / `shrink-[0]` | `grow` / `shrink-0` | |
|
|
379
|
+
| `line-clamp-[3]`, `columns-[3]` | `line-clamp-3`, `columns-3` | Any integer ≥ 1 on v4 |
|
|
357
380
|
| `text-[11px]` | `text-2xs` | Requires `customTextTokens` |
|
|
358
381
|
| `text-[8px]`–`text-[10px]` | `text-3xs` | Requires `customTextTokens` |
|
|
382
|
+
| `hover:p-[16px]` | `hover:p-4` | Variants are preserved |
|
|
383
|
+
| `!p-[16px]` / `p-[16px]!` | `!p-4` / `p-4!` | Important modifier kept in place |
|
|
384
|
+
| `-mt-[8px]` / `mt-[-8px]` | `-mt-2` | Negative margin / inset / translate / space / scale / rotate / z / order only |
|
|
359
385
|
|
|
360
386
|
`text-2xs` and `text-3xs` are not stock Tailwind tokens, so they are not in the built-in text size map — without config, `text-[11px]` and `text-[8px]`–`text-[10px]` are left untouched. Enable them with `customTextTokens`:
|
|
361
387
|
|
|
@@ -393,14 +419,36 @@ scale differs per utility, not just per family:
|
|
|
393
419
|
| `top-[20%]` | `top-1/5` | untouched — v3.3 stops at quarters here |
|
|
394
420
|
| `top-[25%]` | `top-1/4` | `top-1/4` |
|
|
395
421
|
| `w-[8.333333%]` | `w-1/12` | `w-1/12` — `w` carries the full set |
|
|
422
|
+
| `duration-[250ms]` | `duration-250` | untouched — not on the v3 scale |
|
|
423
|
+
| `scale-[1.15]` | `scale-115` | untouched — not on the v3 scale |
|
|
424
|
+
| `rotate-[30deg]` | `rotate-30` | untouched — not on the v3 scale |
|
|
425
|
+
| `blur-[4px]` | `blur-xs` | `blur-sm` — v3 names are one step lower |
|
|
426
|
+
| `blur-[8px]` | `blur-sm` | `blur` |
|
|
427
|
+
| `z-[15]` | `z-15` | untouched — v3 stops at `0`–`50` in tens |
|
|
428
|
+
| `leading-[44px]` | `leading-11` | untouched — v3 ships `leading-3`–`leading-10` |
|
|
429
|
+
| `order-[13]` | `order-13` | untouched — v3 ships `1`–`12` |
|
|
430
|
+
| `grow-[2]` | `grow-2` | untouched — v3 ships `grow` and `grow-0` |
|
|
431
|
+
| `line-clamp-[7]` | `line-clamp-7` | untouched — v3 ships `1`–`6` |
|
|
432
|
+
| `columns-[13]` | `columns-13` | untouched — v3 ships `1`–`12` |
|
|
396
433
|
|
|
397
434
|
Spacing steps: `size-*` has no numeric scale in v3.3, and `min-w-*`, `max-w-*`
|
|
398
435
|
and `min-h-*` only ship the zero step — they gained the scale afterwards.
|
|
399
436
|
Fractions: `w` carries all thirteen, `h` stops at sixths, `inset`/`top`/`left`/
|
|
400
437
|
`right`/`bottom`/`translate-x`/`translate-y` only have halves, thirds and
|
|
401
438
|
quarters, and `min-w`/`max-w`/`min-h`/`max-h` have none.
|
|
402
|
-
|
|
403
|
-
`
|
|
439
|
+
`duration-*`/`delay-*` stop at 0, 75, 100, 150, 200, 300, 500, 700 and 1000;
|
|
440
|
+
`scale-*` at 0, 50, 75, 90, 95, 100, 105, 110, 125 and 150; `rotate-*` at 0, 1,
|
|
441
|
+
2, 3, 6, 12, 45, 90 and 180. v4 accepts any integer for all four.
|
|
442
|
+
|
|
443
|
+
`*-px` follows the same per-utility gating: `size-px`, `min-w-px`, `max-w-px`
|
|
444
|
+
and `min-h-px` are left untouched on v3.
|
|
445
|
+
|
|
446
|
+
`rounded-*` follows each major's radius scale: v4 renamed the small end, so
|
|
447
|
+
`rounded-[2px]` → `rounded-xs` and `rounded-[4px]` → `rounded-sm` on v4, but
|
|
448
|
+
`rounded-sm` and bare `rounded` on v3; `rounded-[32px]` → `rounded-4xl` is
|
|
449
|
+
v4-only. `text-*`, `opacity-*`, `font-*` weights, the `leading-*`
|
|
450
|
+
keywords and the `border`/`divide`/`ring`/`outline`
|
|
451
|
+
widths are identical in both majors and are
|
|
404
452
|
never gated. `customSpacingTokens` stays your decision and is applied in both
|
|
405
453
|
modes. All of the above was measured against `tailwindcss@3.3.7`, the
|
|
406
454
|
advertised floor.
|
package/dist/core/rules.d.ts
CHANGED
|
@@ -43,5 +43,10 @@ export type Config = {
|
|
|
43
43
|
/** Target Tailwind major. v3 restricts suggestions to its discrete scale. */
|
|
44
44
|
tailwindVersion?: 3 | 4;
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* Suggests the canonical form of a class, preserving its variants
|
|
48
|
+
* (`hover:`, `md:`, `[&:hover]:`), important modifier (`!p-4` / `p-4!`)
|
|
49
|
+
* and sign (`-mt-[8px]` / `mt-[-8px]` → `-mt-2`).
|
|
50
|
+
*/
|
|
46
51
|
export declare function suggestCanonical(cls: string, config?: Config): Suggestion | null;
|
|
47
52
|
export {};
|
package/dist/core/rules.js
CHANGED
|
@@ -14,14 +14,32 @@ const TEXT_SIZE_MAP = {
|
|
|
14
14
|
128: '9xl',
|
|
15
15
|
};
|
|
16
16
|
const BUILT_IN_TEXT_PX = new Set(Object.keys(TEXT_SIZE_MAP).map(Number));
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Radius px → token suffix. v4 renamed the small end (`rounded-sm` → `-xs`,
|
|
19
|
+
* bare `rounded` → `-sm`) and added `4xl`; `''` is the bare v3 `rounded`.
|
|
20
|
+
*/
|
|
21
|
+
const ROUNDED_MAP_V4 = {
|
|
22
|
+
0: 'none',
|
|
23
|
+
2: 'xs',
|
|
19
24
|
4: 'sm',
|
|
20
25
|
6: 'md',
|
|
21
26
|
8: 'lg',
|
|
22
27
|
12: 'xl',
|
|
23
28
|
16: '2xl',
|
|
24
29
|
24: '3xl',
|
|
30
|
+
32: '4xl',
|
|
31
|
+
9999: 'full',
|
|
32
|
+
};
|
|
33
|
+
const ROUNDED_MAP_V3 = {
|
|
34
|
+
0: 'none',
|
|
35
|
+
2: 'sm',
|
|
36
|
+
4: '',
|
|
37
|
+
6: 'md',
|
|
38
|
+
8: 'lg',
|
|
39
|
+
12: 'xl',
|
|
40
|
+
16: '2xl',
|
|
41
|
+
24: '3xl',
|
|
42
|
+
9999: 'full',
|
|
25
43
|
};
|
|
26
44
|
const FRACTION_MAP = {
|
|
27
45
|
'50': '1/2',
|
|
@@ -58,6 +76,7 @@ const FRACTION_PREFIXES = [
|
|
|
58
76
|
'bottom',
|
|
59
77
|
'translate-x',
|
|
60
78
|
'translate-y',
|
|
79
|
+
'basis',
|
|
61
80
|
];
|
|
62
81
|
const SPACING_PREFIXES = [
|
|
63
82
|
'h',
|
|
@@ -93,10 +112,49 @@ const SPACING_PREFIXES = [
|
|
|
93
112
|
'translate-y',
|
|
94
113
|
'space-x',
|
|
95
114
|
'space-y',
|
|
115
|
+
'leading',
|
|
96
116
|
];
|
|
97
117
|
const SPACING_PX_RE = new RegExp(`^(${SPACING_PREFIXES.join('|')})-\\[(\\d+)px\\]$`);
|
|
98
118
|
const SPACING_REM_RE = new RegExp(`^(${SPACING_PREFIXES.join('|')})-\\[(\\d+(?:\\.\\d+)?)rem\\]$`);
|
|
99
119
|
const FRACTION_RE = new RegExp(`^(${FRACTION_PREFIXES.join('|')})-\\[(\\d+(?:\\.\\d+)?)%\\]$`);
|
|
120
|
+
/** Pixel widths shipped by `border`, `divide`, `ring` and `outline` in both majors. */
|
|
121
|
+
const WIDTH_SCALE = new Set([0, 1, 2, 4, 8]);
|
|
122
|
+
/** Bare form is 1px in both majors. */
|
|
123
|
+
const BARE_WIDTH_RE = /^(border(?:-[xytrblse])?|divide-[xy])-\[(\d+)px\]$/;
|
|
124
|
+
/** No bare form: `ring` is 3px in v3, `outline` is a style in v3. */
|
|
125
|
+
const NUMBERED_WIDTH_RE = /^(ring|outline)-\[(\d+)px\]$/;
|
|
126
|
+
const LEADING_KEYWORDS = {
|
|
127
|
+
'1': 'none',
|
|
128
|
+
'1.25': 'tight',
|
|
129
|
+
'1.375': 'snug',
|
|
130
|
+
'1.5': 'normal',
|
|
131
|
+
'1.625': 'relaxed',
|
|
132
|
+
'2': 'loose',
|
|
133
|
+
};
|
|
134
|
+
const FONT_WEIGHT_MAP = {
|
|
135
|
+
100: 'thin',
|
|
136
|
+
200: 'extralight',
|
|
137
|
+
300: 'light',
|
|
138
|
+
400: 'normal',
|
|
139
|
+
500: 'medium',
|
|
140
|
+
600: 'semibold',
|
|
141
|
+
700: 'bold',
|
|
142
|
+
800: 'extrabold',
|
|
143
|
+
900: 'black',
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Unitless integer utilities. v4 derives each from the bare number, so any
|
|
147
|
+
* integer at or above `min` works; v3 only ships the listed steps.
|
|
148
|
+
*/
|
|
149
|
+
const INTEGER_SCALES = {
|
|
150
|
+
z: { min: 0, v3: new Set([0, 10, 20, 30, 40, 50]) },
|
|
151
|
+
order: { min: 1, v3: new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) },
|
|
152
|
+
'line-clamp': { min: 1, v3: new Set([1, 2, 3, 4, 5, 6]) },
|
|
153
|
+
columns: { min: 1, v3: new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) },
|
|
154
|
+
};
|
|
155
|
+
const INTEGER_SCALE_RE = new RegExp(`^(${Object.keys(INTEGER_SCALES).join('|')})-\\[(\\d+)\\]$`);
|
|
156
|
+
/** Leading steps v3 ships; v4 derives any integer from the spacing scale. */
|
|
157
|
+
const V3_LEADING_STEPS = new Set([3, 4, 5, 6, 7, 8, 9, 10]);
|
|
100
158
|
const OPACITY_SCALE = new Set([
|
|
101
159
|
0, 5, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 95, 100,
|
|
102
160
|
]);
|
|
@@ -140,6 +198,106 @@ const V3_FRACTIONS_BY_PREFIX = {
|
|
|
140
198
|
bottom: new Set(V3_INSET_FRACTIONS),
|
|
141
199
|
'translate-x': new Set(V3_INSET_FRACTIONS),
|
|
142
200
|
'translate-y': new Set(V3_INSET_FRACTIONS),
|
|
201
|
+
basis: new Set([...V3_HEIGHT_FRACTIONS, '1/12', '11/12']),
|
|
202
|
+
};
|
|
203
|
+
const SIZING_PREFIXES = ['w', 'h', 'min-w', 'max-w', 'min-h', 'max-h'];
|
|
204
|
+
const INSET_PREFIXES = ['inset', 'top', 'left', 'right', 'bottom'];
|
|
205
|
+
const MARGIN_PREFIXES = ['m', 'mx', 'my', 'mt', 'mb', 'ml', 'mr'];
|
|
206
|
+
const fullViewport = (keyword, prefixes) => ({
|
|
207
|
+
keyword,
|
|
208
|
+
v4: prefixes,
|
|
209
|
+
v3: [],
|
|
210
|
+
});
|
|
211
|
+
/**
|
|
212
|
+
* Arbitrary value → named keyword, with the prefixes shipping it per major.
|
|
213
|
+
* v3 lists what tailwindcss@3.3.7 generates: no `size-*`, no dynamic
|
|
214
|
+
* viewport units, and `screen` only on the axis it measures.
|
|
215
|
+
*/
|
|
216
|
+
const KEYWORD_VALUES = {
|
|
217
|
+
'100%': {
|
|
218
|
+
keyword: 'full',
|
|
219
|
+
v4: [
|
|
220
|
+
...SIZING_PREFIXES,
|
|
221
|
+
'size',
|
|
222
|
+
...INSET_PREFIXES,
|
|
223
|
+
'translate-x',
|
|
224
|
+
'translate-y',
|
|
225
|
+
'basis',
|
|
226
|
+
],
|
|
227
|
+
v3: [
|
|
228
|
+
...SIZING_PREFIXES,
|
|
229
|
+
...INSET_PREFIXES,
|
|
230
|
+
'translate-x',
|
|
231
|
+
'translate-y',
|
|
232
|
+
'basis',
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
'100vw': { keyword: 'screen', v4: ['w', 'min-w', 'max-w'], v3: ['w'] },
|
|
236
|
+
'100vh': {
|
|
237
|
+
keyword: 'screen',
|
|
238
|
+
v4: ['h', 'min-h', 'max-h'],
|
|
239
|
+
v3: ['h', 'min-h', 'max-h'],
|
|
240
|
+
},
|
|
241
|
+
'100dvh': fullViewport('dvh', ['h', 'min-h', 'max-h']),
|
|
242
|
+
'100svh': fullViewport('svh', ['h', 'min-h', 'max-h']),
|
|
243
|
+
'100lvh': fullViewport('lvh', ['h', 'min-h', 'max-h']),
|
|
244
|
+
'100dvw': fullViewport('dvw', ['w']),
|
|
245
|
+
'100svw': fullViewport('svw', ['w']),
|
|
246
|
+
'100lvw': fullViewport('lvw', ['w']),
|
|
247
|
+
auto: {
|
|
248
|
+
keyword: 'auto',
|
|
249
|
+
v4: [
|
|
250
|
+
'w',
|
|
251
|
+
'h',
|
|
252
|
+
'size',
|
|
253
|
+
...MARGIN_PREFIXES,
|
|
254
|
+
...INSET_PREFIXES,
|
|
255
|
+
'basis',
|
|
256
|
+
'aspect',
|
|
257
|
+
],
|
|
258
|
+
v3: ['w', 'h', ...MARGIN_PREFIXES, ...INSET_PREFIXES, 'basis', 'aspect'],
|
|
259
|
+
},
|
|
260
|
+
'fit-content': {
|
|
261
|
+
keyword: 'fit',
|
|
262
|
+
v4: [...SIZING_PREFIXES, 'size'],
|
|
263
|
+
v3: SIZING_PREFIXES,
|
|
264
|
+
},
|
|
265
|
+
'min-content': {
|
|
266
|
+
keyword: 'min',
|
|
267
|
+
v4: [...SIZING_PREFIXES, 'size'],
|
|
268
|
+
v3: SIZING_PREFIXES,
|
|
269
|
+
},
|
|
270
|
+
'max-content': {
|
|
271
|
+
keyword: 'max',
|
|
272
|
+
v4: [...SIZING_PREFIXES, 'size'],
|
|
273
|
+
v3: SIZING_PREFIXES,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
const KEYWORD_RE = /^([a-z]+(?:-[a-z]+)*)-\[([a-z0-9%-]+)\]$/;
|
|
277
|
+
const SPACING_ZERO_RE = new RegExp(`^(${SPACING_PREFIXES.join('|')})-\\[0\\]$`);
|
|
278
|
+
const ASPECT_RATIO_RE = /^aspect-\[(\d+)_?\/_?(\d+)\]$/;
|
|
279
|
+
/** Discrete v3 scales; v4 accepts any integer for these utilities. */
|
|
280
|
+
const V3_TIME_STEPS = new Set([0, 75, 100, 150, 200, 300, 500, 700, 1000]);
|
|
281
|
+
const V3_SCALE_STEPS = new Set([0, 50, 75, 90, 95, 100, 105, 110, 125, 150]);
|
|
282
|
+
const V3_ROTATE_STEPS = new Set([0, 1, 2, 3, 6, 12, 45, 90, 180]);
|
|
283
|
+
/** px → blur suffix; `''` is the bare `blur` class. */
|
|
284
|
+
const BLUR_MAP_V4 = {
|
|
285
|
+
4: '-xs',
|
|
286
|
+
8: '-sm',
|
|
287
|
+
12: '-md',
|
|
288
|
+
16: '-lg',
|
|
289
|
+
24: '-xl',
|
|
290
|
+
40: '-2xl',
|
|
291
|
+
64: '-3xl',
|
|
292
|
+
};
|
|
293
|
+
const BLUR_MAP_V3 = {
|
|
294
|
+
4: '-sm',
|
|
295
|
+
8: '',
|
|
296
|
+
12: '-md',
|
|
297
|
+
16: '-lg',
|
|
298
|
+
24: '-xl',
|
|
299
|
+
40: '-2xl',
|
|
300
|
+
64: '-3xl',
|
|
143
301
|
};
|
|
144
302
|
function remToPx(rem) {
|
|
145
303
|
const px = rem * 16;
|
|
@@ -153,8 +311,25 @@ function isIgnored(cls, patterns) {
|
|
|
153
311
|
return re.test(cls);
|
|
154
312
|
});
|
|
155
313
|
}
|
|
314
|
+
/** `value × factor` when it lands on an integer, else null (float-safe). */
|
|
315
|
+
function toInteger(value, factor) {
|
|
316
|
+
const scaled = value * factor;
|
|
317
|
+
const rounded = Math.round(scaled);
|
|
318
|
+
return Math.abs(scaled - rounded) <= 1e-9 ? rounded : null;
|
|
319
|
+
}
|
|
320
|
+
function scaleStepSuggestion(cls, prefix, step, v3Steps) {
|
|
321
|
+
if (step === null || (v3Steps && !v3Steps.has(step)))
|
|
322
|
+
return null;
|
|
323
|
+
return {
|
|
324
|
+
original: cls,
|
|
325
|
+
canonical: `${prefix}-${step}`,
|
|
326
|
+
isCustomToken: false,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
156
329
|
/** True when the ÷4 result would be a class v3 never generates. */
|
|
157
330
|
function isMissingInV3(prefix, step) {
|
|
331
|
+
if (prefix === 'leading')
|
|
332
|
+
return !V3_LEADING_STEPS.has(step);
|
|
158
333
|
if (V3_UNSCALED_SPACING_PREFIXES.has(prefix))
|
|
159
334
|
return true;
|
|
160
335
|
if (V3_ZERO_ONLY_SPACING_PREFIXES.has(prefix))
|
|
@@ -179,6 +354,14 @@ function spacingSuggestion(cls, prefix, px, spacingTokens, v3) {
|
|
|
179
354
|
isCustomToken: true,
|
|
180
355
|
};
|
|
181
356
|
}
|
|
357
|
+
if (px === 1) {
|
|
358
|
+
if (v3 &&
|
|
359
|
+
(V3_UNSCALED_SPACING_PREFIXES.has(prefix) ||
|
|
360
|
+
V3_ZERO_ONLY_SPACING_PREFIXES.has(prefix))) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
return { original: cls, canonical: `${prefix}-px`, isCustomToken: false };
|
|
364
|
+
}
|
|
182
365
|
if (px % 4 !== 0)
|
|
183
366
|
return null;
|
|
184
367
|
if (v3 && isMissingInV3(prefix, px / 4))
|
|
@@ -189,9 +372,100 @@ function spacingSuggestion(cls, prefix, px, spacingTokens, v3) {
|
|
|
189
372
|
isCustomToken: false,
|
|
190
373
|
};
|
|
191
374
|
}
|
|
375
|
+
/** Utilities whose arbitrary value may be negated (`-mt-2`, `-translate-x-1/2`). */
|
|
376
|
+
const NEGATABLE_PREFIXES = [
|
|
377
|
+
'm',
|
|
378
|
+
'mx',
|
|
379
|
+
'my',
|
|
380
|
+
'mt',
|
|
381
|
+
'mb',
|
|
382
|
+
'ml',
|
|
383
|
+
'mr',
|
|
384
|
+
'inset',
|
|
385
|
+
'top',
|
|
386
|
+
'left',
|
|
387
|
+
'right',
|
|
388
|
+
'bottom',
|
|
389
|
+
'translate-x',
|
|
390
|
+
'translate-y',
|
|
391
|
+
'space-x',
|
|
392
|
+
'space-y',
|
|
393
|
+
'scale',
|
|
394
|
+
'scale-x',
|
|
395
|
+
'scale-y',
|
|
396
|
+
'rotate',
|
|
397
|
+
'z',
|
|
398
|
+
'order',
|
|
399
|
+
];
|
|
400
|
+
/** Index just past the last top-level `:` — brackets/parens may hold colons. */
|
|
401
|
+
function variantsEnd(cls) {
|
|
402
|
+
let depth = 0;
|
|
403
|
+
let end = 0;
|
|
404
|
+
for (let i = 0; i < cls.length; i++) {
|
|
405
|
+
const ch = cls[i];
|
|
406
|
+
if (ch === '[' || ch === '(')
|
|
407
|
+
depth++;
|
|
408
|
+
else if ((ch === ']' || ch === ')') && depth > 0)
|
|
409
|
+
depth--;
|
|
410
|
+
else if (ch === ':' && depth === 0)
|
|
411
|
+
end = i + 1;
|
|
412
|
+
}
|
|
413
|
+
return end;
|
|
414
|
+
}
|
|
415
|
+
function parseClass(cls) {
|
|
416
|
+
const end = variantsEnd(cls);
|
|
417
|
+
let utility = cls.slice(end);
|
|
418
|
+
const leadingImportant = utility.startsWith('!');
|
|
419
|
+
if (leadingImportant)
|
|
420
|
+
utility = utility.slice(1);
|
|
421
|
+
const trailingImportant = !leadingImportant && utility.endsWith('!');
|
|
422
|
+
if (trailingImportant)
|
|
423
|
+
utility = utility.slice(0, -1);
|
|
424
|
+
let negative = utility.startsWith('-');
|
|
425
|
+
if (negative)
|
|
426
|
+
utility = utility.slice(1);
|
|
427
|
+
const innerNegative = utility.match(/^([a-z-]+)-\[-([^\]]+)\]$/);
|
|
428
|
+
if (innerNegative) {
|
|
429
|
+
negative = !negative;
|
|
430
|
+
utility = `${innerNegative[1]}-[${innerNegative[2]}]`;
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
variants: cls.slice(0, end),
|
|
434
|
+
leadingImportant,
|
|
435
|
+
trailingImportant,
|
|
436
|
+
negative,
|
|
437
|
+
utility,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Suggests the canonical form of a class, preserving its variants
|
|
442
|
+
* (`hover:`, `md:`, `[&:hover]:`), important modifier (`!p-4` / `p-4!`)
|
|
443
|
+
* and sign (`-mt-[8px]` / `mt-[-8px]` → `-mt-2`).
|
|
444
|
+
*/
|
|
192
445
|
export function suggestCanonical(cls, config = {}) {
|
|
193
446
|
if (isIgnored(cls, config.ignorePatterns))
|
|
194
447
|
return null;
|
|
448
|
+
const parsed = parseClass(cls);
|
|
449
|
+
if (parsed.utility !== cls &&
|
|
450
|
+
isIgnored(parsed.utility, config.ignorePatterns))
|
|
451
|
+
return null;
|
|
452
|
+
const base = suggestUtility(parsed.utility, config);
|
|
453
|
+
if (!base)
|
|
454
|
+
return null;
|
|
455
|
+
if (parsed.negative &&
|
|
456
|
+
!NEGATABLE_PREFIXES.some((p) => parsed.utility.startsWith(`${p}-[`))) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
if (parsed.negative && base.canonical.endsWith('-auto'))
|
|
460
|
+
return null;
|
|
461
|
+
const sign = parsed.negative && !/-(0|none)$/.test(base.canonical) ? '-' : '';
|
|
462
|
+
return {
|
|
463
|
+
original: cls,
|
|
464
|
+
canonical: `${parsed.variants}${parsed.leadingImportant ? '!' : ''}${sign}${base.canonical}${parsed.trailingImportant ? '!' : ''}`,
|
|
465
|
+
isCustomToken: base.isCustomToken,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function suggestUtility(cls, config) {
|
|
195
469
|
const textTokens = { ...TEXT_SIZE_MAP, ...config.customTextTokens };
|
|
196
470
|
const spacingTokens = config.customSpacingTokens ?? {};
|
|
197
471
|
const v3 = config.tailwindVersion === 3;
|
|
@@ -206,6 +480,39 @@ export function suggestCanonical(cls, config = {}) {
|
|
|
206
480
|
const px = remToPx(parseFloat(textRemMatch[1]));
|
|
207
481
|
return px === null ? null : textSuggestion(cls, px, textTokens);
|
|
208
482
|
}
|
|
483
|
+
// w-[100%] → w-full, h-[100vh] → h-screen, mx-[auto] → mx-auto
|
|
484
|
+
const keywordMatch = cls.match(KEYWORD_RE);
|
|
485
|
+
const keywordRule = keywordMatch && KEYWORD_VALUES[keywordMatch[2]];
|
|
486
|
+
if (keywordMatch && keywordRule) {
|
|
487
|
+
const prefix = keywordMatch[1];
|
|
488
|
+
if (!(v3 ? keywordRule.v3 : keywordRule.v4).includes(prefix))
|
|
489
|
+
return null;
|
|
490
|
+
return {
|
|
491
|
+
original: cls,
|
|
492
|
+
canonical: `${prefix}-${keywordRule.keyword}`,
|
|
493
|
+
isCustomToken: false,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
// p-[0] → p-0 (unitless zero)
|
|
497
|
+
const spacingZeroMatch = cls.match(SPACING_ZERO_RE);
|
|
498
|
+
if (spacingZeroMatch) {
|
|
499
|
+
return spacingSuggestion(cls, spacingZeroMatch[1], 0, spacingTokens, v3);
|
|
500
|
+
}
|
|
501
|
+
// aspect-[16/9] → aspect-video; other ratios are v4-only
|
|
502
|
+
const aspectMatch = cls.match(ASPECT_RATIO_RE);
|
|
503
|
+
if (aspectMatch) {
|
|
504
|
+
if (/^0+$/.test(aspectMatch[1]) || /^0+$/.test(aspectMatch[2]))
|
|
505
|
+
return null;
|
|
506
|
+
const ratio = `${aspectMatch[1]}/${aspectMatch[2]}`;
|
|
507
|
+
const keyword = { '16/9': 'video', '1/1': 'square' }[ratio];
|
|
508
|
+
if (!keyword && v3)
|
|
509
|
+
return null;
|
|
510
|
+
return {
|
|
511
|
+
original: cls,
|
|
512
|
+
canonical: `aspect-${keyword ?? ratio}`,
|
|
513
|
+
isCustomToken: false,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
209
516
|
// spacing-[Npx]
|
|
210
517
|
const spacingPxMatch = cls.match(SPACING_PX_RE);
|
|
211
518
|
if (spacingPxMatch) {
|
|
@@ -236,8 +543,60 @@ export function suggestCanonical(cls, config = {}) {
|
|
|
236
543
|
isCustomToken: false,
|
|
237
544
|
};
|
|
238
545
|
}
|
|
239
|
-
//
|
|
240
|
-
const
|
|
546
|
+
// leading-[1.5] → leading-normal
|
|
547
|
+
const leadingUnitlessMatch = cls.match(/^leading-\[(\d+(?:\.\d+)?|\.\d+)\]$/);
|
|
548
|
+
if (leadingUnitlessMatch) {
|
|
549
|
+
const keyword = LEADING_KEYWORDS[String(parseFloat(leadingUnitlessMatch[1]))];
|
|
550
|
+
if (!keyword)
|
|
551
|
+
return null;
|
|
552
|
+
return {
|
|
553
|
+
original: cls,
|
|
554
|
+
canonical: `leading-${keyword}`,
|
|
555
|
+
isCustomToken: false,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
// font-[700] → font-bold
|
|
559
|
+
const fontWeightMatch = cls.match(/^font-\[(\d+)\]$/);
|
|
560
|
+
if (fontWeightMatch) {
|
|
561
|
+
const weight = FONT_WEIGHT_MAP[parseInt(fontWeightMatch[1], 10)];
|
|
562
|
+
if (!weight)
|
|
563
|
+
return null;
|
|
564
|
+
return { original: cls, canonical: `font-${weight}`, isCustomToken: false };
|
|
565
|
+
}
|
|
566
|
+
// z/order/line-clamp/columns-[N]
|
|
567
|
+
const integerMatch = cls.match(INTEGER_SCALE_RE);
|
|
568
|
+
if (integerMatch) {
|
|
569
|
+
const prefix = integerMatch[1];
|
|
570
|
+
const value = parseInt(integerMatch[2], 10);
|
|
571
|
+
if (prefix === 'order' && value === 0) {
|
|
572
|
+
return { original: cls, canonical: 'order-none', isCustomToken: false };
|
|
573
|
+
}
|
|
574
|
+
const scale = INTEGER_SCALES[prefix];
|
|
575
|
+
if (value < scale.min)
|
|
576
|
+
return null;
|
|
577
|
+
if (v3 && !scale.v3.has(value))
|
|
578
|
+
return null;
|
|
579
|
+
return {
|
|
580
|
+
original: cls,
|
|
581
|
+
canonical: `${prefix}-${value}`,
|
|
582
|
+
isCustomToken: false,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
// grow-[N] / shrink-[N]
|
|
586
|
+
const flexFactorMatch = cls.match(/^(grow|shrink)-\[(\d+)\]$/);
|
|
587
|
+
if (flexFactorMatch) {
|
|
588
|
+
const prefix = flexFactorMatch[1];
|
|
589
|
+
const value = parseInt(flexFactorMatch[2], 10);
|
|
590
|
+
if (value > 1 && v3)
|
|
591
|
+
return null;
|
|
592
|
+
return {
|
|
593
|
+
original: cls,
|
|
594
|
+
canonical: value === 1 ? prefix : `${prefix}-${value}`,
|
|
595
|
+
isCustomToken: false,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
// opacity-[N.N], opacity-[0.N] or opacity-[.N]
|
|
599
|
+
const opacityMatch = cls.match(/^opacity-\[(\d+(?:\.\d+)?|\.\d+)\]$/);
|
|
241
600
|
if (opacityMatch) {
|
|
242
601
|
const raw = parseFloat(opacityMatch[1]);
|
|
243
602
|
// CSS opacity is a 0..1 fraction; a bare value > 1 clamps to 1 and is not
|
|
@@ -256,19 +615,73 @@ export function suggestCanonical(cls, config = {}) {
|
|
|
256
615
|
isCustomToken: false,
|
|
257
616
|
};
|
|
258
617
|
}
|
|
618
|
+
// border/divide-[Npx]
|
|
619
|
+
const bareWidthMatch = cls.match(BARE_WIDTH_RE);
|
|
620
|
+
if (bareWidthMatch) {
|
|
621
|
+
const px = parseInt(bareWidthMatch[2], 10);
|
|
622
|
+
if (!WIDTH_SCALE.has(px))
|
|
623
|
+
return null;
|
|
624
|
+
return {
|
|
625
|
+
original: cls,
|
|
626
|
+
canonical: px === 1 ? bareWidthMatch[1] : `${bareWidthMatch[1]}-${px}`,
|
|
627
|
+
isCustomToken: false,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
// ring/outline-[Npx]
|
|
631
|
+
const numberedWidthMatch = cls.match(NUMBERED_WIDTH_RE);
|
|
632
|
+
if (numberedWidthMatch) {
|
|
633
|
+
const px = parseInt(numberedWidthMatch[2], 10);
|
|
634
|
+
if (!WIDTH_SCALE.has(px))
|
|
635
|
+
return null;
|
|
636
|
+
return {
|
|
637
|
+
original: cls,
|
|
638
|
+
canonical: `${numberedWidthMatch[1]}-${px}`,
|
|
639
|
+
isCustomToken: false,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
259
642
|
// rounded-[Npx]
|
|
260
643
|
const roundedMatch = cls.match(/^(rounded|rounded-[a-z]+)-\[(\d+)px\]$/);
|
|
261
644
|
if (roundedMatch) {
|
|
262
645
|
const prefix = roundedMatch[1];
|
|
263
646
|
const px = parseInt(roundedMatch[2], 10);
|
|
264
|
-
const token =
|
|
265
|
-
if (
|
|
647
|
+
const token = (v3 ? ROUNDED_MAP_V3 : ROUNDED_MAP_V4)[px];
|
|
648
|
+
if (token === undefined)
|
|
266
649
|
return null;
|
|
267
650
|
return {
|
|
268
651
|
original: cls,
|
|
269
|
-
canonical: `${prefix}-${token}
|
|
652
|
+
canonical: token ? `${prefix}-${token}` : prefix,
|
|
270
653
|
isCustomToken: false,
|
|
271
654
|
};
|
|
272
655
|
}
|
|
656
|
+
// duration-[200ms] / delay-[0.2s]
|
|
657
|
+
const timeMatch = cls.match(/^(duration|delay)-\[(\d+(?:\.\d+)?)(ms|s)\]$/);
|
|
658
|
+
if (timeMatch) {
|
|
659
|
+
const ms = toInteger(parseFloat(timeMatch[2]), timeMatch[3] === 's' ? 1000 : 1);
|
|
660
|
+
return scaleStepSuggestion(cls, timeMatch[1], ms, v3 ? V3_TIME_STEPS : null);
|
|
661
|
+
}
|
|
662
|
+
// scale-[1.05] / scale-x-[105%]
|
|
663
|
+
const scaleMatch = cls.match(/^(scale(?:-[xy])?)-\[(\d+(?:\.\d+)?)(%?)\]$/);
|
|
664
|
+
if (scaleMatch) {
|
|
665
|
+
const pct = toInteger(parseFloat(scaleMatch[2]), scaleMatch[3] ? 1 : 100);
|
|
666
|
+
return scaleStepSuggestion(cls, scaleMatch[1], pct, v3 ? V3_SCALE_STEPS : null);
|
|
667
|
+
}
|
|
668
|
+
// rotate-[45deg]
|
|
669
|
+
const rotateMatch = cls.match(/^rotate-\[(\d+(?:\.\d+)?)deg\]$/);
|
|
670
|
+
if (rotateMatch) {
|
|
671
|
+
const deg = toInteger(parseFloat(rotateMatch[1]), 1);
|
|
672
|
+
return scaleStepSuggestion(cls, 'rotate', deg, v3 ? V3_ROTATE_STEPS : null);
|
|
673
|
+
}
|
|
674
|
+
// blur-[8px] / blur-[0.5rem]
|
|
675
|
+
const blurMatch = cls.match(/^blur-\[(\d+(?:\.\d+)?)(px|rem)\]$/);
|
|
676
|
+
if (blurMatch) {
|
|
677
|
+
const raw = parseFloat(blurMatch[1]);
|
|
678
|
+
const px = blurMatch[2] === 'rem' ? remToPx(raw) : toInteger(raw, 1);
|
|
679
|
+
if (px === null)
|
|
680
|
+
return null;
|
|
681
|
+
const suffix = (v3 ? BLUR_MAP_V3 : BLUR_MAP_V4)[px];
|
|
682
|
+
if (suffix === undefined)
|
|
683
|
+
return null;
|
|
684
|
+
return { original: cls, canonical: `blur${suffix}`, isCustomToken: false };
|
|
685
|
+
}
|
|
273
686
|
return null;
|
|
274
687
|
}
|