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