Skip to content

Commit 4649d14

Browse files
committed
[TASK] Add Selector::parse()
This replaces `DeclarationBlock::parseSelector()`. Part of #1325.
1 parent bea9012 commit 4649d14

3 files changed

Lines changed: 298 additions & 108 deletions

File tree

src/Property/Selector.php

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
namespace Sabberworm\CSS\Property;
66

7+
use Sabberworm\CSS\Comment\Comment;
78
use Sabberworm\CSS\OutputFormat;
9+
use Sabberworm\CSS\Parsing\ParserState;
10+
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
811
use Sabberworm\CSS\Property\Selector\SpecificityCalculator;
912
use Sabberworm\CSS\Renderable;
1013

@@ -63,11 +66,105 @@ public static function isValid(string $selector): bool
6366
return $numberOfMatches === 1;
6467
}
6568

66-
public function __construct(string $selector)
69+
final public function __construct(string $selector)
6770
{
6871
$this->setSelector($selector);
6972
}
7073

74+
/**
75+
* @param list<Comment> $comments
76+
*
77+
* @throws UnexpectedTokenException
78+
*
79+
* @internal
80+
*/
81+
public static function parse(ParserState $parserState, array &$comments = []): self
82+
{
83+
$selectorParts = [];
84+
$stringWrapperCharacter = null;
85+
$functionNestingLevel = 0;
86+
static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ',', ParserState::EOF, ''];
87+
88+
while (true) {
89+
$selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments);
90+
$nextCharacter = $parserState->peek();
91+
switch ($nextCharacter) {
92+
case '':
93+
// EOF
94+
break 2;
95+
case '\'':
96+
// The fallthrough is intentional.
97+
case '"':
98+
if (!\is_string($stringWrapperCharacter)) {
99+
$stringWrapperCharacter = $nextCharacter;
100+
} elseif ($stringWrapperCharacter === $nextCharacter) {
101+
if (\substr(\end($selectorParts), -1) !== '\\') {
102+
$stringWrapperCharacter = null;
103+
}
104+
}
105+
break;
106+
case '(':
107+
if (!\is_string($stringWrapperCharacter)) {
108+
++$functionNestingLevel;
109+
}
110+
break;
111+
case ')':
112+
if (!\is_string($stringWrapperCharacter)) {
113+
if ($functionNestingLevel <= 0) {
114+
throw new UnexpectedTokenException(
115+
'anything but',
116+
')',
117+
'literal',
118+
$parserState->currentLine()
119+
);
120+
}
121+
--$functionNestingLevel;
122+
}
123+
break;
124+
case ',':
125+
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) {
126+
break 2;
127+
}
128+
break;
129+
case '{':
130+
// The fallthrough is intentional.
131+
case '}':
132+
if (!\is_string($stringWrapperCharacter)) {
133+
break 2;
134+
}
135+
break;
136+
}
137+
$selectorParts[] = $parserState->consume(1);
138+
}
139+
140+
if ($functionNestingLevel !== 0) {
141+
throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine());
142+
}
143+
if (\is_string($stringWrapperCharacter)) {
144+
throw new UnexpectedTokenException(
145+
$stringWrapperCharacter,
146+
$nextCharacter,
147+
'literal',
148+
$parserState->currentLine()
149+
);
150+
}
151+
152+
$selector = \trim(\implode('', $selectorParts));
153+
if ($selector === '') {
154+
throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine());
155+
}
156+
if (!self::isValid($selector)) {
157+
throw new UnexpectedTokenException(
158+
"Selector did not match '" . static::SELECTOR_VALIDATION_RX . "'.",
159+
$selector,
160+
'custom',
161+
$parserState->currentLine()
162+
);
163+
}
164+
165+
return new static($selector);
166+
}
167+
71168
public function getSelector(): string
72169
{
73170
return $this->selector;

src/RuleSet/DeclarationBlock.php

Lines changed: 7 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ?
6767
$comments = [];
6868
$result = new DeclarationBlock($parserState->currentLine());
6969
try {
70-
$selectors = self::parseSelectors($parserState, $comments);
70+
$selectors = self::parseSelectors($parserState, $list, $comments);
7171
$result->setSelectors($selectors, $list);
7272
if ($parserState->comes('{')) {
7373
$parserState->consume(1);
@@ -100,11 +100,9 @@ public function setSelectors($selectors, ?CSSList $list = null): void
100100
$selectorsToSet = $selectors;
101101
} else {
102102
// A string of comma-separated selectors requires parsing.
103-
// Parse as if it's the opening part of a rule.
104103
try {
105-
$parserState = new ParserState($selectors . '{', Settings::create());
106-
$selectorsToSet = self::parseSelectors($parserState);
107-
$parserState->consume('{'); // throw exception if this is not next
104+
$parserState = new ParserState($selectors, Settings::create());
105+
$selectorsToSet = self::parseSelectors($parserState, $list);
108106
if (!$parserState->isEnd()) {
109107
throw new UnexpectedTokenException('EOF', 'more');
110108
}
@@ -286,94 +284,22 @@ public function getArrayRepresentation(): array
286284
/**
287285
* @param list<Comment> $comments
288286
*
289-
* @return list<string>
287+
* @return list<Selector>
290288
*
291289
* @throws UnexpectedTokenException
292290
*/
293-
private static function parseSelectors(ParserState $parserState, array &$comments = []): array
291+
private static function parseSelectors(ParserState $parserState, ?CSSList $list, array &$comments = []): array
294292
{
293+
$selectorClass = $list instanceof KeyFrame ? KeyFrameSelector::class : Selector::class;
295294
$selectors = [];
296295

297296
while (true) {
298-
$selectors[] = self::parseSelector($parserState, $comments);
297+
$selectors[] = $selectorClass::parse($parserState, $comments);
299298
if (!$parserState->consumeIfComes(',')) {
300299
break;
301300
}
302301
}
303302

304303
return $selectors;
305304
}
306-
307-
/**
308-
* @param list<Comment> $comments
309-
*
310-
* @throws UnexpectedTokenException
311-
*/
312-
private static function parseSelector(ParserState $parserState, array &$comments = []): string
313-
{
314-
$selectorParts = [];
315-
$stringWrapperCharacter = null;
316-
$functionNestingLevel = 0;
317-
static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ','];
318-
319-
while (true) {
320-
$selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments);
321-
$nextCharacter = $parserState->peek();
322-
switch ($nextCharacter) {
323-
case '\'':
324-
// The fallthrough is intentional.
325-
case '"':
326-
if (!\is_string($stringWrapperCharacter)) {
327-
$stringWrapperCharacter = $nextCharacter;
328-
} elseif ($stringWrapperCharacter === $nextCharacter) {
329-
if (\substr(\end($selectorParts), -1) !== '\\') {
330-
$stringWrapperCharacter = null;
331-
}
332-
}
333-
break;
334-
case '(':
335-
if (!\is_string($stringWrapperCharacter)) {
336-
++$functionNestingLevel;
337-
}
338-
break;
339-
case ')':
340-
if (!\is_string($stringWrapperCharacter)) {
341-
if ($functionNestingLevel <= 0) {
342-
throw new UnexpectedTokenException(
343-
'anything but',
344-
')',
345-
'literal',
346-
$parserState->currentLine()
347-
);
348-
}
349-
--$functionNestingLevel;
350-
}
351-
break;
352-
case '{':
353-
// The fallthrough is intentional.
354-
case '}':
355-
if (!\is_string($stringWrapperCharacter)) {
356-
break 2;
357-
}
358-
break;
359-
case ',':
360-
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) {
361-
break 2;
362-
}
363-
break;
364-
}
365-
$selectorParts[] = $parserState->consume(1);
366-
}
367-
368-
if ($functionNestingLevel !== 0) {
369-
throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine());
370-
}
371-
372-
$selector = \trim(\implode('', $selectorParts));
373-
if ($selector === '') {
374-
throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine());
375-
}
376-
377-
return $selector;
378-
}
379305
}

0 commit comments

Comments
 (0)