4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\CssSelector\Parser;
14 use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
15 use Symfony\Component\CssSelector\Node;
16 use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer;
19 * CSS selector parser.
21 * This component is a port of the Python cssselect library,
22 * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
24 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
28 class Parser implements ParserInterface
32 public function __construct(Tokenizer $tokenizer = null)
34 $this->tokenizer = $tokenizer ?: new Tokenizer();
40 public function parse($source)
42 $reader = new Reader($source);
43 $stream = $this->tokenizer->tokenize($reader);
45 return $this->parseSelectorList($stream);
49 * Parses the arguments for ":nth-child()" and friends.
51 * @param Token[] $tokens
55 * @throws SyntaxErrorException
57 public static function parseSeries(array $tokens)
59 foreach ($tokens as $token) {
60 if ($token->isString()) {
61 throw SyntaxErrorException::stringAsFunctionArgument();
65 $joined = trim(implode('', array_map(function (Token $token) {
66 return $token->getValue();
69 $int = function ($string) {
70 if (!is_numeric($string)) {
71 throw SyntaxErrorException::stringAsFunctionArgument();
78 case 'odd' === $joined:
80 case 'even' === $joined:
84 case false === strpos($joined, 'n'):
85 return array(0, $int($joined));
88 $split = explode('n', $joined);
89 $first = isset($split[0]) ? $split[0] : null;
92 $first ? ('-' === $first || '+' === $first ? $int($first.'1') : $int($first)) : 1,
93 isset($split[1]) && $split[1] ? $int($split[1]) : 0,
98 * Parses selector nodes.
102 private function parseSelectorList(TokenStream $stream)
104 $stream->skipWhitespace();
105 $selectors = array();
108 $selectors[] = $this->parserSelectorNode($stream);
110 if ($stream->getPeek()->isDelimiter(array(','))) {
112 $stream->skipWhitespace();
122 * Parses next selector or combined node.
124 * @return Node\SelectorNode
126 * @throws SyntaxErrorException
128 private function parserSelectorNode(TokenStream $stream)
130 list($result, $pseudoElement) = $this->parseSimpleSelector($stream);
133 $stream->skipWhitespace();
134 $peek = $stream->getPeek();
136 if ($peek->isFileEnd() || $peek->isDelimiter(array(','))) {
140 if (null !== $pseudoElement) {
141 throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
144 if ($peek->isDelimiter(array('+', '>', '~'))) {
145 $combinator = $stream->getNext()->getValue();
146 $stream->skipWhitespace();
151 list($nextSelector, $pseudoElement) = $this->parseSimpleSelector($stream);
152 $result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector);
155 return new Node\SelectorNode($result, $pseudoElement);
159 * Parses next simple node (hash, class, pseudo, negation).
161 * @param TokenStream $stream
162 * @param bool $insideNegation
166 * @throws SyntaxErrorException
168 private function parseSimpleSelector(TokenStream $stream, $insideNegation = false)
170 $stream->skipWhitespace();
172 $selectorStart = \count($stream->getUsed());
173 $result = $this->parseElementNode($stream);
174 $pseudoElement = null;
177 $peek = $stream->getPeek();
178 if ($peek->isWhitespace()
179 || $peek->isFileEnd()
180 || $peek->isDelimiter(array(',', '+', '>', '~'))
181 || ($insideNegation && $peek->isDelimiter(array(')')))
186 if (null !== $pseudoElement) {
187 throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
190 if ($peek->isHash()) {
191 $result = new Node\HashNode($result, $stream->getNext()->getValue());
192 } elseif ($peek->isDelimiter(array('.'))) {
194 $result = new Node\ClassNode($result, $stream->getNextIdentifier());
195 } elseif ($peek->isDelimiter(array('['))) {
197 $result = $this->parseAttributeNode($result, $stream);
198 } elseif ($peek->isDelimiter(array(':'))) {
201 if ($stream->getPeek()->isDelimiter(array(':'))) {
203 $pseudoElement = $stream->getNextIdentifier();
208 $identifier = $stream->getNextIdentifier();
209 if (\in_array(strtolower($identifier), array('first-line', 'first-letter', 'before', 'after'))) {
210 // Special case: CSS 2.1 pseudo-elements can have a single ':'.
211 // Any new pseudo-element must have two.
212 $pseudoElement = $identifier;
217 if (!$stream->getPeek()->isDelimiter(array('('))) {
218 $result = new Node\PseudoNode($result, $identifier);
224 $stream->skipWhitespace();
226 if ('not' === strtolower($identifier)) {
227 if ($insideNegation) {
228 throw SyntaxErrorException::nestedNot();
231 list($argument, $argumentPseudoElement) = $this->parseSimpleSelector($stream, true);
232 $next = $stream->getNext();
234 if (null !== $argumentPseudoElement) {
235 throw SyntaxErrorException::pseudoElementFound($argumentPseudoElement, 'inside ::not()');
238 if (!$next->isDelimiter(array(')'))) {
239 throw SyntaxErrorException::unexpectedToken('")"', $next);
242 $result = new Node\NegationNode($result, $argument);
244 $arguments = array();
248 $stream->skipWhitespace();
249 $next = $stream->getNext();
251 if ($next->isIdentifier()
254 || $next->isDelimiter(array('+', '-'))
256 $arguments[] = $next;
257 } elseif ($next->isDelimiter(array(')'))) {
260 throw SyntaxErrorException::unexpectedToken('an argument', $next);
264 if (empty($arguments)) {
265 throw SyntaxErrorException::unexpectedToken('at least one argument', $next);
268 $result = new Node\FunctionNode($result, $identifier, $arguments);
271 throw SyntaxErrorException::unexpectedToken('selector', $peek);
275 if (\count($stream->getUsed()) === $selectorStart) {
276 throw SyntaxErrorException::unexpectedToken('selector', $stream->getPeek());
279 return array($result, $pseudoElement);
283 * Parses next element node.
285 * @return Node\ElementNode
287 private function parseElementNode(TokenStream $stream)
289 $peek = $stream->getPeek();
291 if ($peek->isIdentifier() || $peek->isDelimiter(array('*'))) {
292 if ($peek->isIdentifier()) {
293 $namespace = $stream->getNext()->getValue();
299 if ($stream->getPeek()->isDelimiter(array('|'))) {
301 $element = $stream->getNextIdentifierOrStar();
303 $element = $namespace;
307 $element = $namespace = null;
310 return new Node\ElementNode($namespace, $element);
314 * Parses next attribute node.
316 * @return Node\AttributeNode
318 * @throws SyntaxErrorException
320 private function parseAttributeNode(Node\NodeInterface $selector, TokenStream $stream)
322 $stream->skipWhitespace();
323 $attribute = $stream->getNextIdentifierOrStar();
325 if (null === $attribute && !$stream->getPeek()->isDelimiter(array('|'))) {
326 throw SyntaxErrorException::unexpectedToken('"|"', $stream->getPeek());
329 if ($stream->getPeek()->isDelimiter(array('|'))) {
332 if ($stream->getPeek()->isDelimiter(array('='))) {
337 $namespace = $attribute;
338 $attribute = $stream->getNextIdentifier();
342 $namespace = $operator = null;
345 if (null === $operator) {
346 $stream->skipWhitespace();
347 $next = $stream->getNext();
349 if ($next->isDelimiter(array(']'))) {
350 return new Node\AttributeNode($selector, $namespace, $attribute, 'exists', null);
351 } elseif ($next->isDelimiter(array('='))) {
353 } elseif ($next->isDelimiter(array('^', '$', '*', '~', '|', '!'))
354 && $stream->getPeek()->isDelimiter(array('='))
356 $operator = $next->getValue().'=';
359 throw SyntaxErrorException::unexpectedToken('operator', $next);
363 $stream->skipWhitespace();
364 $value = $stream->getNext();
366 if ($value->isNumber()) {
367 // if the value is a number, it's casted into a string
368 $value = new Token(Token::TYPE_STRING, (string) $value->getValue(), $value->getPosition());
371 if (!($value->isIdentifier() || $value->isString())) {
372 throw SyntaxErrorException::unexpectedToken('string or identifier', $value);
375 $stream->skipWhitespace();
376 $next = $stream->getNext();
378 if (!$next->isDelimiter(array(']'))) {
379 throw SyntaxErrorException::unexpectedToken('"]"', $next);
382 return new Node\AttributeNode($selector, $namespace, $attribute, $operator, $value->getValue());