diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index b7165b3b4213..97900aabe690 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -4,6 +4,7 @@ import { compileCandidates } from './compile' import * as CSS from './css-parser' import { buildDesignSystem } from './design-system' import { Theme } from './theme' +import { isSimpleClassSelector } from './utils/is-simple-class-selector' export function compile(css: string): { build(candidates: string[]): string @@ -33,7 +34,15 @@ export function compile(css: string): { if (node.kind !== 'rule') return // Track all user-defined classes for `@apply` support - if (containsAtApply && node.selector[0] === '.' && !node.selector.includes(' ')) { + if ( + containsAtApply && + // Verify that it is a valid applyable-class. An applyable class is a + // class that is a very simple selector, like `.foo` or `.bar`, but doesn't + // contain any spaces, combinators, pseudo-selectors, pseudo-elements, or + // attribute selectors. + node.selector[0] === '.' && + isSimpleClassSelector(node.selector) + ) { // Convert the class `.foo` into a candidate `foo` let candidate = node.selector.slice(1) diff --git a/packages/tailwindcss/src/utils/is-simple-class-selector.test.ts b/packages/tailwindcss/src/utils/is-simple-class-selector.test.ts new file mode 100644 index 000000000000..8618b79ce057 --- /dev/null +++ b/packages/tailwindcss/src/utils/is-simple-class-selector.test.ts @@ -0,0 +1,39 @@ +import { expect, it } from 'vitest' +import { isSimpleClassSelector } from './is-simple-class-selector' + +it.each([ + // Simple class selector + ['.foo', true], + + // Class selectors with escaped characters + ['.w-\\[123px\\]', true], + ['.content-\\[\\+\\>\\~\\*\\]', true], + + // ID selector + ['#foo', false], + ['.foo#foo', false], + + // Element selector + ['h1', false], + ['h1.foo', false], + + // Attribute selector + ['[data-foo]', false], + ['.foo[data-foo]', false], + ['[data-foo].foo', false], + + // Pseudo-class selector + ['.foo:hover', false], + + // Combinator + ['.foo>.bar', false], + ['.foo+.bar', false], + ['.foo~.bar', false], + ['.foo .bar', false], + + // Selector list + ['.foo, .bar', false], + ['.foo,.bar', false], +])('should validate %s', (selector, expected) => { + expect(isSimpleClassSelector(selector)).toBe(expected) +}) diff --git a/packages/tailwindcss/src/utils/is-simple-class-selector.ts b/packages/tailwindcss/src/utils/is-simple-class-selector.ts new file mode 100644 index 000000000000..07c992edabc2 --- /dev/null +++ b/packages/tailwindcss/src/utils/is-simple-class-selector.ts @@ -0,0 +1,25 @@ +export function isSimpleClassSelector(selector: string): boolean { + // The selector must start with a dot, otherwise it's not a class selector. + if (selector[0] !== '.') return false + + for (let i = 1; i < selector.length; i++) { + switch (selector[i]) { + // The character is escaped, skip the next character + case '\\': + i += 1 + continue + + case ' ': // Descendat combinator + case '#': // ID selector + case '[': // Attribute selector + case ':': // Pseudo-classes and pseudo-elements + case '>': // Child combinator + case '+': // Next-sibling combinator + case '~': // Subsequent-sibling combinator + case ',': // Selector list + return false + } + } + + return true +}