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.
@@ -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*(?:({)(?:"([^"]+)"|'([^']+)'|\`([^\`]+)\`)(})|(?:"([^"]+)"|'([^']+)'|\`([^\`]+)\`))`, 'g');
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 scanCallStrings(content, start) {
37
- const strings = [];
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 === '(' || ch === '[' || ch === '{') {
43
- depth++;
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
- else if (ch === ')' || ch === ']' || ch === '}') {
47
- depth--;
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
- else if (ch === '"' || ch === "'" || ch === '`') {
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
- strings: [],
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
- else {
63
- i++;
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 { strings, consumed: i - start, complete: depth === 0 };
325
+ return { tokens: [], next: content.length, complete: false };
67
326
  }
68
- function transformCallContent(content, start, transform, isSuppressedAt) {
69
- const scanned = scanCallStrings(content, start);
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 = isSuppressedAt?.(string.start - 1)
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
- result = result.replace(buildAttrRegex(attrNames), (_full, before, attr, open, bracedDq, bracedSq, bracedBt, close, dq, sq, bt, offset) => {
105
- const raw = bracedDq ?? bracedSq ?? bracedBt ?? dq ?? sq ?? bt ?? '';
106
- const q = bracedDq !== undefined || dq !== undefined
107
- ? '"'
108
- : bracedSq !== undefined || sq !== undefined
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
- return `${before}${attr}=${open ?? ''}${q}${out}${q}${close ?? ''}`;
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
- for (const m of content.matchAll(buildAttrRegex(attrNames))) {
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 });
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
- results.push(...scanCallStrings(content, start).strings);
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
  }
@@ -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';