tailwind-canonical 0.7.0 → 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 +63 -6
- 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.d.ts +5 -0
- package/dist/core/rules.js +453 -62
- 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.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 {};
|