1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\CssSelector\Parser;
13
14use Symfony\Component\CssSelector\Exception\InternalErrorException;
15use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
16
17/**
18 * CSS selector token stream.
19 *
20 * This component is a port of the Python cssselect library,
21 * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
22 *
23 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
24 *
25 * @internal
26 */
27class TokenStream
28{
29    /**
30     * @var Token[]
31     */
32    private $tokens = array();
33
34    /**
35     * @var Token[]
36     */
37    private $used = array();
38
39    /**
40     * @var int
41     */
42    private $cursor = 0;
43
44    /**
45     * @var Token|null
46     */
47    private $peeked;
48
49    /**
50     * @var bool
51     */
52    private $peeking = false;
53
54    /**
55     * Pushes a token.
56     *
57     * @return $this
58     */
59    public function push(Token $token)
60    {
61        $this->tokens[] = $token;
62
63        return $this;
64    }
65
66    /**
67     * Freezes stream.
68     *
69     * @return $this
70     */
71    public function freeze()
72    {
73        return $this;
74    }
75
76    /**
77     * Returns next token.
78     *
79     * @return Token
80     *
81     * @throws InternalErrorException If there is no more token
82     */
83    public function getNext()
84    {
85        if ($this->peeking) {
86            $this->peeking = false;
87            $this->used[] = $this->peeked;
88
89            return $this->peeked;
90        }
91
92        if (!isset($this->tokens[$this->cursor])) {
93            throw new InternalErrorException('Unexpected token stream end.');
94        }
95
96        return $this->tokens[$this->cursor++];
97    }
98
99    /**
100     * Returns peeked token.
101     *
102     * @return Token
103     */
104    public function getPeek()
105    {
106        if (!$this->peeking) {
107            $this->peeked = $this->getNext();
108            $this->peeking = true;
109        }
110
111        return $this->peeked;
112    }
113
114    /**
115     * Returns used tokens.
116     *
117     * @return Token[]
118     */
119    public function getUsed()
120    {
121        return $this->used;
122    }
123
124    /**
125     * Returns nex identifier token.
126     *
127     * @return string The identifier token value
128     *
129     * @throws SyntaxErrorException If next token is not an identifier
130     */
131    public function getNextIdentifier()
132    {
133        $next = $this->getNext();
134
135        if (!$next->isIdentifier()) {
136            throw SyntaxErrorException::unexpectedToken('identifier', $next);
137        }
138
139        return $next->getValue();
140    }
141
142    /**
143     * Returns nex identifier or star delimiter token.
144     *
145     * @return null|string The identifier token value or null if star found
146     *
147     * @throws SyntaxErrorException If next token is not an identifier or a star delimiter
148     */
149    public function getNextIdentifierOrStar()
150    {
151        $next = $this->getNext();
152
153        if ($next->isIdentifier()) {
154            return $next->getValue();
155        }
156
157        if ($next->isDelimiter(array('*'))) {
158            return;
159        }
160
161        throw SyntaxErrorException::unexpectedToken('identifier or "*"', $next);
162    }
163
164    /**
165     * Skips next whitespace if any.
166     */
167    public function skipWhitespace()
168    {
169        $peek = $this->getPeek();
170
171        if ($peek->isWhitespace()) {
172            $this->getNext();
173        }
174    }
175}
176