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 = [];
33
34    /**
35     * @var Token[]
36     */
37    private $used = [];
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): self
60    {
61        $this->tokens[] = $token;
62
63        return $this;
64    }
65
66    /**
67     * Freezes stream.
68     *
69     * @return $this
70     */
71    public function freeze(): self
72    {
73        return $this;
74    }
75
76    /**
77     * Returns next token.
78     *
79     * @throws InternalErrorException If there is no more token
80     */
81    public function getNext(): Token
82    {
83        if ($this->peeking) {
84            $this->peeking = false;
85            $this->used[] = $this->peeked;
86
87            return $this->peeked;
88        }
89
90        if (!isset($this->tokens[$this->cursor])) {
91            throw new InternalErrorException('Unexpected token stream end.');
92        }
93
94        return $this->tokens[$this->cursor++];
95    }
96
97    /**
98     * Returns peeked token.
99     */
100    public function getPeek(): Token
101    {
102        if (!$this->peeking) {
103            $this->peeked = $this->getNext();
104            $this->peeking = true;
105        }
106
107        return $this->peeked;
108    }
109
110    /**
111     * Returns used tokens.
112     *
113     * @return Token[]
114     */
115    public function getUsed(): array
116    {
117        return $this->used;
118    }
119
120    /**
121     * Returns next identifier token.
122     *
123     * @throws SyntaxErrorException If next token is not an identifier
124     */
125    public function getNextIdentifier(): string
126    {
127        $next = $this->getNext();
128
129        if (!$next->isIdentifier()) {
130            throw SyntaxErrorException::unexpectedToken('identifier', $next);
131        }
132
133        return $next->getValue();
134    }
135
136    /**
137     * Returns next identifier or null if star delimiter token is found.
138     *
139     * @throws SyntaxErrorException If next token is not an identifier or a star delimiter
140     */
141    public function getNextIdentifierOrStar(): ?string
142    {
143        $next = $this->getNext();
144
145        if ($next->isIdentifier()) {
146            return $next->getValue();
147        }
148
149        if ($next->isDelimiter(['*'])) {
150            return null;
151        }
152
153        throw SyntaxErrorException::unexpectedToken('identifier or "*"', $next);
154    }
155
156    /**
157     * Skips next whitespace if any.
158     */
159    public function skipWhitespace()
160    {
161        $peek = $this->getPeek();
162
163        if ($peek->isWhitespace()) {
164            $this->getNext();
165        }
166    }
167}
168