1<?php
2/**
3* The attribute parser parses a simple attribute selector.
4*
5* @license http://www.opensource.org/licenses/mit-license.php The MIT License
6* @copyright Copyright 2010-2014 PhpCss Team
7*/
8namespace PhpCss\Parser {
9
10  use LogicException;
11  use PhpCss;
12  use PhpCss\Ast;
13  use PhpCss\Exception\UnknownPseudoElementException;
14  use PhpCss\Scanner;
15  /**
16  * The attribute parser parses a simple attribute selector.
17  *
18  * The attribute value can be an string if a string start is found it delegates to a string
19  * parser.
20  */
21  class PseudoClass extends PhpCss\Parser {
22
23    private const PARAMETER_NONE = 1;
24    private const PARAMETER_IDENTIFIER = 2;
25    private const PARAMETER_POSITION = 4;
26    private const PARAMETER_SIMPLE_SELECTOR = 8;
27    private const PARAMETER_STRING = 16;
28    private const PARAMETER_NUMBER = 32;
29
30    /**
31     * @throws PhpCss\Exception\ParserException
32     * @throws PhpCss\Exception\UnknownPseudoClassException
33     * @throws UnknownPseudoElementException
34     */
35    public function parse(): Ast\Node {
36      $token = $this->read(Scanner\Token::PSEUDO_CLASS);
37      $name = substr($token->content, 1);
38      if ($mode = $this->getParameterMode($name)) {
39        if ($mode === self::PARAMETER_NONE) {
40          return new Ast\Selector\Simple\PseudoClass($name);
41        }
42        $this->read(Scanner\Token::PARENTHESES_START);
43        $this->ignore(Scanner\Token::WHITESPACE);
44        switch ($mode) {
45        case self::PARAMETER_IDENTIFIER :
46          $parameterToken = $this->read(Scanner\Token::IDENTIFIER);
47          $class = new Ast\Value\Language($parameterToken->content);
48          break;
49        case self::PARAMETER_POSITION :
50          $parameterToken = $this->read(
51            array(
52              Scanner\Token::IDENTIFIER,
53              Scanner\Token::NUMBER,
54              Scanner\Token::PSEUDO_CLASS_POSITION
55            )
56          );
57          $class = new Ast\Selector\Simple\PseudoClass(
58            $name, $this->createPseudoClassPosition($parameterToken->content)
59          );
60          break;
61        case self::PARAMETER_STRING :
62          $this->read(
63            [Scanner\Token::SINGLEQUOTE_STRING_START, Scanner\Token::DOUBLEQUOTE_STRING_START]
64          );
65          $parameter = $this->delegate(Text::CLASS);
66          $class = new Ast\Selector\Simple\PseudoClass(
67            $name, $parameter
68          );
69          break;
70        case self::PARAMETER_NUMBER :
71          $parameter = $this->read(Scanner\Token::NUMBER);
72          $class = new Ast\Selector\Simple\PseudoClass(
73            $name, new Ast\Value\Number((int)$parameter->content)
74          );
75          break;
76        case self::PARAMETER_SIMPLE_SELECTOR :
77          $parameterToken = $this->lookahead(
78            array(
79              Scanner\Token::IDENTIFIER,
80              Scanner\Token::ID_SELECTOR,
81              Scanner\Token::CLASS_SELECTOR,
82              Scanner\Token::PSEUDO_CLASS,
83              Scanner\Token::PSEUDO_ELEMENT,
84              Scanner\Token::ATTRIBUTE_SELECTOR_START
85            )
86          );
87          switch ($parameterToken->type) {
88          case Scanner\Token::IDENTIFIER :
89          case Scanner\Token::ID_SELECTOR :
90          case Scanner\Token::CLASS_SELECTOR :
91            $this->read($parameterToken->type);
92            $parameter = $this->createSelector($parameterToken);
93            break;
94          case Scanner\Token::PSEUDO_CLASS :
95            if ($parameterToken->content === ':not') {
96              throw new LogicException(
97                'not not allowed in not - @todo implement exception'
98              );
99            }
100            $parameter = $this->delegate(self::CLASS);
101            break;
102          case Scanner\Token::PSEUDO_ELEMENT :
103            $this->read($parameterToken->type);
104            $parameter = $this->createPseudoElement($parameterToken);
105            break;
106          case Scanner\Token::ATTRIBUTE_SELECTOR_START :
107            $this->read($parameterToken->type);
108            $parameter = $this->delegate(Attribute::CLASS);
109            break;
110          default :
111            $parameter = NULL;
112          }
113          $class = new Ast\Selector\Simple\PseudoClass(
114            $name, $parameter
115          );
116          break;
117        default :
118          $class = NULL;
119        }
120        $this->ignore(Scanner\Token::WHITESPACE);
121        $this->read(Scanner\Token::PARENTHESES_END);
122        return $class;
123      }
124      throw new PhpCss\Exception\UnknownPseudoClassException($token);
125    }
126
127    private function getParameterMode($name): ?int {
128      switch ($name) {
129      case 'not' :
130      case 'has' :
131        return self::PARAMETER_SIMPLE_SELECTOR;
132      case 'lang' :
133        return self::PARAMETER_IDENTIFIER;
134      case 'nth-child' :
135      case 'nth-last-child' :
136      case 'nth-of-type' :
137      case 'nth-last-of-type' :
138        return self::PARAMETER_POSITION;
139      case 'contains':
140        return self::PARAMETER_STRING;
141      case 'gt':
142      case 'lt':
143        return self::PARAMETER_NUMBER;
144      case 'root' :
145      case 'first-child' :
146      case 'last-child' :
147      case 'first-of-type' :
148      case 'last-of-type' :
149      case 'only-child' :
150      case 'only-of-type' :
151      case 'empty' :
152      case 'link' :
153      case 'visited' :
154      case 'active' :
155      case 'hover' :
156      case 'focus' :
157      case 'target' :
158      case 'enabled' :
159      case 'disabled' :
160      case 'checked' :
161      case 'odd' :
162      case 'even' :
163        return self::PARAMETER_NONE;
164      }
165      return NULL;
166    }
167
168    private function createSelector(Scanner\Token $token) {
169      switch ($token->type) {
170      case Scanner\Token::IDENTIFIER :
171        if (FALSE !== strpos($token->content, '|')) {
172          [$prefix, $name] = explode('|', $token->content);
173        } else {
174          $prefix = '';
175          $name = $token->content;
176        }
177        if ($name === '*') {
178          return new Ast\Selector\Simple\Universal($prefix);
179        }
180        return new Ast\Selector\Simple\Type($name, $prefix);
181      case Scanner\Token::ID_SELECTOR :
182        return new Ast\Selector\Simple\Id(substr($token->content, 1));
183      case Scanner\Token::CLASS_SELECTOR :
184        return new Ast\Selector\Simple\ClassName(substr($token->content, 1));
185      }
186      return NULL;
187    }
188
189    /**
190     * @throws UnknownPseudoElementException
191     */
192    private function createPseudoElement(Scanner\Token $token): Ast\Selector\Simple\PseudoElement {
193      $name = substr($token->content, 2);
194      switch ($name) {
195      case 'first-line' :
196      case 'first-letter' :
197      case 'after' :
198      case 'before' :
199        return new Ast\Selector\Simple\PseudoElement($name);
200      }
201      throw new UnknownPseudoElementException($token);
202    }
203
204    private function createPseudoClassPosition($string): Ast\Value\Position {
205      $string = str_replace(' ', '', $string);
206      if ($string === 'n') {
207        $position = new Ast\Value\Position(1, 0);
208      } elseif ($string === 'odd') {
209        $position = new Ast\Value\Position(2, 1);
210      } elseif ($string === 'even') {
211        $position = new Ast\Value\Position(2, 0);
212      } elseif (preg_match('(^[+-]?\d+$)D', $string)) {
213        $position = new Ast\Value\Position(0, (int)$string);
214      } elseif (
215          preg_match('(^(?P<repeat>\d+)n$)D', $string, $matches) ||
216          preg_match('(^(?P<repeat>[+-]?\d*)n(?P<add>[+-]\d+)$)D', $string, $matches)
217        ) {
218        $position = new Ast\Value\Position(
219          isset($matches['repeat']) && $matches['repeat'] !== ''
220            ? (int)$matches['repeat'] : 1,
221          isset($matches['add']) ? (int)$matches['add'] : 0
222        );
223      } else {
224        throw new LogicException(
225          'Invalid pseudo class position - @todo implement exception'
226        );
227      }
228      return $position;
229    }
230  }
231}
232