Skip to content

Commit 11776b0

Browse files
committed
[BUGFIX] Allow comma-separated arguments in selectors
Fixes #138, #360 and #1289.
1 parent 1d8b855 commit 11776b0

File tree

4 files changed

+99
-2
lines changed

4 files changed

+99
-2
lines changed

CHANGELOG.md

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

111111
### Fixed
112112

113+
- Selector functions (like `:not`) with comma-separated arguments are now
114+
parsed correclty (#1292)
113115
- Allow comma in selectors (e.g. `:not(html, body)`) (#1293)
114116
- Insert `Rule` before sibling even with different property name
115117
(in `RuleSet::addRule()`) (#1270)

src/Parsing/ParserState.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,10 @@ public function consumeUntil(
345345
$start = $this->currentPosition;
346346

347347
while (!$this->isEnd()) {
348+
$comment = $this->consumeComment();
349+
if ($comment instanceof Comment) {
350+
$comments[] = $comment;
351+
}
348352
$character = $this->consume(1);
349353
if (\in_array($character, $stopCharacters, true)) {
350354
if ($includeEnd) {

src/RuleSet/DeclarationBlock.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ?
4040
$comments = [];
4141
$result = new DeclarationBlock($parserState->currentLine());
4242
try {
43+
$selectors = [];
4344
$selectorParts = [];
4445
$stringWrapperCharacter = null;
46+
$functionNestingLevel = 0;
4547
do {
4648
$selectorParts[] = $parserState->consume(1)
47-
. $parserState->consumeUntil(['{', '}', '\'', '"'], false, false, $comments);
49+
. $parserState->consumeUntil(['{', '}', '\'', '"', '(', ')', ','], false, false, $comments);
4850
$nextCharacter = $parserState->peek();
4951
switch ($nextCharacter) {
5052
case '\'':
@@ -58,9 +60,30 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ?
5860
}
5961
}
6062
break;
63+
case '(':
64+
if (!isset($stringWrapperCharacter)) {
65+
++$functionNestingLevel;
66+
}
67+
break;
68+
case ')':
69+
if (!isset($stringWrapperCharacter)) {
70+
if ($functionNestingLevel <= 0) {
71+
throw new UnexpectedTokenException('anything but', ')');
72+
}
73+
--$functionNestingLevel;
74+
}
75+
break;
76+
case ',':
77+
if (!isset($stringWrapperCharacter) && $functionNestingLevel === 0) {
78+
$selectors[] = \implode('', $selectorParts);
79+
$selectorParts = [];
80+
$parserState->consume(1);
81+
}
82+
break;
6183
}
6284
} while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter));
63-
$result->setSelectors(\implode('', $selectorParts), $list);
85+
$selectors[] = \implode('', $selectorParts); // add final or only selector
86+
$result->setSelectors($selectors, $list);
6487
if ($parserState->comes('{')) {
6588
$parserState->consume(1);
6689
}

tests/Unit/RuleSet/DeclarationBlockTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
use Sabberworm\CSS\CSSElement;
99
use Sabberworm\CSS\CSSList\CSSListItem;
1010
use Sabberworm\CSS\Position\Positionable;
11+
use Sabberworm\CSS\Parsing\ParserState;
12+
use Sabberworm\CSS\Property\Selector;
1113
use Sabberworm\CSS\RuleSet\DeclarationBlock;
14+
use Sabberworm\CSS\Settings;
15+
use TRegx\PhpUnit\DataProviders\DataProvider;
1216

1317
/**
1418
* @covers \Sabberworm\CSS\RuleSet\DeclarationBlock
@@ -48,4 +52,68 @@ public function implementsPositionable(): void
4852
{
4953
self::assertInstanceOf(Positionable::class, $this->subject);
5054
}
55+
56+
/**
57+
* @return array<non-empty-string, array{0: non-empty-string}>
58+
*/
59+
public static function provideSelector(): array
60+
{
61+
return [
62+
'type' => ['body'],
63+
'class' => ['.teapot'],
64+
'id' => ['#my-mug'],
65+
'`not`' => [':not(#your-mug)'],
66+
'`not` with multiple arguments' => [':not(#your-mug, .their-mug)'],
67+
];
68+
}
69+
70+
/**
71+
* @test
72+
*
73+
* @param non-empty-string $selector
74+
*
75+
* @dataProvider provideSelector
76+
*/
77+
public function parsesAndReturnsSingleSelector(string $selector): void
78+
{
79+
$subject = DeclarationBlock::parse(new ParserState($selector . '{}', Settings::create()));
80+
81+
$resultSelectorStrings = \array_map(
82+
static function (Selector $selectorObject): string {
83+
return $selectorObject->getSelector();
84+
},
85+
$subject->getSelectors()
86+
);
87+
self::assertSame([$selector], $resultSelectorStrings);
88+
}
89+
90+
/**
91+
* @return DataProvider<non-empty-string, array{0: non-empty-string, 1: non-empty-string}>
92+
*/
93+
public static function provideTwoSelectors(): DataProvider
94+
{
95+
return DataProvider::cross(self::provideSelector(), self::provideSelector());
96+
}
97+
98+
/**
99+
* @test
100+
*
101+
* @param non-empty-string $firstSelector
102+
* @param non-empty-string $secondSelector
103+
*
104+
* @dataProvider provideTwoSelectors
105+
*/
106+
public function parsesAndReturnsTwoCommaSeparatedSelectors(string $firstSelector, string $secondSelector): void
107+
{
108+
$joinedSelectors = $firstSelector . ',' . $secondSelector;
109+
$subject = DeclarationBlock::parse(new ParserState($joinedSelectors . '{}', Settings::create()));
110+
111+
$resultSelectorStrings = \array_map(
112+
static function (Selector $selectorObject): string {
113+
return $selectorObject->getSelector();
114+
},
115+
$subject->getSelectors()
116+
);
117+
self::assertSame([$firstSelector, $secondSelector], $resultSelectorStrings);
118+
}
51119
}

0 commit comments

Comments
 (0)