tailwind-canonical 0.9.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 +80 -33
- package/dist/cli/cli.js +33 -5
- package/dist/cli/watch.d.ts +4 -2
- package/dist/cli/watch.js +15 -6
- package/dist/core/class-strings.d.ts +2 -0
- package/dist/core/class-strings.js +158 -130
- package/dist/core/config.js +11 -3
- package/dist/core/consistency.js +4 -4
- package/dist/core/deduplicator.d.ts +1 -1
- package/dist/core/deduplicator.js +32 -8
- package/dist/core/lexicon.d.ts +7 -1
- package/dist/core/lexicon.js +43 -6
- package/dist/core/merger.js +7 -5
- package/dist/core/rules.d.ts +2 -0
- package/dist/core/rules.js +5 -1
- package/dist/core/scanner.d.ts +4 -0
- package/dist/core/typos.js +4 -5
- package/dist/eslint/plugin.js +9 -2
- package/dist/io/config.js +3 -0
- package/dist/io/scanner.js +48 -17
- package/package.json +4 -3
|
@@ -5,10 +5,11 @@ export function toClassStringOpts(config) {
|
|
|
5
5
|
functionNames: config.functionNames,
|
|
6
6
|
attributeNames: config.attributeNames,
|
|
7
7
|
customTextTokens: config.customTextTokens,
|
|
8
|
+
tailwindVersion: config.tailwindVersion,
|
|
8
9
|
};
|
|
9
10
|
}
|
|
10
11
|
const DEFAULT_ATTR_NAMES = ['className'];
|
|
11
|
-
export const SINGLE_CLASS_REGEX =
|
|
12
|
+
export const SINGLE_CLASS_REGEX = /\S+/g;
|
|
12
13
|
function escapeRegex(s) {
|
|
13
14
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
14
15
|
}
|
|
@@ -16,45 +17,43 @@ function buildAttrRegex(names) {
|
|
|
16
17
|
const alts = names.map(escapeRegex).join('|');
|
|
17
18
|
return new RegExp(`(^|[^\\w:$-])(${alts})\\s*=\\s*`, 'g');
|
|
18
19
|
}
|
|
19
|
-
function attributeStrings(content, names) {
|
|
20
|
+
function attributeStrings(content, names, functionNames = []) {
|
|
20
21
|
const result = [];
|
|
21
22
|
for (const match of content.matchAll(buildAttrRegex(names))) {
|
|
22
23
|
const attributeStart = (match.index ?? 0) + match[1].length;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
const start = (match.index ?? 0) + match[0].length;
|
|
25
|
+
if (content[start] === '{') {
|
|
26
|
+
const expression = readCallTokens(content, start + 1, '}');
|
|
27
|
+
if (expression.complete) {
|
|
28
|
+
result.push(...classExpression(expression.tokens, functionNames).map((string) => ({ ...string, attributeStart })));
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
29
31
|
}
|
|
30
|
-
if (
|
|
32
|
+
if (!['"', "'", '`'].includes(content[start]))
|
|
31
33
|
continue;
|
|
32
|
-
const quoted = readQuotedString(content,
|
|
34
|
+
const quoted = readQuotedString(content, start);
|
|
33
35
|
if (!quoted)
|
|
34
36
|
continue;
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Only a single string expression is static. A concatenation, method
|
|
40
|
-
// call or tagged template can change the meaning of its string pieces.
|
|
41
|
-
if (content[i] !== '}')
|
|
37
|
+
if (match[2].startsWith(':') || match[2].startsWith('v-bind:')) {
|
|
38
|
+
// Vue bindings contain JavaScript, not a class list. Entities require an
|
|
39
|
+
// HTML decoder/source map, so leave those expressions intact for now.
|
|
40
|
+
if (/&(?:#\d+|#x[\da-f]+|[a-z]+);/i.test(quoted.value))
|
|
42
41
|
continue;
|
|
42
|
+
const expression = readCallTokens(content, quoted.start, content[start]);
|
|
43
|
+
if (expression.complete && expression.next === quoted.next) {
|
|
44
|
+
result.push(...classExpression(expression.tokens, functionNames).map((string) => ({ ...string, attributeStart })));
|
|
45
|
+
}
|
|
43
46
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
start: quoted.start,
|
|
47
|
-
end: quoted.end,
|
|
48
|
-
attributeStart,
|
|
49
|
-
});
|
|
47
|
+
else
|
|
48
|
+
result.push({ ...quoted, attributeStart, braceExpressions: true });
|
|
50
49
|
}
|
|
51
50
|
return result;
|
|
52
51
|
}
|
|
53
52
|
// A template expression is JavaScript, never a class list. Keep its adjacent
|
|
54
53
|
// token fragments as well: `text-[12px]${suffix}` is not a static class.
|
|
55
|
-
function interpolationEnd(content, start) {
|
|
54
|
+
function interpolationEnd(content, start, openingLength = 2) {
|
|
56
55
|
let depth = 1;
|
|
57
|
-
let i = start +
|
|
56
|
+
let i = start + openingLength;
|
|
58
57
|
while (i < content.length) {
|
|
59
58
|
const ch = content[i];
|
|
60
59
|
if (ch === '"' || ch === "'" || ch === '`') {
|
|
@@ -72,10 +71,7 @@ function interpolationEnd(content, start) {
|
|
|
72
71
|
i = end < 0 ? content.length : end + 2;
|
|
73
72
|
}
|
|
74
73
|
else if (ch === '/') {
|
|
75
|
-
const preceding = content
|
|
76
|
-
.slice(start + 2, i)
|
|
77
|
-
.trimEnd()
|
|
78
|
-
.match(/(?:[\w$]+|=>|&&|\|\||\?\?|.)$/)?.[0];
|
|
74
|
+
const preceding = previousToken(content, i);
|
|
79
75
|
if (regexMayStart(preceding)) {
|
|
80
76
|
const end = readRegex(content, i);
|
|
81
77
|
if (!end)
|
|
@@ -95,15 +91,27 @@ function interpolationEnd(content, start) {
|
|
|
95
91
|
}
|
|
96
92
|
return content.length;
|
|
97
93
|
}
|
|
98
|
-
function staticClassRanges(value) {
|
|
94
|
+
function staticClassRanges(value, braceExpressions = false) {
|
|
99
95
|
const ranges = [];
|
|
100
96
|
let start = 0;
|
|
101
97
|
let i = 0;
|
|
98
|
+
let bracketDepth = 0;
|
|
102
99
|
while (i < value.length) {
|
|
103
100
|
let escapes = 0;
|
|
104
101
|
for (let j = i - 1; j >= 0 && value[j] === '\\'; j--)
|
|
105
102
|
escapes++;
|
|
106
|
-
|
|
103
|
+
const openingLength = value.startsWith('${', i)
|
|
104
|
+
? 2
|
|
105
|
+
: braceExpressions && bracketDepth === 0 && value[i] === '{'
|
|
106
|
+
? 1
|
|
107
|
+
: 0;
|
|
108
|
+
if (!openingLength || escapes % 2 === 1) {
|
|
109
|
+
if (escapes % 2 === 0) {
|
|
110
|
+
if (value[i] === '[')
|
|
111
|
+
bracketDepth++;
|
|
112
|
+
else if (value[i] === ']')
|
|
113
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
114
|
+
}
|
|
107
115
|
i++;
|
|
108
116
|
continue;
|
|
109
117
|
}
|
|
@@ -112,7 +120,7 @@ function staticClassRanges(value) {
|
|
|
112
120
|
before--;
|
|
113
121
|
if (before > start)
|
|
114
122
|
ranges.push({ value: value.slice(start, before), start, end: before });
|
|
115
|
-
i = interpolationEnd(value, i);
|
|
123
|
+
i = interpolationEnd(value, i, openingLength);
|
|
116
124
|
while (i < value.length && !/\s/.test(value[i]))
|
|
117
125
|
i++;
|
|
118
126
|
start = i;
|
|
@@ -121,7 +129,7 @@ function staticClassRanges(value) {
|
|
|
121
129
|
ranges.push({ value: value.slice(start), start, end: value.length });
|
|
122
130
|
return ranges;
|
|
123
131
|
}
|
|
124
|
-
function transformStaticClasses(value, transform, isSuppressedAt, offset = 0) {
|
|
132
|
+
function transformStaticClasses(value, transform, isSuppressedAt, offset = 0, braceExpressions = false) {
|
|
125
133
|
const suppressed = [];
|
|
126
134
|
if (isSuppressedAt) {
|
|
127
135
|
let lineStart = 0;
|
|
@@ -133,7 +141,7 @@ function transformStaticClasses(value, transform, isSuppressedAt, offset = 0) {
|
|
|
133
141
|
lineStart = end;
|
|
134
142
|
}
|
|
135
143
|
}
|
|
136
|
-
const ranges = staticClassRanges(value).flatMap((range) => {
|
|
144
|
+
const ranges = staticClassRanges(value, braceExpressions).flatMap((range) => {
|
|
137
145
|
const safe = [];
|
|
138
146
|
let start = range.start;
|
|
139
147
|
for (const line of suppressed) {
|
|
@@ -155,7 +163,9 @@ function transformStaticClasses(value, transform, isSuppressedAt, offset = 0) {
|
|
|
155
163
|
});
|
|
156
164
|
return safe;
|
|
157
165
|
});
|
|
158
|
-
const preserveEdges = value.includes('${') ||
|
|
166
|
+
const preserveEdges = value.includes('${') ||
|
|
167
|
+
(braceExpressions && value.includes('{')) ||
|
|
168
|
+
suppressed.length > 0;
|
|
159
169
|
let out = '';
|
|
160
170
|
let pos = 0;
|
|
161
171
|
for (const range of ranges) {
|
|
@@ -258,7 +268,32 @@ const REGEX_PREFIX_TOKENS = new Set([
|
|
|
258
268
|
'await',
|
|
259
269
|
'in',
|
|
260
270
|
'instanceof',
|
|
271
|
+
'else',
|
|
272
|
+
'do',
|
|
273
|
+
'new',
|
|
274
|
+
'of',
|
|
275
|
+
// A slash after a statement block may open a regex. Treat a valid-looking
|
|
276
|
+
// literal conservatively even when an object/division expression is ambiguous.
|
|
277
|
+
'}',
|
|
261
278
|
]);
|
|
279
|
+
// Read only the token immediately before an offset. Searching an entire source
|
|
280
|
+
// prefix with an end-anchored regexp once per helper call is quadratic.
|
|
281
|
+
function previousToken(content, offset) {
|
|
282
|
+
let end = offset;
|
|
283
|
+
while (end > 0 && /\s/.test(content[end - 1]))
|
|
284
|
+
end--;
|
|
285
|
+
if (end === 0)
|
|
286
|
+
return undefined;
|
|
287
|
+
let start = end;
|
|
288
|
+
while (start > 0 && /[\w$]/.test(content[start - 1]))
|
|
289
|
+
start--;
|
|
290
|
+
if (start < end)
|
|
291
|
+
return content.slice(start, end);
|
|
292
|
+
const pair = content.slice(Math.max(0, end - 2), end);
|
|
293
|
+
if (['=>', '&&', '||', '??'].includes(pair))
|
|
294
|
+
return pair;
|
|
295
|
+
return content[end - 1];
|
|
296
|
+
}
|
|
262
297
|
function regexMayStart(previous) {
|
|
263
298
|
return previous === undefined || REGEX_PREFIX_TOKENS.has(previous);
|
|
264
299
|
}
|
|
@@ -353,8 +388,14 @@ function classExpression(tokens, functionNames, objects = true) {
|
|
|
353
388
|
return [token.string];
|
|
354
389
|
if (token.text === '[' && token.children)
|
|
355
390
|
return splitTokens(token.children, ',').flatMap((part) => classExpression(part, functionNames, objects));
|
|
356
|
-
if (token.text === '{' && objects)
|
|
391
|
+
if (token.text === '{' && objects) {
|
|
392
|
+
// Rewriting keys of a multi-property object can create a duplicate key
|
|
393
|
+
// and discard a predicate. Unknown/spread properties can collide too.
|
|
394
|
+
const properties = splitTokens(token.children ?? [], ',').filter((property) => property.length > 0);
|
|
395
|
+
if (properties.length !== 1)
|
|
396
|
+
return [];
|
|
357
397
|
return objectProperties(token).flatMap(({ key }) => key.string ? [key.string] : []);
|
|
398
|
+
}
|
|
358
399
|
// A grouping pair does not establish class context, but preserves one.
|
|
359
400
|
if (token.text === '(' && token.children)
|
|
360
401
|
return classExpression(token.children, functionNames, objects);
|
|
@@ -429,7 +470,7 @@ function classCall(name, tokens, functionNames) {
|
|
|
429
470
|
}
|
|
430
471
|
function scanCallStrings(content, start, functionNames = []) {
|
|
431
472
|
const scanned = readCallTokens(content, start);
|
|
432
|
-
const name = content
|
|
473
|
+
const name = previousToken(content, start - 1) ?? '';
|
|
433
474
|
return {
|
|
434
475
|
strings: scanned.complete
|
|
435
476
|
? classCall(name, scanned.tokens, functionNames)
|
|
@@ -438,26 +479,6 @@ function scanCallStrings(content, start, functionNames = []) {
|
|
|
438
479
|
complete: scanned.complete,
|
|
439
480
|
};
|
|
440
481
|
}
|
|
441
|
-
function transformCallContent(content, start, transform, isSuppressedAt, functionNames = []) {
|
|
442
|
-
const scanned = scanCallStrings(content, start, functionNames);
|
|
443
|
-
const original = content.slice(start, start + scanned.consumed);
|
|
444
|
-
if (!scanned.complete) {
|
|
445
|
-
return { processed: original, consumed: scanned.consumed, count: 0 };
|
|
446
|
-
}
|
|
447
|
-
const parts = [];
|
|
448
|
-
let pos = start;
|
|
449
|
-
let count = 0;
|
|
450
|
-
for (const string of scanned.strings) {
|
|
451
|
-
parts.push(content.slice(pos, string.start));
|
|
452
|
-
const out = transformStaticClasses(string.value, transform, isSuppressedAt, string.start);
|
|
453
|
-
if (out !== string.value)
|
|
454
|
-
count++;
|
|
455
|
-
parts.push(out);
|
|
456
|
-
pos = string.end;
|
|
457
|
-
}
|
|
458
|
-
parts.push(content.slice(pos, start + scanned.consumed));
|
|
459
|
-
return { processed: parts.join(''), consumed: scanned.consumed, count };
|
|
460
|
-
}
|
|
461
482
|
function offsetSuppressor(content, isSuppressed) {
|
|
462
483
|
if (!isSuppressed)
|
|
463
484
|
return undefined;
|
|
@@ -471,6 +492,8 @@ function protectedSourceOffsets(content) {
|
|
|
471
492
|
const tags = [];
|
|
472
493
|
const pendingTags = [];
|
|
473
494
|
let expressionEnd = 0;
|
|
495
|
+
const controlParens = [];
|
|
496
|
+
let lastControlParenEnd = -1;
|
|
474
497
|
let i = 0;
|
|
475
498
|
while (i < content.length) {
|
|
476
499
|
const start = i;
|
|
@@ -488,7 +511,7 @@ function protectedSourceOffsets(content) {
|
|
|
488
511
|
if (!tag && ch === '<' && !content.startsWith('<!--', i)) {
|
|
489
512
|
const match = content
|
|
490
513
|
.slice(i)
|
|
491
|
-
.match(/^<(\/?)([A-Za-z][\w:.-]*)(?=[\s/>])/);
|
|
514
|
+
.match(/^<(\/?)(?:([A-Za-z][\w:.-]*)(?=[\s/>])|(?=>))/);
|
|
492
515
|
if (match) {
|
|
493
516
|
let end = i + match[0].length;
|
|
494
517
|
while (end < content.length && content[end] !== '>') {
|
|
@@ -505,8 +528,9 @@ function protectedSourceOffsets(content) {
|
|
|
505
528
|
end++;
|
|
506
529
|
}
|
|
507
530
|
const closing = match[1] === '/';
|
|
508
|
-
const selfClosing = content[end - 1] === '/'
|
|
509
|
-
|
|
531
|
+
const selfClosing = content[end - 1] === '/' ||
|
|
532
|
+
/^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/.test(match[2] ?? '');
|
|
533
|
+
const name = match[2] ?? '';
|
|
510
534
|
if (end < content.length &&
|
|
511
535
|
(tags.length > 0 ||
|
|
512
536
|
selfClosing ||
|
|
@@ -514,7 +538,18 @@ function protectedSourceOffsets(content) {
|
|
|
514
538
|
pendingTags.push({ end, name, closing, selfClosing });
|
|
515
539
|
}
|
|
516
540
|
}
|
|
517
|
-
|
|
541
|
+
const rawTag = tags.at(-1)?.toLowerCase();
|
|
542
|
+
if (!tag && rawTag === 'style' && !content.startsWith('</style', i)) {
|
|
543
|
+
const end = content.indexOf('</style', i);
|
|
544
|
+
i = end < 0 ? content.length : end;
|
|
545
|
+
ranges.push({ start, end: i });
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (!tag &&
|
|
549
|
+
tags.length > 0 &&
|
|
550
|
+
rawTag !== 'script' &&
|
|
551
|
+
i >= expressionEnd &&
|
|
552
|
+
ch !== '<') {
|
|
518
553
|
if (ch === '{')
|
|
519
554
|
expressionEnd = readCallTokens(content, i + 1, '}').next;
|
|
520
555
|
else {
|
|
@@ -538,11 +573,14 @@ function protectedSourceOffsets(content) {
|
|
|
538
573
|
i = end < 0 ? content.length : end + ending.length;
|
|
539
574
|
}
|
|
540
575
|
else if (ch === '/' && content[i - 1] !== '<' && content[i + 1] !== '>') {
|
|
541
|
-
const preceding = content
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
576
|
+
const preceding = previousToken(content, i);
|
|
577
|
+
let before = i - 1;
|
|
578
|
+
while (before >= 0 && /\s/.test(content[before]))
|
|
579
|
+
before--;
|
|
580
|
+
const afterControlCondition = preceding === ')' && before === lastControlParenEnd;
|
|
581
|
+
const end = regexMayStart(preceding) || afterControlCondition
|
|
582
|
+
? readRegex(content, i)
|
|
583
|
+
: null;
|
|
546
584
|
if (!end) {
|
|
547
585
|
i++;
|
|
548
586
|
continue;
|
|
@@ -550,6 +588,11 @@ function protectedSourceOffsets(content) {
|
|
|
550
588
|
i = end;
|
|
551
589
|
}
|
|
552
590
|
else {
|
|
591
|
+
if (ch === '(') {
|
|
592
|
+
controlParens.push(['if', 'while', 'for', 'with', 'switch', 'catch'].includes(previousToken(content, i) ?? ''));
|
|
593
|
+
}
|
|
594
|
+
else if (ch === ')' && controlParens.pop())
|
|
595
|
+
lastControlParenEnd = i;
|
|
553
596
|
i++;
|
|
554
597
|
continue;
|
|
555
598
|
}
|
|
@@ -568,82 +611,67 @@ function protectedSourceOffsets(content) {
|
|
|
568
611
|
return low < ranges.length && ranges[low].start <= offset;
|
|
569
612
|
};
|
|
570
613
|
}
|
|
571
|
-
|
|
614
|
+
/** Collect disjoint ranges against the original source before any rewrite.
|
|
615
|
+
* Source offsets and suppression line numbers must not depend on an earlier
|
|
616
|
+
* attribute transform changing the number of lines in the file.
|
|
617
|
+
*/
|
|
618
|
+
function collectClassStrings(content, opts) {
|
|
572
619
|
const attrNames = opts.attributeNames?.length
|
|
573
620
|
? opts.attributeNames
|
|
574
621
|
: DEFAULT_ATTR_NAMES;
|
|
575
|
-
let result = content;
|
|
576
|
-
let count = 0;
|
|
577
|
-
const isSuppressed = opts.isSuppressed;
|
|
578
622
|
const isProtected = protectedSourceOffsets(content);
|
|
579
|
-
const
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
623
|
+
const strings = attributeStrings(content, attrNames, opts.functionNames).filter((string) => !isProtected(string.attributeStart));
|
|
624
|
+
const funcNames = opts.functionNames;
|
|
625
|
+
if (funcNames?.length) {
|
|
626
|
+
const funcRe = new RegExp(`\\b(?:${funcNames.map(escapeRegex).join('|')})\\s*\\(`, 'g');
|
|
627
|
+
let consumedUntil = 0;
|
|
628
|
+
for (const match of content.matchAll(funcRe)) {
|
|
629
|
+
const offset = match.index ?? 0;
|
|
630
|
+
// A configured identifier must not select a suffix of a member call or
|
|
631
|
+
// a larger JavaScript identifier (where `$` is also an identifier char).
|
|
632
|
+
if (previousToken(content, offset) === '.' ||
|
|
633
|
+
/[\w$]/.test(content[offset - 1] ?? ''))
|
|
634
|
+
continue;
|
|
635
|
+
if (isProtected(offset) || offset < consumedUntil)
|
|
636
|
+
continue;
|
|
637
|
+
const start = (match.index ?? 0) + match[0].length;
|
|
638
|
+
const scanned = scanCallStrings(content, start, funcNames);
|
|
639
|
+
strings.push(...scanned.strings);
|
|
640
|
+
consumedUntil = start + scanned.consumed;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const seen = new Set();
|
|
644
|
+
return strings
|
|
645
|
+
.filter((string) => {
|
|
646
|
+
const key = `${string.start}:${string.end}`;
|
|
647
|
+
if (seen.has(key))
|
|
648
|
+
return false;
|
|
649
|
+
seen.add(key);
|
|
650
|
+
return true;
|
|
651
|
+
})
|
|
652
|
+
.sort((left, right) => left.start - right.start);
|
|
653
|
+
}
|
|
654
|
+
export function replaceClassStrings(content, transform, opts = {}) {
|
|
655
|
+
const isSuppressedAt = offsetSuppressor(content, opts.isSuppressed);
|
|
656
|
+
const parts = [];
|
|
657
|
+
let cursor = 0;
|
|
658
|
+
let count = 0;
|
|
659
|
+
for (const string of collectClassStrings(content, opts)) {
|
|
660
|
+
const out = transformStaticClasses(string.value, transform, isSuppressedAt, string.start, string.braceExpressions);
|
|
586
661
|
if (out !== string.value)
|
|
587
662
|
count++;
|
|
588
|
-
|
|
589
|
-
|
|
663
|
+
parts.push(content.slice(cursor, string.start), out);
|
|
664
|
+
cursor = string.end;
|
|
590
665
|
}
|
|
591
|
-
|
|
592
|
-
result
|
|
593
|
-
const funcNames = opts.functionNames;
|
|
594
|
-
if (!funcNames?.length)
|
|
595
|
-
return { result, count };
|
|
596
|
-
const funcRe = new RegExp(`\\b(?:${funcNames.map(escapeRegex).join('|')})\\s*\\(`, 'g');
|
|
597
|
-
const buf = [];
|
|
598
|
-
let pos = 0;
|
|
599
|
-
const isCallSuppressed = offsetSuppressor(result, isSuppressed);
|
|
600
|
-
const isCallProtected = protectedSourceOffsets(result);
|
|
601
|
-
for (const m of result.matchAll(funcRe)) {
|
|
602
|
-
const callStart = (m.index ?? 0) + m[0].length;
|
|
603
|
-
if (callStart < pos || isCallProtected(m.index ?? 0))
|
|
604
|
-
continue;
|
|
605
|
-
buf.push(result.slice(pos, callStart));
|
|
606
|
-
const { processed, consumed, count: c, } = transformCallContent(result, callStart, transform, isCallSuppressed, funcNames);
|
|
607
|
-
buf.push(processed);
|
|
608
|
-
pos = callStart + consumed;
|
|
609
|
-
count += c;
|
|
610
|
-
funcRe.lastIndex = pos;
|
|
611
|
-
}
|
|
612
|
-
buf.push(result.slice(pos));
|
|
613
|
-
return { result: buf.join(''), count };
|
|
666
|
+
parts.push(content.slice(cursor));
|
|
667
|
+
return { result: parts.join(''), count };
|
|
614
668
|
}
|
|
615
669
|
export function extractClassStrings(content, opts = {}) {
|
|
616
|
-
|
|
617
|
-
? opts.attributeNames
|
|
618
|
-
: DEFAULT_ATTR_NAMES;
|
|
619
|
-
const results = [];
|
|
620
|
-
const isProtected = protectedSourceOffsets(content);
|
|
621
|
-
for (const string of attributeStrings(content, attrNames)) {
|
|
622
|
-
if (!isProtected(string.attributeStart))
|
|
623
|
-
results.push({
|
|
624
|
-
value: string.value,
|
|
625
|
-
start: string.start,
|
|
626
|
-
end: string.end,
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
const funcNames = opts.functionNames;
|
|
630
|
-
if (!funcNames?.length)
|
|
631
|
-
return staticRanges(results);
|
|
632
|
-
const funcRe = new RegExp(`\\b(?:${funcNames.map(escapeRegex).join('|')})\\s*\\(`, 'g');
|
|
633
|
-
let consumedUntil = 0;
|
|
634
|
-
for (const m of content.matchAll(funcRe)) {
|
|
635
|
-
if (isProtected(m.index ?? 0) || (m.index ?? 0) < consumedUntil)
|
|
636
|
-
continue;
|
|
637
|
-
const start = (m.index ?? 0) + m[0].length;
|
|
638
|
-
const scanned = scanCallStrings(content, start, funcNames);
|
|
639
|
-
results.push(...scanned.strings);
|
|
640
|
-
consumedUntil = start + scanned.consumed;
|
|
641
|
-
}
|
|
642
|
-
return staticRanges(results);
|
|
670
|
+
return staticRanges(collectClassStrings(content, opts));
|
|
643
671
|
}
|
|
644
672
|
function staticRanges(strings) {
|
|
645
673
|
const seen = new Set();
|
|
646
|
-
return strings.flatMap((string) => staticClassRanges(string.value).flatMap((range) => {
|
|
674
|
+
return strings.flatMap((string) => staticClassRanges(string.value, string.braceExpressions).flatMap((range) => {
|
|
647
675
|
const start = string.start + range.start;
|
|
648
676
|
const end = string.start + range.end;
|
|
649
677
|
const key = `${start}:${end}`;
|
package/dist/core/config.js
CHANGED
|
@@ -8,6 +8,7 @@ export const CONFIG_FILENAMES = [
|
|
|
8
8
|
];
|
|
9
9
|
const KNOWN_KEYS = [
|
|
10
10
|
'useDefaultTheme',
|
|
11
|
+
'allowTextSizeLineHeightChanges',
|
|
11
12
|
'customTextTokens',
|
|
12
13
|
'customSpacingTokens',
|
|
13
14
|
'ignorePatterns',
|
|
@@ -54,7 +55,9 @@ function fail(filename, message) {
|
|
|
54
55
|
throw new Error(`Invalid ${filename}: ${message}`);
|
|
55
56
|
}
|
|
56
57
|
function isPlainObject(value) {
|
|
57
|
-
return typeof value === 'object' &&
|
|
58
|
+
return (typeof value === 'object' &&
|
|
59
|
+
value !== null &&
|
|
60
|
+
[Object.prototype, null].includes(Object.getPrototypeOf(value)));
|
|
58
61
|
}
|
|
59
62
|
function assertKnownKeys(cfg, known, label, filename) {
|
|
60
63
|
for (const key of Object.keys(cfg)) {
|
|
@@ -72,6 +75,8 @@ function assertPxTokenMap(value, key, filename) {
|
|
|
72
75
|
fail(filename, `${key} keys must be integers (got "${k}")`);
|
|
73
76
|
if (typeof v !== 'string')
|
|
74
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`);
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
function assertStringArray(value, key, filename) {
|
|
@@ -137,13 +142,16 @@ function assertSortOrder(value, filename) {
|
|
|
137
142
|
export function validateConfig(input, filename = CONFIG_FILENAME) {
|
|
138
143
|
if (input === undefined || input === null)
|
|
139
144
|
return {};
|
|
140
|
-
if (
|
|
141
|
-
fail(filename, 'default export must be an object');
|
|
145
|
+
if (!isPlainObject(input)) {
|
|
146
|
+
fail(filename, 'default export must be an object (plain objects only)');
|
|
142
147
|
}
|
|
143
148
|
const cfg = input;
|
|
144
149
|
assertKnownKeys(cfg, KNOWN_KEYS, 'unknown key', filename);
|
|
145
150
|
if ('useDefaultTheme' in cfg && typeof cfg.useDefaultTheme !== 'boolean')
|
|
146
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');
|
|
147
155
|
if ('customTextTokens' in cfg)
|
|
148
156
|
assertPxTokenMap(cfg.customTextTokens, 'customTextTokens', filename);
|
|
149
157
|
if ('customSpacingTokens' in cfg)
|
package/dist/core/consistency.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
|
|
@@ -16,14 +16,14 @@ function parseColor(cls, families, knownColors) {
|
|
|
16
16
|
: null;
|
|
17
17
|
if (!family)
|
|
18
18
|
return null;
|
|
19
|
-
return { property, family, token: `${color}-${shade}` };
|
|
19
|
+
return { property, family, token: `${color}-${shade}${opacity}` };
|
|
20
20
|
}
|
|
21
21
|
// A scale value is a number, a fraction, or an arbitrary [..] value. Keyword
|
|
22
22
|
// values (auto, px, full, screen, …) are not grid values, so they are excluded
|
|
23
23
|
// to avoid false "inconsistency" reports like mt-4 vs mt-auto.
|
|
24
24
|
const SCALE_VALUE_RE = /^(\d+(?:\.\d+)?|\d+\/\d+|\[.+\])$/;
|
|
25
25
|
function parseScale(cls, scaleProperties) {
|
|
26
|
-
const base =
|
|
26
|
+
const { utility: base } = splitClassModifiers(cls);
|
|
27
27
|
const negative = base.startsWith('-');
|
|
28
28
|
const body = negative ? base.slice(1) : base;
|
|
29
29
|
const bracket = body.indexOf('[');
|
|
@@ -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,7 +195,7 @@ const CORNER_MAP = {
|
|
|
186
195
|
br: ['br'],
|
|
187
196
|
};
|
|
188
197
|
function parseRoundedCorner(cls) {
|
|
189
|
-
if (/^rounded-(?:s|e|ss|se|es|ee)(?:-|$)/.test(cls))
|
|
198
|
+
if (cls.endsWith('!') || /^rounded-(?:s|e|ss|se|es|ee)(?:-|$)/.test(cls))
|
|
190
199
|
return null;
|
|
191
200
|
const m = cls.match(/^rounded-(tl|tr|bl|br|t|b|l|r)(?:-(.+))?$/) ??
|
|
192
201
|
cls.match(/^rounded()(?:-(.+))?$/);
|
|
@@ -322,7 +331,7 @@ function keepLastOf(slots, indices) {
|
|
|
322
331
|
for (const i of indices.slice(0, -1))
|
|
323
332
|
slots[i] = [];
|
|
324
333
|
}
|
|
325
|
-
export function deduplicateClasses(classStr) {
|
|
334
|
+
export function deduplicateClasses(classStr, opts = {}) {
|
|
326
335
|
if (classStr.includes('${'))
|
|
327
336
|
return classStr;
|
|
328
337
|
const original = classStr.split(/\s+/).filter(Boolean);
|
|
@@ -347,12 +356,20 @@ export function deduplicateClasses(classStr) {
|
|
|
347
356
|
}
|
|
348
357
|
const parsed = parseBoxClass(cls);
|
|
349
358
|
if (parsed) {
|
|
350
|
-
const { family, sides } = parsed;
|
|
359
|
+
const { family, sides, kind } = parsed;
|
|
351
360
|
let group = boxGroups.get(family.kind);
|
|
352
361
|
if (!group) {
|
|
353
|
-
group = {
|
|
362
|
+
group = {
|
|
363
|
+
family,
|
|
364
|
+
box: { t: '', b: '', l: '', r: '' },
|
|
365
|
+
indices: [],
|
|
366
|
+
hasAxis: false,
|
|
367
|
+
hasSide: false,
|
|
368
|
+
};
|
|
354
369
|
boxGroups.set(family.kind, group);
|
|
355
370
|
}
|
|
371
|
+
group.hasAxis ||= kind === 'axis';
|
|
372
|
+
group.hasSide ||= kind === 'side';
|
|
356
373
|
group.indices.push(idx);
|
|
357
374
|
Object.assign(group.box, sides);
|
|
358
375
|
return;
|
|
@@ -370,8 +387,15 @@ export function deduplicateClasses(classStr) {
|
|
|
370
387
|
});
|
|
371
388
|
keepLastOf(slots, displayIndices);
|
|
372
389
|
keepLastOf(slots, positionIndices);
|
|
373
|
-
for (const { family, box, indices } of boxGroups.values()) {
|
|
374
|
-
|
|
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);
|
|
375
399
|
}
|
|
376
400
|
if (cornerIndices.length > 0) {
|
|
377
401
|
placeCollapsed(slots, classes, cornerIndices, collapseCorners(corners));
|
|
@@ -382,7 +406,7 @@ export function deduplicateClasses(classStr) {
|
|
|
382
406
|
return unchanged ? classStr : result.join(' ');
|
|
383
407
|
}
|
|
384
408
|
export function dedupeContent(content, opts = {}) {
|
|
385
|
-
return replaceClassStrings(content, deduplicateClasses, {
|
|
409
|
+
return replaceClassStrings(content, (classes) => deduplicateClasses(classes, opts), {
|
|
386
410
|
...opts,
|
|
387
411
|
isSuppressed: makeLineSuppressor(content),
|
|
388
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>;
|