|
8 | 8 | use Sabberworm\CSS\OutputFormat; |
9 | 9 | use Sabberworm\CSS\Parsing\ParserState; |
10 | 10 | use Sabberworm\CSS\Parsing\UnexpectedTokenException; |
| 11 | +use Sabberworm\CSS\Property\Selector\Combinator; |
| 12 | +use Sabberworm\CSS\Property\Selector\CompoundSelector; |
11 | 13 | use Sabberworm\CSS\Property\Selector\SpecificityCalculator; |
12 | 14 | use Sabberworm\CSS\Renderable; |
13 | 15 |
|
@@ -80,98 +82,54 @@ final public function __construct(string $selector) |
80 | 82 | */ |
81 | 83 | public static function parse(ParserState $parserState, array &$comments = []): self |
82 | 84 | { |
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); |
87 | 89 |
|
| 90 | + $selectorParts = []; |
88 | 91 | 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); |
126 | 99 | 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; |
145 | 109 | } |
146 | | - $selectorParts[] = $parserState->consume(1); |
147 | 110 | } |
148 | 111 |
|
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)) { |
153 | 114 | throw new UnexpectedTokenException( |
154 | | - $stringWrapperCharacter, |
155 | | - $nextCharacter, |
| 115 | + '`,`, `{`, `}` or EOF', |
| 116 | + $parserState->peek(5), |
156 | 117 | 'literal', |
157 | 118 | $parserState->currentLine() |
158 | 119 | ); |
159 | 120 | } |
160 | 121 |
|
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 | + } |
172 | 130 | } |
173 | 131 |
|
174 | | - return new static($selector); |
| 132 | + return new static($selectorString); |
175 | 133 | } |
176 | 134 |
|
177 | 135 | public function getSelector(): string |
|
0 commit comments