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
|
@@ -5,24 +5,199 @@ 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
|
}
|
|
15
16
|
function buildAttrRegex(names) {
|
|
16
17
|
const alts = names.map(escapeRegex).join('|');
|
|
17
|
-
return new RegExp(`(^|[^\\w:$-])(${alts})\\s*=\\s
|
|
18
|
+
return new RegExp(`(^|[^\\w:$-])(${alts})\\s*=\\s*`, 'g');
|
|
19
|
+
}
|
|
20
|
+
function attributeStrings(content, names, functionNames = []) {
|
|
21
|
+
const result = [];
|
|
22
|
+
for (const match of content.matchAll(buildAttrRegex(names))) {
|
|
23
|
+
const attributeStart = (match.index ?? 0) + match[1].length;
|
|
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;
|
|
31
|
+
}
|
|
32
|
+
if (!['"', "'", '`'].includes(content[start]))
|
|
33
|
+
continue;
|
|
34
|
+
const quoted = readQuotedString(content, start);
|
|
35
|
+
if (!quoted)
|
|
36
|
+
continue;
|
|
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))
|
|
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
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else
|
|
48
|
+
result.push({ ...quoted, attributeStart, braceExpressions: true });
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
// A template expression is JavaScript, never a class list. Keep its adjacent
|
|
53
|
+
// token fragments as well: `text-[12px]${suffix}` is not a static class.
|
|
54
|
+
function interpolationEnd(content, start, openingLength = 2) {
|
|
55
|
+
let depth = 1;
|
|
56
|
+
let i = start + openingLength;
|
|
57
|
+
while (i < content.length) {
|
|
58
|
+
const ch = content[i];
|
|
59
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
60
|
+
const quoted = readQuotedString(content, i);
|
|
61
|
+
if (!quoted)
|
|
62
|
+
return content.length;
|
|
63
|
+
i = quoted.next;
|
|
64
|
+
}
|
|
65
|
+
else if (content.startsWith('//', i)) {
|
|
66
|
+
const end = content.indexOf('\n', i + 2);
|
|
67
|
+
i = end < 0 ? content.length : end;
|
|
68
|
+
}
|
|
69
|
+
else if (content.startsWith('/*', i)) {
|
|
70
|
+
const end = content.indexOf('*/', i + 2);
|
|
71
|
+
i = end < 0 ? content.length : end + 2;
|
|
72
|
+
}
|
|
73
|
+
else if (ch === '/') {
|
|
74
|
+
const preceding = previousToken(content, i);
|
|
75
|
+
if (regexMayStart(preceding)) {
|
|
76
|
+
const end = readRegex(content, i);
|
|
77
|
+
if (!end)
|
|
78
|
+
return content.length;
|
|
79
|
+
i = end;
|
|
80
|
+
}
|
|
81
|
+
else
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
if (ch === '{')
|
|
86
|
+
depth++;
|
|
87
|
+
else if (ch === '}' && --depth === 0)
|
|
88
|
+
return i + 1;
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return content.length;
|
|
93
|
+
}
|
|
94
|
+
function staticClassRanges(value, braceExpressions = false) {
|
|
95
|
+
const ranges = [];
|
|
96
|
+
let start = 0;
|
|
97
|
+
let i = 0;
|
|
98
|
+
let bracketDepth = 0;
|
|
99
|
+
while (i < value.length) {
|
|
100
|
+
let escapes = 0;
|
|
101
|
+
for (let j = i - 1; j >= 0 && value[j] === '\\'; j--)
|
|
102
|
+
escapes++;
|
|
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
|
+
}
|
|
115
|
+
i++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
let before = i;
|
|
119
|
+
while (before > start && !/\s/.test(value[before - 1]))
|
|
120
|
+
before--;
|
|
121
|
+
if (before > start)
|
|
122
|
+
ranges.push({ value: value.slice(start, before), start, end: before });
|
|
123
|
+
i = interpolationEnd(value, i, openingLength);
|
|
124
|
+
while (i < value.length && !/\s/.test(value[i]))
|
|
125
|
+
i++;
|
|
126
|
+
start = i;
|
|
127
|
+
}
|
|
128
|
+
if (start < value.length)
|
|
129
|
+
ranges.push({ value: value.slice(start), start, end: value.length });
|
|
130
|
+
return ranges;
|
|
131
|
+
}
|
|
132
|
+
function transformStaticClasses(value, transform, isSuppressedAt, offset = 0, braceExpressions = false) {
|
|
133
|
+
const suppressed = [];
|
|
134
|
+
if (isSuppressedAt) {
|
|
135
|
+
let lineStart = 0;
|
|
136
|
+
while (lineStart < value.length) {
|
|
137
|
+
const newline = value.indexOf('\n', lineStart);
|
|
138
|
+
const end = newline < 0 ? value.length : newline + 1;
|
|
139
|
+
if (isSuppressedAt(offset + lineStart))
|
|
140
|
+
suppressed.push({ start: lineStart, end });
|
|
141
|
+
lineStart = end;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const ranges = staticClassRanges(value, braceExpressions).flatMap((range) => {
|
|
145
|
+
const safe = [];
|
|
146
|
+
let start = range.start;
|
|
147
|
+
for (const line of suppressed) {
|
|
148
|
+
if (line.end <= start || line.start >= range.end)
|
|
149
|
+
continue;
|
|
150
|
+
if (line.start > start)
|
|
151
|
+
safe.push({
|
|
152
|
+
value: value.slice(start, line.start),
|
|
153
|
+
start,
|
|
154
|
+
end: line.start,
|
|
155
|
+
});
|
|
156
|
+
start = Math.max(start, line.end);
|
|
157
|
+
}
|
|
158
|
+
if (start < range.end)
|
|
159
|
+
safe.push({
|
|
160
|
+
value: value.slice(start, range.end),
|
|
161
|
+
start,
|
|
162
|
+
end: range.end,
|
|
163
|
+
});
|
|
164
|
+
return safe;
|
|
165
|
+
});
|
|
166
|
+
const preserveEdges = value.includes('${') ||
|
|
167
|
+
(braceExpressions && value.includes('{')) ||
|
|
168
|
+
suppressed.length > 0;
|
|
169
|
+
let out = '';
|
|
170
|
+
let pos = 0;
|
|
171
|
+
for (const range of ranges) {
|
|
172
|
+
const segment = range.value;
|
|
173
|
+
if (!segment.trim()) {
|
|
174
|
+
out += value.slice(pos, range.end);
|
|
175
|
+
pos = range.end;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const leading = preserveEdges ? (segment.match(/^\s*/)?.[0] ?? '') : '';
|
|
179
|
+
const trailing = preserveEdges ? (segment.match(/\s*$/)?.[0] ?? '') : '';
|
|
180
|
+
const inner = segment.slice(leading.length, segment.length - trailing.length);
|
|
181
|
+
out +=
|
|
182
|
+
value.slice(pos, range.start) +
|
|
183
|
+
leading +
|
|
184
|
+
(inner ? transform(inner) : '') +
|
|
185
|
+
trailing;
|
|
186
|
+
pos = range.end;
|
|
187
|
+
}
|
|
188
|
+
return out + value.slice(pos);
|
|
18
189
|
}
|
|
19
190
|
function readQuotedString(content, quoteStart) {
|
|
20
191
|
const quote = content[quoteStart];
|
|
21
192
|
let quoteEnd = quoteStart + 1;
|
|
22
193
|
while (quoteEnd < content.length && content[quoteEnd] !== quote) {
|
|
23
194
|
if (content[quoteEnd] === '\\')
|
|
195
|
+
quoteEnd += 2;
|
|
196
|
+
else if (quote === '`' && content.startsWith('${', quoteEnd)) {
|
|
197
|
+
quoteEnd = interpolationEnd(content, quoteEnd);
|
|
198
|
+
}
|
|
199
|
+
else
|
|
24
200
|
quoteEnd++;
|
|
25
|
-
quoteEnd++;
|
|
26
201
|
}
|
|
27
202
|
if (quoteEnd >= content.length)
|
|
28
203
|
return null;
|
|
@@ -33,59 +208,276 @@ function readQuotedString(content, quoteStart) {
|
|
|
33
208
|
next: quoteEnd + 1,
|
|
34
209
|
};
|
|
35
210
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
let i = start;
|
|
39
|
-
let depth = 1;
|
|
40
|
-
while (i < content.length && depth > 0) {
|
|
211
|
+
function readRegex(content, start) {
|
|
212
|
+
let bracket = false;
|
|
213
|
+
for (let i = start + 1; i < content.length && content[i] !== '\n'; i++) {
|
|
41
214
|
const ch = content[i];
|
|
42
|
-
if (ch === '
|
|
43
|
-
|
|
215
|
+
if (ch === '\\')
|
|
216
|
+
i++;
|
|
217
|
+
else if (ch === '[')
|
|
218
|
+
bracket = true;
|
|
219
|
+
else if (ch === ']')
|
|
220
|
+
bracket = false;
|
|
221
|
+
else if (ch === '/' && !bracket) {
|
|
44
222
|
i++;
|
|
223
|
+
while (/[a-z]/i.test(content[i] ?? '') && i < content.length)
|
|
224
|
+
i++;
|
|
225
|
+
return i;
|
|
45
226
|
}
|
|
46
|
-
|
|
47
|
-
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const REGEX_PREFIX_TOKENS = new Set([
|
|
231
|
+
',',
|
|
232
|
+
':',
|
|
233
|
+
'?',
|
|
234
|
+
'=',
|
|
235
|
+
'!',
|
|
236
|
+
'(',
|
|
237
|
+
'[',
|
|
238
|
+
'{',
|
|
239
|
+
';',
|
|
240
|
+
'&',
|
|
241
|
+
'|',
|
|
242
|
+
'=>',
|
|
243
|
+
'&&',
|
|
244
|
+
'||',
|
|
245
|
+
'??',
|
|
246
|
+
'+',
|
|
247
|
+
'-',
|
|
248
|
+
'*',
|
|
249
|
+
'/',
|
|
250
|
+
'%',
|
|
251
|
+
'>',
|
|
252
|
+
'<',
|
|
253
|
+
'^',
|
|
254
|
+
'~',
|
|
255
|
+
'==',
|
|
256
|
+
'===',
|
|
257
|
+
'!=',
|
|
258
|
+
'!==',
|
|
259
|
+
'<=',
|
|
260
|
+
'>=',
|
|
261
|
+
'return',
|
|
262
|
+
'throw',
|
|
263
|
+
'case',
|
|
264
|
+
'typeof',
|
|
265
|
+
'void',
|
|
266
|
+
'delete',
|
|
267
|
+
'yield',
|
|
268
|
+
'await',
|
|
269
|
+
'in',
|
|
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
|
+
'}',
|
|
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
|
+
}
|
|
297
|
+
function regexMayStart(previous) {
|
|
298
|
+
return previous === undefined || REGEX_PREFIX_TOKENS.has(previous);
|
|
299
|
+
}
|
|
300
|
+
// Delimiter groups are enough to identify static class positions. This does
|
|
301
|
+
// not evaluate expressions or interpret arbitrary JavaScript as a class list.
|
|
302
|
+
function readCallTokens(content, start, closing = ')') {
|
|
303
|
+
const tokens = [];
|
|
304
|
+
let i = start;
|
|
305
|
+
while (i < content.length) {
|
|
306
|
+
const ch = content[i];
|
|
307
|
+
if (/\s/.test(ch)) {
|
|
48
308
|
i++;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (content.startsWith('//', i)) {
|
|
312
|
+
const end = content.indexOf('\n', i + 2);
|
|
313
|
+
i = end < 0 ? content.length : end;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (content.startsWith('/*', i)) {
|
|
317
|
+
const end = content.indexOf('*/', i + 2);
|
|
318
|
+
if (end < 0)
|
|
319
|
+
return { tokens: [], next: content.length, complete: false };
|
|
320
|
+
i = end + 2;
|
|
321
|
+
continue;
|
|
49
322
|
}
|
|
50
|
-
|
|
323
|
+
if (ch === closing)
|
|
324
|
+
return { tokens, next: i + 1, complete: true };
|
|
325
|
+
if (ch === ')' || ch === ']' || ch === '}')
|
|
326
|
+
return { tokens: [], next: i + 1, complete: false };
|
|
327
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
51
328
|
const quoted = readQuotedString(content, i);
|
|
52
|
-
if (!quoted)
|
|
53
|
-
return {
|
|
54
|
-
|
|
55
|
-
consumed: content.length - start,
|
|
56
|
-
complete: false,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
strings.push(quoted);
|
|
329
|
+
if (!quoted)
|
|
330
|
+
return { tokens: [], next: content.length, complete: false };
|
|
331
|
+
tokens.push({ text: content.slice(i, quoted.next), string: quoted });
|
|
60
332
|
i = quoted.next;
|
|
333
|
+
continue;
|
|
61
334
|
}
|
|
62
|
-
|
|
63
|
-
|
|
335
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
336
|
+
const end = { '(': ')', '[': ']', '{': '}' }[ch];
|
|
337
|
+
const group = readCallTokens(content, i + 1, end);
|
|
338
|
+
if (!group.complete)
|
|
339
|
+
return { tokens: [], next: group.next, complete: false };
|
|
340
|
+
tokens.push({ text: ch, children: group.tokens });
|
|
341
|
+
i = group.next;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (ch === '/' && regexMayStart(tokens.at(-1)?.text)) {
|
|
345
|
+
const end = readRegex(content, i);
|
|
346
|
+
if (!end)
|
|
347
|
+
return { tokens: [], next: content.length, complete: false };
|
|
348
|
+
tokens.push({ text: content.slice(i, end) });
|
|
349
|
+
i = end;
|
|
350
|
+
continue;
|
|
64
351
|
}
|
|
352
|
+
const word = content.slice(i).match(/^[\w$]+/);
|
|
353
|
+
const operator = content
|
|
354
|
+
.slice(i)
|
|
355
|
+
.match(/^(?:===|!==|=>|&&|\|\||\?\?|==|!=|<=|>=|\?\.|\+\+|--|\*\*|\.\.\.)/);
|
|
356
|
+
const text = word?.[0] ?? operator?.[0] ?? ch;
|
|
357
|
+
tokens.push({ text });
|
|
358
|
+
i += text.length;
|
|
65
359
|
}
|
|
66
|
-
return {
|
|
360
|
+
return { tokens: [], next: content.length, complete: false };
|
|
67
361
|
}
|
|
68
|
-
function
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
362
|
+
function splitTokens(tokens, separator) {
|
|
363
|
+
const result = [[]];
|
|
364
|
+
for (const token of tokens) {
|
|
365
|
+
if (token.text === separator)
|
|
366
|
+
result.push([]);
|
|
367
|
+
else
|
|
368
|
+
result[result.length - 1].push(token);
|
|
73
369
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
function objectProperties(token) {
|
|
373
|
+
if (token?.text !== '{' || !token.children)
|
|
374
|
+
return [];
|
|
375
|
+
return splitTokens(token.children, ',').flatMap((property) => property.length >= 3 &&
|
|
376
|
+
property[1].text === ':' &&
|
|
377
|
+
(property[0].string || /^[\w$]+$/.test(property[0].text))
|
|
378
|
+
? [{ key: property[0], value: property.slice(2) }]
|
|
379
|
+
: []);
|
|
380
|
+
}
|
|
381
|
+
function propertyName(key) {
|
|
382
|
+
return key.string?.value ?? key.text;
|
|
383
|
+
}
|
|
384
|
+
function classExpression(tokens, functionNames, objects = true) {
|
|
385
|
+
if (tokens.length === 1) {
|
|
386
|
+
const token = tokens[0];
|
|
387
|
+
if (token.string)
|
|
388
|
+
return [token.string];
|
|
389
|
+
if (token.text === '[' && token.children)
|
|
390
|
+
return splitTokens(token.children, ',').flatMap((part) => classExpression(part, functionNames, 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 [];
|
|
397
|
+
return objectProperties(token).flatMap(({ key }) => key.string ? [key.string] : []);
|
|
398
|
+
}
|
|
399
|
+
// A grouping pair does not establish class context, but preserves one.
|
|
400
|
+
if (token.text === '(' && token.children)
|
|
401
|
+
return classExpression(token.children, functionNames, objects);
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
const question = tokens.findIndex(({ text }) => text === '?');
|
|
405
|
+
if (question >= 0) {
|
|
406
|
+
let nested = 0;
|
|
407
|
+
for (let i = question + 1; i < tokens.length; i++) {
|
|
408
|
+
if (tokens[i].text === '?')
|
|
409
|
+
nested++;
|
|
410
|
+
else if (tokens[i].text === ':' && nested-- === 0)
|
|
411
|
+
return [
|
|
412
|
+
...classExpression(tokens.slice(question + 1, i), functionNames, objects),
|
|
413
|
+
...classExpression(tokens.slice(i + 1), functionNames, objects),
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
const logical = tokens.findIndex(({ text }) => text === '&&' || text === '||' || text === '??');
|
|
419
|
+
if (logical >= 0)
|
|
420
|
+
return [
|
|
421
|
+
...(tokens[logical].text === '&&'
|
|
422
|
+
? []
|
|
423
|
+
: classExpression(tokens.slice(0, logical), functionNames, objects)),
|
|
424
|
+
...classExpression(tokens.slice(logical + 1), functionNames, objects),
|
|
425
|
+
];
|
|
426
|
+
if (tokens.length === 2 &&
|
|
427
|
+
functionNames.includes(tokens[0].text) &&
|
|
428
|
+
tokens[1].text === '(' &&
|
|
429
|
+
tokens[1].children)
|
|
430
|
+
return classCall(tokens[0].text, tokens[1].children, functionNames);
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
function cvaOptions(token, functionNames) {
|
|
434
|
+
const result = [];
|
|
435
|
+
for (const { key, value } of objectProperties(token)) {
|
|
436
|
+
if (value.length !== 1)
|
|
437
|
+
continue;
|
|
438
|
+
if (propertyName(key) === 'variants') {
|
|
439
|
+
for (const variant of objectProperties(value[0])) {
|
|
440
|
+
if (variant.value.length !== 1)
|
|
441
|
+
continue;
|
|
442
|
+
for (const choice of objectProperties(variant.value[0]))
|
|
443
|
+
result.push(...classExpression(choice.value, functionNames, false));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else if (propertyName(key) === 'compoundVariants' &&
|
|
447
|
+
value[0].text === '[' &&
|
|
448
|
+
value[0].children) {
|
|
449
|
+
for (const entry of splitTokens(value[0].children, ',')) {
|
|
450
|
+
if (entry.length !== 1)
|
|
451
|
+
continue;
|
|
452
|
+
for (const compound of objectProperties(entry[0])) {
|
|
453
|
+
if (propertyName(compound.key) === 'class' ||
|
|
454
|
+
propertyName(compound.key) === 'className')
|
|
455
|
+
result.push(...classExpression(compound.value, functionNames, false));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
86
459
|
}
|
|
87
|
-
|
|
88
|
-
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
function classCall(name, tokens, functionNames) {
|
|
463
|
+
const args = splitTokens(tokens, ',');
|
|
464
|
+
if (name === 'cva')
|
|
465
|
+
return [
|
|
466
|
+
...classExpression(args[0] ?? [], functionNames, false),
|
|
467
|
+
...(args[1]?.length === 1 ? cvaOptions(args[1][0], functionNames) : []),
|
|
468
|
+
];
|
|
469
|
+
return args.flatMap((arg) => classExpression(arg, functionNames));
|
|
470
|
+
}
|
|
471
|
+
function scanCallStrings(content, start, functionNames = []) {
|
|
472
|
+
const scanned = readCallTokens(content, start);
|
|
473
|
+
const name = previousToken(content, start - 1) ?? '';
|
|
474
|
+
return {
|
|
475
|
+
strings: scanned.complete
|
|
476
|
+
? classCall(name, scanned.tokens, functionNames)
|
|
477
|
+
: [],
|
|
478
|
+
consumed: scanned.next - start,
|
|
479
|
+
complete: scanned.complete,
|
|
480
|
+
};
|
|
89
481
|
}
|
|
90
482
|
function offsetSuppressor(content, isSuppressed) {
|
|
91
483
|
if (!isSuppressed)
|
|
@@ -93,65 +485,199 @@ function offsetSuppressor(content, isSuppressed) {
|
|
|
93
485
|
const positionAt = createPositionLookup(content);
|
|
94
486
|
return (offset) => isSuppressed(positionAt(offset).line);
|
|
95
487
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const funcNames = opts.functionNames;
|
|
118
|
-
if (!funcNames?.length)
|
|
119
|
-
return { result, count };
|
|
120
|
-
const funcRe = new RegExp(`\\b(?:${funcNames.map(escapeRegex).join('|')})\\s*\\(`, 'g');
|
|
121
|
-
const buf = [];
|
|
122
|
-
let pos = 0;
|
|
123
|
-
const isCallSuppressed = offsetSuppressor(result, isSuppressed);
|
|
124
|
-
for (const m of result.matchAll(funcRe)) {
|
|
125
|
-
const callStart = (m.index ?? 0) + m[0].length;
|
|
126
|
-
if (callStart < pos)
|
|
488
|
+
// Ignore source comments, quoted prose and regex literals before looking for
|
|
489
|
+
// attribute/function entry points. Ambiguous syntax is deliberately skipped.
|
|
490
|
+
function protectedSourceOffsets(content) {
|
|
491
|
+
const ranges = [];
|
|
492
|
+
const tags = [];
|
|
493
|
+
const pendingTags = [];
|
|
494
|
+
let expressionEnd = 0;
|
|
495
|
+
const controlParens = [];
|
|
496
|
+
let lastControlParenEnd = -1;
|
|
497
|
+
let i = 0;
|
|
498
|
+
while (i < content.length) {
|
|
499
|
+
const start = i;
|
|
500
|
+
const ch = content[i];
|
|
501
|
+
const tag = pendingTags.at(0);
|
|
502
|
+
if (tag?.end === i) {
|
|
503
|
+
if (tag.closing)
|
|
504
|
+
tags.pop();
|
|
505
|
+
else if (!tag.selfClosing)
|
|
506
|
+
tags.push(tag.name);
|
|
507
|
+
pendingTags.length = 0;
|
|
508
|
+
i++;
|
|
127
509
|
continue;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
510
|
+
}
|
|
511
|
+
if (!tag && ch === '<' && !content.startsWith('<!--', i)) {
|
|
512
|
+
const match = content
|
|
513
|
+
.slice(i)
|
|
514
|
+
.match(/^<(\/?)(?:([A-Za-z][\w:.-]*)(?=[\s/>])|(?=>))/);
|
|
515
|
+
if (match) {
|
|
516
|
+
let end = i + match[0].length;
|
|
517
|
+
while (end < content.length && content[end] !== '>') {
|
|
518
|
+
if (content[end] === '"' ||
|
|
519
|
+
content[end] === "'" ||
|
|
520
|
+
content[end] === '`') {
|
|
521
|
+
end = readQuotedString(content, end)?.next ?? content.length;
|
|
522
|
+
}
|
|
523
|
+
else if (content[end] === '{') {
|
|
524
|
+
const expression = readCallTokens(content, end + 1, '}');
|
|
525
|
+
end = expression.next;
|
|
526
|
+
}
|
|
527
|
+
else
|
|
528
|
+
end++;
|
|
529
|
+
}
|
|
530
|
+
const closing = match[1] === '/';
|
|
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] ?? '';
|
|
534
|
+
if (end < content.length &&
|
|
535
|
+
(tags.length > 0 ||
|
|
536
|
+
selfClosing ||
|
|
537
|
+
content.includes(`</${name}>`, end)))
|
|
538
|
+
pendingTags.push({ end, name, closing, selfClosing });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
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 !== '<') {
|
|
553
|
+
if (ch === '{')
|
|
554
|
+
expressionEnd = readCallTokens(content, i + 1, '}').next;
|
|
555
|
+
else {
|
|
556
|
+
while (i < content.length && content[i] !== '<' && content[i] !== '{')
|
|
557
|
+
i++;
|
|
558
|
+
ranges.push({ start, end: i });
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
563
|
+
i = readQuotedString(content, i)?.next ?? content.length;
|
|
564
|
+
}
|
|
565
|
+
else if (content.startsWith('//', i)) {
|
|
566
|
+
const end = content.indexOf('\n', i + 2);
|
|
567
|
+
i = end < 0 ? content.length : end;
|
|
568
|
+
}
|
|
569
|
+
else if (content.startsWith('/*', i) || content.startsWith('<!--', i)) {
|
|
570
|
+
const html = content.startsWith('<!--', i);
|
|
571
|
+
const ending = html ? '-->' : '*/';
|
|
572
|
+
const end = content.indexOf(ending, i + (html ? 4 : 2));
|
|
573
|
+
i = end < 0 ? content.length : end + ending.length;
|
|
574
|
+
}
|
|
575
|
+
else if (ch === '/' && content[i - 1] !== '<' && content[i + 1] !== '>') {
|
|
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;
|
|
584
|
+
if (!end) {
|
|
585
|
+
i++;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
i = end;
|
|
589
|
+
}
|
|
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;
|
|
596
|
+
i++;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
ranges.push({ start, end: i });
|
|
134
600
|
}
|
|
135
|
-
|
|
136
|
-
|
|
601
|
+
return (offset) => {
|
|
602
|
+
let low = 0;
|
|
603
|
+
let high = ranges.length;
|
|
604
|
+
while (low < high) {
|
|
605
|
+
const mid = (low + high) >>> 1;
|
|
606
|
+
if (ranges[mid].end <= offset)
|
|
607
|
+
low = mid + 1;
|
|
608
|
+
else
|
|
609
|
+
high = mid;
|
|
610
|
+
}
|
|
611
|
+
return low < ranges.length && ranges[low].start <= offset;
|
|
612
|
+
};
|
|
137
613
|
}
|
|
138
|
-
|
|
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) {
|
|
139
619
|
const attrNames = opts.attributeNames?.length
|
|
140
620
|
? opts.attributeNames
|
|
141
621
|
: DEFAULT_ATTR_NAMES;
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
const raw = m[4] ?? m[5] ?? m[6] ?? m[8] ?? m[9] ?? m[10] ?? '';
|
|
145
|
-
const start = (m.index ?? 0) + m[0].lastIndexOf(raw);
|
|
146
|
-
results.push({ value: raw, start, end: start + raw.length });
|
|
147
|
-
}
|
|
622
|
+
const isProtected = protectedSourceOffsets(content);
|
|
623
|
+
const strings = attributeStrings(content, attrNames, opts.functionNames).filter((string) => !isProtected(string.attributeStart));
|
|
148
624
|
const funcNames = opts.functionNames;
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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);
|
|
661
|
+
if (out !== string.value)
|
|
662
|
+
count++;
|
|
663
|
+
parts.push(content.slice(cursor, string.start), out);
|
|
664
|
+
cursor = string.end;
|
|
155
665
|
}
|
|
156
|
-
|
|
666
|
+
parts.push(content.slice(cursor));
|
|
667
|
+
return { result: parts.join(''), count };
|
|
668
|
+
}
|
|
669
|
+
export function extractClassStrings(content, opts = {}) {
|
|
670
|
+
return staticRanges(collectClassStrings(content, opts));
|
|
671
|
+
}
|
|
672
|
+
function staticRanges(strings) {
|
|
673
|
+
const seen = new Set();
|
|
674
|
+
return strings.flatMap((string) => staticClassRanges(string.value, string.braceExpressions).flatMap((range) => {
|
|
675
|
+
const start = string.start + range.start;
|
|
676
|
+
const end = string.start + range.end;
|
|
677
|
+
const key = `${start}:${end}`;
|
|
678
|
+
if (seen.has(key))
|
|
679
|
+
return [];
|
|
680
|
+
seen.add(key);
|
|
681
|
+
return [{ value: range.value, start, end }];
|
|
682
|
+
}));
|
|
157
683
|
}
|