1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 * (c) Armin Ronacher
8 *
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
11 */
12
13namespace Twig;
14
15use Twig\Error\SyntaxError;
16
17/**
18 * Represents a token stream.
19 *
20 * @author Fabien Potencier <fabien@symfony.com>
21 */
22final class TokenStream
23{
24    private $current = 0;
25
26    public function __construct(
27        private array $tokens,
28        private ?Source $source = null,
29    ) {
30        if (null === $this->source) {
31            trigger_deprecation('twig/twig', '3.16', \sprintf('Not passing a "%s" object to "%s" constructor is deprecated.', Source::class, __CLASS__));
32
33            $this->source = new Source('', '');
34        }
35    }
36
37    public function __toString(): string
38    {
39        return implode("\n", $this->tokens);
40    }
41
42    /**
43     * @return void
44     */
45    public function injectTokens(array $tokens)
46    {
47        $this->tokens = array_merge(\array_slice($this->tokens, 0, $this->current), $tokens, \array_slice($this->tokens, $this->current));
48    }
49
50    /**
51     * Sets the pointer to the next token and returns the old one.
52     */
53    public function next(): Token
54    {
55        if (!isset($this->tokens[++$this->current])) {
56            throw new SyntaxError('Unexpected end of template.', $this->tokens[$this->current - 1]->getLine(), $this->source);
57        }
58
59        return $this->tokens[$this->current - 1];
60    }
61
62    /**
63     * Tests a token, sets the pointer to the next one and returns it or throws a syntax error.
64     *
65     * @return Token|null The next token if the condition is true, null otherwise
66     */
67    public function nextIf($primary, $secondary = null)
68    {
69        return $this->tokens[$this->current]->test($primary, $secondary) ? $this->next() : null;
70    }
71
72    /**
73     * Tests a token and returns it or throws a syntax error.
74     */
75    public function expect($type, $value = null, ?string $message = null): Token
76    {
77        $token = $this->tokens[$this->current];
78        if (!$token->test($type, $value)) {
79            $line = $token->getLine();
80            throw new SyntaxError(\sprintf('%sUnexpected token "%s"%s ("%s" expected%s).',
81                $message ? $message.'. ' : '',
82                $token->toEnglish(),
83                $token->getValue() ? \sprintf(' of value "%s"', $token->getValue()) : '',
84                Token::typeToEnglish($type), $value ? \sprintf(' with value "%s"', $value) : ''),
85                $line,
86                $this->source
87            );
88        }
89        $this->next();
90
91        return $token;
92    }
93
94    /**
95     * Looks at the next token.
96     */
97    public function look(int $number = 1): Token
98    {
99        if (!isset($this->tokens[$this->current + $number])) {
100            throw new SyntaxError('Unexpected end of template.', $this->tokens[$this->current + $number - 1]->getLine(), $this->source);
101        }
102
103        return $this->tokens[$this->current + $number];
104    }
105
106    /**
107     * Tests the current token.
108     */
109    public function test($primary, $secondary = null): bool
110    {
111        return $this->tokens[$this->current]->test($primary, $secondary);
112    }
113
114    /**
115     * Checks if end of stream was reached.
116     */
117    public function isEOF(): bool
118    {
119        return $this->tokens[$this->current]->test(Token::EOF_TYPE);
120    }
121
122    public function getCurrent(): Token
123    {
124        return $this->tokens[$this->current];
125    }
126
127    public function getSourceContext(): Source
128    {
129        return $this->source;
130    }
131}
132