Skip to content

Commit e7836e7

Browse files
committed
validate the selector
1 parent 6488984 commit e7836e7

File tree

3 files changed

+74
-1
lines changed

3 files changed

+74
-1
lines changed

packages/tailwindcss/src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { compileCandidates } from './compile'
44
import * as CSS from './css-parser'
55
import { buildDesignSystem } from './design-system'
66
import { Theme } from './theme'
7+
import { isSimpleClassSelector } from './utils/is-simple-class-selector'
78

89
export function compile(css: string): {
910
build(candidates: string[]): string
@@ -33,7 +34,15 @@ export function compile(css: string): {
3334
if (node.kind !== 'rule') return
3435

3536
// Track all user-defined classes for `@apply` support
36-
if (containsAtApply && node.selector[0] === '.' && !node.selector.includes(' ')) {
37+
if (
38+
containsAtApply &&
39+
// Verify that it is a valid applyable-class. An applyable class is a
40+
// class that is a very simple selector, like `.foo` or `.bar`, but doesn't
41+
// contain any spaces, combinators, pseudo-selectors, pseudo-elements, or
42+
// attribute selectors.
43+
node.selector[0] === '.' &&
44+
isSimpleClassSelector(node.selector)
45+
) {
3746
// Convert the class `.foo` into a candidate `foo`
3847
let candidate = node.selector.slice(1)
3948

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { expect, it } from 'vitest'
2+
import { isSimpleClassSelector } from './is-simple-class-selector'
3+
4+
it.each([
5+
// Simple class selector
6+
['.foo', true],
7+
8+
// Class selectors with escaped characters
9+
['.w-\\[123px\\]', true],
10+
['.content-\\[\\+\\>\\~\\*\\]', true],
11+
12+
// ID selector
13+
['#foo', false],
14+
['.foo#foo', false],
15+
16+
// Element selector
17+
['h1', false],
18+
['h1.foo', false],
19+
20+
// Attribute selector
21+
['[data-foo]', false],
22+
['.foo[data-foo]', false],
23+
['[data-foo].foo', false],
24+
25+
// Pseudo-class selector
26+
['.foo:hover', false],
27+
28+
// Combinator
29+
['.foo>.bar', false],
30+
['.foo+.bar', false],
31+
['.foo~.bar', false],
32+
['.foo .bar', false],
33+
34+
// Selector list
35+
['.foo, .bar', false],
36+
['.foo,.bar', false],
37+
])('should validate %s', (selector, expected) => {
38+
expect(isSimpleClassSelector(selector)).toBe(expected)
39+
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export function isSimpleClassSelector(selector: string): boolean {
2+
// The selector must start with a dot, otherwise it's not a class selector.
3+
if (selector[0] !== '.') return false
4+
5+
for (let i = 1; i < selector.length; i++) {
6+
switch (selector[i]) {
7+
// The character is escaped, skip the next character
8+
case '\\':
9+
i += 1
10+
continue
11+
12+
case ' ': // Descendat combinator
13+
case '#': // ID selector
14+
case '[': // Attribute selector
15+
case ':': // Pseudo-classes and pseudo-elements
16+
case '>': // Child combinator
17+
case '+': // Next-sibling combinator
18+
case '~': // Subsequent-sibling combinator
19+
case ',': // Selector list
20+
return false
21+
}
22+
}
23+
24+
return true
25+
}

0 commit comments

Comments
 (0)