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.
- package/README.md +103 -21
- package/dist/cli/cli.d.ts +2 -12
- package/dist/cli/cli.js +241 -512
- package/dist/cli/format.d.ts +3 -0
- package/dist/cli/format.js +11 -0
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -1
- 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.d.ts +0 -1
- package/dist/core/analyzer.js +3 -3
- package/dist/core/class-strings.d.ts +0 -1
- package/dist/core/class-strings.js +13 -8
- package/dist/core/config.d.ts +2 -3
- package/dist/core/config.js +34 -44
- package/dist/core/consistency.d.ts +1 -2
- package/dist/core/consistency.js +1 -6
- package/dist/core/deduplicator.d.ts +0 -1
- package/dist/core/deduplicator.js +119 -175
- package/dist/core/fixer.d.ts +0 -1
- package/dist/core/fixer.js +0 -1
- package/dist/core/lexicon.d.ts +1 -1
- package/dist/core/lexicon.js +1 -2
- package/dist/core/merger.d.ts +0 -1
- package/dist/core/merger.js +41 -29
- package/dist/core/rules.d.ts +2 -1
- package/dist/core/rules.js +90 -57
- package/dist/core/scanner.d.ts +0 -1
- package/dist/core/scanner.js +23 -21
- package/dist/core/sorter.d.ts +0 -1
- package/dist/core/sorter.js +85 -10
- package/dist/core/suppressions.d.ts +1 -3
- package/dist/core/suppressions.js +17 -14
- package/dist/core/typos.d.ts +0 -1
- package/dist/core/typos.js +3 -3
- package/dist/eslint/plugin.d.ts +0 -1
- package/dist/eslint/plugin.js +64 -30
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/io/analyzer.d.ts +0 -1
- package/dist/io/analyzer.js +0 -1
- package/dist/io/config.d.ts +9 -1
- package/dist/io/config.js +66 -5
- package/dist/io/consistency.d.ts +0 -1
- package/dist/io/consistency.js +0 -1
- package/dist/io/deduplicator.d.ts +0 -1
- package/dist/io/deduplicator.js +0 -1
- package/dist/io/fixer.d.ts +0 -1
- package/dist/io/fixer.js +0 -1
- package/dist/io/merger.d.ts +0 -1
- package/dist/io/merger.js +0 -1
- package/dist/io/scanner.d.ts +0 -1
- package/dist/io/scanner.js +0 -1
- package/dist/io/sorter.d.ts +0 -1
- package/dist/io/sorter.js +0 -1
- package/dist/io/typos.d.ts +0 -1
- package/dist/io/typos.js +0 -1
- package/package.json +10 -9
- package/dist/cli/cli.d.ts.map +0 -1
- package/dist/cli/cli.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/core/analyzer.d.ts.map +0 -1
- package/dist/core/analyzer.js.map +0 -1
- package/dist/core/class-strings.d.ts.map +0 -1
- package/dist/core/class-strings.js.map +0 -1
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/consistency.d.ts.map +0 -1
- package/dist/core/consistency.js.map +0 -1
- package/dist/core/deduplicator.d.ts.map +0 -1
- package/dist/core/deduplicator.js.map +0 -1
- package/dist/core/fixer.d.ts.map +0 -1
- package/dist/core/fixer.js.map +0 -1
- package/dist/core/lexicon.d.ts.map +0 -1
- package/dist/core/lexicon.js.map +0 -1
- package/dist/core/merger.d.ts.map +0 -1
- package/dist/core/merger.js.map +0 -1
- package/dist/core/rules.d.ts.map +0 -1
- package/dist/core/rules.js.map +0 -1
- package/dist/core/scanner.d.ts.map +0 -1
- package/dist/core/scanner.js.map +0 -1
- package/dist/core/sorter.d.ts.map +0 -1
- package/dist/core/sorter.js.map +0 -1
- package/dist/core/suppressions.d.ts.map +0 -1
- package/dist/core/suppressions.js.map +0 -1
- package/dist/core/typos.d.ts.map +0 -1
- package/dist/core/typos.js.map +0 -1
- package/dist/eslint/plugin.d.ts.map +0 -1
- package/dist/eslint/plugin.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/io/analyzer.d.ts.map +0 -1
- package/dist/io/analyzer.js.map +0 -1
- package/dist/io/config.d.ts.map +0 -1
- package/dist/io/config.js.map +0 -1
- package/dist/io/consistency.d.ts.map +0 -1
- package/dist/io/consistency.js.map +0 -1
- package/dist/io/deduplicator.d.ts.map +0 -1
- package/dist/io/deduplicator.js.map +0 -1
- package/dist/io/fixer.d.ts.map +0 -1
- package/dist/io/fixer.js.map +0 -1
- package/dist/io/merger.d.ts.map +0 -1
- package/dist/io/merger.js.map +0 -1
- package/dist/io/scanner.d.ts.map +0 -1
- package/dist/io/scanner.js.map +0 -1
- package/dist/io/sorter.d.ts.map +0 -1
- package/dist/io/sorter.js.map +0 -1
- package/dist/io/typos.d.ts.map +0 -1
- package/dist/io/typos.js.map +0 -1
package/dist/core/merger.js
CHANGED
|
@@ -26,20 +26,38 @@ function parseClassToken(token) {
|
|
|
26
26
|
base: parts.at(-1) ?? token,
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
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) ||
|
|
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,
|
|
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
|
|
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
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
package/dist/core/rules.d.ts
CHANGED
|
@@ -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
|
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',
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
217
|
-
|
|
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
|
package/dist/core/scanner.d.ts
CHANGED
package/dist/core/scanner.js
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
package/dist/core/sorter.d.ts
CHANGED
package/dist/core/sorter.js
CHANGED
|
@@ -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
|
-
|
|
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) =>
|
|
134
|
+
if (segments.some((s) => RE_DARK_PRINT.test(s)))
|
|
132
135
|
return 2;
|
|
133
136
|
return 3;
|
|
134
137
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
30
|
-
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
package/dist/core/typos.d.ts
CHANGED
|
@@ -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
|
package/dist/core/typos.js
CHANGED
|
@@ -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 {
|
|
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 } =
|
|
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
|
package/dist/eslint/plugin.d.ts
CHANGED