1<?php
2/**
3 * The Sequence parser parses a list of simple selector tokens into the AST.
4 *
5 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6 * @copyright Copyright 2010-2012 PhpCss Team
7 */
8
9namespace PhpCss\Parser {
10
11  use PhpCss;
12  use PhpCss\Ast;
13  use PhpCss\Scanner;
14
15  /**
16   * The Sequence parser parses a list of simple selector tokens into the AST.
17   *
18   * It delegates to separate parsers for pseudo classes and attributes.
19   *
20   * A css combinator delegates to a new instance of this class.
21   */
22  class Sequence extends PhpCss\Parser {
23
24    /**
25     * Parse the token stream for a simple selector sequence,
26     * after the first element the type selector is not allowed any more,
27     * but a combinator is possible.
28     *
29     * @throws PhpCss\Exception\ParserException
30     */
31    public function parse(): Ast\Node {
32      $sequence = new Ast\Selector\Sequence();
33      $token = $this->lookahead(
34        [
35          Scanner\Token::IDENTIFIER,
36          Scanner\Token::ID_SELECTOR,
37          Scanner\Token::CLASS_SELECTOR,
38          Scanner\Token::PSEUDO_CLASS,
39          Scanner\Token::PSEUDO_ELEMENT,
40          Scanner\Token::ATTRIBUTE_SELECTOR_START,
41          Scanner\Token::COMBINATOR,
42        ]
43      );
44      while (isset($token)) {
45        if ($selector = $this->createSelector($token)) {
46          $this->read($token->type);
47          $sequence->simples[] = $selector;
48        }
49        switch ($token->type) {
50        case Scanner\Token::SEPARATOR :
51          $this->read(Scanner\Token::SEPARATOR);
52          return $sequence;
53        case Scanner\Token::PSEUDO_CLASS :
54          $sequence->simples[] = $this->delegate(PseudoClass::CLASS);
55          break;
56        case Scanner\Token::PSEUDO_ELEMENT :
57          $sequence->simples[] = $this->createPseudoElement($token);
58          $this->read($token->type);
59          break;
60        case Scanner\Token::ATTRIBUTE_SELECTOR_START :
61          $this->read($token->type);
62          $sequence->simples[] = $this->delegate(Attribute::CLASS);
63          break;
64        case Scanner\Token::COMBINATOR :
65        case Scanner\Token::WHITESPACE :
66          $this->read($token->type);
67          $subSequence = $this->delegate(get_class($this));
68          /**
69           * @var Ast\Selector\Sequence $subSequence
70           */
71          $sequence->combinator = $this->createCombinator(
72            $token, $subSequence
73          );
74          return $sequence;
75        }
76        if ($this->endOfTokens()) {
77          $token = NULL;
78          continue;
79        }
80        $token = $this->lookahead(
81          [
82            Scanner\Token::ID_SELECTOR,
83            Scanner\Token::CLASS_SELECTOR,
84            Scanner\Token::PSEUDO_CLASS,
85            Scanner\Token::PSEUDO_ELEMENT,
86            Scanner\Token::ATTRIBUTE_SELECTOR_START,
87            Scanner\Token::COMBINATOR,
88            Scanner\Token::WHITESPACE,
89            Scanner\Token::SEPARATOR,
90          ]
91        );
92      }
93      return $sequence;
94    }
95
96    private function createSelector(Scanner\Token $token) {
97      switch ($token->type) {
98      case Scanner\Token::IDENTIFIER :
99        if (FALSE !== strpos($token->content, '|')) {
100          [$prefix, $name] = explode('|', $token->content);
101        } else {
102          $prefix = '';
103          $name = $token->content;
104        }
105        if ($name === '*') {
106          return new Ast\Selector\Simple\Universal($prefix);
107        }
108        return new Ast\Selector\Simple\Type($name, $prefix);
109      case Scanner\Token::ID_SELECTOR :
110        return new Ast\Selector\Simple\Id(substr($token->content, 1));
111      case Scanner\Token::CLASS_SELECTOR :
112        return new Ast\Selector\Simple\ClassName(substr($token->content, 1));
113      }
114      return NULL;
115    }
116
117    private function createCombinator(
118      Scanner\Token $token,
119      Ast\Selector\Sequence $sequence
120    ) {
121      switch (trim($token->content)) {
122      case '>' :
123        return new Ast\Selector\Combinator\Child($sequence);
124      case '+' :
125        return new Ast\Selector\Combinator\Next($sequence);
126      case '~' :
127        return new Ast\Selector\Combinator\Follower($sequence);
128      default :
129        return new Ast\Selector\Combinator\Descendant($sequence);
130      }
131    }
132
133    /**
134     * @throws PhpCss\Exception\UnknownPseudoElementException
135     */
136    private function createPseudoElement($token): Ast\Selector\Simple\PseudoElement {
137      $name = substr($token->content, 2);
138      switch ($name) {
139      case 'first-line' :
140      case 'first-letter' :
141      case 'after' :
142      case 'before' :
143        return new Ast\Selector\Simple\PseudoElement($name);
144      }
145      throw new PhpCss\Exception\UnknownPseudoElementException($token);
146    }
147  }
148}
149