tailwind-canonical 0.7.1 → 0.9.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 +131 -11
- package/dist/cli/cli.js +47 -5
- package/dist/cli/index.js +1 -1
- package/dist/cli/watch.d.ts +12 -2
- package/dist/cli/watch.js +144 -24
- package/dist/core/class-strings.js +547 -49
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +4 -1
- package/dist/core/consistency.d.ts +5 -0
- package/dist/core/consistency.js +22 -15
- package/dist/core/deduplicator.js +4 -0
- package/dist/core/merger.d.ts +1 -0
- package/dist/core/merger.js +5 -2
- package/dist/core/rules.d.ts +7 -0
- package/dist/core/rules.js +444 -9
- package/dist/core/scanner.d.ts +0 -1
- package/dist/core/scanner.js +1 -50
- package/dist/eslint/plugin.d.ts +1 -1
- package/dist/eslint/plugin.js +105 -28
- package/dist/io/config.d.ts +4 -1
- package/dist/io/config.js +45 -4
- package/dist/io/consistency.js +4 -6
- package/dist/io/scanner.js +26 -10
- package/package.json +8 -6
|
@@ -14,15 +14,180 @@ function escapeRegex(s) {
|
|
|
14
14
|
}
|
|
15
15
|
function buildAttrRegex(names) {
|
|
16
16
|
const alts = names.map(escapeRegex).join('|');
|
|
17
|
-
return new RegExp(`(^|[^\\w:$-])(${alts})\\s*=\\s
|
|
17
|
+
return new RegExp(`(^|[^\\w:$-])(${alts})\\s*=\\s*`, 'g');
|
|
18
|
+
}
|
|
19
|
+
function attributeStrings(content, names) {
|
|
20
|
+
const result = [];
|
|
21
|
+
for (const match of content.matchAll(buildAttrRegex(names))) {
|
|
22
|
+
const attributeStart = (match.index ?? 0) + match[1].length;
|
|
23
|
+
let i = (match.index ?? 0) + match[0].length;
|
|
24
|
+
const braced = content[i] === '{';
|
|
25
|
+
if (braced) {
|
|
26
|
+
i++;
|
|
27
|
+
while (/\s/.test(content[i] ?? '') && i < content.length)
|
|
28
|
+
i++;
|
|
29
|
+
}
|
|
30
|
+
if (content[i] !== '"' && content[i] !== "'" && content[i] !== '`')
|
|
31
|
+
continue;
|
|
32
|
+
const quoted = readQuotedString(content, i);
|
|
33
|
+
if (!quoted)
|
|
34
|
+
continue;
|
|
35
|
+
if (braced) {
|
|
36
|
+
i = quoted.next;
|
|
37
|
+
while (/\s/.test(content[i] ?? '') && i < content.length)
|
|
38
|
+
i++;
|
|
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] !== '}')
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
result.push({
|
|
45
|
+
value: quoted.value,
|
|
46
|
+
start: quoted.start,
|
|
47
|
+
end: quoted.end,
|
|
48
|
+
attributeStart,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
// A template expression is JavaScript, never a class list. Keep its adjacent
|
|
54
|
+
// token fragments as well: `text-[12px]${suffix}` is not a static class.
|
|
55
|
+
function interpolationEnd(content, start) {
|
|
56
|
+
let depth = 1;
|
|
57
|
+
let i = start + 2;
|
|
58
|
+
while (i < content.length) {
|
|
59
|
+
const ch = content[i];
|
|
60
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
61
|
+
const quoted = readQuotedString(content, i);
|
|
62
|
+
if (!quoted)
|
|
63
|
+
return content.length;
|
|
64
|
+
i = quoted.next;
|
|
65
|
+
}
|
|
66
|
+
else if (content.startsWith('//', i)) {
|
|
67
|
+
const end = content.indexOf('\n', i + 2);
|
|
68
|
+
i = end < 0 ? content.length : end;
|
|
69
|
+
}
|
|
70
|
+
else if (content.startsWith('/*', i)) {
|
|
71
|
+
const end = content.indexOf('*/', i + 2);
|
|
72
|
+
i = end < 0 ? content.length : end + 2;
|
|
73
|
+
}
|
|
74
|
+
else if (ch === '/') {
|
|
75
|
+
const preceding = content
|
|
76
|
+
.slice(start + 2, i)
|
|
77
|
+
.trimEnd()
|
|
78
|
+
.match(/(?:[\w$]+|=>|&&|\|\||\?\?|.)$/)?.[0];
|
|
79
|
+
if (regexMayStart(preceding)) {
|
|
80
|
+
const end = readRegex(content, i);
|
|
81
|
+
if (!end)
|
|
82
|
+
return content.length;
|
|
83
|
+
i = end;
|
|
84
|
+
}
|
|
85
|
+
else
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (ch === '{')
|
|
90
|
+
depth++;
|
|
91
|
+
else if (ch === '}' && --depth === 0)
|
|
92
|
+
return i + 1;
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return content.length;
|
|
97
|
+
}
|
|
98
|
+
function staticClassRanges(value) {
|
|
99
|
+
const ranges = [];
|
|
100
|
+
let start = 0;
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < value.length) {
|
|
103
|
+
let escapes = 0;
|
|
104
|
+
for (let j = i - 1; j >= 0 && value[j] === '\\'; j--)
|
|
105
|
+
escapes++;
|
|
106
|
+
if (!value.startsWith('${', i) || escapes % 2 === 1) {
|
|
107
|
+
i++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
let before = i;
|
|
111
|
+
while (before > start && !/\s/.test(value[before - 1]))
|
|
112
|
+
before--;
|
|
113
|
+
if (before > start)
|
|
114
|
+
ranges.push({ value: value.slice(start, before), start, end: before });
|
|
115
|
+
i = interpolationEnd(value, i);
|
|
116
|
+
while (i < value.length && !/\s/.test(value[i]))
|
|
117
|
+
i++;
|
|
118
|
+
start = i;
|
|
119
|
+
}
|
|
120
|
+
if (start < value.length)
|
|
121
|
+
ranges.push({ value: value.slice(start), start, end: value.length });
|
|
122
|
+
return ranges;
|
|
123
|
+
}
|
|
124
|
+
function transformStaticClasses(value, transform, isSuppressedAt, offset = 0) {
|
|
125
|
+
const suppressed = [];
|
|
126
|
+
if (isSuppressedAt) {
|
|
127
|
+
let lineStart = 0;
|
|
128
|
+
while (lineStart < value.length) {
|
|
129
|
+
const newline = value.indexOf('\n', lineStart);
|
|
130
|
+
const end = newline < 0 ? value.length : newline + 1;
|
|
131
|
+
if (isSuppressedAt(offset + lineStart))
|
|
132
|
+
suppressed.push({ start: lineStart, end });
|
|
133
|
+
lineStart = end;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const ranges = staticClassRanges(value).flatMap((range) => {
|
|
137
|
+
const safe = [];
|
|
138
|
+
let start = range.start;
|
|
139
|
+
for (const line of suppressed) {
|
|
140
|
+
if (line.end <= start || line.start >= range.end)
|
|
141
|
+
continue;
|
|
142
|
+
if (line.start > start)
|
|
143
|
+
safe.push({
|
|
144
|
+
value: value.slice(start, line.start),
|
|
145
|
+
start,
|
|
146
|
+
end: line.start,
|
|
147
|
+
});
|
|
148
|
+
start = Math.max(start, line.end);
|
|
149
|
+
}
|
|
150
|
+
if (start < range.end)
|
|
151
|
+
safe.push({
|
|
152
|
+
value: value.slice(start, range.end),
|
|
153
|
+
start,
|
|
154
|
+
end: range.end,
|
|
155
|
+
});
|
|
156
|
+
return safe;
|
|
157
|
+
});
|
|
158
|
+
const preserveEdges = value.includes('${') || suppressed.length > 0;
|
|
159
|
+
let out = '';
|
|
160
|
+
let pos = 0;
|
|
161
|
+
for (const range of ranges) {
|
|
162
|
+
const segment = range.value;
|
|
163
|
+
if (!segment.trim()) {
|
|
164
|
+
out += value.slice(pos, range.end);
|
|
165
|
+
pos = range.end;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const leading = preserveEdges ? (segment.match(/^\s*/)?.[0] ?? '') : '';
|
|
169
|
+
const trailing = preserveEdges ? (segment.match(/\s*$/)?.[0] ?? '') : '';
|
|
170
|
+
const inner = segment.slice(leading.length, segment.length - trailing.length);
|
|
171
|
+
out +=
|
|
172
|
+
value.slice(pos, range.start) +
|
|
173
|
+
leading +
|
|
174
|
+
(inner ? transform(inner) : '') +
|
|
175
|
+
trailing;
|
|
176
|
+
pos = range.end;
|
|
177
|
+
}
|
|
178
|
+
return out + value.slice(pos);
|
|
18
179
|
}
|
|
19
180
|
function readQuotedString(content, quoteStart) {
|
|
20
181
|
const quote = content[quoteStart];
|
|
21
182
|
let quoteEnd = quoteStart + 1;
|
|
22
183
|
while (quoteEnd < content.length && content[quoteEnd] !== quote) {
|
|
23
184
|
if (content[quoteEnd] === '\\')
|
|
185
|
+
quoteEnd += 2;
|
|
186
|
+
else if (quote === '`' && content.startsWith('${', quoteEnd)) {
|
|
187
|
+
quoteEnd = interpolationEnd(content, quoteEnd);
|
|
188
|
+
}
|
|
189
|
+
else
|
|
24
190
|
quoteEnd++;
|
|
25
|
-
quoteEnd++;
|
|
26
191
|
}
|
|
27
192
|
if (quoteEnd >= content.length)
|
|
28
193
|
return null;
|
|
@@ -33,40 +198,248 @@ function readQuotedString(content, quoteStart) {
|
|
|
33
198
|
next: quoteEnd + 1,
|
|
34
199
|
};
|
|
35
200
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
let i = start;
|
|
39
|
-
let depth = 1;
|
|
40
|
-
while (i < content.length && depth > 0) {
|
|
201
|
+
function readRegex(content, start) {
|
|
202
|
+
let bracket = false;
|
|
203
|
+
for (let i = start + 1; i < content.length && content[i] !== '\n'; i++) {
|
|
41
204
|
const ch = content[i];
|
|
42
|
-
if (ch === '
|
|
43
|
-
|
|
205
|
+
if (ch === '\\')
|
|
206
|
+
i++;
|
|
207
|
+
else if (ch === '[')
|
|
208
|
+
bracket = true;
|
|
209
|
+
else if (ch === ']')
|
|
210
|
+
bracket = false;
|
|
211
|
+
else if (ch === '/' && !bracket) {
|
|
44
212
|
i++;
|
|
213
|
+
while (/[a-z]/i.test(content[i] ?? '') && i < content.length)
|
|
214
|
+
i++;
|
|
215
|
+
return i;
|
|
45
216
|
}
|
|
46
|
-
|
|
47
|
-
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const REGEX_PREFIX_TOKENS = new Set([
|
|
221
|
+
',',
|
|
222
|
+
':',
|
|
223
|
+
'?',
|
|
224
|
+
'=',
|
|
225
|
+
'!',
|
|
226
|
+
'(',
|
|
227
|
+
'[',
|
|
228
|
+
'{',
|
|
229
|
+
';',
|
|
230
|
+
'&',
|
|
231
|
+
'|',
|
|
232
|
+
'=>',
|
|
233
|
+
'&&',
|
|
234
|
+
'||',
|
|
235
|
+
'??',
|
|
236
|
+
'+',
|
|
237
|
+
'-',
|
|
238
|
+
'*',
|
|
239
|
+
'/',
|
|
240
|
+
'%',
|
|
241
|
+
'>',
|
|
242
|
+
'<',
|
|
243
|
+
'^',
|
|
244
|
+
'~',
|
|
245
|
+
'==',
|
|
246
|
+
'===',
|
|
247
|
+
'!=',
|
|
248
|
+
'!==',
|
|
249
|
+
'<=',
|
|
250
|
+
'>=',
|
|
251
|
+
'return',
|
|
252
|
+
'throw',
|
|
253
|
+
'case',
|
|
254
|
+
'typeof',
|
|
255
|
+
'void',
|
|
256
|
+
'delete',
|
|
257
|
+
'yield',
|
|
258
|
+
'await',
|
|
259
|
+
'in',
|
|
260
|
+
'instanceof',
|
|
261
|
+
]);
|
|
262
|
+
function regexMayStart(previous) {
|
|
263
|
+
return previous === undefined || REGEX_PREFIX_TOKENS.has(previous);
|
|
264
|
+
}
|
|
265
|
+
// Delimiter groups are enough to identify static class positions. This does
|
|
266
|
+
// not evaluate expressions or interpret arbitrary JavaScript as a class list.
|
|
267
|
+
function readCallTokens(content, start, closing = ')') {
|
|
268
|
+
const tokens = [];
|
|
269
|
+
let i = start;
|
|
270
|
+
while (i < content.length) {
|
|
271
|
+
const ch = content[i];
|
|
272
|
+
if (/\s/.test(ch)) {
|
|
48
273
|
i++;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (content.startsWith('//', i)) {
|
|
277
|
+
const end = content.indexOf('\n', i + 2);
|
|
278
|
+
i = end < 0 ? content.length : end;
|
|
279
|
+
continue;
|
|
49
280
|
}
|
|
50
|
-
|
|
281
|
+
if (content.startsWith('/*', i)) {
|
|
282
|
+
const end = content.indexOf('*/', i + 2);
|
|
283
|
+
if (end < 0)
|
|
284
|
+
return { tokens: [], next: content.length, complete: false };
|
|
285
|
+
i = end + 2;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (ch === closing)
|
|
289
|
+
return { tokens, next: i + 1, complete: true };
|
|
290
|
+
if (ch === ')' || ch === ']' || ch === '}')
|
|
291
|
+
return { tokens: [], next: i + 1, complete: false };
|
|
292
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
51
293
|
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);
|
|
294
|
+
if (!quoted)
|
|
295
|
+
return { tokens: [], next: content.length, complete: false };
|
|
296
|
+
tokens.push({ text: content.slice(i, quoted.next), string: quoted });
|
|
60
297
|
i = quoted.next;
|
|
298
|
+
continue;
|
|
61
299
|
}
|
|
62
|
-
|
|
63
|
-
|
|
300
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
301
|
+
const end = { '(': ')', '[': ']', '{': '}' }[ch];
|
|
302
|
+
const group = readCallTokens(content, i + 1, end);
|
|
303
|
+
if (!group.complete)
|
|
304
|
+
return { tokens: [], next: group.next, complete: false };
|
|
305
|
+
tokens.push({ text: ch, children: group.tokens });
|
|
306
|
+
i = group.next;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (ch === '/' && regexMayStart(tokens.at(-1)?.text)) {
|
|
310
|
+
const end = readRegex(content, i);
|
|
311
|
+
if (!end)
|
|
312
|
+
return { tokens: [], next: content.length, complete: false };
|
|
313
|
+
tokens.push({ text: content.slice(i, end) });
|
|
314
|
+
i = end;
|
|
315
|
+
continue;
|
|
64
316
|
}
|
|
317
|
+
const word = content.slice(i).match(/^[\w$]+/);
|
|
318
|
+
const operator = content
|
|
319
|
+
.slice(i)
|
|
320
|
+
.match(/^(?:===|!==|=>|&&|\|\||\?\?|==|!=|<=|>=|\?\.|\+\+|--|\*\*|\.\.\.)/);
|
|
321
|
+
const text = word?.[0] ?? operator?.[0] ?? ch;
|
|
322
|
+
tokens.push({ text });
|
|
323
|
+
i += text.length;
|
|
65
324
|
}
|
|
66
|
-
return {
|
|
325
|
+
return { tokens: [], next: content.length, complete: false };
|
|
67
326
|
}
|
|
68
|
-
function
|
|
69
|
-
const
|
|
327
|
+
function splitTokens(tokens, separator) {
|
|
328
|
+
const result = [[]];
|
|
329
|
+
for (const token of tokens) {
|
|
330
|
+
if (token.text === separator)
|
|
331
|
+
result.push([]);
|
|
332
|
+
else
|
|
333
|
+
result[result.length - 1].push(token);
|
|
334
|
+
}
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
function objectProperties(token) {
|
|
338
|
+
if (token?.text !== '{' || !token.children)
|
|
339
|
+
return [];
|
|
340
|
+
return splitTokens(token.children, ',').flatMap((property) => property.length >= 3 &&
|
|
341
|
+
property[1].text === ':' &&
|
|
342
|
+
(property[0].string || /^[\w$]+$/.test(property[0].text))
|
|
343
|
+
? [{ key: property[0], value: property.slice(2) }]
|
|
344
|
+
: []);
|
|
345
|
+
}
|
|
346
|
+
function propertyName(key) {
|
|
347
|
+
return key.string?.value ?? key.text;
|
|
348
|
+
}
|
|
349
|
+
function classExpression(tokens, functionNames, objects = true) {
|
|
350
|
+
if (tokens.length === 1) {
|
|
351
|
+
const token = tokens[0];
|
|
352
|
+
if (token.string)
|
|
353
|
+
return [token.string];
|
|
354
|
+
if (token.text === '[' && token.children)
|
|
355
|
+
return splitTokens(token.children, ',').flatMap((part) => classExpression(part, functionNames, objects));
|
|
356
|
+
if (token.text === '{' && objects)
|
|
357
|
+
return objectProperties(token).flatMap(({ key }) => key.string ? [key.string] : []);
|
|
358
|
+
// A grouping pair does not establish class context, but preserves one.
|
|
359
|
+
if (token.text === '(' && token.children)
|
|
360
|
+
return classExpression(token.children, functionNames, objects);
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
const question = tokens.findIndex(({ text }) => text === '?');
|
|
364
|
+
if (question >= 0) {
|
|
365
|
+
let nested = 0;
|
|
366
|
+
for (let i = question + 1; i < tokens.length; i++) {
|
|
367
|
+
if (tokens[i].text === '?')
|
|
368
|
+
nested++;
|
|
369
|
+
else if (tokens[i].text === ':' && nested-- === 0)
|
|
370
|
+
return [
|
|
371
|
+
...classExpression(tokens.slice(question + 1, i), functionNames, objects),
|
|
372
|
+
...classExpression(tokens.slice(i + 1), functionNames, objects),
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
const logical = tokens.findIndex(({ text }) => text === '&&' || text === '||' || text === '??');
|
|
378
|
+
if (logical >= 0)
|
|
379
|
+
return [
|
|
380
|
+
...(tokens[logical].text === '&&'
|
|
381
|
+
? []
|
|
382
|
+
: classExpression(tokens.slice(0, logical), functionNames, objects)),
|
|
383
|
+
...classExpression(tokens.slice(logical + 1), functionNames, objects),
|
|
384
|
+
];
|
|
385
|
+
if (tokens.length === 2 &&
|
|
386
|
+
functionNames.includes(tokens[0].text) &&
|
|
387
|
+
tokens[1].text === '(' &&
|
|
388
|
+
tokens[1].children)
|
|
389
|
+
return classCall(tokens[0].text, tokens[1].children, functionNames);
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
function cvaOptions(token, functionNames) {
|
|
393
|
+
const result = [];
|
|
394
|
+
for (const { key, value } of objectProperties(token)) {
|
|
395
|
+
if (value.length !== 1)
|
|
396
|
+
continue;
|
|
397
|
+
if (propertyName(key) === 'variants') {
|
|
398
|
+
for (const variant of objectProperties(value[0])) {
|
|
399
|
+
if (variant.value.length !== 1)
|
|
400
|
+
continue;
|
|
401
|
+
for (const choice of objectProperties(variant.value[0]))
|
|
402
|
+
result.push(...classExpression(choice.value, functionNames, false));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else if (propertyName(key) === 'compoundVariants' &&
|
|
406
|
+
value[0].text === '[' &&
|
|
407
|
+
value[0].children) {
|
|
408
|
+
for (const entry of splitTokens(value[0].children, ',')) {
|
|
409
|
+
if (entry.length !== 1)
|
|
410
|
+
continue;
|
|
411
|
+
for (const compound of objectProperties(entry[0])) {
|
|
412
|
+
if (propertyName(compound.key) === 'class' ||
|
|
413
|
+
propertyName(compound.key) === 'className')
|
|
414
|
+
result.push(...classExpression(compound.value, functionNames, false));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
function classCall(name, tokens, functionNames) {
|
|
422
|
+
const args = splitTokens(tokens, ',');
|
|
423
|
+
if (name === 'cva')
|
|
424
|
+
return [
|
|
425
|
+
...classExpression(args[0] ?? [], functionNames, false),
|
|
426
|
+
...(args[1]?.length === 1 ? cvaOptions(args[1][0], functionNames) : []),
|
|
427
|
+
];
|
|
428
|
+
return args.flatMap((arg) => classExpression(arg, functionNames));
|
|
429
|
+
}
|
|
430
|
+
function scanCallStrings(content, start, functionNames = []) {
|
|
431
|
+
const scanned = readCallTokens(content, start);
|
|
432
|
+
const name = content.slice(0, start - 1).match(/([\w$]+)\s*$/)?.[1] ?? '';
|
|
433
|
+
return {
|
|
434
|
+
strings: scanned.complete
|
|
435
|
+
? classCall(name, scanned.tokens, functionNames)
|
|
436
|
+
: [],
|
|
437
|
+
consumed: scanned.next - start,
|
|
438
|
+
complete: scanned.complete,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function transformCallContent(content, start, transform, isSuppressedAt, functionNames = []) {
|
|
442
|
+
const scanned = scanCallStrings(content, start, functionNames);
|
|
70
443
|
const original = content.slice(start, start + scanned.consumed);
|
|
71
444
|
if (!scanned.complete) {
|
|
72
445
|
return { processed: original, consumed: scanned.consumed, count: 0 };
|
|
@@ -76,9 +449,7 @@ function transformCallContent(content, start, transform, isSuppressedAt) {
|
|
|
76
449
|
let count = 0;
|
|
77
450
|
for (const string of scanned.strings) {
|
|
78
451
|
parts.push(content.slice(pos, string.start));
|
|
79
|
-
const out =
|
|
80
|
-
? string.value
|
|
81
|
-
: transform(string.value);
|
|
452
|
+
const out = transformStaticClasses(string.value, transform, isSuppressedAt, string.start);
|
|
82
453
|
if (out !== string.value)
|
|
83
454
|
count++;
|
|
84
455
|
parts.push(out);
|
|
@@ -93,6 +464,110 @@ function offsetSuppressor(content, isSuppressed) {
|
|
|
93
464
|
const positionAt = createPositionLookup(content);
|
|
94
465
|
return (offset) => isSuppressed(positionAt(offset).line);
|
|
95
466
|
}
|
|
467
|
+
// Ignore source comments, quoted prose and regex literals before looking for
|
|
468
|
+
// attribute/function entry points. Ambiguous syntax is deliberately skipped.
|
|
469
|
+
function protectedSourceOffsets(content) {
|
|
470
|
+
const ranges = [];
|
|
471
|
+
const tags = [];
|
|
472
|
+
const pendingTags = [];
|
|
473
|
+
let expressionEnd = 0;
|
|
474
|
+
let i = 0;
|
|
475
|
+
while (i < content.length) {
|
|
476
|
+
const start = i;
|
|
477
|
+
const ch = content[i];
|
|
478
|
+
const tag = pendingTags.at(0);
|
|
479
|
+
if (tag?.end === i) {
|
|
480
|
+
if (tag.closing)
|
|
481
|
+
tags.pop();
|
|
482
|
+
else if (!tag.selfClosing)
|
|
483
|
+
tags.push(tag.name);
|
|
484
|
+
pendingTags.length = 0;
|
|
485
|
+
i++;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (!tag && ch === '<' && !content.startsWith('<!--', i)) {
|
|
489
|
+
const match = content
|
|
490
|
+
.slice(i)
|
|
491
|
+
.match(/^<(\/?)([A-Za-z][\w:.-]*)(?=[\s/>])/);
|
|
492
|
+
if (match) {
|
|
493
|
+
let end = i + match[0].length;
|
|
494
|
+
while (end < content.length && content[end] !== '>') {
|
|
495
|
+
if (content[end] === '"' ||
|
|
496
|
+
content[end] === "'" ||
|
|
497
|
+
content[end] === '`') {
|
|
498
|
+
end = readQuotedString(content, end)?.next ?? content.length;
|
|
499
|
+
}
|
|
500
|
+
else if (content[end] === '{') {
|
|
501
|
+
const expression = readCallTokens(content, end + 1, '}');
|
|
502
|
+
end = expression.next;
|
|
503
|
+
}
|
|
504
|
+
else
|
|
505
|
+
end++;
|
|
506
|
+
}
|
|
507
|
+
const closing = match[1] === '/';
|
|
508
|
+
const selfClosing = content[end - 1] === '/';
|
|
509
|
+
const name = match[2];
|
|
510
|
+
if (end < content.length &&
|
|
511
|
+
(tags.length > 0 ||
|
|
512
|
+
selfClosing ||
|
|
513
|
+
content.includes(`</${name}>`, end)))
|
|
514
|
+
pendingTags.push({ end, name, closing, selfClosing });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (!tag && tags.length > 0 && i >= expressionEnd && ch !== '<') {
|
|
518
|
+
if (ch === '{')
|
|
519
|
+
expressionEnd = readCallTokens(content, i + 1, '}').next;
|
|
520
|
+
else {
|
|
521
|
+
while (i < content.length && content[i] !== '<' && content[i] !== '{')
|
|
522
|
+
i++;
|
|
523
|
+
ranges.push({ start, end: i });
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
528
|
+
i = readQuotedString(content, i)?.next ?? content.length;
|
|
529
|
+
}
|
|
530
|
+
else if (content.startsWith('//', i)) {
|
|
531
|
+
const end = content.indexOf('\n', i + 2);
|
|
532
|
+
i = end < 0 ? content.length : end;
|
|
533
|
+
}
|
|
534
|
+
else if (content.startsWith('/*', i) || content.startsWith('<!--', i)) {
|
|
535
|
+
const html = content.startsWith('<!--', i);
|
|
536
|
+
const ending = html ? '-->' : '*/';
|
|
537
|
+
const end = content.indexOf(ending, i + (html ? 4 : 2));
|
|
538
|
+
i = end < 0 ? content.length : end + ending.length;
|
|
539
|
+
}
|
|
540
|
+
else if (ch === '/' && content[i - 1] !== '<' && content[i + 1] !== '>') {
|
|
541
|
+
const preceding = content
|
|
542
|
+
.slice(0, i)
|
|
543
|
+
.trimEnd()
|
|
544
|
+
.match(/(?:[\w$]+|=>|&&|\|\||\?\?|.)$/)?.[0];
|
|
545
|
+
const end = regexMayStart(preceding) ? readRegex(content, i) : null;
|
|
546
|
+
if (!end) {
|
|
547
|
+
i++;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
i = end;
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
i++;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
ranges.push({ start, end: i });
|
|
557
|
+
}
|
|
558
|
+
return (offset) => {
|
|
559
|
+
let low = 0;
|
|
560
|
+
let high = ranges.length;
|
|
561
|
+
while (low < high) {
|
|
562
|
+
const mid = (low + high) >>> 1;
|
|
563
|
+
if (ranges[mid].end <= offset)
|
|
564
|
+
low = mid + 1;
|
|
565
|
+
else
|
|
566
|
+
high = mid;
|
|
567
|
+
}
|
|
568
|
+
return low < ranges.length && ranges[low].start <= offset;
|
|
569
|
+
};
|
|
570
|
+
}
|
|
96
571
|
export function replaceClassStrings(content, transform, opts = {}) {
|
|
97
572
|
const attrNames = opts.attributeNames?.length
|
|
98
573
|
? opts.attributeNames
|
|
@@ -100,20 +575,21 @@ export function replaceClassStrings(content, transform, opts = {}) {
|
|
|
100
575
|
let result = content;
|
|
101
576
|
let count = 0;
|
|
102
577
|
const isSuppressed = opts.isSuppressed;
|
|
578
|
+
const isProtected = protectedSourceOffsets(content);
|
|
103
579
|
const isAttributeSuppressed = offsetSuppressor(content, isSuppressed);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const attrOffset = offset + before.length;
|
|
112
|
-
const out = isAttributeSuppressed?.(attrOffset) ? raw : transform(raw);
|
|
113
|
-
if (out !== raw)
|
|
580
|
+
const attributeParts = [];
|
|
581
|
+
let attributeCursor = 0;
|
|
582
|
+
for (const string of attributeStrings(content, attrNames)) {
|
|
583
|
+
if (isProtected(string.attributeStart))
|
|
584
|
+
continue;
|
|
585
|
+
const out = transformStaticClasses(string.value, transform, isAttributeSuppressed, string.start);
|
|
586
|
+
if (out !== string.value)
|
|
114
587
|
count++;
|
|
115
|
-
|
|
116
|
-
|
|
588
|
+
attributeParts.push(content.slice(attributeCursor, string.start), out);
|
|
589
|
+
attributeCursor = string.end;
|
|
590
|
+
}
|
|
591
|
+
attributeParts.push(content.slice(attributeCursor));
|
|
592
|
+
result = attributeParts.join('');
|
|
117
593
|
const funcNames = opts.functionNames;
|
|
118
594
|
if (!funcNames?.length)
|
|
119
595
|
return { result, count };
|
|
@@ -121,12 +597,13 @@ export function replaceClassStrings(content, transform, opts = {}) {
|
|
|
121
597
|
const buf = [];
|
|
122
598
|
let pos = 0;
|
|
123
599
|
const isCallSuppressed = offsetSuppressor(result, isSuppressed);
|
|
600
|
+
const isCallProtected = protectedSourceOffsets(result);
|
|
124
601
|
for (const m of result.matchAll(funcRe)) {
|
|
125
602
|
const callStart = (m.index ?? 0) + m[0].length;
|
|
126
|
-
if (callStart < pos)
|
|
603
|
+
if (callStart < pos || isCallProtected(m.index ?? 0))
|
|
127
604
|
continue;
|
|
128
605
|
buf.push(result.slice(pos, callStart));
|
|
129
|
-
const { processed, consumed, count: c, } = transformCallContent(result, callStart, transform, isCallSuppressed);
|
|
606
|
+
const { processed, consumed, count: c, } = transformCallContent(result, callStart, transform, isCallSuppressed, funcNames);
|
|
130
607
|
buf.push(processed);
|
|
131
608
|
pos = callStart + consumed;
|
|
132
609
|
count += c;
|
|
@@ -140,18 +617,39 @@ export function extractClassStrings(content, opts = {}) {
|
|
|
140
617
|
? opts.attributeNames
|
|
141
618
|
: DEFAULT_ATTR_NAMES;
|
|
142
619
|
const results = [];
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
});
|
|
147
628
|
}
|
|
148
629
|
const funcNames = opts.functionNames;
|
|
149
630
|
if (!funcNames?.length)
|
|
150
|
-
return results;
|
|
631
|
+
return staticRanges(results);
|
|
151
632
|
const funcRe = new RegExp(`\\b(?:${funcNames.map(escapeRegex).join('|')})\\s*\\(`, 'g');
|
|
633
|
+
let consumedUntil = 0;
|
|
152
634
|
for (const m of content.matchAll(funcRe)) {
|
|
635
|
+
if (isProtected(m.index ?? 0) || (m.index ?? 0) < consumedUntil)
|
|
636
|
+
continue;
|
|
153
637
|
const start = (m.index ?? 0) + m[0].length;
|
|
154
|
-
|
|
638
|
+
const scanned = scanCallStrings(content, start, funcNames);
|
|
639
|
+
results.push(...scanned.strings);
|
|
640
|
+
consumedUntil = start + scanned.consumed;
|
|
155
641
|
}
|
|
156
|
-
return results;
|
|
642
|
+
return staticRanges(results);
|
|
643
|
+
}
|
|
644
|
+
function staticRanges(strings) {
|
|
645
|
+
const seen = new Set();
|
|
646
|
+
return strings.flatMap((string) => staticClassRanges(string.value).flatMap((range) => {
|
|
647
|
+
const start = string.start + range.start;
|
|
648
|
+
const end = string.start + range.end;
|
|
649
|
+
const key = `${start}:${end}`;
|
|
650
|
+
if (seen.has(key))
|
|
651
|
+
return [];
|
|
652
|
+
seen.add(key);
|
|
653
|
+
return [{ value: range.value, start, end }];
|
|
654
|
+
}));
|
|
157
655
|
}
|
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';
|