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.
@@ -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';
@@ -7,6 +7,8 @@ export const CONFIG_FILENAMES = [
7
7
  'tailwind-canonical.config.mjs',
8
8
  ];
9
9
  const KNOWN_KEYS = [
10
+ 'useDefaultTheme',
11
+ 'allowTextSizeLineHeightChanges',
10
12
  'customTextTokens',
11
13
  'customSpacingTokens',
12
14
  'ignorePatterns',
@@ -53,7 +55,9 @@ function fail(filename, message) {
53
55
  throw new Error(`Invalid ${filename}: ${message}`);
54
56
  }
55
57
  function isPlainObject(value) {
56
- return typeof value === 'object' && value !== null && !Array.isArray(value);
58
+ return (typeof value === 'object' &&
59
+ value !== null &&
60
+ [Object.prototype, null].includes(Object.getPrototypeOf(value)));
57
61
  }
58
62
  function assertKnownKeys(cfg, known, label, filename) {
59
63
  for (const key of Object.keys(cfg)) {
@@ -71,6 +75,8 @@ function assertPxTokenMap(value, key, filename) {
71
75
  fail(filename, `${key} keys must be integers (got "${k}")`);
72
76
  if (typeof v !== 'string')
73
77
  fail(filename, `${key}[${k}] must be a string`);
78
+ if (v.length === 0 || /[\s"'`{}:\\]/.test(v))
79
+ fail(filename, `${key}[${k}] must be a nonempty token name without whitespace, quotes, backticks, braces, colons or backslashes`);
74
80
  }
75
81
  }
76
82
  function assertStringArray(value, key, filename) {
@@ -136,11 +142,16 @@ function assertSortOrder(value, filename) {
136
142
  export function validateConfig(input, filename = CONFIG_FILENAME) {
137
143
  if (input === undefined || input === null)
138
144
  return {};
139
- if (typeof input !== 'object' || Array.isArray(input)) {
140
- fail(filename, 'default export must be an object');
145
+ if (!isPlainObject(input)) {
146
+ fail(filename, 'default export must be an object (plain objects only)');
141
147
  }
142
148
  const cfg = input;
143
149
  assertKnownKeys(cfg, KNOWN_KEYS, 'unknown key', filename);
150
+ if ('useDefaultTheme' in cfg && typeof cfg.useDefaultTheme !== 'boolean')
151
+ fail(filename, 'useDefaultTheme must be a boolean');
152
+ if ('allowTextSizeLineHeightChanges' in cfg &&
153
+ typeof cfg.allowTextSizeLineHeightChanges !== 'boolean')
154
+ fail(filename, 'allowTextSizeLineHeightChanges must be a boolean');
144
155
  if ('customTextTokens' in cfg)
145
156
  assertPxTokenMap(cfg.customTextTokens, 'customTextTokens', filename);
146
157
  if ('customSpacingTokens' in cfg)
@@ -176,4 +187,4 @@ export function validateConfig(input, filename = CONFIG_FILENAME) {
176
187
  }
177
188
  return cfg;
178
189
  }
179
- export { loadConfig, resolveInitFilename } from '../io/config.js';
190
+ export { findConfigPath, loadConfig, resolveInitFilename, } from '../io/config.js';
@@ -2,7 +2,12 @@ import { type ClassStringOpts } from './class-strings.js';
2
2
  import type { Config } from './rules.js';
3
3
  export type FileClasses = {
4
4
  file: string;
5
+ /** All class occurrences, used for color and scale statistics. */
5
6
  classes: string[];
7
+ /** Individual class strings, used for repeated-pattern detection.
8
+ * When omitted, `classes` is treated as one class string for compatibility.
9
+ */
10
+ classLists?: string[][];
6
11
  };
7
12
  export type ColorVariant = {
8
13
  token: string;
@@ -1,28 +1,33 @@
1
1
  import { extractClassStrings, SINGLE_CLASS_REGEX, } from './class-strings.js';
2
- import { COLOR_FAMILIES, parseColorClass, SCALE_PROPERTIES, stripVariants, TAILWIND_COLORS, } from './lexicon.js';
2
+ import { COLOR_FAMILIES, parseColorClass, SCALE_PROPERTIES, splitClassModifiers, TAILWIND_COLORS, } from './lexicon.js';
3
3
  function parseColor(cls, families, knownColors) {
4
4
  const parsed = parseColorClass(cls);
5
5
  if (!parsed)
6
6
  return null;
7
- const { property, color, shade } = parsed;
7
+ const { property, color, shade, opacity } = parsed;
8
8
  if (shade === '')
9
9
  return null;
10
10
  // A known color without an explicit family forms its own family, so colors
11
11
  // newly added to the palette are grouped rather than silently dropped.
12
- const family = families[color] ?? (knownColors.has(color) ? color : null);
12
+ const family = Object.hasOwn(families, color)
13
+ ? families[color]
14
+ : knownColors.has(color)
15
+ ? color
16
+ : null;
13
17
  if (!family)
14
18
  return null;
15
- return { property, family, token: `${color}-${shade}` };
19
+ return { property, family, token: `${color}-${shade}${opacity}` };
16
20
  }
17
21
  // A scale value is a number, a fraction, or an arbitrary [..] value. Keyword
18
22
  // values (auto, px, full, screen, …) are not grid values, so they are excluded
19
23
  // to avoid false "inconsistency" reports like mt-4 vs mt-auto.
20
24
  const SCALE_VALUE_RE = /^(\d+(?:\.\d+)?|\d+\/\d+|\[.+\])$/;
21
25
  function parseScale(cls, scaleProperties) {
22
- const base = stripVariants(cls);
26
+ const { utility: base } = splitClassModifiers(cls);
23
27
  const negative = base.startsWith('-');
24
28
  const body = negative ? base.slice(1) : base;
25
- const dash = body.lastIndexOf('-');
29
+ const bracket = body.indexOf('[');
30
+ const dash = body.lastIndexOf('-', bracket === -1 ? body.length : bracket);
26
31
  if (dash === -1)
27
32
  return null;
28
33
  const property = body.slice(0, dash);
@@ -149,20 +154,22 @@ function detectRareScaleValues(inconsistencies, minPropertyOccurrences, maxFiles
149
154
  // frequent sub-patterns (no k-subset itemset mining).
150
155
  function detectCombinations(input, minSize, minFiles, max) {
151
156
  const combos = new Map();
152
- for (const { file, classes } of input) {
153
- if (classes.length < minSize)
154
- continue;
155
- const unique = [...new Set(classes)].sort();
156
- if (unique.length < minSize)
157
- continue;
158
- const key = unique.join(' ');
159
- let entry = combos.get(key);
160
- if (!entry) {
161
- entry = { classes: unique, count: 0, files: new Set() };
162
- combos.set(key, entry);
157
+ for (const { file, classes: allClasses, classLists } of input) {
158
+ for (const classes of classLists ?? [allClasses]) {
159
+ if (classes.length < minSize)
160
+ continue;
161
+ const unique = [...new Set(classes)].sort();
162
+ if (unique.length < minSize)
163
+ continue;
164
+ const key = unique.join(' ');
165
+ let entry = combos.get(key);
166
+ if (!entry) {
167
+ entry = { classes: unique, count: 0, files: new Set() };
168
+ combos.set(key, entry);
169
+ }
170
+ entry.count++;
171
+ entry.files.add(file);
163
172
  }
164
- entry.count++;
165
- entry.files.add(file);
166
173
  }
167
174
  return [...combos.values()]
168
175
  .filter((c) => c.files.size >= minFiles)
@@ -1,5 +1,5 @@
1
1
  import { type ClassStringOpts } from './class-strings.js';
2
- export declare function deduplicateClasses(classStr: string): string;
2
+ export declare function deduplicateClasses(classStr: string, opts?: Pick<ClassStringOpts, 'tailwindVersion'>): string;
3
3
  export declare function dedupeContent(content: string, opts?: ClassStringOpts): {
4
4
  result: string;
5
5
  count: number;
@@ -128,6 +128,10 @@ for (const f of FAMILIES) {
128
128
  SIDE_MAP[prefix] ??= sides;
129
129
  }
130
130
  function parseBoxClass(cls) {
131
+ // Important utilities occupy a different cascade priority. Keep their
132
+ // declarations intact instead of mixing their suffix into a box value.
133
+ if (cls.endsWith('!'))
134
+ return null;
131
135
  for (const family of FAMILIES) {
132
136
  const m = cls.match(family.regex);
133
137
  if (!m)
@@ -135,7 +139,12 @@ function parseBoxClass(cls) {
135
139
  const sides = {};
136
140
  for (const side of SIDE_MAP[m[1]] ?? [])
137
141
  sides[side] = m[2];
138
- return { family, sides };
142
+ const kind = m[1] === family.full
143
+ ? 'full'
144
+ : m[1] === family.x || m[1] === family.y
145
+ ? 'axis'
146
+ : 'side';
147
+ return { family, sides, kind };
139
148
  }
140
149
  return null;
141
150
  }
@@ -186,6 +195,8 @@ const CORNER_MAP = {
186
195
  br: ['br'],
187
196
  };
188
197
  function parseRoundedCorner(cls) {
198
+ if (cls.endsWith('!') || /^rounded-(?:s|e|ss|se|es|ee)(?:-|$)/.test(cls))
199
+ return null;
189
200
  const m = cls.match(/^rounded-(tl|tr|bl|br|t|b|l|r)(?:-(.+))?$/) ??
190
201
  cls.match(/^rounded()(?:-(.+))?$/);
191
202
  if (!m)
@@ -320,7 +331,9 @@ function keepLastOf(slots, indices) {
320
331
  for (const i of indices.slice(0, -1))
321
332
  slots[i] = [];
322
333
  }
323
- export function deduplicateClasses(classStr) {
334
+ export function deduplicateClasses(classStr, opts = {}) {
335
+ if (classStr.includes('${'))
336
+ return classStr;
324
337
  const original = classStr.split(/\s+/).filter(Boolean);
325
338
  if (original.length <= 1)
326
339
  return classStr;
@@ -343,12 +356,20 @@ export function deduplicateClasses(classStr) {
343
356
  }
344
357
  const parsed = parseBoxClass(cls);
345
358
  if (parsed) {
346
- const { family, sides } = parsed;
359
+ const { family, sides, kind } = parsed;
347
360
  let group = boxGroups.get(family.kind);
348
361
  if (!group) {
349
- group = { family, box: { t: '', b: '', l: '', r: '' }, indices: [] };
362
+ group = {
363
+ family,
364
+ box: { t: '', b: '', l: '', r: '' },
365
+ indices: [],
366
+ hasAxis: false,
367
+ hasSide: false,
368
+ };
350
369
  boxGroups.set(family.kind, group);
351
370
  }
371
+ group.hasAxis ||= kind === 'axis';
372
+ group.hasSide ||= kind === 'side';
352
373
  group.indices.push(idx);
353
374
  Object.assign(group.box, sides);
354
375
  return;
@@ -366,8 +387,15 @@ export function deduplicateClasses(classStr) {
366
387
  });
367
388
  keepLastOf(slots, displayIndices);
368
389
  keepLastOf(slots, positionIndices);
369
- for (const { family, box, indices } of boxGroups.values()) {
370
- placeCollapsed(slots, classes, indices, collapseBox(box, family));
390
+ for (const { family, box, indices, hasAxis, hasSide } of boxGroups.values()) {
391
+ // v4's x/y utilities use inline/block properties, unlike the physical
392
+ // t/b/l/r utilities. They cannot be interchanged in vertical writing modes.
393
+ const allEqual = box.t && box.t === box.b && box.t === box.l && box.t === box.r;
394
+ const preservePhysical = opts.tailwindVersion !== 3 && hasSide && (hasAxis || !allEqual);
395
+ const collapsed = preservePhysical
396
+ ? indices.map((index) => classes[index]).join(' ')
397
+ : collapseBox(box, family);
398
+ placeCollapsed(slots, classes, indices, collapsed);
371
399
  }
372
400
  if (cornerIndices.length > 0) {
373
401
  placeCollapsed(slots, classes, cornerIndices, collapseCorners(corners));
@@ -378,7 +406,7 @@ export function deduplicateClasses(classStr) {
378
406
  return unchanged ? classStr : result.join(' ');
379
407
  }
380
408
  export function dedupeContent(content, opts = {}) {
381
- return replaceClassStrings(content, deduplicateClasses, {
409
+ return replaceClassStrings(content, (classes) => deduplicateClasses(classes, opts), {
382
410
  ...opts,
383
411
  isSuppressed: makeLineSuppressor(content),
384
412
  });
@@ -1,6 +1,11 @@
1
1
  export declare const TAILWIND_COLORS: Set<string>;
2
2
  export declare const COLOR_FAMILIES: Record<string, string>;
3
- export declare function stripVariants(cls: string): string;
3
+ export declare function splitClassModifiers(cls: string): {
4
+ prefix: string;
5
+ utility: string;
6
+ importantBefore: string;
7
+ importantAfter: string;
8
+ };
4
9
  /**
5
10
  * Parses a color utility class into its property, color name and (optional)
6
11
  * numeric shade. Strips leading variants (`hover:` …). Returns `null` when the
@@ -12,5 +17,6 @@ export declare function parseColorClass(cls: string): {
12
17
  property: string;
13
18
  color: string;
14
19
  shade: string;
20
+ opacity: string;
15
21
  } | null;
16
22
  export declare const SCALE_PROPERTIES: Set<string>;
@@ -31,6 +31,14 @@ const COLOR_PROPERTIES = new Set([
31
31
  'text',
32
32
  'bg',
33
33
  'border',
34
+ 'border-t',
35
+ 'border-b',
36
+ 'border-l',
37
+ 'border-r',
38
+ 'border-x',
39
+ 'border-y',
40
+ 'border-s',
41
+ 'border-e',
34
42
  'ring',
35
43
  'ring-offset',
36
44
  'divide',
@@ -44,6 +52,8 @@ const COLOR_PROPERTIES = new Set([
44
52
  'accent',
45
53
  'caret',
46
54
  'shadow',
55
+ 'text-shadow',
56
+ 'inset-shadow',
47
57
  'placeholder',
48
58
  ]);
49
59
  export const COLOR_FAMILIES = {
@@ -70,9 +80,32 @@ export const COLOR_FAMILIES = {
70
80
  neutral: 'gray',
71
81
  stone: 'gray',
72
82
  };
73
- export function stripVariants(cls) {
74
- const idx = cls.lastIndexOf(':');
75
- return idx === -1 ? cls : cls.slice(idx + 1);
83
+ function stripVariants(cls) {
84
+ let depth = 0;
85
+ let colon = -1;
86
+ for (let i = 0; i < cls.length; i++) {
87
+ const ch = cls[i];
88
+ if (ch === '\\')
89
+ i++;
90
+ else if (ch === '[' || ch === '(')
91
+ depth++;
92
+ else if (ch === ']' || ch === ')')
93
+ depth--;
94
+ else if (ch === ':' && depth === 0)
95
+ colon = i;
96
+ }
97
+ return cls.slice(colon + 1);
98
+ }
99
+ export function splitClassModifiers(cls) {
100
+ const base = stripVariants(cls);
101
+ const importantBefore = base.startsWith('!') ? '!' : '';
102
+ const importantAfter = base.endsWith('!') ? '!' : '';
103
+ return {
104
+ prefix: cls.slice(0, cls.length - base.length),
105
+ utility: base.slice(importantBefore.length, base.length - importantAfter.length),
106
+ importantBefore,
107
+ importantAfter,
108
+ };
76
109
  }
77
110
  /**
78
111
  * Parses a color utility class into its property, color name and (optional)
@@ -82,8 +115,11 @@ export function stripVariants(cls) {
82
115
  * otherwise `shade` is `''`. Shared by `typos.ts` and `consistency.ts`.
83
116
  */
84
117
  export function parseColorClass(cls) {
85
- const base = stripVariants(cls);
86
- if (base.includes('['))
118
+ const { utility } = splitClassModifiers(cls);
119
+ const slash = utility.indexOf('/');
120
+ const base = slash === -1 ? utility : utility.slice(0, slash);
121
+ const opacity = slash === -1 ? '' : utility.slice(slash);
122
+ if (base.includes('[') || base.includes('('))
87
123
  return null;
88
124
  const firstDash = base.indexOf('-');
89
125
  if (firstDash === -1)
@@ -106,9 +142,10 @@ export function parseColorClass(cls) {
106
142
  property,
107
143
  color: rest.slice(0, lastDash),
108
144
  shade: rest.slice(lastDash + 1),
145
+ opacity,
109
146
  };
110
147
  }
111
- return { property, color: rest, shade: '' };
148
+ return { property, color: rest, shade: '', opacity };
112
149
  }
113
150
  export const SCALE_PROPERTIES = new Set([
114
151
  'p',
@@ -1,4 +1,5 @@
1
1
  import { type ClassStringOpts } from './class-strings.js';
2
+ export declare function mergeClasses(classes: string, twMerge: (classes: string) => string, opts?: Pick<ClassStringOpts, 'customTextTokens'>): string;
2
3
  export declare function mergeContent(content: string, twMerge: (classes: string) => string, opts?: ClassStringOpts): {
3
4
  result: string;
4
5
  count: number;
@@ -6,9 +6,9 @@ function splitOutsideBrackets(value, separator) {
6
6
  let start = 0;
7
7
  for (let i = 0; i < value.length; i += 1) {
8
8
  const ch = value[i];
9
- if (ch === '[')
9
+ if (ch === '[' || ch === '(')
10
10
  depth += 1;
11
- else if (ch === ']')
11
+ else if (ch === ']' || ch === ')')
12
12
  depth = Math.max(0, depth - 1);
13
13
  else if (ch === separator && depth === 0) {
14
14
  parts.push(value.slice(start, i));
@@ -20,14 +20,16 @@ function splitOutsideBrackets(value, separator) {
20
20
  }
21
21
  function parseClassToken(token) {
22
22
  const parts = splitOutsideBrackets(token, ':');
23
+ const rawBase = parts.at(-1) ?? token;
24
+ const important = rawBase.startsWith('!') || rawBase.endsWith('!');
23
25
  return {
24
26
  token,
25
- modifiers: parts.slice(0, -1).join(':'),
26
- base: parts.at(-1) ?? token,
27
+ modifiers: `${parts.slice(0, -1).join(':')}${important ? '!' : ''}`,
28
+ base: rawBase.replace(/^!|!$/g, ''),
27
29
  };
28
30
  }
29
31
  const RE_LEADING = /^!?leading-/;
30
- const RE_ARBITRARY_TEXT_SIZE = /^!?text-\[-?(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|ch|ex|lh|rlh|vw|vh|vmin|vmax|vi|vb|svw|lvw|dvw|svh|lvh|dvh)\]$/;
32
+ const RE_ARBITRARY_TEXT_SIZE = /^text-(?:\[(?:length:.+|-?(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|ch|ex|lh|rlh|vw|vh|vmin|vmax|vi|vb|svw|lvw|dvw|svh|lvh|dvh|%)|(?:calc|min|max|clamp)\(.+\))\]|\(length:--[^)]+\))$/;
31
33
  function isUnsafeTextSize(base, customTextClasses) {
32
34
  const normalized = base.startsWith('!') ? base.slice(1) : base;
33
35
  return (customTextClasses.has(normalized) || RE_ARBITRARY_TEXT_SIZE.test(normalized));
@@ -84,9 +86,12 @@ function restoreLeadingForUnsafeTextSizes(original, merged, customTextClasses) {
84
86
  }
85
87
  return restored.join(' ');
86
88
  }
87
- export function mergeContent(content, twMerge, opts = {}) {
89
+ export function mergeClasses(classes, twMerge, opts = {}) {
88
90
  const customTextClasses = new Set(Object.values(opts.customTextTokens ?? {}).map((token) => `text-${token}`));
89
- return replaceClassStrings(content, (classes) => restoreLeadingForUnsafeTextSizes(classes, twMerge(classes), customTextClasses), {
91
+ return restoreLeadingForUnsafeTextSizes(classes, twMerge(classes), customTextClasses);
92
+ }
93
+ export function mergeContent(content, twMerge, opts = {}) {
94
+ return replaceClassStrings(content, (classes) => mergeClasses(classes, twMerge, opts), {
90
95
  ...opts,
91
96
  isSuppressed: makeLineSuppressor(content),
92
97
  });
@@ -26,6 +26,10 @@ type DefaultCommandConfig = {
26
26
  targets?: string[];
27
27
  };
28
28
  export type Config = {
29
+ /** Set false for custom themes: only explicitly configured px mappings apply. */
30
+ useDefaultTheme?: boolean;
31
+ /** Opt in to named text sizes, whose bundled line-height can change rendering. */
32
+ allowTextSizeLineHeightChanges?: boolean;
29
33
  customTextTokens?: Record<number, string>;
30
34
  customSpacingTokens?: Record<number, string>;
31
35
  ignorePatterns?: RegExp[];
@@ -13,7 +13,6 @@ const TEXT_SIZE_MAP = {
13
13
  96: '8xl',
14
14
  128: '9xl',
15
15
  };
16
- const BUILT_IN_TEXT_PX = new Set(Object.keys(TEXT_SIZE_MAP).map(Number));
17
16
  /**
18
17
  * Radius px → token suffix. v4 renamed the small end (`rounded-sm` → `-xs`,
19
18
  * bare `rounded` → `-sm`) and added `4xl`; `''` is the bare v3 `rounded`.
@@ -343,7 +342,7 @@ function textSuggestion(cls, px, textTokens) {
343
342
  return {
344
343
  original: cls,
345
344
  canonical: `text-${token}`,
346
- isCustomToken: !BUILT_IN_TEXT_PX.has(px),
345
+ isCustomToken: textTokens[px] !== TEXT_SIZE_MAP[px],
347
346
  };
348
347
  }
349
348
  function spacingSuggestion(cls, prefix, px, spacingTokens, v3) {
@@ -356,7 +355,8 @@ function spacingSuggestion(cls, prefix, px, spacingTokens, v3) {
356
355
  }
357
356
  if (px === 1) {
358
357
  if (v3 &&
359
- (V3_UNSCALED_SPACING_PREFIXES.has(prefix) ||
358
+ (prefix === 'leading' ||
359
+ V3_UNSCALED_SPACING_PREFIXES.has(prefix) ||
360
360
  V3_ZERO_ONLY_SPACING_PREFIXES.has(prefix))) {
361
361
  return null;
362
362
  }
@@ -466,6 +466,32 @@ export function suggestCanonical(cls, config = {}) {
466
466
  };
467
467
  }
468
468
  function suggestUtility(cls, config) {
469
+ if (config.allowTextSizeLineHeightChanges !== true &&
470
+ /^text-\[\d+(?:\.\d+)?(?:px|rem)\]$/.test(cls))
471
+ return null;
472
+ if (config.useDefaultTheme === false) {
473
+ const text = cls.match(/^text-\[(\d+)px\]$/);
474
+ if (text) {
475
+ const token = config.customTextTokens?.[Number(text[1])];
476
+ return token
477
+ ? { original: cls, canonical: `text-${token}`, isCustomToken: true }
478
+ : null;
479
+ }
480
+ const spacing = cls.match(SPACING_PX_RE);
481
+ if (spacing) {
482
+ const token = config.customSpacingTokens?.[Number(spacing[2])];
483
+ return token
484
+ ? {
485
+ original: cls,
486
+ canonical: `${spacing[1]}-${token}`,
487
+ isCustomToken: true,
488
+ }
489
+ : null;
490
+ }
491
+ // Even rem-to-px equivalences depend on the consumer's root font size.
492
+ // A custom theme must opt in through explicit mappings, never inference.
493
+ return null;
494
+ }
469
495
  const textTokens = { ...TEXT_SIZE_MAP, ...config.customTextTokens };
470
496
  const spacingTokens = config.customSpacingTokens ?? {};
471
497
  const v3 = config.tailwindVersion === 3;
@@ -3,7 +3,10 @@ export declare const DEFAULT_IGNORE: string[];
3
3
  export type ScanOptions = {
4
4
  extensions?: string[];
5
5
  ignore?: string[];
6
+ /** Preserve target interpretations across repeated scans (e.g. watch mode).
7
+ * Keys use forward slashes and omit the leading exclusion marker.
8
+ */
9
+ targetKinds?: ReadonlyMap<string, 'literal' | 'glob'>;
6
10
  };
7
11
  export declare function isGlob(target: string): boolean;
8
- export declare function globToRegex(pattern: string): RegExp;
9
12
  export { resolveTargets, scanFiles } from '../io/scanner.js';
@@ -15,55 +15,6 @@ export const DEFAULT_IGNORE = [
15
15
  'coverage',
16
16
  ];
17
17
  export function isGlob(target) {
18
- return target.includes('*') || target.includes('?') || target.includes('{');
19
- }
20
- function escapeRegex(text) {
21
- return text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
22
- }
23
- function wildcardToRegex(text) {
24
- return text
25
- .split('*')
26
- .map((part) => part.split('?').map(escapeRegex).join('[^/]'))
27
- .join('[^/]*');
28
- }
29
- export function globToRegex(pattern) {
30
- let i = 0;
31
- const anchored = pattern.startsWith('/') || /^[A-Za-z]:/.test(pattern);
32
- let re = anchored ? '^' : '(?:^|/)';
33
- while (i < pattern.length) {
34
- const c = pattern[i];
35
- if (c === '*' && pattern[i + 1] === '*') {
36
- i += 2;
37
- if (pattern[i] === '/') {
38
- re += '(?:[^/]+/)*';
39
- i++;
40
- }
41
- else {
42
- re += '.*';
43
- }
44
- }
45
- else if (c === '*') {
46
- re += '[^/]*';
47
- i++;
48
- }
49
- else if (c === '?') {
50
- re += '[^/]';
51
- i++;
52
- }
53
- else if (c === '{' && pattern.indexOf('}', i) !== -1) {
54
- const close = pattern.indexOf('}', i);
55
- const alts = pattern
56
- .slice(i + 1, close)
57
- .split(',')
58
- .map(wildcardToRegex);
59
- re += `(?:${alts.join('|')})`;
60
- i = close + 1;
61
- }
62
- else {
63
- re += escapeRegex(c);
64
- i++;
65
- }
66
- }
67
- return new RegExp(`${re}$`);
18
+ return /[*?{[(]/.test(target);
68
19
  }
69
20
  export { resolveTargets, scanFiles } from '../io/scanner.js';
@@ -1,5 +1,5 @@
1
1
  import { extractClassStrings, SINGLE_CLASS_REGEX, toClassStringOpts, } from './class-strings.js';
2
- import { parseColorClass, TAILWIND_COLORS } from './lexicon.js';
2
+ import { parseColorClass, splitClassModifiers, TAILWIND_COLORS, } from './lexicon.js';
3
3
  import { createPositionLookup, getSuppressedLines } from './suppressions.js';
4
4
  const MIN_COLOR_LEN = 3;
5
5
  function levenshtein(a, b) {
@@ -28,18 +28,17 @@ function nearestColor(name) {
28
28
  return null;
29
29
  }
30
30
  export function detectTypo(cls, extraColors = new Set()) {
31
- const colon = cls.lastIndexOf(':');
32
- const prefix = colon === -1 ? '' : cls.slice(0, colon + 1);
31
+ const { prefix, importantBefore, importantAfter } = splitClassModifiers(cls);
33
32
  const parsed = parseColorClass(cls);
34
33
  if (!parsed)
35
34
  return null;
36
- const { property, color, shade } = parsed;
35
+ const { property, color, shade, opacity } = parsed;
37
36
  if (TAILWIND_COLORS.has(color) || extraColors.has(color))
38
37
  return null;
39
38
  const near = nearestColor(color);
40
39
  if (!near)
41
40
  return null;
42
- const suggestion = `${prefix}${property}-${near}${shade ? `-${shade}` : ''}`;
41
+ const suggestion = `${prefix}${importantBefore}${property}-${near}${shade ? `-${shade}` : ''}${opacity}${importantAfter}`;
43
42
  return { suggestion };
44
43
  }
45
44
  export function analyzeTyposContent(filePath, content, config = {}) {
@@ -9,7 +9,7 @@ interface TailwindCanonicalPlugin {
9
9
  plugins: {
10
10
  'tailwind-canonical': TailwindCanonicalPlugin;
11
11
  };
12
- rules: Record<string, string>;
12
+ rules: Record<string, 'warn'>;
13
13
  };
14
14
  };
15
15
  }