*/ class Compiler { private ?int $lastLine; private string $source; private int $indentation; private array $debugInfo; private int $sourceOffset; private int $sourceLine; private int $varNameSalt; public function __construct( private Environment $env, ) { $this->reset(); } public function getEnvironment(): Environment { return $this->env; } public function getSource(): string { return $this->source; } /** * @return $this */ public function reset(int $indentation = 0) { $this->lastLine = null; $this->source = ''; $this->debugInfo = []; $this->sourceOffset = 0; // source code starts at 1 (as we then increment it when we encounter new lines) $this->sourceLine = 1; $this->indentation = $indentation; $this->varNameSalt = 0; return $this; } /** * @return $this */ public function compile(Node $node, int $indentation = 0) { $this->reset($indentation); $node->compile($this); return $this; } /** * @return $this */ public function subcompile(Node $node, bool $raw = true) { if (!$raw) { $this->source .= str_repeat(' ', $this->indentation * 4); } $node->compile($this); return $this; } /** * Adds a raw string to the compiled code. * * @return $this */ public function raw(string $string) { $this->source .= $string; return $this; } /** * Writes a string to the compiled code by adding indentation. * * @return $this */ public function write(...$strings) { foreach ($strings as $string) { $this->source .= str_repeat(' ', $this->indentation * 4).$string; } return $this; } /** * Adds a quoted string to the compiled code. * * @return $this */ public function string(string $value) { // Single quotes are encoded as \x27 (not \') as a defense-in-depth measure: // it guarantees that the compiled output never contains a literal "'" derived // from user input, which prevents breaking out of a surrounding single-quoted // PHP context if a caller mistakenly concatenates the result into one. // \' is not a recognized escape sequence in PHP double-quoted strings (the // backslash would be kept literally), so \x27 is used instead. $this->source .= \sprintf('"%s"', str_replace("'", '\\x27', addcslashes($value, "\0\t\"\$\\"))); return $this; } /** * Returns a PHP representation of a given value. * * @return $this */ public function repr($value) { if (\is_int($value) || \is_float($value)) { if (false !== $locale = setlocale(\LC_NUMERIC, '0')) { setlocale(\LC_NUMERIC, 'C'); } $this->raw(var_export($value, true)); if (false !== $locale) { setlocale(\LC_NUMERIC, $locale); } } elseif (null === $value) { $this->raw('null'); } elseif (\is_bool($value)) { $this->raw($value ? 'true' : 'false'); } elseif (\is_array($value)) { $this->raw('['); $first = true; foreach ($value as $key => $v) { if (!$first) { $this->raw(', '); } $first = false; $this->repr($key); $this->raw(' => '); $this->repr($v); } $this->raw(']'); } else { $this->string($value); } return $this; } /** * @return $this */ public function addDebugInfo(Node $node) { if ($node->getTemplateLine() != $this->lastLine) { $this->write(\sprintf("// line %d\n", $node->getTemplateLine())); $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset); $this->sourceOffset = \strlen($this->source); $this->debugInfo[$this->sourceLine] = $node->getTemplateLine(); $this->lastLine = $node->getTemplateLine(); } return $this; } public function getDebugInfo(): array { ksort($this->debugInfo); return $this->debugInfo; } /** * @return $this */ public function indent(int $step = 1) { $this->indentation += $step; return $this; } /** * @return $this * * @throws \LogicException When trying to outdent too much so the indentation would become negative */ public function outdent(int $step = 1) { // can't outdent by more steps than the current indentation level if ($this->indentation < $step) { throw new \LogicException('Unable to call outdent() as the indentation would become negative.'); } $this->indentation -= $step; return $this; } public function getVarName(): string { return \sprintf('_v%d', $this->varNameSalt++); } }