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\Node\Node;
16
17/**
18 * @author Fabien Potencier <fabien@symfony.com>
19 */
20class Compiler
21{
22    private $lastLine;
23    private $source;
24    private $indentation;
25    private $debugInfo = [];
26    private $sourceOffset;
27    private $sourceLine;
28    private $varNameSalt = 0;
29    private $didUseEcho = false;
30    private $didUseEchoStack = [];
31
32    public function __construct(
33        private Environment $env,
34    ) {
35    }
36
37    public function getEnvironment(): Environment
38    {
39        return $this->env;
40    }
41
42    public function getSource(): string
43    {
44        return $this->source;
45    }
46
47    /**
48     * @return $this
49     */
50    public function reset(int $indentation = 0)
51    {
52        $this->lastLine = null;
53        $this->source = '';
54        $this->debugInfo = [];
55        $this->sourceOffset = 0;
56        // source code starts at 1 (as we then increment it when we encounter new lines)
57        $this->sourceLine = 1;
58        $this->indentation = $indentation;
59        $this->varNameSalt = 0;
60
61        return $this;
62    }
63
64    /**
65     * @return $this
66     */
67    public function compile(Node $node, int $indentation = 0)
68    {
69        $this->reset($indentation);
70        $this->didUseEchoStack[] = $this->didUseEcho;
71
72        try {
73            $this->didUseEcho = false;
74            $node->compile($this);
75
76            if ($this->didUseEcho) {
77                trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, $node::class);
78            }
79
80            return $this;
81        } finally {
82            $this->didUseEcho = array_pop($this->didUseEchoStack);
83        }
84    }
85
86    /**
87     * @return $this
88     */
89    public function subcompile(Node $node, bool $raw = true)
90    {
91        if (!$raw) {
92            $this->source .= str_repeat(' ', $this->indentation * 4);
93        }
94
95        $this->didUseEchoStack[] = $this->didUseEcho;
96
97        try {
98            $this->didUseEcho = false;
99            $node->compile($this);
100
101            if ($this->didUseEcho) {
102                trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, $node::class);
103            }
104
105            return $this;
106        } finally {
107            $this->didUseEcho = array_pop($this->didUseEchoStack);
108        }
109    }
110
111    /**
112     * Adds a raw string to the compiled code.
113     *
114     * @return $this
115     */
116    public function raw(string $string)
117    {
118        $this->checkForEcho($string);
119        $this->source .= $string;
120
121        return $this;
122    }
123
124    /**
125     * Writes a string to the compiled code by adding indentation.
126     *
127     * @return $this
128     */
129    public function write(...$strings)
130    {
131        foreach ($strings as $string) {
132            $this->checkForEcho($string);
133            $this->source .= str_repeat(' ', $this->indentation * 4).$string;
134        }
135
136        return $this;
137    }
138
139    /**
140     * Adds a quoted string to the compiled code.
141     *
142     * @return $this
143     */
144    public function string(string $value)
145    {
146        // Single quotes are encoded as \x27 (not \') as a defense-in-depth measure:
147        // it guarantees that the compiled output never contains a literal "'" derived
148        // from user input, which prevents breaking out of a surrounding single-quoted
149        // PHP context if a caller mistakenly concatenates the result into one.
150        // \' is not a recognized escape sequence in PHP double-quoted strings (the
151        // backslash would be kept literally), so \x27 is used instead.
152        $this->source .= \sprintf('"%s"', str_replace("'", '\\x27', addcslashes($value, "\0\t\"\$\\")));
153
154        return $this;
155    }
156
157    /**
158     * Returns a PHP representation of a given value.
159     *
160     * @return $this
161     */
162    public function repr($value)
163    {
164        if (\is_int($value) || \is_float($value)) {
165            if (false !== $locale = setlocale(\LC_NUMERIC, '0')) {
166                setlocale(\LC_NUMERIC, 'C');
167            }
168
169            $this->raw(var_export($value, true));
170
171            if (false !== $locale) {
172                setlocale(\LC_NUMERIC, $locale);
173            }
174        } elseif (null === $value) {
175            $this->raw('null');
176        } elseif (\is_bool($value)) {
177            $this->raw($value ? 'true' : 'false');
178        } elseif (\is_array($value)) {
179            $this->raw('[');
180            $first = true;
181            foreach ($value as $key => $v) {
182                if (!$first) {
183                    $this->raw(', ');
184                }
185                $first = false;
186                $this->repr($key);
187                $this->raw(' => ');
188                $this->repr($v);
189            }
190            $this->raw(']');
191        } else {
192            $this->string($value);
193        }
194
195        return $this;
196    }
197
198    /**
199     * @return $this
200     */
201    public function addDebugInfo(Node $node)
202    {
203        if ($node->getTemplateLine() != $this->lastLine) {
204            $this->write(\sprintf("// line %d\n", $node->getTemplateLine()));
205
206            $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset);
207            $this->sourceOffset = \strlen($this->source);
208            $this->debugInfo[$this->sourceLine] = $node->getTemplateLine();
209
210            $this->lastLine = $node->getTemplateLine();
211        }
212
213        return $this;
214    }
215
216    public function getDebugInfo(): array
217    {
218        ksort($this->debugInfo);
219
220        return $this->debugInfo;
221    }
222
223    /**
224     * @return $this
225     */
226    public function indent(int $step = 1)
227    {
228        $this->indentation += $step;
229
230        return $this;
231    }
232
233    /**
234     * @return $this
235     *
236     * @throws \LogicException When trying to outdent too much so the indentation would become negative
237     */
238    public function outdent(int $step = 1)
239    {
240        // can't outdent by more steps than the current indentation level
241        if ($this->indentation < $step) {
242            throw new \LogicException('Unable to call outdent() as the indentation would become negative.');
243        }
244
245        $this->indentation -= $step;
246
247        return $this;
248    }
249
250    public function getVarName(): string
251    {
252        return \sprintf('_v%d', $this->varNameSalt++);
253    }
254
255    private function checkForEcho(string $string): void
256    {
257        if ($this->didUseEcho) {
258            return;
259        }
260
261        $this->didUseEcho = preg_match('/^\s*+(echo|print)\b/', $string, $m) ? $m[1] : false;
262    }
263}
264