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