1<?php 2/** 3* Abstract class implementing functionality to ease parsing in extending 4* subparsers. 5* 6* @license http://www.opensource.org/licenses/mit-license.php The MIT License 7* @copyright Copyright 2010-2014 PhpCss Team 8*/ 9 10namespace PhpCss { 11 12 use PhpCss\Exception\ParserException; 13 14 /** 15 * Abstract class implementing functionality to ease parsing in extending 16 * subparsers. 17 */ 18 abstract class Parser { 19 20 /** 21 * List of tokens from scanner 22 * 23 * @var array(Scanner\Token) 24 */ 25 protected $_tokens = array(); 26 27 /** 28 * Construct a parser object taking the token list to operate on as 29 * argument 30 */ 31 public function __construct(array &$tokens) { 32 $this->_tokens = &$tokens; 33 } 34 35 /** 36 * Return parser tokens list 37 */ 38 public function getTokens(): array { 39 return $this->_tokens; 40 } 41 42 /** 43 * Execute the parsing process on the provided token stream 44 * 45 * This method is supposed to handle all the steps needed to parse the 46 * current subsegment of the token stream. It is supposed to return a valid 47 * PhpCssAst. 48 * 49 * If the parsing process can't be completed because of invalid input a 50 * PhpCssParserException needs to be thrown. 51 * 52 * The methods protected methods read and lookahead should be used to 53 * operate on the token stream. They will throw PhpCssParserExceptions 54 * automatically in case they do not succeed. 55 * 56 * @return Ast\Node 57 */ 58 abstract public function parse(): Ast\Node; 59 60 /** 61 * Try to read any of the $expectedTokens from the token list and return 62 * the matching one. 63 * 64 * This method tries to match the current token list against all of the 65 * provided tokens. If a match is found it is removed from the token list 66 * and returned. 67 * 68 * If no match can be found a PhpCssParserException will thrown indicating what 69 * has been expected and what was found. 70 * 71 * The $expectedTokens parameter may be an array of tokens or a scalar 72 * value, which is handled the same way an array with only one entry would 73 * be. 74 * 75 * The special Token Scanner\Token::ANY may be used to indicate 76 * everything is valid and may be matched. However if it is used no other 77 * token may be specified, which does not make any sense, anyway. 78 * 79 * @param array|integer|string $expectedTokens 80 * @throws ParserException 81 * @return Scanner\Token 82 */ 83 protected function read($expectedTokens): Scanner\Token { 84 // Allow scalar token values for better readability 85 if (!is_array($expectedTokens)) { 86 return $this->read(array($expectedTokens)); 87 } 88 89 foreach($expectedTokens as $token) { 90 if ($this->matchToken(0, $token)) { 91 return array_shift($this->_tokens); 92 } 93 } 94 95 // None of the given tokens matched 96 throw $this->handleMismatch($expectedTokens); 97 } 98 99 /** 100 * Try to match any of the $expectedTokens against the given token stream 101 * position and return the matching one. 102 * 103 * This method tries to match the current token stream at the provided 104 * lookahead position against all of the provided tokens. If a match is 105 * found it simply returned. The token stream remains unchanged. 106 * 107 * If no match can be found a PhpCssParserException will thrown indicating what 108 * has been expected and what was found. 109 * 110 * The $expectedTokens parameter may be an array of tokens or a scalar 111 * value, which is handled the same way an array with only one entry would 112 * be. 113 * 114 * The special Token Scanner\Token::ANY may be used to indicate 115 * everything is valid and may be matched. However if it is used no other 116 * token may be specified, which does not make any sense, anyway. 117 * 118 * The position parameter may be provided to enforce a match on an 119 * arbitrary token stream position. Therefore unlimited lookahead is 120 * provided. 121 * 122 * @param array|integer|string $expectedTokens 123 * @param int $position 124 * @param bool $allowEndOfTokens 125 * @throws ParserException 126 * @return Scanner\Token|NULL 127 */ 128 protected function lookahead( 129 $expectedTokens, int $position = 0, bool $allowEndOfTokens = FALSE 130 ): ?Scanner\Token { 131 // Allow scalar token values for better readability 132 if (!is_array($expectedTokens)) { 133 return $this->lookahead(array($expectedTokens), $position, $allowEndOfTokens); 134 } 135 136 // If the the requested characters is not available on the token stream 137 // and this state is allowed return a special ANY token 138 if ($allowEndOfTokens === TRUE && (!isset($this->_tokens[$position]))) { 139 return new Scanner\Token(Scanner\Token::ANY, '', 0); 140 } 141 142 foreach($expectedTokens as $token) { 143 if ($this->matchToken($position, $token)) { 144 return $this->_tokens[$position]; 145 } 146 } 147 148 // None of the given tokens matched 149 throw $this->handleMismatch($expectedTokens, $position); 150 } 151 152 /** 153 * Validate if the of the token stream is reached. The position parameter 154 * may be provided to look forward. 155 * 156 * @param int $position 157 * @return bool 158 */ 159 protected function endOfTokens(int $position = 0): bool { 160 return (count($this->_tokens) <= $position); 161 } 162 163 /** 164 * Try to read any of the $expectedTokens from the token stream and remove them 165 * from it. 166 * 167 * This method tries to match the current token list against all of the 168 * provided tokens. Matching tokens are removed from the list until a non 169 * matching token is found or the token list ends. 170 * 171 * The $expectedTokens parameter may be an array of tokens or a scalar 172 * value, which is handled the same way an array with only one entry would 173 * be. 174 * 175 * The special Token Scanner\Token::ANY is not valid here. 176 * 177 * The method return TRUE if tokens were removed, otherwise FALSE. 178 * 179 * @param array|integer|string $expectedTokens 180 * @param boolean 181 * @return bool 182 */ 183 protected function ignore($expectedTokens): bool { 184 // Allow scalar token values for better readability 185 if (!is_array($expectedTokens)) { 186 return $this->ignore(array($expectedTokens)); 187 } 188 189 // increase position until the end of the token stream is reached or 190 // a non matching token is found 191 $position = 0; 192 $found = FALSE; 193 while (count($this->_tokens) > $position) { 194 foreach ($expectedTokens as $token) { 195 if ($found = $this->matchToken($position, $token)) { 196 ++$position; 197 } 198 } 199 if ($found) { 200 continue; 201 } 202 break; 203 } 204 205 // remove the tokens from the stream 206 if ($position > 0) { 207 array_splice($this->_tokens, 0, $position); 208 return TRUE; 209 } 210 return FALSE; 211 } 212 213 /** 214 * Delegate the parsing process to a subparser 215 * 216 * The result of the subparser is returned 217 * 218 * Only the name of the subparser is expected here, the method takes care 219 * of providing the current token stream as well as instantiating the 220 * subparser. 221 * 222 * @param string $parserClass 223 * @return Ast\Node 224 */ 225 protected function delegate(string $parserClass): Ast\Node { 226 /** @var Parser $parser */ 227 $parser = new $parserClass($this->_tokens); 228 return $parser->parse(); 229 } 230 231 /** 232 * Match a token on the token stream against a token type. 233 * 234 * Returns true if the token at the given position exists and the provided 235 * token type matches type of the token at this position, false otherwise. 236 * 237 * @param int $position 238 * @param int $type 239 * @return bool 240 */ 241 protected function matchToken(int $position, int $type): bool { 242 if (!isset($this->_tokens[$position])) { 243 return false; 244 } 245 246 if ($type === Scanner\Token::ANY) { 247 // A token has been found. We do not care which one it was 248 return true; 249 } 250 251 return ($this->_tokens[$position]->type === $type); 252 } 253 254 /** 255 * Handle the case if none of the expected tokens could be found. 256 * 257 * @param array() $expectedTokens 258 * @param int $position 259 * @return ParserException 260 */ 261 private function handleMismatch($expectedTokens, $position = 0) { 262 // If the token stream ended unexpectedly throw an appropriate exception 263 if (!isset($this->_tokens[$position])) { 264 return new Exception\UnexpectedEndOfFileException($expectedTokens); 265 } 266 267 // We found a token but none of the expected ones. 268 return new Exception\TokenMismatchException($this->_tokens[$position], $expectedTokens); 269 } 270 } 271} 272