tailwind-canonical 0.7.0 → 0.7.1
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 +12 -3
- package/dist/cli/cli.d.ts +2 -11
- package/dist/cli/cli.js +176 -471
- package/dist/cli/format.d.ts +3 -0
- package/dist/cli/format.js +11 -0
- package/dist/cli/reporters.d.ts +69 -0
- package/dist/cli/reporters.js +333 -0
- package/dist/cli/types.d.ts +16 -0
- package/dist/cli/types.js +1 -0
- package/dist/cli/watch.d.ts +11 -0
- package/dist/cli/watch.js +33 -0
- package/dist/core/analyzer.js +3 -2
- package/dist/core/class-strings.js +13 -7
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +25 -42
- package/dist/core/consistency.d.ts +1 -1
- package/dist/core/deduplicator.js +110 -170
- package/dist/core/merger.js +9 -3
- package/dist/core/rules.js +38 -60
- package/dist/core/scanner.js +23 -20
- package/dist/core/sorter.js +80 -7
- package/dist/core/suppressions.d.ts +1 -2
- package/dist/core/suppressions.js +17 -13
- package/dist/core/typos.js +3 -2
- package/dist/eslint/plugin.js +62 -28
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/core/config.js
CHANGED
|
@@ -32,8 +32,7 @@ const ANALYZE_KEYS = [
|
|
|
32
32
|
'maxRareValues',
|
|
33
33
|
'maxPatterns',
|
|
34
34
|
];
|
|
35
|
-
const
|
|
36
|
-
const DEFAULT_COMMAND_KEYS = [
|
|
35
|
+
const DEFAULT_COMMAND_BOOLEAN_KEYS = [
|
|
37
36
|
'fix',
|
|
38
37
|
'merge',
|
|
39
38
|
'dedup',
|
|
@@ -42,21 +41,29 @@ const DEFAULT_COMMAND_KEYS = [
|
|
|
42
41
|
'analyze',
|
|
43
42
|
'typos',
|
|
44
43
|
'watch',
|
|
44
|
+
];
|
|
45
|
+
const DEFAULT_COMMAND_KEYS = [
|
|
46
|
+
...DEFAULT_COMMAND_BOOLEAN_KEYS,
|
|
45
47
|
'reporter',
|
|
46
48
|
'targets',
|
|
47
49
|
];
|
|
48
|
-
const DEFAULT_COMMAND_KEY_SET = new Set(DEFAULT_COMMAND_KEYS);
|
|
49
50
|
const REPORTERS = new Set(['text', 'json', 'sarif']);
|
|
50
|
-
const KNOWN_KEY_SET = new Set(KNOWN_KEYS);
|
|
51
51
|
const SORT_CATEGORIES = new Set(DEFAULT_SORT_ORDER);
|
|
52
|
-
function invalidConfig(filename, message) {
|
|
53
|
-
return new Error(`Invalid ${filename}: ${message}`);
|
|
54
|
-
}
|
|
55
52
|
function fail(filename, message) {
|
|
56
|
-
throw
|
|
53
|
+
throw new Error(`Invalid ${filename}: ${message}`);
|
|
54
|
+
}
|
|
55
|
+
function isPlainObject(value) {
|
|
56
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
function assertKnownKeys(cfg, known, label, filename) {
|
|
59
|
+
for (const key of Object.keys(cfg)) {
|
|
60
|
+
if (!known.includes(key)) {
|
|
61
|
+
fail(filename, `${label} "${key}" (expected one of: ${known.join(', ')})`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
57
64
|
}
|
|
58
65
|
function assertPxTokenMap(value, key, filename) {
|
|
59
|
-
if (
|
|
66
|
+
if (!isPlainObject(value)) {
|
|
60
67
|
fail(filename, `${key} must be an object mapping px numbers to token names`);
|
|
61
68
|
}
|
|
62
69
|
for (const [k, v] of Object.entries(value)) {
|
|
@@ -77,7 +84,7 @@ function assertRegExpArray(value, key, filename) {
|
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
function assertStringRecord(value, key, filename) {
|
|
80
|
-
if (
|
|
87
|
+
if (!isPlainObject(value)) {
|
|
81
88
|
fail(filename, `${key} must be an object mapping strings to strings`);
|
|
82
89
|
}
|
|
83
90
|
for (const [k, v] of Object.entries(value)) {
|
|
@@ -91,40 +98,20 @@ function assertPositiveInteger(value, key, filename) {
|
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
function assertAnalyzeConfig(value, filename) {
|
|
94
|
-
if (
|
|
101
|
+
if (!isPlainObject(value))
|
|
95
102
|
fail(filename, 'analyze must be an object');
|
|
96
|
-
|
|
97
|
-
const cfg = value;
|
|
98
|
-
for (const key of Object.keys(cfg)) {
|
|
99
|
-
if (!ANALYZE_KEY_SET.has(key)) {
|
|
100
|
-
fail(filename, `analyze contains unknown key "${key}" (expected one of: ${ANALYZE_KEYS.join(', ')})`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
+
assertKnownKeys(value, ANALYZE_KEYS, 'analyze contains unknown key', filename);
|
|
103
104
|
for (const key of ANALYZE_KEYS) {
|
|
104
|
-
if (key in
|
|
105
|
-
assertPositiveInteger(
|
|
105
|
+
if (key in value)
|
|
106
|
+
assertPositiveInteger(value[key], `analyze.${key}`, filename);
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
function assertDefaultCommandConfig(value, filename) {
|
|
109
|
-
if (
|
|
110
|
+
if (!isPlainObject(value))
|
|
110
111
|
fail(filename, 'defaultCommand must be an object');
|
|
111
|
-
}
|
|
112
112
|
const cfg = value;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
fail(filename, `defaultCommand contains unknown key "${key}" (expected one of: ${DEFAULT_COMMAND_KEYS.join(', ')})`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
for (const key of [
|
|
119
|
-
'fix',
|
|
120
|
-
'merge',
|
|
121
|
-
'dedup',
|
|
122
|
-
'sort',
|
|
123
|
-
'check',
|
|
124
|
-
'analyze',
|
|
125
|
-
'typos',
|
|
126
|
-
'watch',
|
|
127
|
-
]) {
|
|
113
|
+
assertKnownKeys(cfg, DEFAULT_COMMAND_KEYS, 'defaultCommand contains unknown key', filename);
|
|
114
|
+
for (const key of DEFAULT_COMMAND_BOOLEAN_KEYS) {
|
|
128
115
|
if (key in cfg && typeof cfg[key] !== 'boolean') {
|
|
129
116
|
fail(filename, `defaultCommand.${key} must be a boolean`);
|
|
130
117
|
}
|
|
@@ -153,11 +140,7 @@ export function validateConfig(input, filename = CONFIG_FILENAME) {
|
|
|
153
140
|
fail(filename, 'default export must be an object');
|
|
154
141
|
}
|
|
155
142
|
const cfg = input;
|
|
156
|
-
|
|
157
|
-
if (!KNOWN_KEY_SET.has(key)) {
|
|
158
|
-
fail(filename, `unknown key "${key}" (expected one of: ${KNOWN_KEYS.join(', ')})`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
143
|
+
assertKnownKeys(cfg, KNOWN_KEYS, 'unknown key', filename);
|
|
161
144
|
if ('customTextTokens' in cfg)
|
|
162
145
|
assertPxTokenMap(cfg.customTextTokens, 'customTextTokens', filename);
|
|
163
146
|
if ('customSpacingTokens' in cfg)
|
|
@@ -30,53 +30,6 @@ const POSITION_GROUP = new Set([
|
|
|
30
30
|
'relative',
|
|
31
31
|
'sticky',
|
|
32
32
|
]);
|
|
33
|
-
const SIDE_MAP = {
|
|
34
|
-
p: ['t', 'b', 'l', 'r'],
|
|
35
|
-
m: ['t', 'b', 'l', 'r'],
|
|
36
|
-
border: ['t', 'b', 'l', 'r'],
|
|
37
|
-
inset: ['t', 'b', 'l', 'r'],
|
|
38
|
-
gap: ['t', 'b', 'l', 'r'],
|
|
39
|
-
'scroll-p': ['t', 'b', 'l', 'r'],
|
|
40
|
-
'scroll-m': ['t', 'b', 'l', 'r'],
|
|
41
|
-
px: ['l', 'r'],
|
|
42
|
-
mx: ['l', 'r'],
|
|
43
|
-
'border-x': ['l', 'r'],
|
|
44
|
-
'inset-x': ['l', 'r'],
|
|
45
|
-
'gap-x': ['l', 'r'],
|
|
46
|
-
'scroll-px': ['l', 'r'],
|
|
47
|
-
'scroll-mx': ['l', 'r'],
|
|
48
|
-
py: ['t', 'b'],
|
|
49
|
-
my: ['t', 'b'],
|
|
50
|
-
'border-y': ['t', 'b'],
|
|
51
|
-
'inset-y': ['t', 'b'],
|
|
52
|
-
'gap-y': ['t', 'b'],
|
|
53
|
-
'scroll-py': ['t', 'b'],
|
|
54
|
-
'scroll-my': ['t', 'b'],
|
|
55
|
-
pt: ['t'],
|
|
56
|
-
mt: ['t'],
|
|
57
|
-
'border-t': ['t'],
|
|
58
|
-
top: ['t'],
|
|
59
|
-
'scroll-pt': ['t'],
|
|
60
|
-
'scroll-mt': ['t'],
|
|
61
|
-
pb: ['b'],
|
|
62
|
-
mb: ['b'],
|
|
63
|
-
'border-b': ['b'],
|
|
64
|
-
bottom: ['b'],
|
|
65
|
-
'scroll-pb': ['b'],
|
|
66
|
-
'scroll-mb': ['b'],
|
|
67
|
-
pl: ['l'],
|
|
68
|
-
ml: ['l'],
|
|
69
|
-
'border-l': ['l'],
|
|
70
|
-
left: ['l'],
|
|
71
|
-
'scroll-pl': ['l'],
|
|
72
|
-
'scroll-ml': ['l'],
|
|
73
|
-
pr: ['r'],
|
|
74
|
-
mr: ['r'],
|
|
75
|
-
'border-r': ['r'],
|
|
76
|
-
right: ['r'],
|
|
77
|
-
'scroll-pr': ['r'],
|
|
78
|
-
'scroll-mr': ['r'],
|
|
79
|
-
};
|
|
80
33
|
const FAMILIES = [
|
|
81
34
|
{
|
|
82
35
|
kind: 'p',
|
|
@@ -160,6 +113,20 @@ const FAMILIES = [
|
|
|
160
113
|
regex: /^(scroll-m|scroll-mx|scroll-my|scroll-mt|scroll-mb|scroll-ml|scroll-mr)-(.+)$/,
|
|
161
114
|
},
|
|
162
115
|
];
|
|
116
|
+
const SIDE_MAP = {};
|
|
117
|
+
for (const f of FAMILIES) {
|
|
118
|
+
const entries = [
|
|
119
|
+
[f.full, ['t', 'b', 'l', 'r']],
|
|
120
|
+
[f.x, ['l', 'r']],
|
|
121
|
+
[f.y, ['t', 'b']],
|
|
122
|
+
[f.t, ['t']],
|
|
123
|
+
[f.b, ['b']],
|
|
124
|
+
[f.l, ['l']],
|
|
125
|
+
[f.r, ['r']],
|
|
126
|
+
];
|
|
127
|
+
for (const [prefix, sides] of entries)
|
|
128
|
+
SIDE_MAP[prefix] ??= sides;
|
|
129
|
+
}
|
|
163
130
|
function parseBoxClass(cls) {
|
|
164
131
|
for (const family of FAMILIES) {
|
|
165
132
|
const m = cls.match(family.regex);
|
|
@@ -207,39 +174,27 @@ function collapseBox(box, f) {
|
|
|
207
174
|
}
|
|
208
175
|
return parts.join(' ');
|
|
209
176
|
}
|
|
177
|
+
const CORNER_MAP = {
|
|
178
|
+
'': ['tl', 'tr', 'bl', 'br'],
|
|
179
|
+
t: ['tl', 'tr'],
|
|
180
|
+
b: ['bl', 'br'],
|
|
181
|
+
l: ['tl', 'bl'],
|
|
182
|
+
r: ['tr', 'br'],
|
|
183
|
+
tl: ['tl'],
|
|
184
|
+
tr: ['tr'],
|
|
185
|
+
bl: ['bl'],
|
|
186
|
+
br: ['br'],
|
|
187
|
+
};
|
|
210
188
|
function parseRoundedCorner(cls) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
else if (side === 'b') {
|
|
221
|
-
c.bl = val;
|
|
222
|
-
c.br = val;
|
|
223
|
-
}
|
|
224
|
-
else if (side === 'l') {
|
|
225
|
-
c.tl = val;
|
|
226
|
-
c.bl = val;
|
|
227
|
-
}
|
|
228
|
-
else if (side === 'r') {
|
|
229
|
-
c.tr = val;
|
|
230
|
-
c.br = val;
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
c[side] = val;
|
|
234
|
-
}
|
|
235
|
-
return { corners: c };
|
|
236
|
-
}
|
|
237
|
-
const allM = cls.match(/^rounded(?:-(.+))?$/);
|
|
238
|
-
if (allM) {
|
|
239
|
-
const val = allM[1] ?? '';
|
|
240
|
-
return { corners: { tl: val, tr: val, bl: val, br: val } };
|
|
241
|
-
}
|
|
242
|
-
return null;
|
|
189
|
+
const m = cls.match(/^rounded-(tl|tr|bl|br|t|b|l|r)(?:-(.+))?$/) ??
|
|
190
|
+
cls.match(/^rounded()(?:-(.+))?$/);
|
|
191
|
+
if (!m)
|
|
192
|
+
return null;
|
|
193
|
+
const value = m[2] ?? '';
|
|
194
|
+
const corners = {};
|
|
195
|
+
for (const corner of CORNER_MAP[m[1]])
|
|
196
|
+
corners[corner] = value;
|
|
197
|
+
return corners;
|
|
243
198
|
}
|
|
244
199
|
function collapseCorners(c) {
|
|
245
200
|
const { tl, tr, bl, br } = c;
|
|
@@ -310,132 +265,117 @@ const BP_RANK = {
|
|
|
310
265
|
'2xl': 5,
|
|
311
266
|
};
|
|
312
267
|
function collapseResponsiveCascade(classes) {
|
|
313
|
-
// Last-wins keyword utilities (display, position) share a single CSS property
|
|
314
|
-
// even though their class names differ, so they must group by property — not
|
|
315
|
-
// by class name — or an intermediate override (e.g. md:block between flex and
|
|
316
|
-
// lg:flex) is missed and the restoring class is wrongly dropped.
|
|
317
|
-
const propertyKey = (base) => {
|
|
318
|
-
if (DISPLAY_GROUP.has(base))
|
|
319
|
-
return 'display';
|
|
320
|
-
if (POSITION_GROUP.has(base))
|
|
321
|
-
return 'position';
|
|
322
|
-
const dash = base.lastIndexOf('-');
|
|
323
|
-
return dash === -1 ? base : base.slice(0, dash);
|
|
324
|
-
};
|
|
325
268
|
const entries = classes.map((cls, idx) => {
|
|
326
269
|
for (const bp of RESPONSIVE_BREAKPOINTS) {
|
|
327
270
|
const prefix = `${bp}:`;
|
|
328
271
|
if (cls.startsWith(prefix) && !cls.slice(prefix.length).includes(':')) {
|
|
329
272
|
const base = cls.slice(prefix.length);
|
|
330
|
-
return { bp, base,
|
|
273
|
+
return { bp, base, idx };
|
|
331
274
|
}
|
|
332
275
|
}
|
|
333
|
-
return { bp: '', base: cls,
|
|
276
|
+
return { bp: '', base: cls, idx };
|
|
334
277
|
});
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
groups.set(e.key, [e]);
|
|
342
|
-
}
|
|
278
|
+
// Unknown variants cannot be ordered against the standard breakpoints safely.
|
|
279
|
+
if (entries.some((entry) => entry.base.includes(':')))
|
|
280
|
+
return classes;
|
|
281
|
+
if (!entries.some((entry) => entry.bp !== ''))
|
|
282
|
+
return classes;
|
|
283
|
+
const sorted = [...entries].sort((a, b) => BP_RANK[a.bp] - BP_RANK[b.bp] || a.idx - b.idx);
|
|
343
284
|
const toRemove = new Set();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (entry.base === prev) {
|
|
351
|
-
toRemove.add(entry.idx);
|
|
352
|
-
}
|
|
353
|
-
else {
|
|
354
|
-
prev = entry.base;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
285
|
+
let prev = null;
|
|
286
|
+
for (const entry of sorted) {
|
|
287
|
+
if (entry.base === prev)
|
|
288
|
+
toRemove.add(entry.idx);
|
|
289
|
+
else
|
|
290
|
+
prev = entry.base;
|
|
357
291
|
}
|
|
358
292
|
return classes.filter((_, i) => !toRemove.has(i));
|
|
359
293
|
}
|
|
360
294
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
295
|
+
// Survivors keep their slot; new shorthands take the slot of the first token
|
|
296
|
+
// they replace, so an unchanged group leaves the string byte-for-byte intact.
|
|
297
|
+
function placeCollapsed(slots, classes, indices, collapsed) {
|
|
298
|
+
const firstIndex = new Map();
|
|
299
|
+
for (const i of indices) {
|
|
300
|
+
if (!firstIndex.has(classes[i]))
|
|
301
|
+
firstIndex.set(classes[i], i);
|
|
302
|
+
}
|
|
303
|
+
const kept = new Set();
|
|
304
|
+
const fresh = [];
|
|
305
|
+
for (const token of collapsed.split(' ')) {
|
|
306
|
+
const i = firstIndex.get(token);
|
|
307
|
+
if (i === undefined)
|
|
308
|
+
fresh.push(token);
|
|
309
|
+
else
|
|
310
|
+
kept.add(i);
|
|
311
|
+
}
|
|
312
|
+
for (const i of indices)
|
|
313
|
+
slots[i] = kept.has(i) ? [classes[i]] : [];
|
|
314
|
+
if (fresh.length === 0)
|
|
315
|
+
return;
|
|
316
|
+
const anchor = indices.find((i) => !kept.has(i)) ?? indices[0];
|
|
317
|
+
slots[anchor] = [...slots[anchor], ...fresh];
|
|
318
|
+
}
|
|
319
|
+
function keepLastOf(slots, indices) {
|
|
320
|
+
for (const i of indices.slice(0, -1))
|
|
321
|
+
slots[i] = [];
|
|
364
322
|
}
|
|
365
323
|
export function deduplicateClasses(classStr) {
|
|
366
|
-
|
|
367
|
-
if (
|
|
324
|
+
const original = classStr.split(/\s+/).filter(Boolean);
|
|
325
|
+
if (original.length <= 1)
|
|
368
326
|
return classStr;
|
|
369
|
-
classes = collapseResponsiveCascade(
|
|
370
|
-
|
|
371
|
-
|
|
327
|
+
const classes = collapseResponsiveCascade(original);
|
|
328
|
+
const slots = classes.map((cls) => [cls]);
|
|
329
|
+
const displayIndices = [];
|
|
330
|
+
const positionIndices = [];
|
|
372
331
|
const seen = new Set();
|
|
373
|
-
const others = [];
|
|
374
332
|
const boxGroups = new Map();
|
|
375
333
|
const corners = { tl: null, tr: null, bl: null, br: null };
|
|
376
|
-
const
|
|
377
|
-
|
|
334
|
+
const cornerIndices = [];
|
|
335
|
+
classes.forEach((cls, idx) => {
|
|
378
336
|
if (DISPLAY_GROUP.has(cls)) {
|
|
379
|
-
|
|
380
|
-
|
|
337
|
+
displayIndices.push(idx);
|
|
338
|
+
return;
|
|
381
339
|
}
|
|
382
340
|
if (POSITION_GROUP.has(cls)) {
|
|
383
|
-
|
|
384
|
-
|
|
341
|
+
positionIndices.push(idx);
|
|
342
|
+
return;
|
|
385
343
|
}
|
|
386
344
|
const parsed = parseBoxClass(cls);
|
|
387
345
|
if (parsed) {
|
|
388
346
|
const { family, sides } = parsed;
|
|
389
347
|
let group = boxGroups.get(family.kind);
|
|
390
348
|
if (!group) {
|
|
391
|
-
group = { family, box: { t: '', b: '', l: '', r: '' },
|
|
349
|
+
group = { family, box: { t: '', b: '', l: '', r: '' }, indices: [] };
|
|
392
350
|
boxGroups.set(family.kind, group);
|
|
393
351
|
}
|
|
394
|
-
group.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (sides.b !== undefined)
|
|
398
|
-
group.box.b = sides.b;
|
|
399
|
-
if (sides.l !== undefined)
|
|
400
|
-
group.box.l = sides.l;
|
|
401
|
-
if (sides.r !== undefined)
|
|
402
|
-
group.box.r = sides.r;
|
|
403
|
-
continue;
|
|
352
|
+
group.indices.push(idx);
|
|
353
|
+
Object.assign(group.box, sides);
|
|
354
|
+
return;
|
|
404
355
|
}
|
|
405
|
-
const
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
corners.tl = c.tl;
|
|
411
|
-
if (c.tr !== undefined)
|
|
412
|
-
corners.tr = c.tr;
|
|
413
|
-
if (c.bl !== undefined)
|
|
414
|
-
corners.bl = c.bl;
|
|
415
|
-
if (c.br !== undefined)
|
|
416
|
-
corners.br = c.br;
|
|
417
|
-
continue;
|
|
356
|
+
const roundedCorners = parseRoundedCorner(cls);
|
|
357
|
+
if (roundedCorners) {
|
|
358
|
+
cornerIndices.push(idx);
|
|
359
|
+
Object.assign(corners, roundedCorners);
|
|
360
|
+
return;
|
|
418
361
|
}
|
|
419
|
-
if (
|
|
362
|
+
if (seen.has(cls))
|
|
363
|
+
slots[idx] = [];
|
|
364
|
+
else
|
|
420
365
|
seen.add(cls);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
result.push(displayWinner);
|
|
427
|
-
if (positionWinner)
|
|
428
|
-
result.push(positionWinner);
|
|
429
|
-
for (const { family, box, classes: groupClasses } of boxGroups.values()) {
|
|
430
|
-
if (groupClasses.length === 0)
|
|
431
|
-
continue;
|
|
432
|
-
pushCollapsed(result, collapseBox(box, family), groupClasses);
|
|
366
|
+
});
|
|
367
|
+
keepLastOf(slots, displayIndices);
|
|
368
|
+
keepLastOf(slots, positionIndices);
|
|
369
|
+
for (const { family, box, indices } of boxGroups.values()) {
|
|
370
|
+
placeCollapsed(slots, classes, indices, collapseBox(box, family));
|
|
433
371
|
}
|
|
434
|
-
if (
|
|
435
|
-
|
|
372
|
+
if (cornerIndices.length > 0) {
|
|
373
|
+
placeCollapsed(slots, classes, cornerIndices, collapseCorners(corners));
|
|
436
374
|
}
|
|
437
|
-
result.
|
|
438
|
-
|
|
375
|
+
const result = slots.flat();
|
|
376
|
+
const unchanged = result.length === original.length &&
|
|
377
|
+
result.every((cls, i) => cls === original[i]);
|
|
378
|
+
return unchanged ? classStr : result.join(' ');
|
|
439
379
|
}
|
|
440
380
|
export function dedupeContent(content, opts = {}) {
|
|
441
381
|
return replaceClassStrings(content, deduplicateClasses, {
|
package/dist/core/merger.js
CHANGED
|
@@ -38,9 +38,15 @@ function isUnsafeTextSize(base, customTextClasses) {
|
|
|
38
38
|
* `cls`'s own position — a leading declared after it never applied to it.
|
|
39
39
|
*/
|
|
40
40
|
function lastLeadingBefore(originalClasses, cls, mergedTokens) {
|
|
41
|
-
// twMerge
|
|
42
|
-
//
|
|
43
|
-
|
|
41
|
+
// twMerge keeps the last occurrence of repeated class tokens. Use that
|
|
42
|
+
// occurrence when restoring its preceding leading utility.
|
|
43
|
+
let at = -1;
|
|
44
|
+
for (let i = originalClasses.length - 1; i >= 0; i -= 1) {
|
|
45
|
+
if (originalClasses[i].token === cls.token) {
|
|
46
|
+
at = i;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
44
50
|
for (let i = at - 1; i >= 0; i -= 1) {
|
|
45
51
|
const candidate = originalClasses[i];
|
|
46
52
|
if (RE_LEADING.test(candidate.base) &&
|
package/dist/core/rules.js
CHANGED
|
@@ -13,9 +13,7 @@ const TEXT_SIZE_MAP = {
|
|
|
13
13
|
96: '8xl',
|
|
14
14
|
128: '9xl',
|
|
15
15
|
};
|
|
16
|
-
const BUILT_IN_TEXT_PX = new Set(
|
|
17
|
-
12, 14, 16, 18, 20, 24, 30, 36, 48, 60, 72, 96, 128,
|
|
18
|
-
]);
|
|
16
|
+
const BUILT_IN_TEXT_PX = new Set(Object.keys(TEXT_SIZE_MAP).map(Number));
|
|
19
17
|
const ROUNDED_MAP = {
|
|
20
18
|
2: 'sm',
|
|
21
19
|
4: 'sm',
|
|
@@ -163,6 +161,34 @@ function isMissingInV3(prefix, step) {
|
|
|
163
161
|
return step !== 0;
|
|
164
162
|
return !V3_SPACING_STEPS.has(step);
|
|
165
163
|
}
|
|
164
|
+
function textSuggestion(cls, px, textTokens) {
|
|
165
|
+
const token = textTokens[px];
|
|
166
|
+
if (!token)
|
|
167
|
+
return null;
|
|
168
|
+
return {
|
|
169
|
+
original: cls,
|
|
170
|
+
canonical: `text-${token}`,
|
|
171
|
+
isCustomToken: !BUILT_IN_TEXT_PX.has(px),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function spacingSuggestion(cls, prefix, px, spacingTokens, v3) {
|
|
175
|
+
if (spacingTokens[px]) {
|
|
176
|
+
return {
|
|
177
|
+
original: cls,
|
|
178
|
+
canonical: `${prefix}-${spacingTokens[px]}`,
|
|
179
|
+
isCustomToken: true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (px % 4 !== 0)
|
|
183
|
+
return null;
|
|
184
|
+
if (v3 && isMissingInV3(prefix, px / 4))
|
|
185
|
+
return null;
|
|
186
|
+
return {
|
|
187
|
+
original: cls,
|
|
188
|
+
canonical: `${prefix}-${px / 4}`,
|
|
189
|
+
isCustomToken: false,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
166
192
|
export function suggestCanonical(cls, config = {}) {
|
|
167
193
|
if (isIgnored(cls, config.ignorePatterns))
|
|
168
194
|
return null;
|
|
@@ -172,78 +198,27 @@ export function suggestCanonical(cls, config = {}) {
|
|
|
172
198
|
// text-[Npx]
|
|
173
199
|
const textPxMatch = cls.match(/^text-\[(\d+)px\]$/);
|
|
174
200
|
if (textPxMatch) {
|
|
175
|
-
|
|
176
|
-
const token = textTokens[px];
|
|
177
|
-
if (!token)
|
|
178
|
-
return null;
|
|
179
|
-
return {
|
|
180
|
-
original: cls,
|
|
181
|
-
canonical: `text-${token}`,
|
|
182
|
-
isCustomToken: !BUILT_IN_TEXT_PX.has(px),
|
|
183
|
-
};
|
|
201
|
+
return textSuggestion(cls, parseInt(textPxMatch[1], 10), textTokens);
|
|
184
202
|
}
|
|
185
203
|
// text-[N.Nrem]
|
|
186
204
|
const textRemMatch = cls.match(/^text-\[(\d+(?:\.\d+)?)rem\]$/);
|
|
187
205
|
if (textRemMatch) {
|
|
188
206
|
const px = remToPx(parseFloat(textRemMatch[1]));
|
|
189
|
-
|
|
190
|
-
return null;
|
|
191
|
-
const token = textTokens[px];
|
|
192
|
-
if (!token)
|
|
193
|
-
return null;
|
|
194
|
-
return {
|
|
195
|
-
original: cls,
|
|
196
|
-
canonical: `text-${token}`,
|
|
197
|
-
isCustomToken: !BUILT_IN_TEXT_PX.has(px),
|
|
198
|
-
};
|
|
207
|
+
return px === null ? null : textSuggestion(cls, px, textTokens);
|
|
199
208
|
}
|
|
200
209
|
// spacing-[Npx]
|
|
201
210
|
const spacingPxMatch = cls.match(SPACING_PX_RE);
|
|
202
211
|
if (spacingPxMatch) {
|
|
203
|
-
const prefix = spacingPxMatch[1];
|
|
204
212
|
const px = parseInt(spacingPxMatch[2], 10);
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
original: cls,
|
|
208
|
-
canonical: `${prefix}-${spacingTokens[px]}`,
|
|
209
|
-
isCustomToken: true,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
if (px % 4 === 0) {
|
|
213
|
-
if (v3 && isMissingInV3(prefix, px / 4))
|
|
214
|
-
return null;
|
|
215
|
-
return {
|
|
216
|
-
original: cls,
|
|
217
|
-
canonical: `${prefix}-${px / 4}`,
|
|
218
|
-
isCustomToken: false,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
return null;
|
|
213
|
+
return spacingSuggestion(cls, spacingPxMatch[1], px, spacingTokens, v3);
|
|
222
214
|
}
|
|
223
215
|
// spacing-[N.Nrem]
|
|
224
216
|
const spacingRemMatch = cls.match(SPACING_REM_RE);
|
|
225
217
|
if (spacingRemMatch) {
|
|
226
|
-
const prefix = spacingRemMatch[1];
|
|
227
218
|
const px = remToPx(parseFloat(spacingRemMatch[2]));
|
|
228
219
|
if (px === null)
|
|
229
220
|
return null;
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
original: cls,
|
|
233
|
-
canonical: `${prefix}-${spacingTokens[px]}`,
|
|
234
|
-
isCustomToken: true,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
if (px % 4 === 0) {
|
|
238
|
-
if (v3 && isMissingInV3(prefix, px / 4))
|
|
239
|
-
return null;
|
|
240
|
-
return {
|
|
241
|
-
original: cls,
|
|
242
|
-
canonical: `${prefix}-${px / 4}`,
|
|
243
|
-
isCustomToken: false,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
return null;
|
|
221
|
+
return spacingSuggestion(cls, spacingRemMatch[1], px, spacingTokens, v3);
|
|
247
222
|
}
|
|
248
223
|
// w/h/inset/etc-[N.N%]
|
|
249
224
|
const fractionMatch = cls.match(FRACTION_RE);
|
|
@@ -269,9 +244,12 @@ export function suggestCanonical(cls, config = {}) {
|
|
|
269
244
|
// equivalent to opacity-<N> (a percentage), so leave it untouched.
|
|
270
245
|
if (raw > 1)
|
|
271
246
|
return null;
|
|
272
|
-
const
|
|
273
|
-
|
|
247
|
+
const scaled = raw * 100;
|
|
248
|
+
const value = Math.round(scaled);
|
|
249
|
+
if (!(OPACITY_SCALE.has(value) &&
|
|
250
|
+
Math.abs(scaled - value) <= Number.EPSILON * 100)) {
|
|
274
251
|
return null;
|
|
252
|
+
}
|
|
275
253
|
return {
|
|
276
254
|
original: cls,
|
|
277
255
|
canonical: `opacity-${value}`,
|