<?php

/**
 * This file is part of the Latte (https://latte.nette.org)
 * Copyright (c) 2008 David Grudl (https://davidgrudl.com)
 */
declare (strict_types=1);
namespace Packetery\Latte;

/**
 * PHP code generator helpers.
 */
class PhpWriter
{
    use Strict;
    /** @var MacroTokens */
    private $tokens;
    /** @var string */
    private $modifiers;
    /** @var array{string, mixed}|null */
    private $context;
    /** @var Policy|null */
    private $policy;
    /** @var string[] */
    private $functions = [];
    /** @var string[] */
    private $filters = [];
    /** @var int|null */
    private $line;
    public static function using(MacroNode $node, ?Compiler $compiler = null) : self
    {
        $me = new static($node->tokenizer, null, $node->context);
        $me->modifiers =& $node->modifiers;
        $me->functions = $compiler ? $compiler->getFunctions() : [];
        $me->filters = $compiler ? $compiler->getFilters() : [];
        $me->policy = $compiler ? $compiler->getPolicy() : null;
        $me->line = $node->startLine;
        return $me;
    }
    /**
     * @param  array{string, mixed}|null  $context
     */
    public function __construct(MacroTokens $tokens, ?string $modifiers = null, ?array $context = null)
    {
        $this->tokens = $tokens;
        $this->modifiers = $modifiers;
        $this->context = $context;
    }
    /**
     * Expands %node.word, %node.array, %node.args, %node.line, %escape(), %modify(), %var, %raw, %word in code.
     * @param  mixed  ...$args
     */
    public function write(string $mask, ...$args) : string
    {
        $mask = \preg_replace('#%(node|\\d+)\\.#', '%$1_', $mask);
        $mask = \preg_replace_callback('#%escape(\\(([^()]*+|(?1))+\\))#', function ($m) {
            return $this->escapePass(new MacroTokens(\substr($m[1], 1, -1)))->joinAll();
        }, $mask);
        $mask = \preg_replace_callback('#%modify(Content)?(\\(([^()]*+|(?2))+\\))#', function ($m) {
            return $this->formatModifiers(\substr($m[2], 1, -1), (bool) $m[1]);
        }, $mask);
        $pos = $this->tokens->position;
        $word = null;
        if (\strpos($mask, '%node_word') !== \false) {
            $word = $this->tokens->fetchWord();
            if ($word === null) {
                throw new CompileException('Invalid content of tag');
            }
        }
        $code = \preg_replace_callback('#([,+]?\\s*)?%(node_|\\d+_|)(word|var|raw|array|args|line)(\\?)?(\\s*\\+\\s*)?()#', function ($m) use($word, &$args) {
            [, $l, $source, $format, $cond, $r] = $m;
            switch ($source) {
                case 'node_':
                    $arg = $word;
                    break;
                case '':
                    $arg = \current($args);
                    \next($args);
                    break;
                default:
                    $arg = $args[(int) $source];
                    break;
            }
            switch ($format) {
                case 'word':
                    $code = $this->formatWord($arg);
                    break;
                case 'args':
                    $code = $this->formatArgs();
                    break;
                case 'array':
                    $code = $this->formatArray();
                    $code = $cond && $code === '[]' ? '' : $code;
                    break;
                case 'var':
                    $code = PhpHelpers::dump($arg);
                    break;
                case 'raw':
                    $code = (string) $arg;
                    break;
                case 'line':
                    $l = \trim($l);
                    $code = $this->line ? " /* line {$this->line} */" : '';
                    break;
            }
            if ($cond && $code === '') {
                return $r ? $l : $r;
            } else {
                return $l . $code . $r;
            }
        }, $mask);
        $this->tokens->position = $pos;
        return $code;
    }
    /**
     * Formats modifiers calling.
     */
    public function formatModifiers(string $var, bool $isContent = \false) : string
    {
        static $uniq;
        $uniq = $uniq ?? '$' . \bin2hex(\random_bytes(5));
        $tokens = new MacroTokens(\ltrim($this->modifiers, '|'));
        $tokens = $this->preprocess($tokens);
        $tokens = $this->modifierPass($tokens, $uniq, $isContent);
        $tokens = $this->quotingPass($tokens);
        $this->validateKeywords($tokens);
        return \str_replace($uniq, $var, $tokens->joinAll());
    }
    /**
     * Formats macro arguments to PHP code. (It advances tokenizer to the end as a side effect.)
     */
    public function formatArgs(?MacroTokens $tokens = null) : string
    {
        $tokens = $this->preprocess($tokens);
        $tokens = $this->quotingPass($tokens);
        $this->validateKeywords($tokens);
        return $tokens->joinAll();
    }
    /**
     * Formats macro arguments to PHP array. (It advances tokenizer to the end as a side effect.)
     */
    public function formatArray(?MacroTokens $tokens = null) : string
    {
        $tokens = $this->preprocess($tokens);
        $tokens = $this->namedArgumentsPass($tokens);
        $tokens = $this->expandCastPass($tokens);
        $tokens = $this->quotingPass($tokens);
        $this->validateKeywords($tokens);
        return $tokens->joinAll();
    }
    /**
     * Formats parameter to PHP string.
     */
    public function formatWord(string $s) : string
    {
        if (\is_numeric($s) || \preg_match('#^[$([]|[\'"\\ ]|^(true|TRUE)$|^(false|FALSE)$|^(null|NULL)$|^[\\w\\\\]{3,}::[A-Z][A-Za-z0-9_]{2,}$#D', $s)) {
            $s = \preg_match('#\\s#', $s) ? "({$s})" : $s;
            return $this->formatArgs(new MacroTokens($s));
        }
        if ($s && !\preg_match('~^\\w+(?:-+\\w+)*$~', $s) && !\preg_match('~ ([./@_a-z0-9#!-] | :(?!:) | \\{\\$ [_a-z0-9\\[\\]()>-]+ })++ $ ~xAi', $s)) {
            if (\preg_match('~[\\w-]+\\$~A', $s)) {
                $hint = \preg_replace('~\\$\\w+(->\\w+)?~', '{$0}', $s);
                \trigger_error("Put variables in curly brackets, replace '{$s}' with '{$hint}' (or wrap whole expression in double quotes)", \E_USER_DEPRECATED);
            } else {
                \trigger_error("Expression '{$s}' should be wrapped in double quotes.", \E_USER_DEPRECATED);
            }
        }
        return '"' . $s . '"';
    }
    /**
     * Preprocessor for tokens. (It advances tokenizer to the end as a side effect.)
     */
    public function preprocess(?MacroTokens $tokens = null) : MacroTokens
    {
        $tokens = $tokens ?? $this->tokens;
        $this->validateTokens($tokens);
        $tokens = $this->removeCommentsPass($tokens);
        $tokens = $this->optionalChainingPass($tokens);
        $tokens = $this->shortTernaryPass($tokens);
        $tokens = $this->inOperatorPass($tokens);
        $tokens = $this->sandboxPass($tokens);
        $tokens = $this->replaceFunctionsPass($tokens);
        $tokens = $this->inlineModifierPass($tokens);
        $tokens = $this->modernArraySyntax($tokens);
        return $tokens;
    }
    /** @throws CompileException */
    public function validateTokens(MacroTokens $tokens) : void
    {
        $brackets = [];
        $pos = $tokens->position;
        while ($tokens->nextToken()) {
            $tokenValue = $tokens->currentValue();
            if ($tokens->isCurrent('?>') || $tokens->isCurrent('#')) {
                throw new CompileException("Forbidden {$tokenValue} inside tag");
            } elseif ($tokens->isCurrent('/') && $tokens->isNext('/')) {
                throw new CompileException('Forbidden // inside tag');
            } elseif ($tokens->isCurrent('(', '[', '{')) {
                static $counterpart = ['(' => ')', '[' => ']', '{' => '}'];
                $brackets[] = $counterpart[$tokenValue];
            } elseif ($tokens->isCurrent(')', ']', '}') && $tokenValue !== \array_pop($brackets)) {
                throw new CompileException('Unexpected ' . $tokenValue);
            } elseif ($tokens->isCurrent('`')) {
                throw new CompileException('Backtick operator is forbidden in Latte.');
            } elseif ($this->policy && $tokens->isCurrent($tokens::T_STRING) && $tokenValue[0] === '"' && (\strpos($tokenValue, '{$') !== \false || \strpos($tokenValue, '${') !== \false)) {
                throw new CompileException('Forbidden complex expressions in strings.');
            } elseif (Helpers::startsWith($tokenValue, '$ʟ_') || $this->policy && $tokens->isCurrent('$this')) {
                throw new CompileException("Forbidden variable {$tokenValue}.");
            } elseif ($tokenValue === '$iterations') {
                \trigger_error("Variable \$iterations is deprecated (on line {$tokens->currentToken()[2]})", \E_USER_DEPRECATED);
            }
        }
        if ($brackets) {
            throw new CompileException('Missing ' . \array_pop($brackets));
        }
        $tokens->position = $pos;
    }
    /** @throws CompileException */
    public function validateKeywords(MacroTokens $tokens) : void
    {
        $pos = $tokens->position;
        while ($tokens->nextToken()) {
            if (!$tokens->isPrev('::', '->', '?->', '??->') && ($tokens->isCurrent('__halt_compiler', 'declare', 'die', 'eval', 'exit', 'include', 'include_once', 'require', 'require_once') || $this->policy && $tokens->isCurrent(...['break', 'case', 'catch', 'continue', 'do', 'echo', 'else', 'elseif', 'endfor', 'endforeach', 'endswitch', 'endwhile', 'finally', 'for', 'foreach', 'if', 'new', 'print', 'switch', 'throw', 'try', 'while']) || ($this->policy || !$tokens->depth) && $tokens->isCurrent('return', 'yield') || !$tokens->isNext('(') && $tokens->isCurrent('function', 'use') || $tokens->isCurrent(...['abstract', 'class', 'const', 'enddeclare', 'extends', 'final', 'global', 'goto', 'implements', 'insteadof', 'interface', 'namespace', 'private', 'protected', 'public', 'static', 'trait', 'var']))) {
                throw new CompileException("Forbidden keyword '{$tokens->currentValue()}' inside tag.");
            }
        }
        $tokens->position = $pos;
    }
    /**
     * Removes PHP comments.
     */
    public function removeCommentsPass(MacroTokens $tokens) : MacroTokens
    {
        $res = new MacroTokens();
        while ($tokens->nextToken()) {
            $res->append($tokens->isCurrent($tokens::T_COMMENT) ? ' ' : $tokens->currentToken());
        }
        return $res;
    }
    /**
     * Replace global functions with custom ones.
     */
    public function replaceFunctionsPass(MacroTokens $tokens) : MacroTokens
    {
        $res = new MacroTokens();
        while ($tokens->nextToken()) {
            $name = $tokens->currentValue();
            if ($tokens->isCurrent($tokens::T_SYMBOL) && ($orig = $this->functions[\strtolower($name)] ?? null) && $tokens->isNext('(') && !$tokens->isPrev('::', '->', '?->', '??->', '\\')) {
                if ($name !== $orig) {
                    \trigger_error("Case mismatch on function name '{$name}', correct name is '{$orig}'.", \E_USER_WARNING);
                }
                $res->append('($this->global->fn->' . $orig . ')');
            } else {
                $res->append($tokens->currentToken());
            }
        }
        return $res;
    }
    /**
     * Simplified ternary expressions without third part.
     */
    public function shortTernaryPass(MacroTokens $tokens) : MacroTokens
    {
        $res = new MacroTokens();
        $inTernary = [];
        while ($tokens->nextToken()) {
            if ($tokens->isCurrent('?') && $tokens->isNext(...$tokens::SIGNIFICANT) && !$tokens->isNext(',', ')', ']', '|', '[')) {
                $inTernary[] = $tokens->depth;
            } elseif ($tokens->isCurrent(':')) {
                \array_pop($inTernary);
            } elseif ($tokens->isCurrent(',', ')', ']', '|') && \end($inTernary) === $tokens->depth + $tokens->isCurrent(')', ']')) {
                $res->append(' : null');
                \array_pop($inTernary);
            }
            $res->append($tokens->currentToken());
        }
        if ($inTernary) {
            $res->append(' : null');
        }
        return $res;
    }
    /**
     * Nullsafe operator $var?->prop?->elem[1]?->call()?->item
     * Null-coalescing-safe operator $var??->prop??->elem[1]??->call()??->item
     */
    public function optionalChainingPass(MacroTokens $tokens) : MacroTokens
    {
        $startDepth = $tokens->depth;
        $res = new MacroTokens();
        while ($tokens->depth >= $startDepth && $tokens->nextToken()) {
            if (!$tokens->isCurrent($tokens::T_VARIABLE) || $tokens->isPrev('::', '$')) {
                $res->append($tokens->currentToken());
                continue;
            }
            $addBraces = '';
            $expr = new MacroTokens([$tokens->currentToken()]);
            do {
                if ($tokens->nextToken('?->')) {
                    if (\PHP_VERSION_ID >= 80000) {
                        $expr->append($tokens->currentToken());
                        $expr->append($tokens->nextToken());
                        continue;
                    }
                    $expr->prepend('(($ʟ_tmp = ');
                    $expr->append(') === null ? null : ');
                    $res->tokens = \array_merge($res->tokens, $expr->tokens);
                    $addBraces .= ')';
                    $expr = new MacroTokens('$ʟ_tmp->');
                    if (!$tokens->nextToken($tokens::T_SYMBOL, $tokens::T_VARIABLE)) {
                        $expr->append($addBraces);
                        break;
                    }
                    $expr->append($tokens->currentToken());
                } elseif ($tokens->nextToken('??->')) {
                    $expr->prepend('(($ʟ_tmp = ');
                    $expr->append(' ?? null) === null ? null : ');
                    $res->tokens = \array_merge($res->tokens, $expr->tokens);
                    $addBraces .= ')';
                    $expr = new MacroTokens('$ʟ_tmp->');
                    if (!$tokens->nextToken($tokens::T_SYMBOL, $tokens::T_VARIABLE)) {
                        $expr->append($addBraces);
                        break;
                    }
                    $expr->append($tokens->currentToken());
                } elseif ($tokens->nextToken('->', '::')) {
                    $expr->append($tokens->currentToken());
                    if (!$tokens->nextToken($tokens::T_SYMBOL, $tokens::T_VARIABLE)) {
                        $expr->append($addBraces);
                        break;
                    }
                    $expr->append($tokens->currentToken());
                } elseif ($tokens->nextToken('[', '(')) {
                    $expr->tokens = \array_merge($expr->tokens, [$tokens->currentToken()], $this->optionalChainingPass($tokens)->tokens);
                } else {
                    $expr->append($addBraces);
                    break;
                }
            } while (\true);
            $res->tokens = \array_merge($res->tokens, $expr->tokens);
        }
        return $res;
    }
    /**
     * Pseudocast (expand).
     */
    public function expandCastPass(MacroTokens $tokens) : MacroTokens
    {
        $res = new MacroTokens('[');
        $expand = null;
        while ($tokens->nextToken()) {
            if ($tokens->isCurrent('(expand)') && $tokens->depth === 0) {
                $expand = \true;
                $res->append('],');
            } elseif ($expand && $tokens->isCurrent(',') && !$tokens->depth) {
                $expand = \false;
                $res->append(', [');
            } else {
                $res->append($tokens->currentToken());
            }
        }
        if ($expand === null) {
            $res->append(']');
        } else {
            $res->prepend('array_merge(')->append($expand ? ', [])' : '])');
        }
        return $res;
    }
    /**
     * Quotes symbols to strings.
     */
    public function quotingPass(MacroTokens $tokens) : MacroTokens
    {
        $res = new MacroTokens();
        while ($tokens->nextToken()) {
            $res->append($tokens->isCurrent($tokens::T_SYMBOL) && (!$tokens->isPrev(...$tokens::SIGNIFICANT) || $tokens->isPrev(',', '(', '[', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', '=', 'and', 'or', 'xor', '??')) && (!$tokens->isNext(...$tokens::SIGNIFICANT) || $tokens->isNext(',', ';', ')', ']', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', 'and', 'or', 'xor', '??')) && !((!$tokens->isPrev(...$tokens::SIGNIFICANT) || $tokens->isPrev('(', ',')) && $tokens->isNext(':')) && !\preg_match('#^[A-Z_][A-Z0-9_]{2,}$#', $tokens->currentValue()) && !($tokens->isCurrent('default') && $tokens->isNext('=>')) ? "'" . $tokens->currentValue() . "'" : $tokens->currentToken());
        }
        return $res;
    }
    /**
     * Converts named arguments name: value to 'name' => value
     */
    public function namedArgumentsPass(MacroTokens $tokens) : MacroTokens
    {
        $res = new MacroTokens();
        while ($tokens->nextToken()) {
            if ($tokens->depth === 0 && $tokens->isCurrent($tokens::T_SYMBOL) && (!$tokens->isPrev(...$tokens::SIGNIFICANT) || $tokens->isPrev(',')) && $tokens->isNext(':')) {
                $res->append("'" . $tokens->currentValue() . "' =>");
                $tokens->nextToken(':');
            } else {
                $res->append($tokens->currentToken());
            }
        }
        return $res;
    }
    /**
     * Converts [name: value] to ['name' => value]
     */
    public function modernArraySyntax(MacroTokens $tokens) : MacroTokens
    {
        $res = new MacroTokens();
        $brackets = [];
        while ($tokens->nextToken()) {
            if ($tokens->isCurrent('[', '(', '{')) {
                $brackets[] = $tokens->currentValue();
            } elseif ($tokens->isCurrent(']', ')', '}')) {
                \array_pop($brackets);
            }
            if (\end($brackets) === '[' && $tokens->isCurrent($tokens::T_SYMBOL) && $tokens->isPrev('[', ',') && $tokens->isNext(':')) {
                $res->append("'" . $tokens->currentValue() . "' =>");
                $tokens->nextToken(':');
            } else {
                $res->append($tokens->currentToken());
            }
        }
        return $res;
    }
    /**
     * Syntax $entry in [item1, item2].
     */
    public function inOperatorPass(MacroTokens $tokens) : MacroTokens
    {
        while ($tokens->nextToken()) {
            if ($tokens->isCurrent($tokens::T_VARIABLE)) {
                $start = $tokens->position;
                $depth = $tokens->depth;
                $expr = $arr = [];
                $expr[] = $tokens->currentToken();
                while ($tokens->isNext($tokens::T_VARIABLE, $tokens::T_SYMBOL, $tokens::T_NUMBER, $tokens::T_STRING, '[', ']', '(', ')', '->', '?->') && !$tokens->isNext('in')) {
                    $expr[] = $tokens->nextToken();
                }
                if ($depth === $tokens->depth && $tokens->nextValue('in') && ($arr[] = $tokens->nextToken('['))) {
                    while ($tokens->isNext(...$tokens::SIGNIFICANT)) {
                        $arr[] = $tokens->nextToken();
                        if ($tokens->isCurrent(']') && $tokens->depth === $depth) {
                            $new = \array_merge($tokens->parse('in_array('), $expr, $tokens->parse(', '), $arr, $tokens->parse(', true)'));
                            \array_splice($tokens->tokens, $start, $tokens->position - $start + 1, $new);
                            $tokens->position = $start + \count($new) - 1;
                            continue 2;
                        }
                    }
                }
                $tokens->position = $start;
            }
        }
        return $tokens->reset();
    }
    /**
     * Applies sandbox policy.
     */
    public function sandboxPass(MacroTokens $tokens) : MacroTokens
    {
        static $keywords = ['array' => 1, 'catch' => 1, 'clone' => 1, 'empty' => 1, 'for' => 1, 'foreach' => 1, 'function' => 1, 'if' => 1, 'elseif', 'isset' => 1, 'list' => 1, 'unset' => 1];
        if (!$this->policy) {
            return $tokens;
        }
        $startDepth = $tokens->depth;
        $res = new MacroTokens();
        while ($tokens->depth >= $startDepth && $tokens->nextToken()) {
            $static = \false;
            if ($tokens->isCurrent('[', '(')) {
                // starts with expression
                $expr = new MacroTokens(\array_merge([$tokens->currentToken()], $this->sandboxPass($tokens)->tokens));
            } elseif ($tokens->isCurrent($tokens::T_SYMBOL, '\\') && empty($keywords[$tokens->currentValue()])) {
                $expr = new MacroTokens(\array_merge([$tokens->currentToken()], $tokens->nextAll($tokens::T_SYMBOL, '\\')));
                $static = \true;
            } elseif ($tokens->isCurrent('$')) {
                // $$$var or ${...}
                throw new CompileException('Forbidden variable variables.');
            } elseif ($tokens->isCurrent($tokens::T_VARIABLE, $tokens::T_STRING)) {
                // $var or 'func'
                $expr = new MacroTokens([$tokens->currentToken()]);
            } else {
                // not a begin
                $res->append($tokens->currentToken());
                continue;
            }
            do {
                if ($tokens->nextToken('(')) {
                    // call
                    if ($static) {
                        // global function
                        $name = $expr->joinAll();
                        if (!$this->policy->isFunctionAllowed($name)) {
                            throw new SecurityViolationException("Function {$name}() is not allowed.");
                        }
                        $static = \false;
                        $expr->append('(');
                    } else {
                        // any calling
                        $expr->prepend('$this->call(');
                        $expr->append(')(');
                    }
                    $expr->tokens = \array_merge($expr->tokens, $this->sandboxPass($tokens)->tokens);
                } elseif ($tokens->nextToken('->', '?->', '::')) {
                    // property, method or constant
                    $op = $tokens->currentValue();
                    if ($op === '::' && $tokens->nextToken($tokens::T_SYMBOL)) {
                        // is constant?
                        if ($tokens->isNext('(')) {
                            // go back, it was not
                            $tokens->position--;
                        } else {
                            // it is
                            $expr->append('::');
                            $expr->append($tokens->currentValue());
                            continue;
                        }
                    }
                    if ($static) {
                        // class name
                        $expr->append('::class');
                        $static = \false;
                    }
                    $expr->append(', ');
                    if ($tokens->nextToken($tokens::T_SYMBOL)) {
                        // $obj->member or $obj::member
                        $member = [$tokens->currentToken()];
                        $expr->append(PhpHelpers::dump($tokens->currentValue()));
                    } elseif ($tokens->nextToken($tokens::T_VARIABLE)) {
                        // $obj->$var or $obj::$var
                        $member = [$tokens->currentToken()];
                        if ($op === '::' && !$tokens->isNext('(')) {
                            $expr->append(PhpHelpers::dump(\substr($tokens->currentValue(), 1)));
                        } else {
                            $expr->append($tokens->currentValue());
                        }
                    } elseif ($tokens->nextToken('{')) {
                        // $obj->{...}
                        $member = \array_merge([$tokens->currentToken()], $this->sandboxPass($tokens)->tokens);
                        $expr->append('(string) ');
                        $expr->tokens = \array_merge($expr->tokens, \array_slice($member, 1, -1));
                    } else {
                        // $obj->$$$var or $obj::$$$var
                        $member = $tokens->nextAll($tokens::T_VARIABLE, '$');
                        $expr->tokens = $op === '::' && !$tokens->isNext('(') ? \array_merge($expr->tokens, \array_slice($member, 1)) : \array_merge($expr->tokens, $member);
                    }
                    if ($tokens->nextToken('(')) {
                        $expr->prepend('$this->call([');
                        $expr->append('])(');
                        $expr->tokens = \array_merge($expr->tokens, $this->sandboxPass($tokens)->tokens);
                    } else {
                        $expr->prepend('$this->prop(');
                        $expr->append(')' . $op);
                        $expr->tokens = \array_merge($expr->tokens, $member);
                    }
                } elseif ($tokens->nextToken('[', '{')) {
                    // array access
                    $static = \false;
                    $expr->tokens = \array_merge($expr->tokens, [$tokens->currentToken()], $this->sandboxPass($tokens)->tokens);
                } else {
                    break;
                }
            } while (\true);
            $res->tokens = \array_merge($res->tokens, $expr->tokens);
        }
        return $res;
    }
    /**
     * Process inline filters ($var|filter)
     */
    public function inlineModifierPass(MacroTokens $tokens) : MacroTokens
    {
        $result = new MacroTokens();
        while ($tokens->nextToken()) {
            if ($tokens->isCurrent('(', '[')) {
                $result->tokens = \array_merge($result->tokens, $this->inlineModifierInner($tokens));
            } else {
                $result->append($tokens->currentToken());
            }
        }
        return $result;
    }
    /**
     * @return array<array{string, int, int}>
     */
    private function inlineModifierInner(MacroTokens $tokens) : array
    {
        $isFunctionOrArray = $tokens->isPrev($tokens::T_VARIABLE, $tokens::T_SYMBOL, ')') || $tokens->isCurrent('[');
        $result = new MacroTokens();
        $args = new MacroTokens();
        $modifiers = new MacroTokens();
        $current = $args;
        $anyModifier = \false;
        $result->append($tokens->currentToken());
        while ($tokens->nextToken()) {
            if ($tokens->isCurrent('(', '[')) {
                $current->tokens = \array_merge($current->tokens, $this->inlineModifierInner($tokens));
            } elseif ($current !== $modifiers && $tokens->isCurrent('|')) {
                $anyModifier = \true;
                $current = $modifiers;
            } elseif ($tokens->isCurrent(')', ']') || $isFunctionOrArray && $tokens->isCurrent(',')) {
                $partTokens = \count($modifiers->tokens) ? $this->modifierPass($modifiers, $args->tokens)->tokens : $args->tokens;
                $result->tokens = \array_merge($result->tokens, $partTokens);
                if ($tokens->isCurrent(',')) {
                    $result->append($tokens->currentToken());
                    $args = new MacroTokens();
                    $modifiers = new MacroTokens();
                    $current = $args;
                    continue;
                } elseif ($isFunctionOrArray || !$anyModifier) {
                    $result->append($tokens->currentToken());
                } else {
                    \array_shift($result->tokens);
                }
                return $result->tokens;
            } else {
                $current->append($tokens->currentToken());
            }
        }
        throw new CompileException('Unbalanced brackets.');
    }
    /**
     * Formats modifiers calling.
     * @param  string|array<array{string, int, int}>  $var
     * @throws CompileException
     */
    public function modifierPass(MacroTokens $tokens, $var, bool $isContent = \false) : MacroTokens
    {
        $inside = \false;
        $res = new MacroTokens($var);
        while ($tokens->nextToken()) {
            if ($tokens->isCurrent($tokens::T_WHITESPACE)) {
                $res->append(' ');
            } elseif ($inside) {
                if ($tokens->isCurrent(':', ',') && !$tokens->depth) {
                    $res->append(', ');
                    $tokens->nextAll($tokens::T_WHITESPACE);
                } elseif ($tokens->isCurrent('|') && !$tokens->depth) {
                    $res->append(')');
                    $inside = \false;
                } elseif (!$tokens->depth && $tokens->isCurrent($tokens::T_SYMBOL) && $tokens->isPrev(',', ':') && $tokens->isNext(':')) {
                    $hint = (clone $tokens)->reset()->joinAll();
                    \trigger_error("Colon as argument separator is deprecated, replace ':' with ',' in '{$hint}'", \E_USER_DEPRECATED);
                    $res->append($tokens->currentToken());
                } else {
                    if ($tokens->isNext(':') && !$tokens->depth) {
                        $hint = (clone $tokens)->reset()->joinAll();
                        \trigger_error("Colon as argument separator is deprecated, replace ':' with ',' in '{$hint}'", \E_USER_DEPRECATED);
                    }
                    $res->append($tokens->currentToken());
                }
            } elseif ($tokens->isCurrent($tokens::T_SYMBOL)) {
                if ($tokens->isCurrent('escape')) {
                    if ($isContent) {
                        $res->prepend('LR\\Filters::convertTo($ʟ_fi, ' . PhpHelpers::dump(\implode('', $this->context)) . ', ')->append(')');
                    } else {
                        $res = $this->escapePass($res);
                    }
                    $tokens->nextToken('|');
                } elseif (!\strcasecmp($tokens->currentValue(), 'checkUrl')) {
                    if ($tokens->currentValue() !== 'checkUrl') {
                        \trigger_error("Case mismatch on filter name |{$tokens->currentValue()}, correct name is |checkUrl.", \E_USER_WARNING);
                    }
                    $res->prepend('LR\\Filters::safeUrl(');
                    $inside = \true;
                } elseif (!\strcasecmp($tokens->currentValue(), 'noescape') || !\strcasecmp($tokens->currentValue(), 'nocheck')) {
                    throw new SecurityViolationException("Filter |{$tokens->currentValue()} is not expected here.");
                } else {
                    $name = $tokens->currentValue();
                    if ($this->policy && !$this->policy->isFilterAllowed($name)) {
                        throw new SecurityViolationException("Filter |{$name} is not allowed.");
                    }
                    $lower = \strtolower($name);
                    if (!isset($this->filters[$name])) {
                        $orig = \array_search($lower, $this->filters, \true);
                        if ($orig) {
                            \trigger_error("Case mismatch on filter name |{$name}, correct name is |{$orig}.", \E_USER_WARNING);
                        }
                    }
                    $res->prepend($isContent ? '$this->filters->filterContent(' . PhpHelpers::dump($name) . ', $ʟ_fi, ' : '($this->filters->' . $name . ')(');
                    $inside = \true;
                }
            } else {
                throw new CompileException("Filter name must be alphanumeric string, '{$tokens->currentValue()}' given.");
            }
        }
        if ($inside) {
            $res->append(')');
        }
        return $res;
    }
    /**
     * Escapes expression in tokens.
     */
    public function escapePass(MacroTokens $tokens) : MacroTokens
    {
        $tokens = clone $tokens;
        [$contentType, $context] = $this->context;
        switch ($contentType) {
            case Compiler::CONTENT_XHTML:
            case Compiler::CONTENT_HTML:
                switch ($context) {
                    case Compiler::CONTEXT_HTML_TEXT:
                        return $tokens->prepend('LR\\Filters::escapeHtmlText(')->append(')');
                    case Compiler::CONTEXT_HTML_TAG:
                    case Compiler::CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL:
                        return $tokens->prepend('LR\\Filters::escapeHtmlAttrUnquoted(')->append(')');
                    case Compiler::CONTEXT_HTML_ATTRIBUTE:
                    case Compiler::CONTEXT_HTML_ATTRIBUTE_URL:
                        return $tokens->prepend('LR\\Filters::escapeHtmlAttr(')->append(')');
                    case Compiler::CONTEXT_HTML_ATTRIBUTE_JS:
                        return $tokens->prepend('LR\\Filters::escapeHtmlAttr(LR\\Filters::escapeJs(')->append('))');
                    case Compiler::CONTEXT_HTML_ATTRIBUTE_CSS:
                        return $tokens->prepend('LR\\Filters::escapeHtmlAttr(LR\\Filters::escapeCss(')->append('))');
                    case Compiler::CONTEXT_HTML_COMMENT:
                        return $tokens->prepend('LR\\Filters::escapeHtmlComment(')->append(')');
                    case Compiler::CONTEXT_HTML_BOGUS_COMMENT:
                        return $tokens->prepend('LR\\Filters::escapeHtml(')->append(')');
                    case Compiler::CONTEXT_HTML_JS:
                    case Compiler::CONTEXT_HTML_CSS:
                        return $tokens->prepend('LR\\Filters::escape' . \ucfirst($context) . '(')->append(')');
                    default:
                        throw new CompileException("Unknown context {$contentType}, {$context}.");
                }
            // break omitted
            case Compiler::CONTENT_XML:
                switch ($context) {
                    case Compiler::CONTEXT_XML_TEXT:
                    case Compiler::CONTEXT_XML_ATTRIBUTE:
                    case Compiler::CONTEXT_XML_BOGUS_COMMENT:
                        return $tokens->prepend('LR\\Filters::escapeXml(')->append(')');
                    case Compiler::CONTEXT_XML_COMMENT:
                        return $tokens->prepend('LR\\Filters::escapeHtmlComment(')->append(')');
                    case Compiler::CONTEXT_XML_TAG:
                        return $tokens->prepend('LR\\Filters::escapeXmlAttrUnquoted(')->append(')');
                    default:
                        throw new CompileException("Unknown context {$contentType}, {$context}.");
                }
            // break omitted
            case Compiler::CONTENT_JS:
            case Compiler::CONTENT_CSS:
            case Compiler::CONTENT_ICAL:
                return $tokens->prepend('LR\\Filters::escape' . \ucfirst($contentType) . '(')->append(')');
            case Compiler::CONTENT_TEXT:
                return $tokens;
            case null:
                return $tokens->prepend('($this->filters->escape)(')->append(')');
            default:
                throw new CompileException("Unknown context {$contentType}.");
        }
    }
}
