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
@@ -26,20 +26,38 @@ function parseClassToken(token) {
26
26
  base: parts.at(-1) ?? token,
27
27
  };
28
28
  }
29
- function isLeadingClass(base) {
30
- return /^!?leading-/.test(base);
31
- }
32
- function isArbitraryTextSize(base) {
33
- return /^!?text-\[-?(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|ch|ex|lh|rlh|vw|vh|vmin|vmax|vi|vb|svw|lvw|dvw|svh|lvh|dvh)\]$/.test(base);
34
- }
35
- function customTextClassSet(customTextTokens) {
36
- return new Set(Object.values(customTextTokens ?? {}).map((token) => `text-${token}`));
37
- }
29
+ const RE_LEADING = /^!?leading-/;
30
+ const RE_ARBITRARY_TEXT_SIZE = /^!?text-\[-?(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|ch|ex|lh|rlh|vw|vh|vmin|vmax|vi|vb|svw|lvw|dvw|svh|lvh|dvh)\]$/;
38
31
  function isUnsafeTextSize(base, customTextClasses) {
39
32
  const normalized = base.startsWith('!') ? base.slice(1) : base;
40
- return customTextClasses.has(normalized) || isArbitraryTextSize(normalized);
33
+ return (customTextClasses.has(normalized) || RE_ARBITRARY_TEXT_SIZE.test(normalized));
34
+ }
35
+ /**
36
+ * Last `leading-*` class declared before `cls` in the original string that
37
+ * twMerge dropped, matching `cls`'s variant modifiers. Scans backwards from
38
+ * `cls`'s own position — a leading declared after it never applied to it.
39
+ */
40
+ function lastLeadingBefore(originalClasses, cls, mergedTokens) {
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
+ }
50
+ for (let i = at - 1; i >= 0; i -= 1) {
51
+ const candidate = originalClasses[i];
52
+ if (RE_LEADING.test(candidate.base) &&
53
+ candidate.modifiers === cls.modifiers &&
54
+ !mergedTokens.has(candidate.token)) {
55
+ return candidate;
56
+ }
57
+ }
58
+ return undefined;
41
59
  }
42
- function restoreLeadingForUnsafeTextSizes(original, merged, opts) {
60
+ function restoreLeadingForUnsafeTextSizes(original, merged, customTextClasses) {
43
61
  const originalClasses = original
44
62
  .split(/\s+/)
45
63
  .filter(Boolean)
@@ -49,34 +67,28 @@ function restoreLeadingForUnsafeTextSizes(original, merged, opts) {
49
67
  .filter(Boolean)
50
68
  .map(parseClassToken);
51
69
  const mergedTokens = new Set(mergedClasses.map(({ token }) => token));
52
- const customTextClasses = customTextClassSet(opts.customTextTokens);
70
+ const keptLeadingModifiers = new Set(mergedClasses
71
+ .filter(({ base }) => RE_LEADING.test(base))
72
+ .map(({ modifiers }) => modifiers));
53
73
  const restored = [];
54
74
  for (const cls of mergedClasses) {
55
- const leading = originalClasses
56
- .slice(0, originalClasses.findIndex(({ token }) => token === cls.token))
57
- .filter((candidate) => isLeadingClass(candidate.base) &&
58
- candidate.modifiers === cls.modifiers &&
59
- !mergedTokens.has(candidate.token))
60
- .at(-1);
61
- if (leading &&
62
- isUnsafeTextSize(cls.base, customTextClasses) &&
63
- !mergedClasses.some((candidate) => isLeadingClass(candidate.base) &&
64
- candidate.modifiers === cls.modifiers)) {
65
- restored.push(leading.token);
66
- mergedTokens.add(leading.token);
75
+ if (isUnsafeTextSize(cls.base, customTextClasses) &&
76
+ !keptLeadingModifiers.has(cls.modifiers)) {
77
+ const leading = lastLeadingBefore(originalClasses, cls, mergedTokens);
78
+ if (leading) {
79
+ restored.push(leading.token);
80
+ mergedTokens.add(leading.token);
81
+ }
67
82
  }
68
83
  restored.push(cls.token);
69
84
  }
70
85
  return restored.join(' ');
71
86
  }
72
87
  export function mergeContent(content, twMerge, opts = {}) {
73
- return replaceClassStrings(content, (classes) => {
74
- const merged = twMerge(classes);
75
- return restoreLeadingForUnsafeTextSizes(classes, merged, opts);
76
- }, {
88
+ const customTextClasses = new Set(Object.values(opts.customTextTokens ?? {}).map((token) => `text-${token}`));
89
+ return replaceClassStrings(content, (classes) => restoreLeadingForUnsafeTextSizes(classes, twMerge(classes), customTextClasses), {
77
90
  ...opts,
78
91
  isSuppressed: makeLineSuppressor(content),
79
92
  });
80
93
  }
81
94
  export { mergeFile } from '../io/merger.js';
82
- //# sourceMappingURL=merger.js.map
@@ -40,7 +40,8 @@ export type Config = {
40
40
  rareScaleMaxFiles?: number;
41
41
  rareScaleMaxCount?: number;
42
42
  defaultCommand?: DefaultCommandConfig;
43
+ /** Target Tailwind major. v3 restricts suggestions to its discrete scale. */
44
+ tailwindVersion?: 3 | 4;
43
45
  };
44
46
  export declare function suggestCanonical(cls: string, config?: Config): Suggestion | null;
45
47
  export {};
46
- //# sourceMappingURL=rules.d.ts.map
@@ -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',
@@ -102,6 +100,47 @@ const FRACTION_RE = new RegExp(`^(${FRACTION_PREFIXES.join('|')})-\\[(\\d+(?:\\.
102
100
  const OPACITY_SCALE = new Set([
103
101
  0, 5, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 95, 100,
104
102
  ]);
103
+ /**
104
+ * Numeric spacing steps the ÷4 rule may emit that Tailwind v3 actually ships.
105
+ * v4 derives every step from `calc(var(--spacing) * n)`, so any integer works
106
+ * there; v3 ships a discrete scale and a class outside it generates no CSS.
107
+ * The ÷4 rule only ever yields integers, so v3's half steps are unreachable
108
+ * and absent here. Measured by compiling tailwindcss@3.3.7.
109
+ */
110
+ const V3_SPACING_STEPS = new Set([
111
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44,
112
+ 48, 52, 56, 60, 64, 72, 80, 96,
113
+ ]);
114
+ /** No numeric spacing scale at all in v3.3, the documented floor. */
115
+ const V3_UNSCALED_SPACING_PREFIXES = new Set(['size']);
116
+ /** Only the zero step exists in v3.3; the scale itself arrived after 3.3. */
117
+ const V3_ZERO_ONLY_SPACING_PREFIXES = new Set(['min-w', 'max-w', 'min-h']);
118
+ const V3_INSET_FRACTIONS = ['1/2', '1/3', '2/3', '1/4', '3/4'];
119
+ const V3_HEIGHT_FRACTIONS = [
120
+ ...V3_INSET_FRACTIONS,
121
+ '1/5',
122
+ '2/5',
123
+ '3/5',
124
+ '4/5',
125
+ '1/6',
126
+ '5/6',
127
+ ];
128
+ /**
129
+ * Fractions each utility ships in v3.3 — the scale differs per utility, not
130
+ * just per family, so this is keyed on both. A prefix absent from this table
131
+ * has no fraction scale at all (`min-w`, `max-w`, `min-h`, `max-h`).
132
+ */
133
+ const V3_FRACTIONS_BY_PREFIX = {
134
+ w: new Set([...V3_HEIGHT_FRACTIONS, '1/12', '11/12']),
135
+ h: new Set(V3_HEIGHT_FRACTIONS),
136
+ inset: new Set(V3_INSET_FRACTIONS),
137
+ top: new Set(V3_INSET_FRACTIONS),
138
+ left: new Set(V3_INSET_FRACTIONS),
139
+ right: new Set(V3_INSET_FRACTIONS),
140
+ bottom: new Set(V3_INSET_FRACTIONS),
141
+ 'translate-x': new Set(V3_INSET_FRACTIONS),
142
+ 'translate-y': new Set(V3_INSET_FRACTIONS),
143
+ };
105
144
  function remToPx(rem) {
106
145
  const px = rem * 16;
107
146
  return Number.isInteger(px) ? px : null;
@@ -114,82 +153,72 @@ function isIgnored(cls, patterns) {
114
153
  return re.test(cls);
115
154
  });
116
155
  }
156
+ /** True when the ÷4 result would be a class v3 never generates. */
157
+ function isMissingInV3(prefix, step) {
158
+ if (V3_UNSCALED_SPACING_PREFIXES.has(prefix))
159
+ return true;
160
+ if (V3_ZERO_ONLY_SPACING_PREFIXES.has(prefix))
161
+ return step !== 0;
162
+ return !V3_SPACING_STEPS.has(step);
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
+ }
117
192
  export function suggestCanonical(cls, config = {}) {
118
193
  if (isIgnored(cls, config.ignorePatterns))
119
194
  return null;
120
195
  const textTokens = { ...TEXT_SIZE_MAP, ...config.customTextTokens };
121
196
  const spacingTokens = config.customSpacingTokens ?? {};
197
+ const v3 = config.tailwindVersion === 3;
122
198
  // text-[Npx]
123
199
  const textPxMatch = cls.match(/^text-\[(\d+)px\]$/);
124
200
  if (textPxMatch) {
125
- const px = parseInt(textPxMatch[1], 10);
126
- const token = textTokens[px];
127
- if (!token)
128
- return null;
129
- return {
130
- original: cls,
131
- canonical: `text-${token}`,
132
- isCustomToken: !BUILT_IN_TEXT_PX.has(px),
133
- };
201
+ return textSuggestion(cls, parseInt(textPxMatch[1], 10), textTokens);
134
202
  }
135
203
  // text-[N.Nrem]
136
204
  const textRemMatch = cls.match(/^text-\[(\d+(?:\.\d+)?)rem\]$/);
137
205
  if (textRemMatch) {
138
206
  const px = remToPx(parseFloat(textRemMatch[1]));
139
- if (px === null)
140
- return null;
141
- const token = textTokens[px];
142
- if (!token)
143
- return null;
144
- return {
145
- original: cls,
146
- canonical: `text-${token}`,
147
- isCustomToken: !BUILT_IN_TEXT_PX.has(px),
148
- };
207
+ return px === null ? null : textSuggestion(cls, px, textTokens);
149
208
  }
150
209
  // spacing-[Npx]
151
210
  const spacingPxMatch = cls.match(SPACING_PX_RE);
152
211
  if (spacingPxMatch) {
153
- const prefix = spacingPxMatch[1];
154
212
  const px = parseInt(spacingPxMatch[2], 10);
155
- if (spacingTokens[px]) {
156
- return {
157
- original: cls,
158
- canonical: `${prefix}-${spacingTokens[px]}`,
159
- isCustomToken: true,
160
- };
161
- }
162
- if (px % 4 === 0) {
163
- return {
164
- original: cls,
165
- canonical: `${prefix}-${px / 4}`,
166
- isCustomToken: false,
167
- };
168
- }
169
- return null;
213
+ return spacingSuggestion(cls, spacingPxMatch[1], px, spacingTokens, v3);
170
214
  }
171
215
  // spacing-[N.Nrem]
172
216
  const spacingRemMatch = cls.match(SPACING_REM_RE);
173
217
  if (spacingRemMatch) {
174
- const prefix = spacingRemMatch[1];
175
218
  const px = remToPx(parseFloat(spacingRemMatch[2]));
176
219
  if (px === null)
177
220
  return null;
178
- if (spacingTokens[px]) {
179
- return {
180
- original: cls,
181
- canonical: `${prefix}-${spacingTokens[px]}`,
182
- isCustomToken: true,
183
- };
184
- }
185
- if (px % 4 === 0) {
186
- return {
187
- original: cls,
188
- canonical: `${prefix}-${px / 4}`,
189
- isCustomToken: false,
190
- };
191
- }
192
- return null;
221
+ return spacingSuggestion(cls, spacingRemMatch[1], px, spacingTokens, v3);
193
222
  }
194
223
  // w/h/inset/etc-[N.N%]
195
224
  const fractionMatch = cls.match(FRACTION_RE);
@@ -199,6 +228,8 @@ export function suggestCanonical(cls, config = {}) {
199
228
  const fraction = FRACTION_MAP[pct];
200
229
  if (!fraction)
201
230
  return null;
231
+ if (v3 && !V3_FRACTIONS_BY_PREFIX[prefix]?.has(fraction))
232
+ return null;
202
233
  return {
203
234
  original: cls,
204
235
  canonical: `${prefix}-${fraction}`,
@@ -213,9 +244,12 @@ export function suggestCanonical(cls, config = {}) {
213
244
  // equivalent to opacity-<N> (a percentage), so leave it untouched.
214
245
  if (raw > 1)
215
246
  return null;
216
- const value = Math.round(raw * 100);
217
- if (!OPACITY_SCALE.has(value))
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)) {
218
251
  return null;
252
+ }
219
253
  return {
220
254
  original: cls,
221
255
  canonical: `opacity-${value}`,
@@ -238,4 +272,3 @@ export function suggestCanonical(cls, config = {}) {
238
272
  }
239
273
  return null;
240
274
  }
241
- //# sourceMappingURL=rules.js.map
@@ -7,4 +7,3 @@ export type ScanOptions = {
7
7
  export declare function isGlob(target: string): boolean;
8
8
  export declare function globToRegex(pattern: string): RegExp;
9
9
  export { resolveTargets, scanFiles } from '../io/scanner.js';
10
- //# sourceMappingURL=scanner.d.ts.map
@@ -17,6 +17,15 @@ export const DEFAULT_IGNORE = [
17
17
  export function isGlob(target) {
18
18
  return target.includes('*') || target.includes('?') || target.includes('{');
19
19
  }
20
+ function escapeRegex(text) {
21
+ return text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
22
+ }
23
+ function wildcardToRegex(text) {
24
+ return text
25
+ .split('*')
26
+ .map((part) => part.split('?').map(escapeRegex).join('[^/]'))
27
+ .join('[^/]*');
28
+ }
20
29
  export function globToRegex(pattern) {
21
30
  let i = 0;
22
31
  const anchored = pattern.startsWith('/') || /^[A-Za-z]:/.test(pattern);
@@ -24,10 +33,14 @@ export function globToRegex(pattern) {
24
33
  while (i < pattern.length) {
25
34
  const c = pattern[i];
26
35
  if (c === '*' && pattern[i + 1] === '*') {
27
- re += '.*';
28
36
  i += 2;
29
- if (pattern[i] === '/')
37
+ if (pattern[i] === '/') {
38
+ re += '(?:[^/]+/)*';
30
39
  i++;
40
+ }
41
+ else {
42
+ re += '.*';
43
+ }
31
44
  }
32
45
  else if (c === '*') {
33
46
  re += '[^/]*';
@@ -37,31 +50,20 @@ export function globToRegex(pattern) {
37
50
  re += '[^/]';
38
51
  i++;
39
52
  }
40
- else if (c === '{') {
53
+ else if (c === '{' && pattern.indexOf('}', i) !== -1) {
41
54
  const close = pattern.indexOf('}', i);
42
- if (close === -1) {
43
- re += '\\{';
44
- i++;
45
- }
46
- else {
47
- const alts = pattern
48
- .slice(i + 1, close)
49
- .split(',')
50
- .map((a) => a.replace(/[.+^${}()|[\]\\]/g, '\\$&'));
51
- re += `(?:${alts.join('|')})`;
52
- i = close + 1;
53
- }
54
- }
55
- else if (/[.+^${}()|[\]\\]/.test(c)) {
56
- re += `\\${c}`;
57
- i++;
55
+ const alts = pattern
56
+ .slice(i + 1, close)
57
+ .split(',')
58
+ .map(wildcardToRegex);
59
+ re += `(?:${alts.join('|')})`;
60
+ i = close + 1;
58
61
  }
59
62
  else {
60
- re += c;
63
+ re += escapeRegex(c);
61
64
  i++;
62
65
  }
63
66
  }
64
67
  return new RegExp(`${re}$`);
65
68
  }
66
69
  export { resolveTargets, scanFiles } from '../io/scanner.js';
67
- //# sourceMappingURL=scanner.js.map
@@ -7,4 +7,3 @@ export declare function sortContent(content: string, opts?: ClassStringOpts, sor
7
7
  count: number;
8
8
  };
9
9
  export { sortFile } from '../io/sorter.js';
10
- //# sourceMappingURL=sorter.d.ts.map
@@ -37,6 +37,7 @@ const TYPOGRAPHY_KEYWORDS = new Set([
37
37
  'antialiased',
38
38
  'subpixel-antialiased',
39
39
  ]);
40
+ const RE_DARK_PRINT = /^(dark|print)$/;
40
41
  const TEXT_SIZES = /^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|left|center|right|justify|start|end)$/;
41
42
  const RE_LAYOUT = /^(overflow|overscroll|object|float|clear|isolation|box|columns)[-]/;
42
43
  const RE_INSET = /^(inset|top|right|bottom|left|z)[-]/;
@@ -98,7 +99,9 @@ function getCategory(cls) {
98
99
  return 'colors';
99
100
  if (base === 'shadow' ||
100
101
  base === 'blur' ||
101
- ['grayscale', 'invert', 'sepia'].includes(base) ||
102
+ base === 'grayscale' ||
103
+ base === 'invert' ||
104
+ base === 'sepia' ||
102
105
  RE_EFFECTS.test(base))
103
106
  return 'effects';
104
107
  if (base === 'transition' || RE_TRANSITIONS.test(base))
@@ -128,15 +131,65 @@ function getVariantOrder(cls) {
128
131
  const segments = cls.slice(0, cls.lastIndexOf(':')).split(':');
129
132
  if (segments.some((s) => RE_BREAKPOINTS.test(s)))
130
133
  return 1;
131
- if (segments.some((s) => /^(dark|print)$/.test(s)))
134
+ if (segments.some((s) => RE_DARK_PRINT.test(s)))
132
135
  return 2;
133
136
  return 3;
134
137
  }
135
- export function sortClasses(classStr, sortOrder) {
136
- const classes = classStr.split(/\s+/).filter(Boolean);
137
- if (classes.length <= 1)
138
- return classStr;
139
- const rank = buildRank(sortOrder?.length ? sortOrder : DEFAULT_SORT_ORDER);
138
+ const RE_ARBITRARY = /\[[^\]]*\]/g;
139
+ const RE_IMPLAUSIBLE_CLASS = /^:|:$|["'`{}?]|^[&|]+$/;
140
+ function isPlausibleClass(token) {
141
+ return !RE_IMPLAUSIBLE_CLASS.test(token.replace(RE_ARBITRARY, '[]'));
142
+ }
143
+ function interpolationEnd(str, from) {
144
+ let depth = 0;
145
+ let quote = null;
146
+ for (let j = from + 1; j < str.length; j++) {
147
+ const ch = str[j];
148
+ if (quote) {
149
+ if (ch === '\\')
150
+ j++;
151
+ else if (ch === quote)
152
+ quote = null;
153
+ }
154
+ else if (ch === '"' || ch === "'" || ch === '`')
155
+ quote = ch;
156
+ else if (ch === '{')
157
+ depth++;
158
+ else if (ch === '}' && --depth === 0)
159
+ return j + 1;
160
+ }
161
+ return str.length;
162
+ }
163
+ function splitAtoms(str) {
164
+ const atoms = [];
165
+ let i = 0;
166
+ while (i < str.length) {
167
+ if (/\s/.test(str[i])) {
168
+ i++;
169
+ continue;
170
+ }
171
+ let j = i;
172
+ let interpolated = false;
173
+ while (j < str.length && !/\s/.test(str[j])) {
174
+ if (str.startsWith('${', j)) {
175
+ interpolated = true;
176
+ j = interpolationEnd(str, j);
177
+ }
178
+ else
179
+ j++;
180
+ }
181
+ const text = str.slice(i, j);
182
+ atoms.push({
183
+ text,
184
+ start: i,
185
+ end: j,
186
+ fixed: interpolated || !isPlausibleClass(text),
187
+ });
188
+ i = j;
189
+ }
190
+ return atoms;
191
+ }
192
+ function sortSegment(classes, rank) {
140
193
  return classes
141
194
  .map((cls, i) => ({
142
195
  cls,
@@ -153,8 +206,31 @@ export function sortClasses(classStr, sortOrder) {
153
206
  return cd;
154
207
  return a.i - b.i;
155
208
  })
156
- .map((x) => x.cls)
157
- .join(' ');
209
+ .map((x) => x.cls);
210
+ }
211
+ export function sortClasses(classStr, sortOrder) {
212
+ const atoms = splitAtoms(classStr);
213
+ if (atoms.length <= 1)
214
+ return classStr;
215
+ const rank = buildRank(sortOrder?.length ? sortOrder : DEFAULT_SORT_ORDER);
216
+ if (!atoms.some((a) => a.fixed))
217
+ return sortSegment(atoms.map((a) => a.text), rank).join(' ');
218
+ const texts = atoms.map((a) => a.text);
219
+ let start = 0;
220
+ for (let k = 0; k <= atoms.length; k++) {
221
+ if (k < atoms.length && !atoms[k].fixed)
222
+ continue;
223
+ const sorted = sortSegment(texts.slice(start, k), rank);
224
+ texts.splice(start, sorted.length, ...sorted);
225
+ start = k + 1;
226
+ }
227
+ let out = '';
228
+ let cursor = 0;
229
+ atoms.forEach((atom, k) => {
230
+ out += classStr.slice(cursor, atom.start) + texts[k];
231
+ cursor = atom.end;
232
+ });
233
+ return out + classStr.slice(cursor);
158
234
  }
159
235
  export function sortContent(content, opts = {}, sortOrder) {
160
236
  return replaceClassStrings(content, (s) => sortClasses(s, sortOrder), {
@@ -163,4 +239,3 @@ export function sortContent(content, opts = {}, sortOrder) {
163
239
  });
164
240
  }
165
241
  export { sortFile } from '../io/sorter.js';
166
- //# sourceMappingURL=sorter.js.map
@@ -1,8 +1,6 @@
1
1
  export declare function getSuppressedLines(content: string): Set<number>;
2
2
  export declare function makeLineSuppressor(content: string): (line: number) => boolean;
3
- export declare function indexToLineCol(content: string, index: number): {
3
+ export declare function createPositionLookup(content: string): (index: number) => {
4
4
  line: number;
5
5
  col: number;
6
6
  };
7
- export declare function lineAt(content: string, offset: number): number;
8
- //# sourceMappingURL=suppressions.d.ts.map
@@ -26,19 +26,22 @@ export function makeLineSuppressor(content) {
26
26
  const suppressed = getSuppressedLines(content);
27
27
  return (line) => suppressed.has(line);
28
28
  }
29
- export function indexToLineCol(content, index) {
30
- let line = 1;
31
- let lastNewline = -1;
32
- const end = Math.min(index, content.length);
33
- for (let i = 0; i < end; i++) {
34
- if (content[i] === '\n') {
35
- line++;
36
- lastNewline = i;
37
- }
29
+ export function createPositionLookup(content) {
30
+ const lineStarts = [0];
31
+ for (let i = 0; i < content.length; i++) {
32
+ if (content[i] === '\n')
33
+ lineStarts.push(i + 1);
38
34
  }
39
- return { line, col: index - lastNewline };
40
- }
41
- export function lineAt(content, offset) {
42
- return indexToLineCol(content, offset).line;
35
+ return (index) => {
36
+ let low = 0;
37
+ let high = lineStarts.length;
38
+ while (low + 1 < high) {
39
+ const mid = Math.floor((low + high) / 2);
40
+ if (lineStarts[mid] <= index)
41
+ low = mid;
42
+ else
43
+ high = mid;
44
+ }
45
+ return { line: low + 1, col: index - lineStarts[low] + 1 };
46
+ };
43
47
  }
44
- //# sourceMappingURL=suppressions.js.map
@@ -11,4 +11,3 @@ export declare function detectTypo(cls: string, extraColors?: ReadonlySet<string
11
11
  } | null;
12
12
  export declare function analyzeTyposContent(filePath: string, content: string, config?: Config): TypoFinding[];
13
13
  export { analyzeTyposFile } from '../io/typos.js';
14
- //# sourceMappingURL=typos.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { extractClassStrings, SINGLE_CLASS_REGEX, toClassStringOpts, } from './class-strings.js';
2
2
  import { parseColorClass, TAILWIND_COLORS } from './lexicon.js';
3
- import { getSuppressedLines, indexToLineCol } from './suppressions.js';
3
+ import { createPositionLookup, getSuppressedLines } from './suppressions.js';
4
4
  const MIN_COLOR_LEN = 3;
5
5
  function levenshtein(a, b) {
6
6
  const m = a.length;
@@ -45,6 +45,7 @@ export function detectTypo(cls, extraColors = new Set()) {
45
45
  export function analyzeTyposContent(filePath, content, config = {}) {
46
46
  const findings = [];
47
47
  const suppressed = getSuppressedLines(content);
48
+ const positionAt = createPositionLookup(content);
48
49
  const opts = toClassStringOpts(config);
49
50
  const extraColors = new Set(config.extraColors ?? []);
50
51
  for (const { value, start } of extractClassStrings(content, opts)) {
@@ -53,7 +54,7 @@ export function analyzeTyposContent(filePath, content, config = {}) {
53
54
  if (!typo)
54
55
  continue;
55
56
  const index = start + (clsMatch.index ?? 0);
56
- const { line, col } = indexToLineCol(content, index);
57
+ const { line, col } = positionAt(index);
57
58
  if (suppressed.has(line))
58
59
  continue;
59
60
  findings.push({
@@ -68,4 +69,3 @@ export function analyzeTyposContent(filePath, content, config = {}) {
68
69
  return findings;
69
70
  }
70
71
  export { analyzeTyposFile } from '../io/typos.js';
71
- //# sourceMappingURL=typos.js.map
@@ -15,4 +15,3 @@ interface TailwindCanonicalPlugin {
15
15
  }
16
16
  declare const plugin: TailwindCanonicalPlugin;
17
17
  export default plugin;
18
- //# sourceMappingURL=plugin.d.ts.map