Skip to content

Commit

Permalink
validate the selector
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinMalfait committed Mar 25, 2024
1 parent 6488984 commit e7836e7
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 1 deletion.
11 changes: 10 additions & 1 deletion packages/tailwindcss/src/index.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
39 changes: 39 additions & 0 deletions 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)
})
25 changes: 25 additions & 0 deletions 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
}

0 comments on commit e7836e7

Please sign in to comment.