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