PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.5
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.5
4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
learnpress / vendor / symfony / css-selector / Parser / Parser.php

Parser.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.5, at vendor/symfony/css-selector/Parser/Parser.php

456 lines 15.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Symfony\Component\CssSelector\Parser;
13
14 use Symfony\Component\CssSelector\Exception\InternalErrorException;
15 use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
16 use Symfony\Component\CssSelector\Node;
17 use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer;
18
19 /**
20 * CSS selector parser.
21 *
22 * This component is a port of the Python cssselect library,
23 * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
24 *
25 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
26 *
27 * @internal
28 */
29 class Parser implements ParserInterface
30 {
31 private const HAS_NESTING_LIMIT = 16;
32
33 private Tokenizer $tokenizer;
34 private int $hasNestingDepth = 0;
35
36 public function __construct(?Tokenizer $tokenizer = null)
37 {
38 $this->tokenizer = $tokenizer ?? new Tokenizer();
39 }
40
41 public function parse(string $source): array
42 {
43 $reader = new Reader($source);
44 $stream = $this->tokenizer->tokenize($reader);
45
46 return $this->parseSelectorList($stream);
47 }
48
49 /**
50 * Parses the arguments for ":nth-child()" and friends.
51 *
52 * @param Token[] $tokens
53 *
54 * @throws SyntaxErrorException
55 */
56 public static function parseSeries(array $tokens): array
57 {
58 foreach ($tokens as $token) {
59 if ($token->isString()) {
60 throw SyntaxErrorException::stringAsFunctionArgument();
61 }
62 }
63
64 $joined = trim(implode('', array_map(static fn (Token $token) => $token->getValue(), $tokens)));
65
66 $int = static function ($string) {
67 if (!is_numeric($string)) {
68 throw SyntaxErrorException::stringAsFunctionArgument();
69 }
70
71 return (int) $string;
72 };
73
74 switch (true) {
75 case 'odd' === $joined:
76 return [2, 1];
77 case 'even' === $joined:
78 return [2, 0];
79 case 'n' === $joined:
80 return [1, 0];
81 case !str_contains($joined, 'n'):
82 return [0, $int($joined)];
83 }
84
85 $split = explode('n', $joined);
86 $first = $split[0] ?? null;
87
88 return [
89 $first ? ('-' === $first || '+' === $first ? $int($first.'1') : $int($first)) : 1,
90 isset($split[1]) && $split[1] ? $int($split[1]) : 0,
91 ];
92 }
93
94 private function parseSelectorList(TokenStream $stream, bool $isArgument = false): array
95 {
96 $stream->skipWhitespace();
97 $selectors = [];
98
99 while (true) {
100 if ($isArgument && $stream->getPeek()->isDelimiter([')'])) {
101 break;
102 }
103
104 $selectors[] = $this->parserSelectorNode($stream, $isArgument);
105
106 if ($stream->getPeek()->isDelimiter([','])) {
107 $stream->getNext();
108 $stream->skipWhitespace();
109 } else {
110 break;
111 }
112 }
113
114 return $selectors;
115 }
116
117 private function parserSelectorNode(TokenStream $stream, bool $isArgument = false, bool $insideRelativeSelector = false): Node\SelectorNode
118 {
119 [$result, $pseudoElement] = $this->parseSimpleSelector($stream, false, $isArgument, $insideRelativeSelector);
120
121 while (true) {
122 $stream->skipWhitespace();
123 $peek = $stream->getPeek();
124
125 if (
126 $peek->isFileEnd()
127 || $peek->isDelimiter([','])
128 || ($isArgument && $peek->isDelimiter([')']))
129 ) {
130 break;
131 }
132
133 if (null !== $pseudoElement) {
134 throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
135 }
136
137 if ($peek->isDelimiter(['+', '>', '~'])) {
138 $combinator = $stream->getNext()->getValue();
139 $stream->skipWhitespace();
140 } else {
141 $combinator = ' ';
142 }
143
144 [$nextSelector, $pseudoElement] = $this->parseSimpleSelector($stream, false, $isArgument, $insideRelativeSelector);
145 $result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector);
146 }
147
148 return new Node\SelectorNode($result, $pseudoElement);
149 }
150
151 /**
152 * @return list<array{0: string, 1: Node\SelectorNode}>
153 *
154 * @throws SyntaxErrorException
155 * @throws InternalErrorException
156 */
157 private function parseRelativeSelector(TokenStream $stream): array
158 {
159 if ($this->hasNestingDepth >= self::HAS_NESTING_LIMIT) {
160 throw SyntaxErrorException::nestedHas();
161 }
162
163 ++$this->hasNestingDepth;
164
165 try {
166 $arguments = [];
167 while (true) {
168 $stream->skipWhitespace();
169 $peek = $stream->getPeek();
170
171 if ($peek->isDelimiter(['+', '>', '~'])) {
172 $combinator = $stream->getNext()->getValue();
173 $stream->skipWhitespace();
174 $peek = $stream->getPeek();
175 } else {
176 $combinator = ' ';
177 }
178
179 if ($peek->isString() || $peek->isNumber()) {
180 throw SyntaxErrorException::unexpectedToken('an argument', $stream->getNext());
181 }
182
183 $selector = $this->parserSelectorNode($stream, true, true);
184
185 if (null !== $pseudoElement = $selector->getPseudoElement()) {
186 throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'inside :has()');
187 }
188
189 $arguments[] = [$combinator, $selector];
190
191 if ($stream->getPeek()->isDelimiter([','])) {
192 $stream->getNext();
193 continue;
194 }
195
196 break;
197 }
198
199 $next = $stream->getNext();
200 if (!$next->isDelimiter([')'])) {
201 throw SyntaxErrorException::unexpectedToken('")"', $next);
202 }
203
204 return $arguments;
205 } finally {
206 --$this->hasNestingDepth;
207 }
208 }
209
210 /**
211 * Parses next simple node (hash, class, pseudo, negation).
212 *
213 * @throws SyntaxErrorException
214 * @throws InternalErrorException
215 */
216 private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = false, bool $isArgument = false, bool $insideRelativeSelector = false): array
217 {
218 $stream->skipWhitespace();
219
220 $selectorStart = \count($stream->getUsed());
221 $result = $this->parseElementNode($stream);
222 $pseudoElement = null;
223
224 while (true) {
225 $peek = $stream->getPeek();
226 if ($peek->isWhitespace()
227 || $peek->isFileEnd()
228 || $peek->isDelimiter([',', '+', '>', '~'])
229 || ($isArgument && $peek->isDelimiter([')']))
230 ) {
231 break;
232 }
233
234 if (null !== $pseudoElement) {
235 throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
236 }
237
238 if ($peek->isHash()) {
239 $result = new Node\HashNode($result, $stream->getNext()->getValue());
240 } elseif ($peek->isDelimiter(['.'])) {
241 $stream->getNext();
242 $result = new Node\ClassNode($result, $stream->getNextIdentifier());
243 } elseif ($peek->isDelimiter(['['])) {
244 $stream->getNext();
245 $result = $this->parseAttributeNode($result, $stream);
246 } elseif ($peek->isDelimiter([':'])) {
247 $stream->getNext();
248
249 if ($stream->getPeek()->isDelimiter([':'])) {
250 $stream->getNext();
251 $pseudoElement = $stream->getNextIdentifier();
252
253 continue;
254 }
255
256 $identifier = $stream->getNextIdentifier();
257 if (\in_array(strtolower($identifier), ['first-line', 'first-letter', 'before', 'after'], true)) {
258 // Special case: CSS 2.1 pseudo-elements can have a single ':'.
259 // Any new pseudo-element must have two.
260 $pseudoElement = $identifier;
261
262 continue;
263 }
264
265 if (!$stream->getPeek()->isDelimiter(['('])) {
266 $result = new Node\PseudoNode($result, $identifier);
267 if ('Pseudo[Element[*]:scope]' === $result->__toString()) {
268 $used = \count($stream->getUsed());
269 $prevSeparators = [','];
270 if ($insideRelativeSelector) {
271 $prevSeparators = [',', '(', '>', '+', '~'];
272 }
273 if (!(2 === $used
274 || 3 === $used && $stream->getUsed()[0]->isWhiteSpace()
275 || $used >= 3 && $stream->getUsed()[$used - 3]->isDelimiter($prevSeparators)
276 || $used >= 4
277 && $stream->getUsed()[$used - 3]->isWhiteSpace()
278 && $stream->getUsed()[$used - 4]->isDelimiter($prevSeparators)
279 )) {
280 throw SyntaxErrorException::notAtTheStartOfASelector('scope');
281 }
282 }
283 continue;
284 }
285
286 $stream->getNext();
287 $stream->skipWhitespace();
288
289 if ('not' === strtolower($identifier)) {
290 if ($insideNegation) {
291 throw SyntaxErrorException::nestedNot();
292 }
293
294 [$argument, $argumentPseudoElement] = $this->parseSimpleSelector($stream, true, true);
295 $next = $stream->getNext();
296
297 if (null !== $argumentPseudoElement) {
298 throw SyntaxErrorException::pseudoElementFound($argumentPseudoElement, 'inside :not()');
299 }
300
301 if (!$next->isDelimiter([')'])) {
302 throw SyntaxErrorException::unexpectedToken('")"', $next);
303 }
304
305 $result = new Node\NegationNode($result, $argument);
306 } elseif ('is' === strtolower($identifier)) {
307 $selectors = $this->parseSelectorList($stream, true);
308
309 $next = $stream->getNext();
310 if (!$next->isDelimiter([')'])) {
311 throw SyntaxErrorException::unexpectedToken('")"', $next);
312 }
313
314 $result = new Node\MatchingNode($result, $selectors);
315 } elseif ('where' === strtolower($identifier)) {
316 $selectors = $this->parseSelectorList($stream, true);
317
318 $next = $stream->getNext();
319 if (!$next->isDelimiter([')'])) {
320 throw SyntaxErrorException::unexpectedToken('")"', $next);
321 }
322
323 $result = new Node\SpecificityAdjustmentNode($result, $selectors);
324 } elseif ('has' === strtolower($identifier)) {
325 $result = new Node\RelationNode($result, $this->parseRelativeSelector($stream));
326 } else {
327 $arguments = [];
328 $next = null;
329
330 while (true) {
331 $stream->skipWhitespace();
332 $next = $stream->getNext();
333
334 if ($next->isIdentifier()
335 || $next->isString()
336 || $next->isNumber()
337 || $next->isDelimiter(['+', '-'])
338 ) {
339 $arguments[] = $next;
340 } elseif ($next->isDelimiter([')'])) {
341 break;
342 } else {
343 throw SyntaxErrorException::unexpectedToken('an argument', $next);
344 }
345 }
346
347 if (!$arguments) {
348 throw SyntaxErrorException::unexpectedToken('at least one argument', $next);
349 }
350
351 $result = new Node\FunctionNode($result, $identifier, $arguments);
352 }
353 } else {
354 throw SyntaxErrorException::unexpectedToken('selector', $peek);
355 }
356 }
357
358 if (\count($stream->getUsed()) === $selectorStart) {
359 throw SyntaxErrorException::unexpectedToken('selector', $stream->getPeek());
360 }
361
362 return [$result, $pseudoElement];
363 }
364
365 private function parseElementNode(TokenStream $stream): Node\ElementNode
366 {
367 $peek = $stream->getPeek();
368
369 if ($peek->isIdentifier() || $peek->isDelimiter(['*'])) {
370 if ($peek->isIdentifier()) {
371 $namespace = $stream->getNext()->getValue();
372 } else {
373 $stream->getNext();
374 $namespace = null;
375 }
376
377 if ($stream->getPeek()->isDelimiter(['|'])) {
378 $stream->getNext();
379 $element = $stream->getNextIdentifierOrStar();
380 } else {
381 $element = $namespace;
382 $namespace = null;
383 }
384 } else {
385 $element = $namespace = null;
386 }
387
388 return new Node\ElementNode($namespace, $element);
389 }
390
391 private function parseAttributeNode(Node\NodeInterface $selector, TokenStream $stream): Node\AttributeNode
392 {
393 $stream->skipWhitespace();
394 $attribute = $stream->getNextIdentifierOrStar();
395
396 if (null === $attribute && !$stream->getPeek()->isDelimiter(['|'])) {
397 throw SyntaxErrorException::unexpectedToken('"|"', $stream->getPeek());
398 }
399
400 if ($stream->getPeek()->isDelimiter(['|'])) {
401 $stream->getNext();
402
403 if ($stream->getPeek()->isDelimiter(['='])) {
404 $namespace = null;
405 $stream->getNext();
406 $operator = '|=';
407 } else {
408 $namespace = $attribute;
409 $attribute = $stream->getNextIdentifier();
410 $operator = null;
411 }
412 } else {
413 $namespace = $operator = null;
414 }
415
416 if (null === $operator) {
417 $stream->skipWhitespace();
418 $next = $stream->getNext();
419
420 if ($next->isDelimiter([']'])) {
421 return new Node\AttributeNode($selector, $namespace, $attribute, 'exists', null);
422 } elseif ($next->isDelimiter(['='])) {
423 $operator = '=';
424 } elseif ($next->isDelimiter(['^', '$', '*', '~', '|', '!'])
425 && $stream->getPeek()->isDelimiter(['='])
426 ) {
427 $operator = $next->getValue().'=';
428 $stream->getNext();
429 } else {
430 throw SyntaxErrorException::unexpectedToken('operator', $next);
431 }
432 }
433
434 $stream->skipWhitespace();
435 $value = $stream->getNext();
436
437 if ($value->isNumber()) {
438 // if the value is a number, it's casted into a string
439 $value = new Token(Token::TYPE_STRING, (string) $value->getValue(), $value->getPosition());
440 }
441
442 if (!($value->isIdentifier() || $value->isString())) {
443 throw SyntaxErrorException::unexpectedToken('string or identifier', $value);
444 }
445
446 $stream->skipWhitespace();
447 $next = $stream->getNext();
448
449 if (!$next->isDelimiter([']'])) {
450 throw SyntaxErrorException::unexpectedToken('"]"', $next);
451 }
452
453 return new Node\AttributeNode($selector, $namespace, $attribute, $operator, $value->getValue());
454 }
455 }
456