diff --git a/packages/tailwindcss/src/utils/is-alpha.bench.ts b/packages/tailwindcss/src/utils/is-alpha.bench.ts deleted file mode 100644 index 8669e2ad367c..000000000000 --- a/packages/tailwindcss/src/utils/is-alpha.bench.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { bench } from 'vitest' -import { isAlpha } from './is-alpha' - -const ALPHA_REGEX_A = /^[a-zA-Z]+$/ -const ALPHA_REGEX_B = /^[a-z]+$/i -const ALPHA_REGEX_C = /^[A-Z]+$/i - -const implementations = new Map boolean>([ - ['RegExp A', (input) => ALPHA_REGEX_A.test(input)], - ['RegExp B', (input) => ALPHA_REGEX_B.test(input)], - ['RegExp C', (input) => ALPHA_REGEX_C.test(input)], - ['Manual', isAlpha], -]) - -for (let [name, check] of implementations) { - bench(name, () => { - for (let i = 0; i < 1e6; i++) { - check('abc') - check('ABC') - check('AbC') - check('a-b-c') - check('123') - } - }) -} diff --git a/packages/tailwindcss/src/utils/is-alpha.ts b/packages/tailwindcss/src/utils/is-alpha.ts deleted file mode 100644 index 8b821d5e088f..000000000000 --- a/packages/tailwindcss/src/utils/is-alpha.ts +++ /dev/null @@ -1,14 +0,0 @@ -const UPPER_A = 65 -const UPPER_Z = 90 -const LOWER_A = 97 -const LOWER_Z = 122 - -export function isAlpha(input: string): boolean { - for (let i = 0; i < input.length; i++) { - let code = input.charCodeAt(i) - if (code < UPPER_A || (code > UPPER_Z && code < LOWER_A) || code > LOWER_Z) { - return false - } - } - return true -} diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index a0cb3e63678a..c6510fc26fdc 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1694,13 +1694,20 @@ test('not', async () => { }) test('in', async () => { - expect(await run(['in-p:flex', 'in-[p]:flex', 'in-[.group]:flex', 'not-in-p:flex'])) - .toMatchInlineSnapshot(` - ".not-in-p\\:flex:not(:where(p) *), :where(p) .in-p\\:flex, :where(.group) .in-\\[\\.group\\]\\:flex, :where(p) .in-\\[p\\]\\:flex { - display: flex; - }" - `) - expect(await run(['in-foo-bar:flex'])).toEqual('') + expect( + await run([ + 'in-[p]:flex', + 'in-[.group]:flex', + 'not-in-[p]:flex', + 'not-in-[.group]:flex', + 'in-data-visible:flex', + ]), + ).toMatchInlineSnapshot(` + ".not-in-\\[\\.group\\]\\:flex:not(:where(.group) *), .not-in-\\[p\\]\\:flex:not(:where(:is(p)) *), :where([data-visible]) .in-data-visible\\:flex, :where(.group) .in-\\[\\.group\\]\\:flex, :where(:is(p)) .in-\\[p\\]\\:flex { + display: flex; + }" + `) + expect(await run(['in-p:flex', 'in-foo-bar:flex'])).toEqual('') }) test('has', async () => { diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index fbab6835a8d5..68a035b3d1f8 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -16,7 +16,6 @@ import type { Theme } from './theme' import { compareBreakpoints } from './utils/compare-breakpoints' import { DefaultMap } from './utils/default-map' import { isPositiveInteger } from './utils/infer-data-type' -import { isAlpha } from './utils/is-alpha' import { segment } from './utils/segment' type VariantFn = ( @@ -712,16 +711,33 @@ export function createVariants(theme: Theme): Variants { staticVariant('inert', ['&:is([inert], [inert] *)']) - variants.functional('in', (ruleNode, variant) => { - if (!variant.value || variant.modifier) return null + variants.compound('in', Compounds.StyleRules, (ruleNode, variant) => { + if (variant.modifier) return null - // Named values should be alpha (tag selector). This prevents `in-foo-bar` - // from being used as a variant. - if (variant.value.kind === 'named' && !isAlpha(variant.value.value)) { - return null - } + let didApply = false + + walk([ruleNode], (node, { path }) => { + if (node.kind !== 'rule') return WalkAction.Continue + + // Throw out any candidates with variants using nested style rules + for (let parent of path.slice(0, -1)) { + if (parent.kind !== 'rule') continue - ruleNode.nodes = [styleRule(`:where(${variant.value.value}) &`, ruleNode.nodes)] + didApply = false + return WalkAction.Stop + } + + // Replace `&` in target variant with `*`, so variants like `&:hover` + // become `:where(*:hover) &`. The `*` will often be optimized away. + node.selector = `:where(${node.selector.replaceAll('&', '*')}) &` + + // Track that the variant was actually applied + didApply = true + }) + + // If the node wasn't modified, this variant is not compatible with + // `in-*` so discard the candidate. + if (!didApply) return null }) variants.compound('has', Compounds.StyleRules, (ruleNode, variant) => {