tailwind-canonical 0.7.0 → 0.7.1

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.
@@ -0,0 +1,3 @@
1
+ export declare function errMsg(error: unknown): string;
2
+ export declare function pluralize(count: number, word: string): string;
3
+ export declare function timestamp(): string;
@@ -0,0 +1,11 @@
1
+ export function errMsg(error) {
2
+ return error instanceof Error ? error.message : String(error);
3
+ }
4
+ export function pluralize(count, word) {
5
+ return `${count} ${word}${count !== 1 ? 's' : ''}`;
6
+ }
7
+ export function timestamp() {
8
+ const date = new Date();
9
+ const pad = (value) => value.toString().padStart(2, '0');
10
+ return `[${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}]`;
11
+ }
@@ -0,0 +1,69 @@
1
+ import type { Finding } from '../core/analyzer.js';
2
+ import type { ConsistencyReport } from '../core/consistency.js';
3
+ import type { Config } from '../core/rules.js';
4
+ import type { TypoFinding } from '../core/typos.js';
5
+ import type { FileCounts, Sink } from './types.js';
6
+ type SarifLevel = 'error' | 'warning' | 'note';
7
+ type SarifLocation = {
8
+ physicalLocation: {
9
+ artifactLocation: {
10
+ uri: string;
11
+ uriBaseId?: string;
12
+ };
13
+ region: {
14
+ startLine: number;
15
+ startColumn: number;
16
+ endLine?: number;
17
+ endColumn?: number;
18
+ };
19
+ };
20
+ };
21
+ type SarifReport = {
22
+ $schema: string;
23
+ version: string;
24
+ runs: Array<{
25
+ tool: {
26
+ driver: {
27
+ name: string;
28
+ version?: string;
29
+ informationUri: string;
30
+ rules: Array<{
31
+ id: string;
32
+ name: string;
33
+ shortDescription: {
34
+ text: string;
35
+ };
36
+ defaultConfiguration: {
37
+ level: SarifLevel;
38
+ };
39
+ }>;
40
+ };
41
+ };
42
+ results: Array<{
43
+ ruleId: string;
44
+ level?: SarifLevel;
45
+ message: {
46
+ text: string;
47
+ };
48
+ locations: SarifLocation[];
49
+ partialFingerprints?: Record<string, string>;
50
+ }>;
51
+ }>;
52
+ };
53
+ type SarifRule = SarifReport['runs'][0]['tool']['driver']['rules'][0];
54
+ type SarifResult = SarifReport['runs'][0]['results'][0];
55
+ export type SarifContext = {
56
+ root: string;
57
+ version?: string;
58
+ };
59
+ export declare function sarifDocument(rules: SarifRule[], results: SarifResult[], ctx: SarifContext): SarifReport;
60
+ export declare function typoSarifDocument(findings: TypoFinding[], ctx: SarifContext): SarifReport;
61
+ export declare function findingsSarifDocument(findings: Finding[], ctx: SarifContext): SarifReport;
62
+ export declare function analyzeSarifDocument(report: ConsistencyReport, ctx: SarifContext): SarifReport;
63
+ export declare function logTransformCounts(counts: FileCounts, file: string, check: boolean, sink: Sink): void;
64
+ export declare function logFindings(findings: Finding[], sink: Sink): void;
65
+ export declare function logWatchFindings(file: string, findings: Finding[], timestamp: string, sink: Sink): void;
66
+ export declare function logTyposText(findings: TypoFinding[], sink: Sink): void;
67
+ export declare function writeJson(sink: Sink, value: unknown): void;
68
+ export declare function logAnalyzeText(report: ConsistencyReport, issueCount: number, config: Config, sink: Sink): void;
69
+ export {};
@@ -0,0 +1,333 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isAbsolute, relative } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { pluralize } from './format.js';
5
+ const SARIF_SCHEMA = 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json';
6
+ const DEFAULT_ANALYZE_TEXT_OPTIONS = {
7
+ maxScaleGroups: 8,
8
+ maxScaleValues: 5,
9
+ maxRareValues: 12,
10
+ maxPatterns: 10,
11
+ };
12
+ const SRCROOT = '%SRCROOT%';
13
+ const FINGERPRINT_KEY = 'tailwindCanonical/v1';
14
+ function artifactLocation(file, root) {
15
+ const rel = relative(root, file);
16
+ if (rel === '' || /^\.\.(?:[\\/]|$)/.test(rel) || isAbsolute(rel)) {
17
+ return { uri: pathToFileURL(file).href };
18
+ }
19
+ const uri = rel.split(/[\\/]/).map(encodeURIComponent).join('/');
20
+ return { uri, uriBaseId: SRCROOT };
21
+ }
22
+ function fingerprint(parts) {
23
+ return createHash('sha256').update(parts.join('\0')).digest('hex');
24
+ }
25
+ function finalizeResults(rules, results, root) {
26
+ const levels = new Map(rules.map((rule) => [rule.id, rule.defaultConfiguration.level]));
27
+ const occurrences = new Map();
28
+ return results.map((result) => {
29
+ const locations = result.locations.map((location) => ({
30
+ physicalLocation: {
31
+ ...location.physicalLocation,
32
+ artifactLocation: artifactLocation(location.physicalLocation.artifactLocation.uri, root),
33
+ },
34
+ }));
35
+ const key = fingerprint([
36
+ result.ruleId,
37
+ result.message.text,
38
+ ...locations.map((location) => location.physicalLocation.artifactLocation.uri),
39
+ ]);
40
+ const index = occurrences.get(key) ?? 0;
41
+ occurrences.set(key, index + 1);
42
+ return {
43
+ ruleId: result.ruleId,
44
+ level: result.level ?? levels.get(result.ruleId) ?? 'warning',
45
+ message: result.message,
46
+ locations,
47
+ partialFingerprints: {
48
+ [FINGERPRINT_KEY]: fingerprint([key, String(index)]),
49
+ },
50
+ };
51
+ });
52
+ }
53
+ export function sarifDocument(rules, results, ctx) {
54
+ return {
55
+ $schema: SARIF_SCHEMA,
56
+ version: '2.1.0',
57
+ runs: [
58
+ {
59
+ tool: {
60
+ driver: {
61
+ name: 'tailwind-canonical',
62
+ ...(ctx.version ? { version: ctx.version } : {}),
63
+ informationUri: 'https://github.com/peak-lab/tailwind-canonical',
64
+ rules,
65
+ },
66
+ },
67
+ results: finalizeResults(rules, results, ctx.root),
68
+ },
69
+ ],
70
+ };
71
+ }
72
+ function pointLocation(uri, line, col, length) {
73
+ const region = length === undefined
74
+ ? { startLine: line, startColumn: col }
75
+ : {
76
+ startLine: line,
77
+ startColumn: col,
78
+ endLine: line,
79
+ endColumn: col + length,
80
+ };
81
+ return { physicalLocation: { artifactLocation: { uri }, region } };
82
+ }
83
+ function fileLocations(files) {
84
+ return files.map((uri) => pointLocation(uri, 1, 1));
85
+ }
86
+ function uniqueFiles(entries) {
87
+ return [...new Set(entries.flatMap((entry) => entry.files))];
88
+ }
89
+ export function typoSarifDocument(findings, ctx) {
90
+ return sarifDocument([
91
+ {
92
+ id: 'color-typo',
93
+ name: 'ColorTypo',
94
+ shortDescription: {
95
+ text: 'Class color name is a likely typo of a Tailwind color',
96
+ },
97
+ defaultConfiguration: { level: 'warning' },
98
+ },
99
+ ], findings.map((f) => ({
100
+ ruleId: 'color-typo',
101
+ message: { text: `${f.original} → ${f.suggestion}` },
102
+ locations: [pointLocation(f.file, f.line, f.col, f.original.length)],
103
+ })), ctx);
104
+ }
105
+ export function findingsSarifDocument(findings, ctx) {
106
+ return sarifDocument([
107
+ {
108
+ id: 'no-arbitrary-canonical',
109
+ name: 'NoArbitraryCanonical',
110
+ shortDescription: {
111
+ text: 'Arbitrary value has a canonical Tailwind equivalent',
112
+ },
113
+ defaultConfiguration: { level: 'warning' },
114
+ },
115
+ ], findings.map((f) => ({
116
+ ruleId: 'no-arbitrary-canonical',
117
+ message: {
118
+ text: `${f.suggestion.original} → ${f.suggestion.canonical}`,
119
+ },
120
+ locations: [
121
+ pointLocation(f.file, f.line, f.col, f.suggestion.original.length),
122
+ ],
123
+ })), ctx);
124
+ }
125
+ const ANALYZE_SARIF_RULES = [
126
+ {
127
+ id: 'color-variant-inconsistency',
128
+ name: 'ColorVariantInconsistency',
129
+ shortDescription: {
130
+ text: 'Multiple color variants of the same family used for one property',
131
+ },
132
+ defaultConfiguration: { level: 'note' },
133
+ },
134
+ {
135
+ id: 'scale-inconsistency',
136
+ name: 'ScaleInconsistency',
137
+ shortDescription: {
138
+ text: 'Inconsistent scale values used for the same property',
139
+ },
140
+ defaultConfiguration: { level: 'note' },
141
+ },
142
+ {
143
+ id: 'rare-scale-value',
144
+ name: 'RareScaleValue',
145
+ shortDescription: {
146
+ text: 'A scale value appears rarely within an otherwise common property',
147
+ },
148
+ defaultConfiguration: { level: 'note' },
149
+ },
150
+ {
151
+ id: 'repeated-combination',
152
+ name: 'RepeatedCombination',
153
+ shortDescription: {
154
+ text: 'Identical class combination repeated across files',
155
+ },
156
+ defaultConfiguration: { level: 'note' },
157
+ },
158
+ ];
159
+ export function analyzeSarifDocument(report, ctx) {
160
+ const colorResults = report.colorVariants.map((group) => ({
161
+ ruleId: 'color-variant-inconsistency',
162
+ message: {
163
+ text: `${group.variants.length} ${group.family} color variants for ${group.property}: ${group.variants.map((v) => v.token).join(', ')}`,
164
+ },
165
+ locations: fileLocations(uniqueFiles(group.variants)),
166
+ }));
167
+ const scaleResults = report.scaleInconsistencies.map((scale) => ({
168
+ ruleId: 'scale-inconsistency',
169
+ message: {
170
+ text: `${scale.property} inconsistency: ${scale.values.map((v) => scaleClass(scale.property, v.value)).join(' vs ')}`,
171
+ },
172
+ locations: fileLocations(uniqueFiles(scale.values)),
173
+ }));
174
+ const rareResults = report.rareScaleValues.map((rare) => ({
175
+ ruleId: 'rare-scale-value',
176
+ message: {
177
+ text: `${rare.className} is rare for ${rare.property}: ${rare.count} occurrence(s) in ${rare.files.length} file(s), within ${rare.propertyCount} ${rare.property} uses`,
178
+ },
179
+ locations: fileLocations(rare.files),
180
+ }));
181
+ const comboResults = report.combinations.map((combo) => ({
182
+ ruleId: 'repeated-combination',
183
+ message: {
184
+ text: `Repeated class combination: ${combo.classes.join(' ')}`,
185
+ },
186
+ locations: fileLocations(combo.files),
187
+ }));
188
+ return sarifDocument(ANALYZE_SARIF_RULES, [...colorResults, ...scaleResults, ...rareResults, ...comboResults], ctx);
189
+ }
190
+ const TRANSFORM_LABELS = [
191
+ {
192
+ key: 'fixed',
193
+ applied: 'fixed ',
194
+ pending: 'would fix',
195
+ unit: 'replacement',
196
+ },
197
+ {
198
+ key: 'deduped',
199
+ applied: 'deduped',
200
+ pending: 'would dedup',
201
+ unit: 'class string',
202
+ },
203
+ {
204
+ key: 'merged',
205
+ applied: 'merged ',
206
+ pending: 'would merge',
207
+ unit: 'conflict',
208
+ },
209
+ {
210
+ key: 'sorted',
211
+ applied: 'sorted ',
212
+ pending: 'would sort',
213
+ unit: 'class string',
214
+ },
215
+ ];
216
+ export function logTransformCounts(counts, file, check, sink) {
217
+ for (const { key, applied, pending, unit } of TRANSFORM_LABELS) {
218
+ const count = counts[key];
219
+ if (count > 0) {
220
+ sink.log(` ${check ? pending : applied} ${file} (${pluralize(count, unit)})`);
221
+ }
222
+ }
223
+ }
224
+ export function logFindings(findings, sink) {
225
+ for (const finding of findings) {
226
+ const tag = finding.suggestion.isCustomToken ? ' [custom token]' : '';
227
+ sink.log(` ${finding.file}:${finding.line}:${finding.col} ${finding.suggestion.original} → ${finding.suggestion.canonical}${tag}`);
228
+ }
229
+ }
230
+ export function logWatchFindings(file, findings, timestamp, sink) {
231
+ if (findings.length === 0)
232
+ return;
233
+ sink.log(`${timestamp} ${file} — ${pluralize(findings.length, 'finding')}`);
234
+ for (const finding of findings) {
235
+ const tag = finding.suggestion.isCustomToken ? ' [custom token]' : '';
236
+ sink.log(` ${finding.line}:${finding.col} ${finding.suggestion.original} → ${finding.suggestion.canonical}${tag}`);
237
+ }
238
+ }
239
+ export function logTyposText(findings, sink) {
240
+ for (const finding of findings) {
241
+ sink.log(` ${finding.file}:${finding.line}:${finding.col} ${finding.original} → ${finding.suggestion} [typo]`);
242
+ }
243
+ sink.log(findings.length === 0
244
+ ? '✓ No likely typos found'
245
+ : `\n✖ Found ${pluralize(findings.length, 'likely typo')}`);
246
+ }
247
+ export function writeJson(sink, value) {
248
+ sink.write(`${JSON.stringify(value, null, 2)}\n`);
249
+ }
250
+ function scaleClass(property, value) {
251
+ return value.startsWith('-')
252
+ ? `-${property}-${value.slice(1)}`
253
+ : `${property}-${value}`;
254
+ }
255
+ function compactPath(file) {
256
+ const normalized = file.replace(/\\/g, '/');
257
+ const parts = normalized.split('/');
258
+ return parts.length <= 4 ? normalized : `.../${parts.slice(-4).join('/')}`;
259
+ }
260
+ function withMore(values, limit, format) {
261
+ const shown = values.slice(0, limit).map(format);
262
+ const remaining = values.length - shown.length;
263
+ if (remaining > 0)
264
+ shown.push(`+${remaining} more`);
265
+ return shown.join(', ');
266
+ }
267
+ function scaleTotalUses(scale) {
268
+ return scale.values.reduce((sum, value) => sum + value.count, 0);
269
+ }
270
+ export function logAnalyzeText(report, issueCount, config, sink) {
271
+ const options = {
272
+ maxScaleGroups: config.analyze?.maxScaleGroups ??
273
+ DEFAULT_ANALYZE_TEXT_OPTIONS.maxScaleGroups,
274
+ maxScaleValues: config.analyze?.maxScaleValues ??
275
+ DEFAULT_ANALYZE_TEXT_OPTIONS.maxScaleValues,
276
+ maxRareValues: config.analyze?.maxRareValues ??
277
+ DEFAULT_ANALYZE_TEXT_OPTIONS.maxRareValues,
278
+ maxPatterns: config.analyze?.maxPatterns ?? DEFAULT_ANALYZE_TEXT_OPTIONS.maxPatterns,
279
+ };
280
+ sink.log('tailwind-canonical analyze');
281
+ sink.log(`Files analyzed: ${report.filesAnalyzed}`);
282
+ sink.log(`Issue groups: ${issueCount} (${pluralize(report.colorVariants.length, 'color')}, ${pluralize(report.scaleInconsistencies.length, 'scale')}, ${pluralize(report.combinations.length, 'pattern')})`);
283
+ if (report.rareScaleValues.length > 0)
284
+ sink.log(`Rare values: ${report.rareScaleValues.length}`);
285
+ if (issueCount === 0) {
286
+ sink.log('\nNo cross-file inconsistencies found');
287
+ return;
288
+ }
289
+ if (report.colorVariants.length > 0) {
290
+ sink.log('\nColor variants');
291
+ for (const group of report.colorVariants) {
292
+ const tokens = group.variants
293
+ .map((value) => `${group.property}-${value.token} x${value.count}`)
294
+ .join(', ');
295
+ sink.log(` - ${group.property}/${group.family}: ${tokens}`);
296
+ }
297
+ }
298
+ if (report.scaleInconsistencies.length > 0) {
299
+ sink.log('\nScale inconsistency groups');
300
+ const scales = [...report.scaleInconsistencies].sort((a, b) => scaleTotalUses(b) - scaleTotalUses(a) ||
301
+ a.property.localeCompare(b.property));
302
+ for (const scale of scales.slice(0, options.maxScaleGroups)) {
303
+ const files = new Set(scale.values.flatMap((value) => value.files));
304
+ sink.log(` - ${scale.property}: ${scale.values.length} values, ${scaleTotalUses(scale)} uses, ${pluralize(files.size, 'file')}`);
305
+ sink.log(` Top: ${withMore(scale.values, options.maxScaleValues, (value) => `${scaleClass(scale.property, value.value)} (${pluralize(value.count, 'use')}, ${pluralize(value.files.length, 'file')})`)}`);
306
+ }
307
+ const remaining = scales.length - options.maxScaleGroups;
308
+ if (remaining > 0)
309
+ sink.log(` - +${remaining} more scale groups`);
310
+ }
311
+ if (report.rareScaleValues.length > 0) {
312
+ sink.log('\nRare scale values');
313
+ for (const rare of report.rareScaleValues.slice(0, options.maxRareValues)) {
314
+ const example = rare.files[0]
315
+ ? `; e.g. ${compactPath(rare.files[0])}`
316
+ : '';
317
+ sink.log(` - ${rare.className}: ${pluralize(rare.count, 'use')} in ${pluralize(rare.files.length, 'file')} (${rare.propertyCount} ${rare.property} uses total)${example}`);
318
+ }
319
+ const remaining = report.rareScaleValues.length - options.maxRareValues;
320
+ if (remaining > 0)
321
+ sink.log(` - +${remaining} more rare values`);
322
+ }
323
+ if (report.combinations.length > 0) {
324
+ sink.log('\nRepeated patterns');
325
+ for (const combo of report.combinations.slice(0, options.maxPatterns)) {
326
+ sink.log(` - Pattern: "${combo.classes.join(' ')}" repeated in ${pluralize(combo.files.length, 'file')}`);
327
+ }
328
+ const remaining = report.combinations.length - options.maxPatterns;
329
+ if (remaining > 0)
330
+ sink.log(` - +${remaining} more repeated patterns`);
331
+ }
332
+ sink.log(`\nFound ${pluralize(issueCount, 'consistency issue')} across ${pluralize(report.filesAnalyzed, 'file')}`);
333
+ }
@@ -0,0 +1,16 @@
1
+ export type Sink = {
2
+ log: (message: string) => void;
3
+ error: (message: string) => void;
4
+ write: (message: string) => void;
5
+ };
6
+ export type RunResult = {
7
+ exitCode: number;
8
+ watching?: boolean;
9
+ };
10
+ export type Reporter = 'text' | 'json' | 'sarif';
11
+ export type FileCounts = {
12
+ fixed: number;
13
+ deduped: number;
14
+ merged: number;
15
+ sorted: number;
16
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { Sink } from './types.js';
2
+ type WatchOptions = {
3
+ files: string[];
4
+ sink: Sink;
5
+ processFile: (file: string) => void;
6
+ };
7
+ export declare function startWatch({ files, sink, processFile }: WatchOptions): {
8
+ exitCode: number;
9
+ watching: true;
10
+ };
11
+ export {};
@@ -0,0 +1,33 @@
1
+ import { watch as fsWatch } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { errMsg, pluralize, timestamp } from './format.js';
4
+ export function startWatch({ files, sink, processFile }) {
5
+ const fileSet = new Set(files);
6
+ const timers = new Map();
7
+ const dirs = new Set(files.map((file) => dirname(file)));
8
+ sink.log(`Watching ${pluralize(files.length, 'file')}... (Ctrl+C to stop)`);
9
+ for (const dir of dirs) {
10
+ fsWatch(dir, { recursive: true }, (_, filename) => {
11
+ if (!filename)
12
+ return;
13
+ const full = resolve(dir, filename);
14
+ if (!fileSet.has(full))
15
+ return;
16
+ clearTimeout(timers.get(full));
17
+ timers.set(full, setTimeout(() => {
18
+ timers.delete(full);
19
+ try {
20
+ processFile(full);
21
+ }
22
+ catch (error) {
23
+ sink.error(`${timestamp()} ${full} — error: ${errMsg(error)}`);
24
+ }
25
+ }, 50));
26
+ });
27
+ }
28
+ process.once('SIGINT', () => {
29
+ sink.log('\nWatcher stopped.');
30
+ process.exit(0);
31
+ });
32
+ return { exitCode: 0, watching: true };
33
+ }
@@ -1,9 +1,10 @@
1
1
  import { extractClassStrings, SINGLE_CLASS_REGEX, toClassStringOpts, } from './class-strings.js';
2
2
  import { suggestCanonical } from './rules.js';
3
- import { getSuppressedLines, indexToLineCol } from './suppressions.js';
3
+ import { createPositionLookup, getSuppressedLines } from './suppressions.js';
4
4
  export function analyzeContent(filePath, content, config = {}) {
5
5
  const findings = [];
6
6
  const suppressed = getSuppressedLines(content);
7
+ const positionAt = createPositionLookup(content);
7
8
  const opts = toClassStringOpts(config);
8
9
  for (const { value, start } of extractClassStrings(content, opts)) {
9
10
  for (const clsMatch of value.matchAll(SINGLE_CLASS_REGEX)) {
@@ -11,7 +12,7 @@ export function analyzeContent(filePath, content, config = {}) {
11
12
  if (!suggestion)
12
13
  continue;
13
14
  const index = start + (clsMatch.index ?? 0);
14
- const { line, col } = indexToLineCol(content, index);
15
+ const { line, col } = positionAt(index);
15
16
  if (suppressed.has(line))
16
17
  continue;
17
18
  findings.push({ file: filePath, line, col, suggestion });
@@ -1,4 +1,4 @@
1
- import { lineAt } from './suppressions.js';
1
+ import { createPositionLookup } from './suppressions.js';
2
2
  /** Builds the shared {functionNames, attributeNames} extraction options from a config. */
3
3
  export function toClassStringOpts(config) {
4
4
  return {
@@ -65,7 +65,7 @@ function scanCallStrings(content, start) {
65
65
  }
66
66
  return { strings, consumed: i - start, complete: depth === 0 };
67
67
  }
68
- function transformCallContent(content, start, transform, isSuppressed) {
68
+ function transformCallContent(content, start, transform, isSuppressedAt) {
69
69
  const scanned = scanCallStrings(content, start);
70
70
  const original = content.slice(start, start + scanned.consumed);
71
71
  if (!scanned.complete) {
@@ -76,7 +76,7 @@ function transformCallContent(content, start, transform, isSuppressed) {
76
76
  let count = 0;
77
77
  for (const string of scanned.strings) {
78
78
  parts.push(content.slice(pos, string.start));
79
- const out = isSuppressed?.(lineAt(content, string.start - 1))
79
+ const out = isSuppressedAt?.(string.start - 1)
80
80
  ? string.value
81
81
  : transform(string.value);
82
82
  if (out !== string.value)
@@ -87,6 +87,12 @@ function transformCallContent(content, start, transform, isSuppressed) {
87
87
  parts.push(content.slice(pos, start + scanned.consumed));
88
88
  return { processed: parts.join(''), consumed: scanned.consumed, count };
89
89
  }
90
+ function offsetSuppressor(content, isSuppressed) {
91
+ if (!isSuppressed)
92
+ return undefined;
93
+ const positionAt = createPositionLookup(content);
94
+ return (offset) => isSuppressed(positionAt(offset).line);
95
+ }
90
96
  export function replaceClassStrings(content, transform, opts = {}) {
91
97
  const attrNames = opts.attributeNames?.length
92
98
  ? opts.attributeNames
@@ -94,6 +100,7 @@ export function replaceClassStrings(content, transform, opts = {}) {
94
100
  let result = content;
95
101
  let count = 0;
96
102
  const isSuppressed = opts.isSuppressed;
103
+ const isAttributeSuppressed = offsetSuppressor(content, isSuppressed);
97
104
  result = result.replace(buildAttrRegex(attrNames), (_full, before, attr, open, bracedDq, bracedSq, bracedBt, close, dq, sq, bt, offset) => {
98
105
  const raw = bracedDq ?? bracedSq ?? bracedBt ?? dq ?? sq ?? bt ?? '';
99
106
  const q = bracedDq !== undefined || dq !== undefined
@@ -102,9 +109,7 @@ export function replaceClassStrings(content, transform, opts = {}) {
102
109
  ? "'"
103
110
  : '`';
104
111
  const attrOffset = offset + before.length;
105
- const out = isSuppressed?.(lineAt(content, attrOffset))
106
- ? raw
107
- : transform(raw);
112
+ const out = isAttributeSuppressed?.(attrOffset) ? raw : transform(raw);
108
113
  if (out !== raw)
109
114
  count++;
110
115
  return `${before}${attr}=${open ?? ''}${q}${out}${q}${close ?? ''}`;
@@ -115,12 +120,13 @@ export function replaceClassStrings(content, transform, opts = {}) {
115
120
  const funcRe = new RegExp(`\\b(?:${funcNames.map(escapeRegex).join('|')})\\s*\\(`, 'g');
116
121
  const buf = [];
117
122
  let pos = 0;
123
+ const isCallSuppressed = offsetSuppressor(result, isSuppressed);
118
124
  for (const m of result.matchAll(funcRe)) {
119
125
  const callStart = (m.index ?? 0) + m[0].length;
120
126
  if (callStart < pos)
121
127
  continue;
122
128
  buf.push(result.slice(pos, callStart));
123
- const { processed, consumed, count: c, } = transformCallContent(result, callStart, transform, isSuppressed);
129
+ const { processed, consumed, count: c, } = transformCallContent(result, callStart, transform, isCallSuppressed);
124
130
  buf.push(processed);
125
131
  pos = callStart + consumed;
126
132
  count += c;
@@ -1,4 +1,4 @@
1
1
  import type { Config } from './rules.js';
2
- export declare const CONFIG_FILENAMES: readonly ["tailwind-canonical.config.ts", "tailwind-canonical.config.mts", "tailwind-canonical.config.js", "tailwind-canonical.config.mjs"];
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
4
  export { loadConfig, resolveInitFilename } from '../io/config.js';