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