Skip to content

Commit 6f398f8

Browse files
authored
[TASK] Use SelectorComponent and Combinator in Selector::parse() (#1471)
Ultimately `Selector` will be represented by instances of these classes rather than a string. This change is a stepping-stone towards that goal, to use the parsing functionality of those classes, but for now just converting the parsed result to a single string for internal representation. Part of #1325.
1 parent 2f99028 commit 6f398f8

1 file changed

Lines changed: 36 additions & 78 deletions

File tree

src/Property/Selector.php

Lines changed: 36 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Sabberworm\CSS\OutputFormat;
99
use Sabberworm\CSS\Parsing\ParserState;
1010
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
11+
use Sabberworm\CSS\Property\Selector\Combinator;
12+
use Sabberworm\CSS\Property\Selector\CompoundSelector;
1113
use Sabberworm\CSS\Property\Selector\SpecificityCalculator;
1214
use Sabberworm\CSS\Renderable;
1315

@@ -80,98 +82,54 @@ final public function __construct(string $selector)
8082
*/
8183
public static function parse(ParserState $parserState, array &$comments = []): self
8284
{
83-
$selectorParts = [];
84-
$stringWrapperCharacter = null;
85-
$functionNestingLevel = 0;
86-
static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ',', ParserState::EOF, ''];
85+
// Whitespace is a descendent combinator, not allowed around a compound selector.
86+
// (It is allowed within, e.g. as part of a string or within a function like `:not()`.)
87+
// Gobble any up now to get a clean start.
88+
$parserState->consumeWhiteSpace($comments);
8789

90+
$selectorParts = [];
8891
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-
$lastPart = \end($selectorParts);
99-
$backslashCount = \strspn(\strrev($lastPart), '\\');
100-
$quoteIsEscaped = ($backslashCount % 2 === 1);
101-
if (!$quoteIsEscaped) {
102-
if (!\is_string($stringWrapperCharacter)) {
103-
$stringWrapperCharacter = $nextCharacter;
104-
} elseif ($stringWrapperCharacter === $nextCharacter) {
105-
$stringWrapperCharacter = null;
106-
}
107-
}
108-
break;
109-
case '(':
110-
if (!\is_string($stringWrapperCharacter)) {
111-
++$functionNestingLevel;
112-
}
113-
break;
114-
case ')':
115-
if (!\is_string($stringWrapperCharacter)) {
116-
if ($functionNestingLevel <= 0) {
117-
throw new UnexpectedTokenException(
118-
'anything but',
119-
')',
120-
'literal',
121-
$parserState->currentLine()
122-
);
123-
}
124-
--$functionNestingLevel;
125-
}
92+
try {
93+
$selectorParts[] = CompoundSelector::parse($parserState, $comments);
94+
} catch (UnexpectedTokenException $e) {
95+
if ($selectorParts !== [] && \end($selectorParts)->getValue() === ' ') {
96+
// The whitespace was not a descendent combinator, and was, in fact, arbitrary,
97+
// after the end of the selector. Discard it.
98+
\array_pop($selectorParts);
12699
break;
127-
case ',':
128-
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) {
129-
break 2;
130-
}
131-
break;
132-
case '{':
133-
// The fallthrough is intentional.
134-
case '}':
135-
if (!\is_string($stringWrapperCharacter)) {
136-
break 2;
137-
}
138-
break;
139-
default:
140-
// This will never happen unless something gets broken in `ParserState`.
141-
throw new \UnexpectedValueException(
142-
'Unexpected character \'' . $nextCharacter
143-
. '\' returned from `ParserState::peek()` in `Selector::parse()`'
144-
);
100+
} else {
101+
throw $e;
102+
}
103+
}
104+
try {
105+
$selectorParts[] = Combinator::parse($parserState, $comments);
106+
} catch (UnexpectedTokenException $e) {
107+
// End of selector has been reached.
108+
break;
145109
}
146-
$selectorParts[] = $parserState->consume(1);
147110
}
148111

149-
if ($functionNestingLevel !== 0) {
150-
throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine());
151-
}
152-
if (\is_string($stringWrapperCharacter)) {
112+
// Check that the selector has been fully parsed:
113+
if (!\in_array($parserState->peek(), ['{', '}', ',', ''], true)) {
153114
throw new UnexpectedTokenException(
154-
$stringWrapperCharacter,
155-
$nextCharacter,
115+
'`,`, `{`, `}` or EOF',
116+
$parserState->peek(5),
156117
'literal',
157118
$parserState->currentLine()
158119
);
159120
}
160121

161-
$selector = \trim(\implode('', $selectorParts));
162-
if ($selector === '') {
163-
throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine());
164-
}
165-
if (!self::isValid($selector)) {
166-
throw new UnexpectedTokenException(
167-
"Selector did not match '" . static::SELECTOR_VALIDATION_RX . "'.",
168-
$selector,
169-
'custom',
170-
$parserState->currentLine()
171-
);
122+
$selectorString = '';
123+
foreach ($selectorParts as $selectorPart) {
124+
$selectorPartValue = $selectorPart->getValue();
125+
if (\in_array($selectorPartValue, ['>', '+', '~'], true)) {
126+
$selectorString .= ' ' . $selectorPartValue . ' ';
127+
} else {
128+
$selectorString .= $selectorPartValue;
129+
}
172130
}
173131

174-
return new static($selector);
132+
return new static($selectorString);
175133
}
176134

177135
public function getSelector(): string

0 commit comments

Comments
 (0)