From e7836e78667c7c858ba99943a1216920b295d283 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 25 Mar 2024 17:11:06 +0100 Subject: [PATCH] validate the selector --- packages/tailwindcss/src/index.ts | 11 +++++- .../utils/is-simple-class-selector.test.ts | 39 +++++++++++++++++++ .../src/utils/is-simple-class-selector.ts | 25 ++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 packages/tailwindcss/src/utils/is-simple-class-selector.test.ts create mode 100644 packages/tailwindcss/src/utils/is-simple-class-selector.ts 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 +}