tailwind-canonical 0.6.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.
Files changed (114) hide show
  1. package/README.md +103 -21
  2. package/dist/cli/cli.d.ts +2 -12
  3. package/dist/cli/cli.js +241 -512
  4. package/dist/cli/format.d.ts +3 -0
  5. package/dist/cli/format.js +11 -0
  6. package/dist/cli/index.d.ts +0 -1
  7. package/dist/cli/index.js +0 -1
  8. package/dist/cli/reporters.d.ts +69 -0
  9. package/dist/cli/reporters.js +333 -0
  10. package/dist/cli/types.d.ts +16 -0
  11. package/dist/cli/types.js +1 -0
  12. package/dist/cli/watch.d.ts +11 -0
  13. package/dist/cli/watch.js +33 -0
  14. package/dist/core/analyzer.d.ts +0 -1
  15. package/dist/core/analyzer.js +3 -3
  16. package/dist/core/class-strings.d.ts +0 -1
  17. package/dist/core/class-strings.js +13 -8
  18. package/dist/core/config.d.ts +2 -3
  19. package/dist/core/config.js +34 -44
  20. package/dist/core/consistency.d.ts +1 -2
  21. package/dist/core/consistency.js +1 -6
  22. package/dist/core/deduplicator.d.ts +0 -1
  23. package/dist/core/deduplicator.js +119 -175
  24. package/dist/core/fixer.d.ts +0 -1
  25. package/dist/core/fixer.js +0 -1
  26. package/dist/core/lexicon.d.ts +1 -1
  27. package/dist/core/lexicon.js +1 -2
  28. package/dist/core/merger.d.ts +0 -1
  29. package/dist/core/merger.js +41 -29
  30. package/dist/core/rules.d.ts +2 -1
  31. package/dist/core/rules.js +90 -57
  32. package/dist/core/scanner.d.ts +0 -1
  33. package/dist/core/scanner.js +23 -21
  34. package/dist/core/sorter.d.ts +0 -1
  35. package/dist/core/sorter.js +85 -10
  36. package/dist/core/suppressions.d.ts +1 -3
  37. package/dist/core/suppressions.js +17 -14
  38. package/dist/core/typos.d.ts +0 -1
  39. package/dist/core/typos.js +3 -3
  40. package/dist/eslint/plugin.d.ts +0 -1
  41. package/dist/eslint/plugin.js +64 -30
  42. package/dist/index.d.ts +1 -2
  43. package/dist/index.js +1 -2
  44. package/dist/io/analyzer.d.ts +0 -1
  45. package/dist/io/analyzer.js +0 -1
  46. package/dist/io/config.d.ts +9 -1
  47. package/dist/io/config.js +66 -5
  48. package/dist/io/consistency.d.ts +0 -1
  49. package/dist/io/consistency.js +0 -1
  50. package/dist/io/deduplicator.d.ts +0 -1
  51. package/dist/io/deduplicator.js +0 -1
  52. package/dist/io/fixer.d.ts +0 -1
  53. package/dist/io/fixer.js +0 -1
  54. package/dist/io/merger.d.ts +0 -1
  55. package/dist/io/merger.js +0 -1
  56. package/dist/io/scanner.d.ts +0 -1
  57. package/dist/io/scanner.js +0 -1
  58. package/dist/io/sorter.d.ts +0 -1
  59. package/dist/io/sorter.js +0 -1
  60. package/dist/io/typos.d.ts +0 -1
  61. package/dist/io/typos.js +0 -1
  62. package/package.json +10 -9
  63. package/dist/cli/cli.d.ts.map +0 -1
  64. package/dist/cli/cli.js.map +0 -1
  65. package/dist/cli/index.d.ts.map +0 -1
  66. package/dist/cli/index.js.map +0 -1
  67. package/dist/core/analyzer.d.ts.map +0 -1
  68. package/dist/core/analyzer.js.map +0 -1
  69. package/dist/core/class-strings.d.ts.map +0 -1
  70. package/dist/core/class-strings.js.map +0 -1
  71. package/dist/core/config.d.ts.map +0 -1
  72. package/dist/core/config.js.map +0 -1
  73. package/dist/core/consistency.d.ts.map +0 -1
  74. package/dist/core/consistency.js.map +0 -1
  75. package/dist/core/deduplicator.d.ts.map +0 -1
  76. package/dist/core/deduplicator.js.map +0 -1
  77. package/dist/core/fixer.d.ts.map +0 -1
  78. package/dist/core/fixer.js.map +0 -1
  79. package/dist/core/lexicon.d.ts.map +0 -1
  80. package/dist/core/lexicon.js.map +0 -1
  81. package/dist/core/merger.d.ts.map +0 -1
  82. package/dist/core/merger.js.map +0 -1
  83. package/dist/core/rules.d.ts.map +0 -1
  84. package/dist/core/rules.js.map +0 -1
  85. package/dist/core/scanner.d.ts.map +0 -1
  86. package/dist/core/scanner.js.map +0 -1
  87. package/dist/core/sorter.d.ts.map +0 -1
  88. package/dist/core/sorter.js.map +0 -1
  89. package/dist/core/suppressions.d.ts.map +0 -1
  90. package/dist/core/suppressions.js.map +0 -1
  91. package/dist/core/typos.d.ts.map +0 -1
  92. package/dist/core/typos.js.map +0 -1
  93. package/dist/eslint/plugin.d.ts.map +0 -1
  94. package/dist/eslint/plugin.js.map +0 -1
  95. package/dist/index.d.ts.map +0 -1
  96. package/dist/index.js.map +0 -1
  97. package/dist/io/analyzer.d.ts.map +0 -1
  98. package/dist/io/analyzer.js.map +0 -1
  99. package/dist/io/config.d.ts.map +0 -1
  100. package/dist/io/config.js.map +0 -1
  101. package/dist/io/consistency.d.ts.map +0 -1
  102. package/dist/io/consistency.js.map +0 -1
  103. package/dist/io/deduplicator.d.ts.map +0 -1
  104. package/dist/io/deduplicator.js.map +0 -1
  105. package/dist/io/fixer.d.ts.map +0 -1
  106. package/dist/io/fixer.js.map +0 -1
  107. package/dist/io/merger.d.ts.map +0 -1
  108. package/dist/io/merger.js.map +0 -1
  109. package/dist/io/scanner.d.ts.map +0 -1
  110. package/dist/io/scanner.js.map +0 -1
  111. package/dist/io/sorter.d.ts.map +0 -1
  112. package/dist/io/sorter.js.map +0 -1
  113. package/dist/io/typos.d.ts.map +0 -1
  114. package/dist/io/typos.js.map +0 -1
@@ -2,7 +2,9 @@ import { DEFAULT_SORT_ORDER } from './sorter.js';
2
2
  const CONFIG_FILENAME = 'tailwind-canonical.config.ts';
3
3
  export const CONFIG_FILENAMES = [
4
4
  CONFIG_FILENAME,
5
+ 'tailwind-canonical.config.mts',
5
6
  'tailwind-canonical.config.js',
7
+ 'tailwind-canonical.config.mjs',
6
8
  ];
7
9
  const KNOWN_KEYS = [
8
10
  'customTextTokens',
@@ -19,6 +21,7 @@ const KNOWN_KEYS = [
19
21
  'rareScaleMaxFiles',
20
22
  'rareScaleMaxCount',
21
23
  'defaultCommand',
24
+ 'tailwindVersion',
22
25
  ];
23
26
  const ANALYZE_KEYS = [
24
27
  'minRareScalePropertyOccurrences',
@@ -29,8 +32,7 @@ const ANALYZE_KEYS = [
29
32
  'maxRareValues',
30
33
  'maxPatterns',
31
34
  ];
32
- const ANALYZE_KEY_SET = new Set(ANALYZE_KEYS);
33
- const DEFAULT_COMMAND_KEYS = [
35
+ const DEFAULT_COMMAND_BOOLEAN_KEYS = [
34
36
  'fix',
35
37
  'merge',
36
38
  'dedup',
@@ -39,21 +41,29 @@ const DEFAULT_COMMAND_KEYS = [
39
41
  'analyze',
40
42
  'typos',
41
43
  'watch',
44
+ ];
45
+ const DEFAULT_COMMAND_KEYS = [
46
+ ...DEFAULT_COMMAND_BOOLEAN_KEYS,
42
47
  'reporter',
43
48
  'targets',
44
49
  ];
45
- const DEFAULT_COMMAND_KEY_SET = new Set(DEFAULT_COMMAND_KEYS);
46
50
  const REPORTERS = new Set(['text', 'json', 'sarif']);
47
- const KNOWN_KEY_SET = new Set(KNOWN_KEYS);
48
51
  const SORT_CATEGORIES = new Set(DEFAULT_SORT_ORDER);
49
- function invalidConfig(filename, message) {
50
- return new Error(`Invalid ${filename}: ${message}`);
51
- }
52
52
  function fail(filename, message) {
53
- throw invalidConfig(filename, message);
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
+ }
54
64
  }
55
65
  function assertPxTokenMap(value, key, filename) {
56
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
66
+ if (!isPlainObject(value)) {
57
67
  fail(filename, `${key} must be an object mapping px numbers to token names`);
58
68
  }
59
69
  for (const [k, v] of Object.entries(value)) {
@@ -74,7 +84,7 @@ function assertRegExpArray(value, key, filename) {
74
84
  }
75
85
  }
76
86
  function assertStringRecord(value, key, filename) {
77
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
87
+ if (!isPlainObject(value)) {
78
88
  fail(filename, `${key} must be an object mapping strings to strings`);
79
89
  }
80
90
  for (const [k, v] of Object.entries(value)) {
@@ -88,40 +98,20 @@ function assertPositiveInteger(value, key, filename) {
88
98
  }
89
99
  }
90
100
  function assertAnalyzeConfig(value, filename) {
91
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
101
+ if (!isPlainObject(value))
92
102
  fail(filename, 'analyze must be an object');
93
- }
94
- const cfg = value;
95
- for (const key of Object.keys(cfg)) {
96
- if (!ANALYZE_KEY_SET.has(key)) {
97
- fail(filename, `analyze contains unknown key "${key}" (expected one of: ${ANALYZE_KEYS.join(', ')})`);
98
- }
99
- }
103
+ assertKnownKeys(value, ANALYZE_KEYS, 'analyze contains unknown key', filename);
100
104
  for (const key of ANALYZE_KEYS) {
101
- if (key in cfg)
102
- assertPositiveInteger(cfg[key], `analyze.${key}`, filename);
105
+ if (key in value)
106
+ assertPositiveInteger(value[key], `analyze.${key}`, filename);
103
107
  }
104
108
  }
105
109
  function assertDefaultCommandConfig(value, filename) {
106
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
110
+ if (!isPlainObject(value))
107
111
  fail(filename, 'defaultCommand must be an object');
108
- }
109
112
  const cfg = value;
110
- for (const key of Object.keys(cfg)) {
111
- if (!DEFAULT_COMMAND_KEY_SET.has(key)) {
112
- fail(filename, `defaultCommand contains unknown key "${key}" (expected one of: ${DEFAULT_COMMAND_KEYS.join(', ')})`);
113
- }
114
- }
115
- for (const key of [
116
- 'fix',
117
- 'merge',
118
- 'dedup',
119
- 'sort',
120
- 'check',
121
- 'analyze',
122
- 'typos',
123
- 'watch',
124
- ]) {
113
+ assertKnownKeys(cfg, DEFAULT_COMMAND_KEYS, 'defaultCommand contains unknown key', filename);
114
+ for (const key of DEFAULT_COMMAND_BOOLEAN_KEYS) {
125
115
  if (key in cfg && typeof cfg[key] !== 'boolean') {
126
116
  fail(filename, `defaultCommand.${key} must be a boolean`);
127
117
  }
@@ -150,11 +140,7 @@ export function validateConfig(input, filename = CONFIG_FILENAME) {
150
140
  fail(filename, 'default export must be an object');
151
141
  }
152
142
  const cfg = input;
153
- for (const key of Object.keys(cfg)) {
154
- if (!KNOWN_KEY_SET.has(key)) {
155
- fail(filename, `unknown key "${key}" (expected one of: ${KNOWN_KEYS.join(', ')})`);
156
- }
157
- }
143
+ assertKnownKeys(cfg, KNOWN_KEYS, 'unknown key', filename);
158
144
  if ('customTextTokens' in cfg)
159
145
  assertPxTokenMap(cfg.customTextTokens, 'customTextTokens', filename);
160
146
  if ('customSpacingTokens' in cfg)
@@ -183,7 +169,11 @@ export function validateConfig(input, filename = CONFIG_FILENAME) {
183
169
  assertPositiveInteger(cfg.rareScaleMaxCount, 'rareScaleMaxCount', filename);
184
170
  if ('defaultCommand' in cfg)
185
171
  assertDefaultCommandConfig(cfg.defaultCommand, filename);
172
+ if ('tailwindVersion' in cfg &&
173
+ cfg.tailwindVersion !== 3 &&
174
+ cfg.tailwindVersion !== 4) {
175
+ fail(filename, 'tailwindVersion must be 3 or 4');
176
+ }
186
177
  return cfg;
187
178
  }
188
- export { loadConfig } from '../io/config.js';
189
- //# sourceMappingURL=config.js.map
179
+ export { loadConfig, resolveInitFilename } from '../io/config.js';
@@ -23,7 +23,7 @@ export type ScaleInconsistency = {
23
23
  property: string;
24
24
  values: ScaleValue[];
25
25
  };
26
- type RareScaleValue = {
26
+ export type RareScaleValue = {
27
27
  property: string;
28
28
  value: string;
29
29
  className: string;
@@ -60,4 +60,3 @@ export declare function analyzeConsistency(input: FileClasses[], options?: Consi
60
60
  export declare function collectClasses(content: string, opts?: ClassStringOpts): string[];
61
61
  export declare function toConsistencyOptions(config?: Config): ConsistencyOptions;
62
62
  export { analyzeConsistencyFiles } from '../io/consistency.js';
63
- //# sourceMappingURL=consistency.d.ts.map
@@ -1,9 +1,5 @@
1
1
  import { extractClassStrings, SINGLE_CLASS_REGEX, } from './class-strings.js';
2
- import { COLOR_FAMILIES, parseColorClass, SCALE_PROPERTIES, TAILWIND_COLORS, } from './lexicon.js';
3
- function stripVariants(cls) {
4
- const idx = cls.lastIndexOf(':');
5
- return idx === -1 ? cls : cls.slice(idx + 1);
6
- }
2
+ import { COLOR_FAMILIES, parseColorClass, SCALE_PROPERTIES, stripVariants, TAILWIND_COLORS, } from './lexicon.js';
7
3
  function parseColor(cls, families, knownColors) {
8
4
  const parsed = parseColorClass(cls);
9
5
  if (!parsed)
@@ -223,4 +219,3 @@ export function toConsistencyOptions(config = {}) {
223
219
  };
224
220
  }
225
221
  export { analyzeConsistencyFiles } from '../io/consistency.js';
226
- //# sourceMappingURL=consistency.js.map
@@ -5,4 +5,3 @@ export declare function dedupeContent(content: string, opts?: ClassStringOpts):
5
5
  count: number;
6
6
  };
7
7
  export { dedupeFile } from '../io/deduplicator.js';
8
- //# sourceMappingURL=deduplicator.d.ts.map
@@ -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 sideM = cls.match(/^rounded-(tl|tr|bl|br|t|b|l|r)(?:-(.+))?$/);
212
- if (sideM) {
213
- const side = sideM[1];
214
- const val = sideM[2] ?? '';
215
- const c = {};
216
- if (side === 't') {
217
- c.tl = val;
218
- c.tr = val;
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;
@@ -300,137 +255,127 @@ function collapseCorners(c) {
300
255
  }
301
256
  // ─── Responsive cascade collapse ─────────────────────────────────────────────
302
257
  const RESPONSIVE_BREAKPOINTS = ['sm', 'md', 'lg', 'xl', '2xl'];
303
- const BP_ORDER = [
304
- '',
305
- ...RESPONSIVE_BREAKPOINTS,
306
- ];
258
+ // Exhaustive by type: adding a breakpoint above fails the build until ranked.
259
+ const BP_RANK = {
260
+ '': 0,
261
+ sm: 1,
262
+ md: 2,
263
+ lg: 3,
264
+ xl: 4,
265
+ '2xl': 5,
266
+ };
307
267
  function collapseResponsiveCascade(classes) {
308
- // Last-wins keyword utilities (display, position) share a single CSS property
309
- // even though their class names differ, so they must group by property — not
310
- // by class name — or an intermediate override (e.g. md:block between flex and
311
- // lg:flex) is missed and the restoring class is wrongly dropped.
312
- const propertyKey = (base) => {
313
- if (DISPLAY_GROUP.has(base))
314
- return 'display';
315
- if (POSITION_GROUP.has(base))
316
- return 'position';
317
- const dash = base.lastIndexOf('-');
318
- return dash === -1 ? base : base.slice(0, dash);
319
- };
320
268
  const entries = classes.map((cls, idx) => {
321
269
  for (const bp of RESPONSIVE_BREAKPOINTS) {
322
270
  const prefix = `${bp}:`;
323
271
  if (cls.startsWith(prefix) && !cls.slice(prefix.length).includes(':')) {
324
272
  const base = cls.slice(prefix.length);
325
- return { bp, base, key: propertyKey(base), idx };
273
+ return { bp, base, idx };
326
274
  }
327
275
  }
328
- return { bp: '', base: cls, key: propertyKey(cls), idx };
276
+ return { bp: '', base: cls, idx };
329
277
  });
330
- const groups = new Map();
331
- for (const e of entries) {
332
- const arr = groups.get(e.key);
333
- if (arr)
334
- arr.push(e);
335
- else
336
- groups.set(e.key, [e]);
337
- }
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);
338
284
  const toRemove = new Set();
339
- for (const group of groups.values()) {
340
- if (group.length < 2 || !group.some((e) => e.bp !== ''))
341
- continue;
342
- const sorted = [...group].sort((a, b) => BP_ORDER.indexOf(a.bp) - BP_ORDER.indexOf(b.bp));
343
- let prev = null;
344
- for (const entry of sorted) {
345
- if (entry.base === prev) {
346
- toRemove.add(entry.idx);
347
- }
348
- else {
349
- prev = entry.base;
350
- }
351
- }
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;
352
291
  }
353
292
  return classes.filter((_, i) => !toRemove.has(i));
354
293
  }
355
294
  // ─── Public API ───────────────────────────────────────────────────────────────
356
- function pushCollapsed(result, collapsed, classes) {
357
- const original = [...new Set(classes)].join(' ');
358
- result.push(...(collapsed === original ? original.split(' ') : collapsed.split(' ')));
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] = [];
359
322
  }
360
323
  export function deduplicateClasses(classStr) {
361
- let classes = classStr.split(/\s+/).filter(Boolean);
362
- if (classes.length <= 1)
324
+ const original = classStr.split(/\s+/).filter(Boolean);
325
+ if (original.length <= 1)
363
326
  return classStr;
364
- classes = collapseResponsiveCascade(classes);
365
- let displayWinner = null;
366
- let positionWinner = null;
327
+ const classes = collapseResponsiveCascade(original);
328
+ const slots = classes.map((cls) => [cls]);
329
+ const displayIndices = [];
330
+ const positionIndices = [];
367
331
  const seen = new Set();
368
- const others = [];
369
332
  const boxGroups = new Map();
370
333
  const corners = { tl: null, tr: null, bl: null, br: null };
371
- const cornersClasses = [];
372
- for (const cls of classes) {
334
+ const cornerIndices = [];
335
+ classes.forEach((cls, idx) => {
373
336
  if (DISPLAY_GROUP.has(cls)) {
374
- displayWinner = cls;
375
- continue;
337
+ displayIndices.push(idx);
338
+ return;
376
339
  }
377
340
  if (POSITION_GROUP.has(cls)) {
378
- positionWinner = cls;
379
- continue;
341
+ positionIndices.push(idx);
342
+ return;
380
343
  }
381
344
  const parsed = parseBoxClass(cls);
382
345
  if (parsed) {
383
346
  const { family, sides } = parsed;
384
347
  let group = boxGroups.get(family.kind);
385
348
  if (!group) {
386
- group = { family, box: { t: '', b: '', l: '', r: '' }, classes: [] };
349
+ group = { family, box: { t: '', b: '', l: '', r: '' }, indices: [] };
387
350
  boxGroups.set(family.kind, group);
388
351
  }
389
- group.classes.push(cls);
390
- if (sides.t !== undefined)
391
- group.box.t = sides.t;
392
- if (sides.b !== undefined)
393
- group.box.b = sides.b;
394
- if (sides.l !== undefined)
395
- group.box.l = sides.l;
396
- if (sides.r !== undefined)
397
- group.box.r = sides.r;
398
- continue;
352
+ group.indices.push(idx);
353
+ Object.assign(group.box, sides);
354
+ return;
399
355
  }
400
- const roundedParsed = parseRoundedCorner(cls);
401
- if (roundedParsed) {
402
- cornersClasses.push(cls);
403
- const { corners: c } = roundedParsed;
404
- if (c.tl !== undefined)
405
- corners.tl = c.tl;
406
- if (c.tr !== undefined)
407
- corners.tr = c.tr;
408
- if (c.bl !== undefined)
409
- corners.bl = c.bl;
410
- if (c.br !== undefined)
411
- corners.br = c.br;
412
- continue;
356
+ const roundedCorners = parseRoundedCorner(cls);
357
+ if (roundedCorners) {
358
+ cornerIndices.push(idx);
359
+ Object.assign(corners, roundedCorners);
360
+ return;
413
361
  }
414
- if (!seen.has(cls)) {
362
+ if (seen.has(cls))
363
+ slots[idx] = [];
364
+ else
415
365
  seen.add(cls);
416
- others.push(cls);
417
- }
418
- }
419
- const result = [];
420
- if (displayWinner)
421
- result.push(displayWinner);
422
- if (positionWinner)
423
- result.push(positionWinner);
424
- for (const { family, box, classes: groupClasses } of boxGroups.values()) {
425
- if (groupClasses.length === 0)
426
- continue;
427
- 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));
428
371
  }
429
- if (cornersClasses.length > 0) {
430
- pushCollapsed(result, collapseCorners(corners), cornersClasses);
372
+ if (cornerIndices.length > 0) {
373
+ placeCollapsed(slots, classes, cornerIndices, collapseCorners(corners));
431
374
  }
432
- result.push(...others);
433
- return result.join(' ');
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(' ');
434
379
  }
435
380
  export function dedupeContent(content, opts = {}) {
436
381
  return replaceClassStrings(content, deduplicateClasses, {
@@ -439,4 +384,3 @@ export function dedupeContent(content, opts = {}) {
439
384
  });
440
385
  }
441
386
  export { dedupeFile } from '../io/deduplicator.js';
442
- //# sourceMappingURL=deduplicator.js.map
@@ -4,4 +4,3 @@ export declare function fixContent(content: string, config?: Config): {
4
4
  count: number;
5
5
  };
6
6
  export { fixFile } from '../io/fixer.js';
7
- //# sourceMappingURL=fixer.d.ts.map
@@ -18,4 +18,3 @@ export function fixContent(content, config = {}) {
18
18
  return { result, count };
19
19
  }
20
20
  export { fixFile } from '../io/fixer.js';
21
- //# sourceMappingURL=fixer.js.map
@@ -1,5 +1,6 @@
1
1
  export declare const TAILWIND_COLORS: Set<string>;
2
2
  export declare const COLOR_FAMILIES: Record<string, string>;
3
+ export declare function stripVariants(cls: string): string;
3
4
  /**
4
5
  * Parses a color utility class into its property, color name and (optional)
5
6
  * numeric shade. Strips leading variants (`hover:` …). Returns `null` when the
@@ -13,4 +14,3 @@ export declare function parseColorClass(cls: string): {
13
14
  shade: string;
14
15
  } | null;
15
16
  export declare const SCALE_PROPERTIES: Set<string>;
16
- //# sourceMappingURL=lexicon.d.ts.map
@@ -70,7 +70,7 @@ export const COLOR_FAMILIES = {
70
70
  neutral: 'gray',
71
71
  stone: 'gray',
72
72
  };
73
- function stripVariants(cls) {
73
+ export function stripVariants(cls) {
74
74
  const idx = cls.lastIndexOf(':');
75
75
  return idx === -1 ? cls : cls.slice(idx + 1);
76
76
  }
@@ -136,4 +136,3 @@ export const SCALE_PROPERTIES = new Set([
136
136
  'space-y',
137
137
  'z',
138
138
  ]);
139
- //# sourceMappingURL=lexicon.js.map
@@ -4,4 +4,3 @@ export declare function mergeContent(content: string, twMerge: (classes: string)
4
4
  count: number;
5
5
  };
6
6
  export { mergeFile } from '../io/merger.js';
7
- //# sourceMappingURL=merger.d.ts.map