Skip to content

Commit d856c3a

Browse files
authored
[BUGFIX] Allow comma-separated arguments in selectors (#1292)
Fixes #138. Fixes #360. Fixes #1289.
1 parent 931c406 commit d856c3a

File tree

3 files changed

+55
-2
lines changed

3 files changed

+55
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Please also have a look at our
110110

111111
### Fixed
112112

113+
- Parse selector functions (like `:not`) with comma-separated arguments (#1292)
113114
- Parse quoted attribute selector value containing comma (#1323)
114115
- Allow comma in selectors (e.g. `:not(html, body)`) (#1293)
115116
- Insert `Rule` before sibling even with different property name

src/RuleSet/DeclarationBlock.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ?
4343
$selectors = [];
4444
$selectorParts = [];
4545
$stringWrapperCharacter = null;
46+
$functionNestingLevel = 0;
4647
$consumedNextCharacter = false;
47-
static $stopCharacters = ['{', '}', '\'', '"', ','];
48+
static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ','];
4849
do {
4950
if (!$consumedNextCharacter) {
5051
$selectorParts[] = $parserState->consume(1);
@@ -64,8 +65,21 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ?
6465
}
6566
}
6667
break;
67-
case ',':
68+
case '(':
6869
if (!\is_string($stringWrapperCharacter)) {
70+
++$functionNestingLevel;
71+
}
72+
break;
73+
case ')':
74+
if (!\is_string($stringWrapperCharacter)) {
75+
if ($functionNestingLevel <= 0) {
76+
throw new UnexpectedTokenException('anything but', ')');
77+
}
78+
--$functionNestingLevel;
79+
}
80+
break;
81+
case ',':
82+
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) {
6983
$selectors[] = \implode('', $selectorParts);
7084
$selectorParts = [];
7185
$parserState->consume(1);
@@ -74,6 +88,9 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ?
7488
break;
7589
}
7690
} while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter));
91+
if ($functionNestingLevel !== 0) {
92+
throw new UnexpectedTokenException(')', $nextCharacter);
93+
}
7794
$selectors[] = \implode('', $selectorParts); // add final or only selector
7895
$result->setSelectors($selectors, $list);
7996
if ($parserState->comes('{')) {

tests/Unit/RuleSet/DeclarationBlockTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public static function provideSelector(): array
6767
'pseudo-class' => [':hover'],
6868
'type & pseudo-class' => ['a:hover'],
6969
'`not`' => [':not(#your-mug)'],
70+
'`not` with multiple arguments' => [':not(#your-mug, .their-mug)'],
7071
'pseudo-element' => ['::before'],
7172
'attribute with `"`' => ['[alt="{}()[]\\"\',"]'],
7273
'attribute with `\'`' => ['[alt=\'{}()[]"\\\',\']'],
@@ -114,6 +115,40 @@ public function parsesTwoCommaSeparatedSelectors(string $firstSelector, string $
114115
self::assertSame([$firstSelector, $secondSelector], self::getSelectorsAsStrings($subject));
115116
}
116117

118+
/**
119+
* @return array<non-empty-string, array{0: non-empty-string}>
120+
*/
121+
public static function provideInvalidSelector(): array
122+
{
123+
// TODO: the `parse` method consumes the first character without inspection,
124+
// so the 'lone' test strings are prefixed with a space.
125+
return [
126+
'lone `(`' => [' ('],
127+
'lone `)`' => [' )'],
128+
'unclosed `(`' => [':not(#your-mug'],
129+
'extra `)`' => [':not(#your-mug))'],
130+
];
131+
}
132+
133+
/**
134+
* @test
135+
*
136+
* @param non-empty-string $selector
137+
*
138+
* @dataProvider provideInvalidSelector
139+
*/
140+
public function parseSkipsBlockWithInvalidSelector(string $selector): void
141+
{
142+
static $nextCss = ' .next {}';
143+
$css = $selector . ' {}' . $nextCss;
144+
$parserState = new ParserState($css, Settings::create());
145+
146+
$subject = DeclarationBlock::parse($parserState);
147+
148+
self::assertNull($subject);
149+
self::assertTrue($parserState->comes($nextCss));
150+
}
151+
117152
/**
118153
* @return array<string>
119154
*/

0 commit comments

Comments
 (0)