Skip to content

Commit d56fd85

Browse files
committed
[BUGFIX] Allow comma-separated arguments in selectors
This is the backport of #1292 and releated changes.
1 parent 3d8b0aa commit d56fd85

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
@@ -55,6 +55,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
5555

5656
### Fixed
5757

58+
- Parse selector functions (like `:not`) with comma-separated arguments (#1292)
5859
- Parse quoted attribute selector value containing comma (#1323)
5960
- Allow comma in selectors (e.g. `:not(html, body)`) (#1293)
6061
- Set line number when `RuleSet::addRule()` called with only column number set

src/RuleSet/DeclarationBlock.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ public static function parse(ParserState $oParserState, $oList = null)
6060
$selectors = [];
6161
$selectorParts = [];
6262
$stringWrapperCharacter = null;
63+
$functionNestingLevel = 0;
6364
$consumedNextCharacter = false;
64-
static $stopCharacters = ['{', '}', '\'', '"', ','];
65+
static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ','];
6566
do {
6667
if (!$consumedNextCharacter) {
6768
$selectorParts[] = $oParserState->consume(1);
@@ -81,8 +82,21 @@ public static function parse(ParserState $oParserState, $oList = null)
8182
}
8283
}
8384
break;
84-
case ',':
85+
case '(':
8586
if (!\is_string($stringWrapperCharacter)) {
87+
++$functionNestingLevel;
88+
}
89+
break;
90+
case ')':
91+
if (!\is_string($stringWrapperCharacter)) {
92+
if ($functionNestingLevel <= 0) {
93+
throw new UnexpectedTokenException('anything but', ')');
94+
}
95+
--$functionNestingLevel;
96+
}
97+
break;
98+
case ',':
99+
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) {
86100
$selectors[] = \implode('', $selectorParts);
87101
$selectorParts = [];
88102
$oParserState->consume(1);
@@ -91,6 +105,9 @@ public static function parse(ParserState $oParserState, $oList = null)
91105
break;
92106
}
93107
} while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter));
108+
if ($functionNestingLevel !== 0) {
109+
throw new UnexpectedTokenException(')', $nextCharacter);
110+
}
94111
$selectors[] = \implode('', $selectorParts); // add final or only selector
95112
$oResult->setSelectors($selectors, $oList);
96113
if ($oParserState->comes('{')) {

tests/Unit/RuleSet/DeclarationBlockTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public static function provideSelector(): array
5858
'pseudo-class' => [':hover'],
5959
'type & pseudo-class' => ['a:hover'],
6060
'`not`' => [':not(#your-mug)'],
61+
'`not` with multiple arguments' => [':not(#your-mug, .their-mug)'],
6162
'pseudo-element' => ['::before'],
6263
'attribute with `"`' => ['[alt="{}()[]\\"\',"]'],
6364
'attribute with `\'`' => ['[alt=\'{}()[]"\\\',\']'],
@@ -105,6 +106,40 @@ public function parsesTwoCommaSeparatedSelectors(string $firstSelector, string $
105106
self::assertSame([$firstSelector, $secondSelector], self::getSelectorsAsStrings($subject));
106107
}
107108

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

0 commit comments

Comments
 (0)