PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.8
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.8
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.8, at vendor/symfony/css-selector/Parser/Parser.php

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