tailwind-canonical 0.8.0 → 0.10.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 +152 -33
- package/dist/cli/cli.js +77 -7
- package/dist/cli/index.js +1 -1
- package/dist/cli/watch.d.ts +14 -2
- package/dist/cli/watch.js +153 -24
- package/dist/core/class-strings.d.ts +2 -0
- package/dist/core/class-strings.js +622 -96
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +15 -4
- package/dist/core/consistency.d.ts +5 -0
- package/dist/core/consistency.js +26 -19
- package/dist/core/deduplicator.d.ts +1 -1
- package/dist/core/deduplicator.js +35 -7
- package/dist/core/lexicon.d.ts +7 -1
- package/dist/core/lexicon.js +43 -6
- package/dist/core/merger.d.ts +1 -0
- package/dist/core/merger.js +12 -7
- package/dist/core/rules.d.ts +4 -0
- package/dist/core/rules.js +29 -3
- package/dist/core/scanner.d.ts +4 -1
- package/dist/core/scanner.js +1 -50
- package/dist/core/typos.js +4 -5
- package/dist/eslint/plugin.d.ts +1 -1
- package/dist/eslint/plugin.js +114 -30
- package/dist/io/config.d.ts +4 -1
- package/dist/io/config.js +48 -4
- package/dist/io/consistency.js +4 -6
- package/dist/io/scanner.js +68 -21
- package/package.json +10 -7
package/dist/core/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Config } from './rules.js';
|
|
2
2
|
export declare const CONFIG_FILENAMES: readonly ["tailwind-canonical.config.ts", 'tailwind-canonical.config.mts', 'tailwind-canonical.config.js', 'tailwind-canonical.config.mjs'];
|
|
3
3
|
export declare function validateConfig(input: unknown, filename?: string): Config;
|
|
4
|
-
export { loadConfig, resolveInitFilename } from '../io/config.js';
|
|
4
|
+
export { findConfigPath, loadConfig, resolveInitFilename, } from '../io/config.js';
|
package/dist/core/config.js
CHANGED
|
@@ -7,6 +7,8 @@ export const CONFIG_FILENAMES = [
|
|
|
7
7
|
'tailwind-canonical.config.mjs',
|
|
8
8
|
];
|
|
9
9
|
const KNOWN_KEYS = [
|
|
10
|
+
'useDefaultTheme',
|
|
11
|
+
'allowTextSizeLineHeightChanges',
|
|
10
12
|
'customTextTokens',
|
|
11
13
|
'customSpacingTokens',
|
|
12
14
|
'ignorePatterns',
|
|
@@ -53,7 +55,9 @@ function fail(filename, message) {
|
|
|
53
55
|
throw new Error(`Invalid ${filename}: ${message}`);
|
|
54
56
|
}
|
|
55
57
|
function isPlainObject(value) {
|
|
56
|
-
return typeof value === 'object' &&
|
|
58
|
+
return (typeof value === 'object' &&
|
|
59
|
+
value !== null &&
|
|
60
|
+
[Object.prototype, null].includes(Object.getPrototypeOf(value)));
|
|
57
61
|
}
|
|
58
62
|
function assertKnownKeys(cfg, known, label, filename) {
|
|
59
63
|
for (const key of Object.keys(cfg)) {
|
|
@@ -71,6 +75,8 @@ function assertPxTokenMap(value, key, filename) {
|
|
|
71
75
|
fail(filename, `${key} keys must be integers (got "${k}")`);
|
|
72
76
|
if (typeof v !== 'string')
|
|
73
77
|
fail(filename, `${key}[${k}] must be a string`);
|
|
78
|
+
if (v.length === 0 || /[\s"'`{}:\\]/.test(v))
|
|
79
|
+
fail(filename, `${key}[${k}] must be a nonempty token name without whitespace, quotes, backticks, braces, colons or backslashes`);
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
function assertStringArray(value, key, filename) {
|
|
@@ -136,11 +142,16 @@ function assertSortOrder(value, filename) {
|
|
|
136
142
|
export function validateConfig(input, filename = CONFIG_FILENAME) {
|
|
137
143
|
if (input === undefined || input === null)
|
|
138
144
|
return {};
|
|
139
|
-
if (
|
|
140
|
-
fail(filename, 'default export must be an object');
|
|
145
|
+
if (!isPlainObject(input)) {
|
|
146
|
+
fail(filename, 'default export must be an object (plain objects only)');
|
|
141
147
|
}
|
|
142
148
|
const cfg = input;
|
|
143
149
|
assertKnownKeys(cfg, KNOWN_KEYS, 'unknown key', filename);
|
|
150
|
+
if ('useDefaultTheme' in cfg && typeof cfg.useDefaultTheme !== 'boolean')
|
|
151
|
+
fail(filename, 'useDefaultTheme must be a boolean');
|
|
152
|
+
if ('allowTextSizeLineHeightChanges' in cfg &&
|
|
153
|
+
typeof cfg.allowTextSizeLineHeightChanges !== 'boolean')
|
|
154
|
+
fail(filename, 'allowTextSizeLineHeightChanges must be a boolean');
|
|
144
155
|
if ('customTextTokens' in cfg)
|
|
145
156
|
assertPxTokenMap(cfg.customTextTokens, 'customTextTokens', filename);
|
|
146
157
|
if ('customSpacingTokens' in cfg)
|
|
@@ -176,4 +187,4 @@ export function validateConfig(input, filename = CONFIG_FILENAME) {
|
|
|
176
187
|
}
|
|
177
188
|
return cfg;
|
|
178
189
|
}
|
|
179
|
-
export { loadConfig, resolveInitFilename } from '../io/config.js';
|
|
190
|
+
export { findConfigPath, loadConfig, resolveInitFilename, } from '../io/config.js';
|
|
@@ -2,7 +2,12 @@ import { type ClassStringOpts } from './class-strings.js';
|
|
|
2
2
|
import type { Config } from './rules.js';
|
|
3
3
|
export type FileClasses = {
|
|
4
4
|
file: string;
|
|
5
|
+
/** All class occurrences, used for color and scale statistics. */
|
|
5
6
|
classes: string[];
|
|
7
|
+
/** Individual class strings, used for repeated-pattern detection.
|
|
8
|
+
* When omitted, `classes` is treated as one class string for compatibility.
|
|
9
|
+
*/
|
|
10
|
+
classLists?: string[][];
|
|
6
11
|
};
|
|
7
12
|
export type ColorVariant = {
|
|
8
13
|
token: string;
|
package/dist/core/consistency.js
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
import { extractClassStrings, SINGLE_CLASS_REGEX, } from './class-strings.js';
|
|
2
|
-
import { COLOR_FAMILIES, parseColorClass, SCALE_PROPERTIES,
|
|
2
|
+
import { COLOR_FAMILIES, parseColorClass, SCALE_PROPERTIES, splitClassModifiers, TAILWIND_COLORS, } from './lexicon.js';
|
|
3
3
|
function parseColor(cls, families, knownColors) {
|
|
4
4
|
const parsed = parseColorClass(cls);
|
|
5
5
|
if (!parsed)
|
|
6
6
|
return null;
|
|
7
|
-
const { property, color, shade } = parsed;
|
|
7
|
+
const { property, color, shade, opacity } = parsed;
|
|
8
8
|
if (shade === '')
|
|
9
9
|
return null;
|
|
10
10
|
// A known color without an explicit family forms its own family, so colors
|
|
11
11
|
// newly added to the palette are grouped rather than silently dropped.
|
|
12
|
-
const family =
|
|
12
|
+
const family = Object.hasOwn(families, color)
|
|
13
|
+
? families[color]
|
|
14
|
+
: knownColors.has(color)
|
|
15
|
+
? color
|
|
16
|
+
: null;
|
|
13
17
|
if (!family)
|
|
14
18
|
return null;
|
|
15
|
-
return { property, family, token: `${color}-${shade}` };
|
|
19
|
+
return { property, family, token: `${color}-${shade}${opacity}` };
|
|
16
20
|
}
|
|
17
21
|
// A scale value is a number, a fraction, or an arbitrary [..] value. Keyword
|
|
18
22
|
// values (auto, px, full, screen, …) are not grid values, so they are excluded
|
|
19
23
|
// to avoid false "inconsistency" reports like mt-4 vs mt-auto.
|
|
20
24
|
const SCALE_VALUE_RE = /^(\d+(?:\.\d+)?|\d+\/\d+|\[.+\])$/;
|
|
21
25
|
function parseScale(cls, scaleProperties) {
|
|
22
|
-
const base =
|
|
26
|
+
const { utility: base } = splitClassModifiers(cls);
|
|
23
27
|
const negative = base.startsWith('-');
|
|
24
28
|
const body = negative ? base.slice(1) : base;
|
|
25
|
-
const
|
|
29
|
+
const bracket = body.indexOf('[');
|
|
30
|
+
const dash = body.lastIndexOf('-', bracket === -1 ? body.length : bracket);
|
|
26
31
|
if (dash === -1)
|
|
27
32
|
return null;
|
|
28
33
|
const property = body.slice(0, dash);
|
|
@@ -149,20 +154,22 @@ function detectRareScaleValues(inconsistencies, minPropertyOccurrences, maxFiles
|
|
|
149
154
|
// frequent sub-patterns (no k-subset itemset mining).
|
|
150
155
|
function detectCombinations(input, minSize, minFiles, max) {
|
|
151
156
|
const combos = new Map();
|
|
152
|
-
for (const { file, classes } of input) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
for (const { file, classes: allClasses, classLists } of input) {
|
|
158
|
+
for (const classes of classLists ?? [allClasses]) {
|
|
159
|
+
if (classes.length < minSize)
|
|
160
|
+
continue;
|
|
161
|
+
const unique = [...new Set(classes)].sort();
|
|
162
|
+
if (unique.length < minSize)
|
|
163
|
+
continue;
|
|
164
|
+
const key = unique.join(' ');
|
|
165
|
+
let entry = combos.get(key);
|
|
166
|
+
if (!entry) {
|
|
167
|
+
entry = { classes: unique, count: 0, files: new Set() };
|
|
168
|
+
combos.set(key, entry);
|
|
169
|
+
}
|
|
170
|
+
entry.count++;
|
|
171
|
+
entry.files.add(file);
|
|
163
172
|
}
|
|
164
|
-
entry.count++;
|
|
165
|
-
entry.files.add(file);
|
|
166
173
|
}
|
|
167
174
|
return [...combos.values()]
|
|
168
175
|
.filter((c) => c.files.size >= minFiles)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ClassStringOpts } from './class-strings.js';
|
|
2
|
-
export declare function deduplicateClasses(classStr: string): string;
|
|
2
|
+
export declare function deduplicateClasses(classStr: string, opts?: Pick<ClassStringOpts, 'tailwindVersion'>): string;
|
|
3
3
|
export declare function dedupeContent(content: string, opts?: ClassStringOpts): {
|
|
4
4
|
result: string;
|
|
5
5
|
count: number;
|
|
@@ -128,6 +128,10 @@ for (const f of FAMILIES) {
|
|
|
128
128
|
SIDE_MAP[prefix] ??= sides;
|
|
129
129
|
}
|
|
130
130
|
function parseBoxClass(cls) {
|
|
131
|
+
// Important utilities occupy a different cascade priority. Keep their
|
|
132
|
+
// declarations intact instead of mixing their suffix into a box value.
|
|
133
|
+
if (cls.endsWith('!'))
|
|
134
|
+
return null;
|
|
131
135
|
for (const family of FAMILIES) {
|
|
132
136
|
const m = cls.match(family.regex);
|
|
133
137
|
if (!m)
|
|
@@ -135,7 +139,12 @@ function parseBoxClass(cls) {
|
|
|
135
139
|
const sides = {};
|
|
136
140
|
for (const side of SIDE_MAP[m[1]] ?? [])
|
|
137
141
|
sides[side] = m[2];
|
|
138
|
-
|
|
142
|
+
const kind = m[1] === family.full
|
|
143
|
+
? 'full'
|
|
144
|
+
: m[1] === family.x || m[1] === family.y
|
|
145
|
+
? 'axis'
|
|
146
|
+
: 'side';
|
|
147
|
+
return { family, sides, kind };
|
|
139
148
|
}
|
|
140
149
|
return null;
|
|
141
150
|
}
|
|
@@ -186,6 +195,8 @@ const CORNER_MAP = {
|
|
|
186
195
|
br: ['br'],
|
|
187
196
|
};
|
|
188
197
|
function parseRoundedCorner(cls) {
|
|
198
|
+
if (cls.endsWith('!') || /^rounded-(?:s|e|ss|se|es|ee)(?:-|$)/.test(cls))
|
|
199
|
+
return null;
|
|
189
200
|
const m = cls.match(/^rounded-(tl|tr|bl|br|t|b|l|r)(?:-(.+))?$/) ??
|
|
190
201
|
cls.match(/^rounded()(?:-(.+))?$/);
|
|
191
202
|
if (!m)
|
|
@@ -320,7 +331,9 @@ function keepLastOf(slots, indices) {
|
|
|
320
331
|
for (const i of indices.slice(0, -1))
|
|
321
332
|
slots[i] = [];
|
|
322
333
|
}
|
|
323
|
-
export function deduplicateClasses(classStr) {
|
|
334
|
+
export function deduplicateClasses(classStr, opts = {}) {
|
|
335
|
+
if (classStr.includes('${'))
|
|
336
|
+
return classStr;
|
|
324
337
|
const original = classStr.split(/\s+/).filter(Boolean);
|
|
325
338
|
if (original.length <= 1)
|
|
326
339
|
return classStr;
|
|
@@ -343,12 +356,20 @@ export function deduplicateClasses(classStr) {
|
|
|
343
356
|
}
|
|
344
357
|
const parsed = parseBoxClass(cls);
|
|
345
358
|
if (parsed) {
|
|
346
|
-
const { family, sides } = parsed;
|
|
359
|
+
const { family, sides, kind } = parsed;
|
|
347
360
|
let group = boxGroups.get(family.kind);
|
|
348
361
|
if (!group) {
|
|
349
|
-
group = {
|
|
362
|
+
group = {
|
|
363
|
+
family,
|
|
364
|
+
box: { t: '', b: '', l: '', r: '' },
|
|
365
|
+
indices: [],
|
|
366
|
+
hasAxis: false,
|
|
367
|
+
hasSide: false,
|
|
368
|
+
};
|
|
350
369
|
boxGroups.set(family.kind, group);
|
|
351
370
|
}
|
|
371
|
+
group.hasAxis ||= kind === 'axis';
|
|
372
|
+
group.hasSide ||= kind === 'side';
|
|
352
373
|
group.indices.push(idx);
|
|
353
374
|
Object.assign(group.box, sides);
|
|
354
375
|
return;
|
|
@@ -366,8 +387,15 @@ export function deduplicateClasses(classStr) {
|
|
|
366
387
|
});
|
|
367
388
|
keepLastOf(slots, displayIndices);
|
|
368
389
|
keepLastOf(slots, positionIndices);
|
|
369
|
-
for (const { family, box, indices } of boxGroups.values()) {
|
|
370
|
-
|
|
390
|
+
for (const { family, box, indices, hasAxis, hasSide } of boxGroups.values()) {
|
|
391
|
+
// v4's x/y utilities use inline/block properties, unlike the physical
|
|
392
|
+
// t/b/l/r utilities. They cannot be interchanged in vertical writing modes.
|
|
393
|
+
const allEqual = box.t && box.t === box.b && box.t === box.l && box.t === box.r;
|
|
394
|
+
const preservePhysical = opts.tailwindVersion !== 3 && hasSide && (hasAxis || !allEqual);
|
|
395
|
+
const collapsed = preservePhysical
|
|
396
|
+
? indices.map((index) => classes[index]).join(' ')
|
|
397
|
+
: collapseBox(box, family);
|
|
398
|
+
placeCollapsed(slots, classes, indices, collapsed);
|
|
371
399
|
}
|
|
372
400
|
if (cornerIndices.length > 0) {
|
|
373
401
|
placeCollapsed(slots, classes, cornerIndices, collapseCorners(corners));
|
|
@@ -378,7 +406,7 @@ export function deduplicateClasses(classStr) {
|
|
|
378
406
|
return unchanged ? classStr : result.join(' ');
|
|
379
407
|
}
|
|
380
408
|
export function dedupeContent(content, opts = {}) {
|
|
381
|
-
return replaceClassStrings(content, deduplicateClasses, {
|
|
409
|
+
return replaceClassStrings(content, (classes) => deduplicateClasses(classes, opts), {
|
|
382
410
|
...opts,
|
|
383
411
|
isSuppressed: makeLineSuppressor(content),
|
|
384
412
|
});
|
package/dist/core/lexicon.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export declare const TAILWIND_COLORS: Set<string>;
|
|
2
2
|
export declare const COLOR_FAMILIES: Record<string, string>;
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function splitClassModifiers(cls: string): {
|
|
4
|
+
prefix: string;
|
|
5
|
+
utility: string;
|
|
6
|
+
importantBefore: string;
|
|
7
|
+
importantAfter: string;
|
|
8
|
+
};
|
|
4
9
|
/**
|
|
5
10
|
* Parses a color utility class into its property, color name and (optional)
|
|
6
11
|
* numeric shade. Strips leading variants (`hover:` …). Returns `null` when the
|
|
@@ -12,5 +17,6 @@ export declare function parseColorClass(cls: string): {
|
|
|
12
17
|
property: string;
|
|
13
18
|
color: string;
|
|
14
19
|
shade: string;
|
|
20
|
+
opacity: string;
|
|
15
21
|
} | null;
|
|
16
22
|
export declare const SCALE_PROPERTIES: Set<string>;
|
package/dist/core/lexicon.js
CHANGED
|
@@ -31,6 +31,14 @@ const COLOR_PROPERTIES = new Set([
|
|
|
31
31
|
'text',
|
|
32
32
|
'bg',
|
|
33
33
|
'border',
|
|
34
|
+
'border-t',
|
|
35
|
+
'border-b',
|
|
36
|
+
'border-l',
|
|
37
|
+
'border-r',
|
|
38
|
+
'border-x',
|
|
39
|
+
'border-y',
|
|
40
|
+
'border-s',
|
|
41
|
+
'border-e',
|
|
34
42
|
'ring',
|
|
35
43
|
'ring-offset',
|
|
36
44
|
'divide',
|
|
@@ -44,6 +52,8 @@ const COLOR_PROPERTIES = new Set([
|
|
|
44
52
|
'accent',
|
|
45
53
|
'caret',
|
|
46
54
|
'shadow',
|
|
55
|
+
'text-shadow',
|
|
56
|
+
'inset-shadow',
|
|
47
57
|
'placeholder',
|
|
48
58
|
]);
|
|
49
59
|
export const COLOR_FAMILIES = {
|
|
@@ -70,9 +80,32 @@ export const COLOR_FAMILIES = {
|
|
|
70
80
|
neutral: 'gray',
|
|
71
81
|
stone: 'gray',
|
|
72
82
|
};
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
function stripVariants(cls) {
|
|
84
|
+
let depth = 0;
|
|
85
|
+
let colon = -1;
|
|
86
|
+
for (let i = 0; i < cls.length; i++) {
|
|
87
|
+
const ch = cls[i];
|
|
88
|
+
if (ch === '\\')
|
|
89
|
+
i++;
|
|
90
|
+
else if (ch === '[' || ch === '(')
|
|
91
|
+
depth++;
|
|
92
|
+
else if (ch === ']' || ch === ')')
|
|
93
|
+
depth--;
|
|
94
|
+
else if (ch === ':' && depth === 0)
|
|
95
|
+
colon = i;
|
|
96
|
+
}
|
|
97
|
+
return cls.slice(colon + 1);
|
|
98
|
+
}
|
|
99
|
+
export function splitClassModifiers(cls) {
|
|
100
|
+
const base = stripVariants(cls);
|
|
101
|
+
const importantBefore = base.startsWith('!') ? '!' : '';
|
|
102
|
+
const importantAfter = base.endsWith('!') ? '!' : '';
|
|
103
|
+
return {
|
|
104
|
+
prefix: cls.slice(0, cls.length - base.length),
|
|
105
|
+
utility: base.slice(importantBefore.length, base.length - importantAfter.length),
|
|
106
|
+
importantBefore,
|
|
107
|
+
importantAfter,
|
|
108
|
+
};
|
|
76
109
|
}
|
|
77
110
|
/**
|
|
78
111
|
* Parses a color utility class into its property, color name and (optional)
|
|
@@ -82,8 +115,11 @@ export function stripVariants(cls) {
|
|
|
82
115
|
* otherwise `shade` is `''`. Shared by `typos.ts` and `consistency.ts`.
|
|
83
116
|
*/
|
|
84
117
|
export function parseColorClass(cls) {
|
|
85
|
-
const
|
|
86
|
-
|
|
118
|
+
const { utility } = splitClassModifiers(cls);
|
|
119
|
+
const slash = utility.indexOf('/');
|
|
120
|
+
const base = slash === -1 ? utility : utility.slice(0, slash);
|
|
121
|
+
const opacity = slash === -1 ? '' : utility.slice(slash);
|
|
122
|
+
if (base.includes('[') || base.includes('('))
|
|
87
123
|
return null;
|
|
88
124
|
const firstDash = base.indexOf('-');
|
|
89
125
|
if (firstDash === -1)
|
|
@@ -106,9 +142,10 @@ export function parseColorClass(cls) {
|
|
|
106
142
|
property,
|
|
107
143
|
color: rest.slice(0, lastDash),
|
|
108
144
|
shade: rest.slice(lastDash + 1),
|
|
145
|
+
opacity,
|
|
109
146
|
};
|
|
110
147
|
}
|
|
111
|
-
return { property, color: rest, shade: '' };
|
|
148
|
+
return { property, color: rest, shade: '', opacity };
|
|
112
149
|
}
|
|
113
150
|
export const SCALE_PROPERTIES = new Set([
|
|
114
151
|
'p',
|
package/dist/core/merger.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ClassStringOpts } from './class-strings.js';
|
|
2
|
+
export declare function mergeClasses(classes: string, twMerge: (classes: string) => string, opts?: Pick<ClassStringOpts, 'customTextTokens'>): string;
|
|
2
3
|
export declare function mergeContent(content: string, twMerge: (classes: string) => string, opts?: ClassStringOpts): {
|
|
3
4
|
result: string;
|
|
4
5
|
count: number;
|
package/dist/core/merger.js
CHANGED
|
@@ -6,9 +6,9 @@ function splitOutsideBrackets(value, separator) {
|
|
|
6
6
|
let start = 0;
|
|
7
7
|
for (let i = 0; i < value.length; i += 1) {
|
|
8
8
|
const ch = value[i];
|
|
9
|
-
if (ch === '[')
|
|
9
|
+
if (ch === '[' || ch === '(')
|
|
10
10
|
depth += 1;
|
|
11
|
-
else if (ch === ']')
|
|
11
|
+
else if (ch === ']' || ch === ')')
|
|
12
12
|
depth = Math.max(0, depth - 1);
|
|
13
13
|
else if (ch === separator && depth === 0) {
|
|
14
14
|
parts.push(value.slice(start, i));
|
|
@@ -20,14 +20,16 @@ function splitOutsideBrackets(value, separator) {
|
|
|
20
20
|
}
|
|
21
21
|
function parseClassToken(token) {
|
|
22
22
|
const parts = splitOutsideBrackets(token, ':');
|
|
23
|
+
const rawBase = parts.at(-1) ?? token;
|
|
24
|
+
const important = rawBase.startsWith('!') || rawBase.endsWith('!');
|
|
23
25
|
return {
|
|
24
26
|
token,
|
|
25
|
-
modifiers: parts.slice(0, -1).join(':')
|
|
26
|
-
base:
|
|
27
|
+
modifiers: `${parts.slice(0, -1).join(':')}${important ? '!' : ''}`,
|
|
28
|
+
base: rawBase.replace(/^!|!$/g, ''),
|
|
27
29
|
};
|
|
28
30
|
}
|
|
29
31
|
const RE_LEADING = /^!?leading-/;
|
|
30
|
-
const RE_ARBITRARY_TEXT_SIZE =
|
|
32
|
+
const RE_ARBITRARY_TEXT_SIZE = /^text-(?:\[(?:length:.+|-?(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|ch|ex|lh|rlh|vw|vh|vmin|vmax|vi|vb|svw|lvw|dvw|svh|lvh|dvh|%)|(?:calc|min|max|clamp)\(.+\))\]|\(length:--[^)]+\))$/;
|
|
31
33
|
function isUnsafeTextSize(base, customTextClasses) {
|
|
32
34
|
const normalized = base.startsWith('!') ? base.slice(1) : base;
|
|
33
35
|
return (customTextClasses.has(normalized) || RE_ARBITRARY_TEXT_SIZE.test(normalized));
|
|
@@ -84,9 +86,12 @@ function restoreLeadingForUnsafeTextSizes(original, merged, customTextClasses) {
|
|
|
84
86
|
}
|
|
85
87
|
return restored.join(' ');
|
|
86
88
|
}
|
|
87
|
-
export function
|
|
89
|
+
export function mergeClasses(classes, twMerge, opts = {}) {
|
|
88
90
|
const customTextClasses = new Set(Object.values(opts.customTextTokens ?? {}).map((token) => `text-${token}`));
|
|
89
|
-
return
|
|
91
|
+
return restoreLeadingForUnsafeTextSizes(classes, twMerge(classes), customTextClasses);
|
|
92
|
+
}
|
|
93
|
+
export function mergeContent(content, twMerge, opts = {}) {
|
|
94
|
+
return replaceClassStrings(content, (classes) => mergeClasses(classes, twMerge, opts), {
|
|
90
95
|
...opts,
|
|
91
96
|
isSuppressed: makeLineSuppressor(content),
|
|
92
97
|
});
|
package/dist/core/rules.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ type DefaultCommandConfig = {
|
|
|
26
26
|
targets?: string[];
|
|
27
27
|
};
|
|
28
28
|
export type Config = {
|
|
29
|
+
/** Set false for custom themes: only explicitly configured px mappings apply. */
|
|
30
|
+
useDefaultTheme?: boolean;
|
|
31
|
+
/** Opt in to named text sizes, whose bundled line-height can change rendering. */
|
|
32
|
+
allowTextSizeLineHeightChanges?: boolean;
|
|
29
33
|
customTextTokens?: Record<number, string>;
|
|
30
34
|
customSpacingTokens?: Record<number, string>;
|
|
31
35
|
ignorePatterns?: RegExp[];
|
package/dist/core/rules.js
CHANGED
|
@@ -13,7 +13,6 @@ const TEXT_SIZE_MAP = {
|
|
|
13
13
|
96: '8xl',
|
|
14
14
|
128: '9xl',
|
|
15
15
|
};
|
|
16
|
-
const BUILT_IN_TEXT_PX = new Set(Object.keys(TEXT_SIZE_MAP).map(Number));
|
|
17
16
|
/**
|
|
18
17
|
* Radius px → token suffix. v4 renamed the small end (`rounded-sm` → `-xs`,
|
|
19
18
|
* bare `rounded` → `-sm`) and added `4xl`; `''` is the bare v3 `rounded`.
|
|
@@ -343,7 +342,7 @@ function textSuggestion(cls, px, textTokens) {
|
|
|
343
342
|
return {
|
|
344
343
|
original: cls,
|
|
345
344
|
canonical: `text-${token}`,
|
|
346
|
-
isCustomToken:
|
|
345
|
+
isCustomToken: textTokens[px] !== TEXT_SIZE_MAP[px],
|
|
347
346
|
};
|
|
348
347
|
}
|
|
349
348
|
function spacingSuggestion(cls, prefix, px, spacingTokens, v3) {
|
|
@@ -356,7 +355,8 @@ function spacingSuggestion(cls, prefix, px, spacingTokens, v3) {
|
|
|
356
355
|
}
|
|
357
356
|
if (px === 1) {
|
|
358
357
|
if (v3 &&
|
|
359
|
-
(
|
|
358
|
+
(prefix === 'leading' ||
|
|
359
|
+
V3_UNSCALED_SPACING_PREFIXES.has(prefix) ||
|
|
360
360
|
V3_ZERO_ONLY_SPACING_PREFIXES.has(prefix))) {
|
|
361
361
|
return null;
|
|
362
362
|
}
|
|
@@ -466,6 +466,32 @@ export function suggestCanonical(cls, config = {}) {
|
|
|
466
466
|
};
|
|
467
467
|
}
|
|
468
468
|
function suggestUtility(cls, config) {
|
|
469
|
+
if (config.allowTextSizeLineHeightChanges !== true &&
|
|
470
|
+
/^text-\[\d+(?:\.\d+)?(?:px|rem)\]$/.test(cls))
|
|
471
|
+
return null;
|
|
472
|
+
if (config.useDefaultTheme === false) {
|
|
473
|
+
const text = cls.match(/^text-\[(\d+)px\]$/);
|
|
474
|
+
if (text) {
|
|
475
|
+
const token = config.customTextTokens?.[Number(text[1])];
|
|
476
|
+
return token
|
|
477
|
+
? { original: cls, canonical: `text-${token}`, isCustomToken: true }
|
|
478
|
+
: null;
|
|
479
|
+
}
|
|
480
|
+
const spacing = cls.match(SPACING_PX_RE);
|
|
481
|
+
if (spacing) {
|
|
482
|
+
const token = config.customSpacingTokens?.[Number(spacing[2])];
|
|
483
|
+
return token
|
|
484
|
+
? {
|
|
485
|
+
original: cls,
|
|
486
|
+
canonical: `${spacing[1]}-${token}`,
|
|
487
|
+
isCustomToken: true,
|
|
488
|
+
}
|
|
489
|
+
: null;
|
|
490
|
+
}
|
|
491
|
+
// Even rem-to-px equivalences depend on the consumer's root font size.
|
|
492
|
+
// A custom theme must opt in through explicit mappings, never inference.
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
469
495
|
const textTokens = { ...TEXT_SIZE_MAP, ...config.customTextTokens };
|
|
470
496
|
const spacingTokens = config.customSpacingTokens ?? {};
|
|
471
497
|
const v3 = config.tailwindVersion === 3;
|
package/dist/core/scanner.d.ts
CHANGED
|
@@ -3,7 +3,10 @@ export declare const DEFAULT_IGNORE: string[];
|
|
|
3
3
|
export type ScanOptions = {
|
|
4
4
|
extensions?: string[];
|
|
5
5
|
ignore?: string[];
|
|
6
|
+
/** Preserve target interpretations across repeated scans (e.g. watch mode).
|
|
7
|
+
* Keys use forward slashes and omit the leading exclusion marker.
|
|
8
|
+
*/
|
|
9
|
+
targetKinds?: ReadonlyMap<string, 'literal' | 'glob'>;
|
|
6
10
|
};
|
|
7
11
|
export declare function isGlob(target: string): boolean;
|
|
8
|
-
export declare function globToRegex(pattern: string): RegExp;
|
|
9
12
|
export { resolveTargets, scanFiles } from '../io/scanner.js';
|
package/dist/core/scanner.js
CHANGED
|
@@ -15,55 +15,6 @@ export const DEFAULT_IGNORE = [
|
|
|
15
15
|
'coverage',
|
|
16
16
|
];
|
|
17
17
|
export function isGlob(target) {
|
|
18
|
-
return
|
|
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
|
-
}
|
|
29
|
-
export function globToRegex(pattern) {
|
|
30
|
-
let i = 0;
|
|
31
|
-
const anchored = pattern.startsWith('/') || /^[A-Za-z]:/.test(pattern);
|
|
32
|
-
let re = anchored ? '^' : '(?:^|/)';
|
|
33
|
-
while (i < pattern.length) {
|
|
34
|
-
const c = pattern[i];
|
|
35
|
-
if (c === '*' && pattern[i + 1] === '*') {
|
|
36
|
-
i += 2;
|
|
37
|
-
if (pattern[i] === '/') {
|
|
38
|
-
re += '(?:[^/]+/)*';
|
|
39
|
-
i++;
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
re += '.*';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
else if (c === '*') {
|
|
46
|
-
re += '[^/]*';
|
|
47
|
-
i++;
|
|
48
|
-
}
|
|
49
|
-
else if (c === '?') {
|
|
50
|
-
re += '[^/]';
|
|
51
|
-
i++;
|
|
52
|
-
}
|
|
53
|
-
else if (c === '{' && pattern.indexOf('}', i) !== -1) {
|
|
54
|
-
const close = pattern.indexOf('}', i);
|
|
55
|
-
const alts = pattern
|
|
56
|
-
.slice(i + 1, close)
|
|
57
|
-
.split(',')
|
|
58
|
-
.map(wildcardToRegex);
|
|
59
|
-
re += `(?:${alts.join('|')})`;
|
|
60
|
-
i = close + 1;
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
re += escapeRegex(c);
|
|
64
|
-
i++;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return new RegExp(`${re}$`);
|
|
18
|
+
return /[*?{[(]/.test(target);
|
|
68
19
|
}
|
|
69
20
|
export { resolveTargets, scanFiles } from '../io/scanner.js';
|
package/dist/core/typos.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { extractClassStrings, SINGLE_CLASS_REGEX, toClassStringOpts, } from './class-strings.js';
|
|
2
|
-
import { parseColorClass, TAILWIND_COLORS } from './lexicon.js';
|
|
2
|
+
import { parseColorClass, splitClassModifiers, TAILWIND_COLORS, } from './lexicon.js';
|
|
3
3
|
import { createPositionLookup, getSuppressedLines } from './suppressions.js';
|
|
4
4
|
const MIN_COLOR_LEN = 3;
|
|
5
5
|
function levenshtein(a, b) {
|
|
@@ -28,18 +28,17 @@ function nearestColor(name) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
export function detectTypo(cls, extraColors = new Set()) {
|
|
31
|
-
const
|
|
32
|
-
const prefix = colon === -1 ? '' : cls.slice(0, colon + 1);
|
|
31
|
+
const { prefix, importantBefore, importantAfter } = splitClassModifiers(cls);
|
|
33
32
|
const parsed = parseColorClass(cls);
|
|
34
33
|
if (!parsed)
|
|
35
34
|
return null;
|
|
36
|
-
const { property, color, shade } = parsed;
|
|
35
|
+
const { property, color, shade, opacity } = parsed;
|
|
37
36
|
if (TAILWIND_COLORS.has(color) || extraColors.has(color))
|
|
38
37
|
return null;
|
|
39
38
|
const near = nearestColor(color);
|
|
40
39
|
if (!near)
|
|
41
40
|
return null;
|
|
42
|
-
const suggestion = `${prefix}${property}-${near}${shade ? `-${shade}` : ''}`;
|
|
41
|
+
const suggestion = `${prefix}${importantBefore}${property}-${near}${shade ? `-${shade}` : ''}${opacity}${importantAfter}`;
|
|
43
42
|
return { suggestion };
|
|
44
43
|
}
|
|
45
44
|
export function analyzeTyposContent(filePath, content, config = {}) {
|