<?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;

/**
 * Latte compiler.
 */
class Compiler
{
    use Strict;
    /** Context-aware escaping content types */
    public const CONTENT_HTML = Engine::CONTENT_HTML, CONTENT_XHTML = Engine::CONTENT_XHTML, CONTENT_XML = Engine::CONTENT_XML, CONTENT_JS = Engine::CONTENT_JS, CONTENT_CSS = Engine::CONTENT_CSS, CONTENT_ICAL = Engine::CONTENT_ICAL, CONTENT_TEXT = Engine::CONTENT_TEXT;
    /** @internal Context-aware escaping HTML contexts */
    public const CONTEXT_HTML_TEXT = null, CONTEXT_HTML_TAG = 'Tag', CONTEXT_HTML_ATTRIBUTE = 'Attr', CONTEXT_HTML_ATTRIBUTE_JS = 'AttrJs', CONTEXT_HTML_ATTRIBUTE_CSS = 'AttrCss', CONTEXT_HTML_ATTRIBUTE_URL = 'AttrUrl', CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL = 'AttrUnquotedUrl', CONTEXT_HTML_COMMENT = 'Comment', CONTEXT_HTML_BOGUS_COMMENT = 'Bogus', CONTEXT_HTML_CSS = 'Css', CONTEXT_HTML_JS = 'Js', CONTEXT_XML_TEXT = self::CONTEXT_HTML_TEXT, CONTEXT_XML_TAG = self::CONTEXT_HTML_TAG, CONTEXT_XML_ATTRIBUTE = self::CONTEXT_HTML_ATTRIBUTE, CONTEXT_XML_COMMENT = self::CONTEXT_HTML_COMMENT, CONTEXT_XML_BOGUS_COMMENT = self::CONTEXT_HTML_BOGUS_COMMENT;
    /** @var string[] @internal */
    public $placeholders = [];
    /** @var string|null */
    public $paramsExtraction;
    /** @var Token[] */
    private $tokens;
    /** @var string pointer to current node content */
    private $output;
    /** @var int  position on source template */
    private $position;
    /** @var array<string, Macro[]> */
    private $macros = [];
    /** @var string[] of orig name */
    private $functions = [];
    /** @var string[] of orig name */
    private $filters = [];
    /** @var int[] Macro flags */
    private $flags;
    /** @var HtmlNode|null */
    private $htmlNode;
    /** @var MacroNode|null */
    private $macroNode;
    /** @var string */
    private $contentType = self::CONTENT_HTML;
    /** @var string|null */
    private $context;
    /** @var mixed */
    private $lastAttrValue;
    /** @var int */
    private $tagOffset;
    /** @var bool */
    private $inHead;
    /** @var array<string, ?array{body: string, arguments: string, returns: string, comment: ?string}> */
    private $methods = [];
    /** @var array<string, mixed> */
    private $properties = [];
    /** @var array<string, mixed> */
    private $constants = [];
    /** @var Policy|null */
    private $policy;
    /**
     * Adds new macro with Macro flags.
     * @return static
     */
    public function addMacro(string $name, Macro $macro, ?int $flags = null)
    {
        if (!\preg_match('#^[a-z_=]\\w*(?:[.:-]\\w+)*$#iD', $name)) {
            throw new \LogicException("Invalid tag name '{$name}'.");
        } elseif (!isset($this->flags[$name])) {
            $this->flags[$name] = $flags ?: Macro::DEFAULT_FLAGS;
        } elseif ($flags && $this->flags[$name] !== $flags) {
            throw new \LogicException("Incompatible flags for tag '{$name}'.");
        }
        $this->macros[$name][] = $macro;
        return $this;
    }
    /**
     * Registers run-time functions.
     * @param  string[]  $names
     * @return static
     */
    public function setFunctions(array $names)
    {
        $this->functions = \array_combine(\array_map('strtolower', $names), $names);
        return $this;
    }
    public function setFilters(array $names)
    {
        $this->filters = $names;
        return $this;
    }
    /**
     * Compiles tokens to PHP file
     * @param  Token[]  $tokens
     */
    public function compile(array $tokens, string $className, ?string $comment = null, bool $strictMode = \false) : string
    {
        $code = "<?php\n\n" . ($strictMode ? "declare(strict_types=1);\n\n" : '') . "use \\Packetery\\Latte\\Runtime as LR;\n\n" . ($comment === null ? '' : '/** ' . \str_replace('*/', '* /', $comment) . " */\n") . "final class {$className} extends \\Packetery\\Latte\\Runtime\\Template\n{\n" . $this->buildClassBody($tokens) . "\n\n}\n";
        $code = PhpHelpers::inlineHtmlToEcho($code);
        $code = PhpHelpers::reformatCode($code);
        return $code;
    }
    /**
     * @param  Token[]  $tokens
     */
    private function buildClassBody(array $tokens) : string
    {
        $this->tokens = $tokens;
        $output = '';
        $this->output =& $output;
        $this->inHead = \true;
        $this->htmlNode = $this->macroNode = $this->context = $this->paramsExtraction = null;
        $this->placeholders = $this->properties = $this->constants = [];
        $this->methods = ['main' => null, 'prepare' => null];
        $macroHandlers = new \SplObjectStorage();
        if ($this->macros) {
            \array_map([$macroHandlers, 'attach'], \array_merge(...\array_values($this->macros)));
        }
        foreach ($macroHandlers as $handler) {
            $handler->initialize($this);
        }
        foreach ($tokens as $this->position => $token) {
            if ($this->inHead && !($token->type === $token::COMMENT || $token->type === $token::MACRO_TAG && ($this->flags[$token->name] ?? null) & Macro::ALLOWED_IN_HEAD || $token->type === $token::TEXT && \trim($token->text) === '')) {
                $this->inHead = \false;
            }
            $this->{"process{$token->type}"}($token);
        }
        while ($this->htmlNode) {
            $this->closeHtmlTag('end');
        }
        while ($this->macroNode) {
            if ($this->macroNode->parentNode) {
                throw new CompileException('Missing {/' . $this->macroNode->name . '}');
            }
            if (~$this->flags[$this->macroNode->name] & Macro::AUTO_CLOSE) {
                throw new CompileException('Missing ' . self::printEndTag($this->macroNode));
            }
            $this->closeMacro($this->macroNode->name);
        }
        $prepare = $epilogs = '';
        foreach ($macroHandlers as $handler) {
            $res = $handler->finalize();
            $prepare .= empty($res[0]) ? '' : "<?php {$res[0]} ?>";
            $epilogs = (empty($res[1]) ? '' : "<?php {$res[1]} ?>") . $epilogs;
        }
        $extractParams = $this->paramsExtraction ?? 'extract($this->params);';
        $this->addMethod('main', $this->expandTokens($extractParams . "?>\n{$output}{$epilogs}<?php return get_defined_vars();"), '', 'array');
        if ($prepare) {
            $this->addMethod('prepare', $extractParams . "?>{$prepare}<?php", '', 'void');
        }
        if ($this->contentType !== self::CONTENT_HTML) {
            $this->addConstant('CONTENT_TYPE', $this->contentType);
        }
        $members = [];
        foreach ($this->constants as $name => $value) {
            $members[] = "\tprotected const {$name} = " . PhpHelpers::dump($value, \true) . ';';
        }
        foreach ($this->properties as $name => $value) {
            $members[] = "\tpublic \${$name} = " . PhpHelpers::dump($value, \true) . ';';
        }
        foreach (\array_filter($this->methods) as $name => $method) {
            $members[] = ($method['comment'] === null ? '' : "\n\t/** " . \str_replace('*/', '* /', $method['comment']) . ' */') . "\n\tpublic function {$name}({$method['arguments']})" . ($method['returns'] ? ': ' . $method['returns'] : '') . "\n\t{\n" . ($method['body'] ? "\t\t{$method['body']}\n" : '') . "\t}";
        }
        return \implode("\n\n", $members);
    }
    /** @return static */
    public function setPolicy(?Policy $policy)
    {
        $this->policy = $policy;
        return $this;
    }
    public function getPolicy() : ?Policy
    {
        return $this->policy;
    }
    /** @return static */
    public function setContentType(string $type)
    {
        $this->contentType = $type;
        $this->context = null;
        return $this;
    }
    public function getContentType() : string
    {
        return $this->contentType;
    }
    public function getMacroNode() : ?MacroNode
    {
        return $this->macroNode;
    }
    /**
     * @return Macro[][]
     */
    public function getMacros() : array
    {
        return $this->macros;
    }
    /**
     * @return string[]
     */
    public function getFunctions() : array
    {
        return $this->functions;
    }
    /**
     * @return string[]
     */
    public function getFilters() : array
    {
        return $this->filters;
    }
    /**
     * Returns current line number.
     */
    public function getLine() : ?int
    {
        return isset($this->tokens[$this->position]) ? $this->tokens[$this->position]->line : null;
    }
    public function isInHead() : bool
    {
        return $this->inHead;
    }
    /**
     * Adds custom method to template.
     * @internal
     */
    public function addMethod(string $name, string $body, string $arguments = '', string $returns = '', ?string $comment = null) : void
    {
        $body = \trim($body);
        $this->methods[$name] = \compact('body', 'arguments', 'returns', 'comment');
    }
    /**
     * Returns custom methods.
     * @return array<string, ?array{body: string, arguments: string, returns: string, comment: ?string}>
     * @internal
     */
    public function getMethods() : array
    {
        return $this->methods;
    }
    /**
     * Adds custom property to template.
     * @param  mixed  $value
     * @internal
     */
    public function addProperty(string $name, $value) : void
    {
        $this->properties[$name] = $value;
    }
    /**
     * Returns custom properites.
     * @return array<string, mixed>
     * @internal
     */
    public function getProperties() : array
    {
        return $this->properties;
    }
    /**
     * Adds custom constant to template.
     * @param  mixed  $value
     * @internal
     */
    public function addConstant(string $name, $value) : void
    {
        $this->constants[$name] = $value;
    }
    /** @internal */
    public function expandTokens(string $s) : string
    {
        return \strtr($s, $this->placeholders);
    }
    private function processText(Token $token) : void
    {
        if ($this->lastAttrValue === '' && $this->context && Helpers::startsWith($this->context, self::CONTEXT_HTML_ATTRIBUTE)) {
            $this->lastAttrValue = $token->text;
        }
        $this->output .= $this->escape($token->text);
    }
    private function processMacroTag(Token $token) : void
    {
        if ($this->context === self::CONTEXT_HTML_TAG || $this->context && Helpers::startsWith($this->context, self::CONTEXT_HTML_ATTRIBUTE)) {
            $this->lastAttrValue = \true;
        }
        $isRightmost = !isset($this->tokens[$this->position + 1]) || \substr($this->tokens[$this->position + 1]->text, 0, 1) === "\n";
        if ($token->closing) {
            $this->closeMacro($token->name, $token->value, $token->modifiers, $isRightmost);
        } else {
            if (!$token->empty && ($this->flags[$token->name] ?? null) & Macro::AUTO_EMPTY) {
                $pos = $this->position;
                while (($t = $this->tokens[++$pos] ?? null) && ($t->type !== Token::MACRO_TAG || $t->name !== $token->name) && ($t->type !== Token::HTML_ATTRIBUTE_BEGIN || $t->name !== Parser::N_PREFIX . $token->name)) {
                }
                $token->empty = $t ? !$t->closing : \true;
                if ($token->empty) {
                    $tmp = \substr($token->text, 0, -1) . ' /}';
                    \trigger_error("Auto-empty behaviour is deprecated, replace {$token->text} with {$tmp} (on line {$this->getLine()})", \E_USER_DEPRECATED);
                }
            }
            $node = $this->openMacro($token->name, $token->value, $token->modifiers, $isRightmost);
            if ($token->empty) {
                if ($node->empty) {
                    throw new CompileException("Unexpected /} in tag {$token->text}");
                }
                $this->closeMacro($token->name, '', '', $isRightmost);
            }
        }
    }
    private function processHtmlTagBegin(Token $token) : void
    {
        if ($token->closing) {
            while ($this->htmlNode) {
                if (\strcasecmp($this->htmlNode->name, $token->name) === 0) {
                    break;
                }
                $this->closeHtmlTag("</{$token->name}>");
            }
            if (!$this->htmlNode) {
                $this->htmlNode = new HtmlNode($token->name);
            }
            $this->htmlNode->closing = \true;
            $this->htmlNode->endLine = $this->getLine();
            $this->context = self::CONTEXT_HTML_TEXT;
        } elseif ($token->text === '<!--') {
            $this->context = self::CONTEXT_HTML_COMMENT;
        } elseif ($token->text === '<?' || $token->text === '<!') {
            $this->context = self::CONTEXT_HTML_BOGUS_COMMENT;
        } else {
            $this->htmlNode = new HtmlNode($token->name, $this->htmlNode);
            $this->htmlNode->startLine = $this->getLine();
            $this->context = self::CONTEXT_HTML_TAG;
        }
        $this->tagOffset = \strlen($this->output);
        $this->output .= $this->escape($token->text);
    }
    private function processHtmlTagEnd(Token $token) : void
    {
        if (\in_array($this->context, [self::CONTEXT_HTML_COMMENT, self::CONTEXT_HTML_BOGUS_COMMENT], \true)) {
            $this->output .= $token->text;
            $this->context = self::CONTEXT_HTML_TEXT;
            return;
        }
        $htmlNode = $this->htmlNode;
        $end = '';
        if (!$htmlNode->closing) {
            $htmlNode->empty = \strpos($token->text, '/') !== \false;
            if (\in_array($this->contentType, [self::CONTENT_HTML, self::CONTENT_XHTML], \true)) {
                $emptyElement = isset(Helpers::$emptyElements[\strtolower($htmlNode->name)]);
                $htmlNode->empty = $htmlNode->empty || $emptyElement;
                if ($htmlNode->empty) {
                    // auto-correct
                    $space = \substr(\strstr($token->text, '>'), 1);
                    if ($emptyElement) {
                        $token->text = ($this->contentType === self::CONTENT_XHTML ? ' />' : '>') . $space;
                    } else {
                        $token->text = '>';
                        $end = "</{$htmlNode->name}>" . $space;
                    }
                }
            }
        }
        if ($htmlNode->macroAttrs) {
            $html = \substr($this->output, $this->tagOffset) . $token->text;
            $this->output = \substr($this->output, 0, $this->tagOffset);
            $this->writeAttrsMacro($html, $emptyElement ?? null);
        } else {
            $this->output .= $token->text . $end;
        }
        if ($htmlNode->empty) {
            $htmlNode->closing = \true;
            if ($htmlNode->macroAttrs) {
                $this->writeAttrsMacro($end);
            }
        }
        $this->context = self::CONTEXT_HTML_TEXT;
        if ($htmlNode->closing) {
            $this->htmlNode = $this->htmlNode->parentNode;
        } elseif ((($lower = \strtolower($htmlNode->name)) === 'script' || $lower === 'style') && (!isset($htmlNode->attrs['type']) || \preg_match('#(java|j|ecma|live)script|module|json|css|plain#i', $htmlNode->attrs['type']))) {
            $this->context = $lower === 'script' ? self::CONTEXT_HTML_JS : self::CONTEXT_HTML_CSS;
        }
    }
    private function processHtmlAttributeBegin(Token $token) : void
    {
        if (Helpers::startsWith($token->name, Parser::N_PREFIX)) {
            $name = \substr($token->name, \strlen(Parser::N_PREFIX));
            if (isset($this->htmlNode->macroAttrs[$name])) {
                throw new CompileException("Found multiple attributes {$token->name}.");
            } elseif ($this->macroNode && $this->macroNode->htmlNode === $this->htmlNode) {
                throw new CompileException("n:attribute must not appear inside tags; found {$token->name} inside {{$this->macroNode->name}}.");
            }
            $this->htmlNode->macroAttrs[$name] = $token->value;
            return;
        }
        $this->lastAttrValue =& $this->htmlNode->attrs[$token->name];
        $this->output .= $this->escape($token->text);
        $lower = \strtolower($token->name);
        if (\in_array($token->value, ['"', "'"], \true)) {
            $this->lastAttrValue = '';
            $this->context = self::CONTEXT_HTML_ATTRIBUTE;
            if (\in_array($this->contentType, [self::CONTENT_HTML, self::CONTENT_XHTML], \true)) {
                if (Helpers::startsWith($lower, 'on')) {
                    $this->context = self::CONTEXT_HTML_ATTRIBUTE_JS;
                } elseif ($lower === 'style') {
                    $this->context = self::CONTEXT_HTML_ATTRIBUTE_CSS;
                }
            }
        } else {
            $this->lastAttrValue = $token->value;
            $this->context = self::CONTEXT_HTML_TAG;
        }
        if (\in_array($this->contentType, [self::CONTENT_HTML, self::CONTENT_XHTML], \true) && (\in_array($lower, ['href', 'src', 'action', 'formaction'], \true) || $lower === 'data' && \strtolower($this->htmlNode->name) === 'object')) {
            $this->context = $this->context === self::CONTEXT_HTML_TAG ? self::CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL : self::CONTEXT_HTML_ATTRIBUTE_URL;
        }
    }
    private function processHtmlAttributeEnd(Token $token) : void
    {
        $this->context = self::CONTEXT_HTML_TAG;
        $this->output .= $token->text;
    }
    private function processComment(Token $token) : void
    {
        $leftOfs = ($tmp = \strrpos($this->output, "\n")) === \false ? 0 : $tmp + 1;
        $isLeftmost = \trim(\substr($this->output, $leftOfs)) === '';
        $isRightmost = \substr($token->text, -1) === "\n";
        if ($isLeftmost && $isRightmost) {
            $this->output = \substr($this->output, 0, $leftOfs);
        } else {
            $this->output .= \substr($token->text, \strlen(\rtrim($token->text, "\n")));
        }
    }
    private function escape(string $s) : string
    {
        return \substr(\str_replace('<?', '<<?php ?>?', $s . '?'), 0, -1);
    }
    /********************* macros ****************d*g**/
    /**
     * Generates code for {macro ...} to the output.
     * @internal
     */
    public function openMacro(string $name, string $args = '', string $modifiers = '', bool $isRightmost = \false, ?string $nPrefix = null) : MacroNode
    {
        $node = $this->expandMacro($name, $args, $modifiers, $nPrefix);
        if ($node->empty) {
            $this->writeCode((string) $node->openingCode, $node->replaced, $isRightmost);
            if ($node->prefix && $node->prefix !== MacroNode::PREFIX_TAG) {
                $this->htmlNode->attrCode .= $node->attrCode;
            }
        } else {
            $this->macroNode = $node;
            $node->saved = [&$this->output, $isRightmost];
            $this->output =& $node->content;
            $this->output = '';
        }
        return $node;
    }
    /**
     * Generates code for {/macro ...} to the output.
     * @internal
     */
    public function closeMacro(string $name, string $args = '', string $modifiers = '', bool $isRightmost = \false, ?string $nPrefix = null) : MacroNode
    {
        $node = $this->macroNode;
        if (!$node || $node->name !== $name && $name !== '' || $modifiers || $args !== '' && $node->args !== '' && !Helpers::startsWith($node->args . ' ', $args . ' ') || $nPrefix !== $node->prefix) {
            $name = $nPrefix ? "</{$this->htmlNode->name}> for " . Parser::N_PREFIX . \implode(' and ' . Parser::N_PREFIX, \array_keys($this->htmlNode->macroAttrs)) : '{/' . $name . ($args ? ' ' . $args : '') . $modifiers . '}';
            throw new CompileException("Unexpected {$name}" . ($node ? ', expecting ' . self::printEndTag($node->prefix ? $this->htmlNode : $node) : ''));
        }
        $this->macroNode = $node->parentNode;
        if ($node->args === '') {
            $node->setArgs($args);
        }
        if ($node->prefix === MacroNode::PREFIX_NONE) {
            $parts = \explode($node->htmlNode->innerMarker, $node->content);
            if (\count($parts) === 3) {
                // markers may be destroyed by inner macro
                $node->innerContent = $parts[1];
            }
        }
        $node->closing = \true;
        $node->endLine = $node->prefix ? $node->htmlNode->endLine : $this->getLine();
        $node->macro->nodeClosed($node);
        if (isset($parts[1]) && $node->innerContent !== $parts[1]) {
            $node->content = \implode($node->htmlNode->innerMarker, [$parts[0], $node->innerContent, $parts[2]]);
        }
        if ($node->prefix && $node->prefix !== MacroNode::PREFIX_TAG) {
            $this->htmlNode->attrCode .= $node->attrCode;
        }
        $this->output =& $node->saved[0];
        $this->writeCode((string) $node->openingCode, $node->replaced, $node->saved[1]);
        $this->output .= $node->content;
        $this->writeCode((string) $node->closingCode, $node->replaced, $isRightmost, \true);
        return $node;
    }
    private function writeCode(string $code, ?bool $isReplaced, ?bool $isRightmost, bool $isClosing = \false) : void
    {
        if ($isRightmost) {
            $leftOfs = ($tmp = \strrpos($this->output, "\n")) === \false ? 0 : $tmp + 1;
            $isLeftmost = \trim(\substr($this->output, $leftOfs)) === '';
            if ($isReplaced === null) {
                $isReplaced = \preg_match('#<\\?php.*\\secho\\s#As', $code);
            }
            if ($isLeftmost && !$isReplaced) {
                $this->output = \substr($this->output, 0, $leftOfs);
                // alone macro without output -> remove indentation
                if (!$isClosing && \substr($code, -2) !== '?>') {
                    $code .= '<?php ?>';
                    // consume new line
                }
            } elseif (\substr($code, -2) === '?>') {
                $code .= "\n";
                // double newline to avoid newline eating by PHP
            }
        }
        $this->output .= $code;
    }
    /**
     * Generates code for macro <tag n:attr> to the output.
     * @internal
     */
    public function writeAttrsMacro(string $html, ?bool $empty = null) : void
    {
        //     none-2 none-1 tag-1 tag-2       <el attr-1 attr-2>   /tag-2 /tag-1 [none-2] [none-1] inner-2 inner-1
        // /inner-1 /inner-2 [none-1] [none-2] tag-1 tag-2  </el>   /tag-2 /tag-1 /none-1 /none-2
        $attrs = $this->htmlNode->macroAttrs;
        $left = $right = [];
        foreach ($this->macros as $name => $foo) {
            $attrName = MacroNode::PREFIX_INNER . "-{$name}";
            if (!isset($attrs[$attrName])) {
                continue;
            }
            if ($empty) {
                \trigger_error("Unexpected n:{$attrName} on void element <{$this->htmlNode->name}> (on line {$this->getLine()}", \E_USER_WARNING);
            }
            if ($this->htmlNode->closing) {
                $left[] = function () use($name) {
                    $this->closeMacro($name, '', '', \false, MacroNode::PREFIX_INNER);
                };
            } else {
                \array_unshift($right, function () use($name, $attrs, $attrName) {
                    if ($this->openMacro($name, $attrs[$attrName], '', \false, MacroNode::PREFIX_INNER)->empty) {
                        throw new CompileException("Unexpected prefix in n:{$attrName}.");
                    }
                });
            }
            unset($attrs[$attrName]);
        }
        $innerMarker = '';
        if ($this->htmlNode->closing) {
            $left[] = function () {
                $this->output .= $this->htmlNode->innerMarker;
            };
        } else {
            \array_unshift($right, function () use(&$innerMarker) {
                $this->output .= $innerMarker;
            });
        }
        foreach (\array_reverse($this->macros) as $name => $foo) {
            $attrName = MacroNode::PREFIX_TAG . "-{$name}";
            if (!isset($attrs[$attrName])) {
                continue;
            }
            if ($empty) {
                \trigger_error("Unexpected n:{$attrName} on void element <{$this->htmlNode->name}> (on line {$this->getLine()}", \E_USER_WARNING);
            }
            $left[] = function () use($name, $attrs, $attrName) {
                if ($this->openMacro($name, $attrs[$attrName], '', \false, MacroNode::PREFIX_TAG)->empty) {
                    throw new CompileException("Unexpected prefix in n:{$attrName}.");
                }
            };
            \array_unshift($right, function () use($name) {
                $this->closeMacro($name, '', '', \false, MacroNode::PREFIX_TAG);
            });
            unset($attrs[$attrName]);
        }
        foreach ($this->macros as $name => $foo) {
            if (isset($attrs[$name])) {
                if ($this->htmlNode->closing) {
                    $right[] = function () use($name) {
                        $this->closeMacro($name, '', '', \false, MacroNode::PREFIX_NONE);
                    };
                } else {
                    \array_unshift($left, function () use($name, $attrs, &$innerMarker) {
                        $node = $this->openMacro($name, $attrs[$name], '', \false, MacroNode::PREFIX_NONE);
                        if ($node->empty) {
                            unset($this->htmlNode->macroAttrs[$name]);
                            // don't call closeMacro
                        } elseif (!$innerMarker) {
                            $this->htmlNode->innerMarker = $innerMarker = '<n:q' . \count($this->placeholders) . 'q>';
                            $this->placeholders[$innerMarker] = '';
                        }
                    });
                }
                unset($attrs[$name]);
            }
        }
        if ($attrs) {
            throw new CompileException('Unknown attribute ' . Parser::N_PREFIX . \implode(' and ' . Parser::N_PREFIX, \array_keys($attrs)) . (($t = Helpers::getSuggestion(\array_keys($this->macros), \key($attrs))) ? ', did you mean ' . Parser::N_PREFIX . $t . '?' : ''));
        }
        if (!$this->htmlNode->closing) {
            $this->htmlNode->attrCode =& $this->placeholders[$uniq = ' n:q' . \count($this->placeholders) . 'q'];
            $html = \substr_replace($html, $uniq, \strrpos($html, '/>') ?: \strrpos($html, '>'), 0);
        }
        foreach ($left as $func) {
            $func();
        }
        $this->output .= $html;
        foreach ($right as $func) {
            $func();
        }
        if ($right && \substr($this->output, -2) === '?>') {
            $this->output .= "\n";
        }
    }
    /**
     * Expands macro and returns node & code.
     * @internal
     */
    public function expandMacro(string $name, string $args, string $modifiers = '', ?string $nPrefix = null) : MacroNode
    {
        if (empty($this->macros[$name])) {
            $hint = (($t = Helpers::getSuggestion(\array_keys($this->macros), $name)) ? ", did you mean {{$t}}?" : '') . (\in_array($this->context, [self::CONTEXT_HTML_JS, self::CONTEXT_HTML_CSS], \true) ? ' (in JavaScript or CSS, try to put a space after bracket or use n:syntax=off)' : '');
            throw new CompileException("Unknown tag {{$name}}{$hint}");
        } elseif ($this->policy && !$this->policy->isMacroAllowed($name)) {
            throw new SecurityViolationException('Tag ' . ($nPrefix ? "n:{$name}" : "{{$name}}") . ' is not allowed.');
        }
        $modifiers = (string) $modifiers;
        if (\strpbrk($name, '=~%^&_')) {
            if (\in_array($this->context, [self::CONTEXT_HTML_ATTRIBUTE_URL, self::CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL], \true)) {
                if (!Helpers::removeFilter($modifiers, 'nocheck')) {
                    if (!\preg_match('#\\|datastream(?=\\s|\\||$)#Di', $modifiers)) {
                        $modifiers .= '|checkUrl';
                    }
                } elseif ($this->policy && !$this->policy->isFilterAllowed('nocheck')) {
                    throw new SecurityViolationException('Filter |nocheck is not allowed.');
                }
            }
            if (!Helpers::removeFilter($modifiers, 'noescape')) {
                $modifiers .= '|escape';
            } elseif ($this->policy && !$this->policy->isFilterAllowed('noescape')) {
                throw new SecurityViolationException('Filter |noescape is not allowed.');
            }
            if ($this->context === self::CONTEXT_HTML_JS && $name === '=' && \preg_match('#["\']$#D', $this->tokens[$this->position - 1]->text)) {
                throw new CompileException("Do not place {$this->tokens[$this->position]->text} inside quotes in JavaScript.");
            }
        }
        if ($nPrefix === MacroNode::PREFIX_INNER && !\strcasecmp($this->htmlNode->name, 'script')) {
            $context = [$this->contentType, self::CONTEXT_HTML_JS];
        } elseif ($nPrefix === MacroNode::PREFIX_INNER && !\strcasecmp($this->htmlNode->name, 'style')) {
            $context = [$this->contentType, self::CONTEXT_HTML_CSS];
        } elseif ($nPrefix) {
            $context = [$this->contentType, self::CONTEXT_HTML_TEXT];
        } else {
            $context = [$this->contentType, $this->context];
        }
        foreach (\array_reverse($this->macros[$name]) as $macro) {
            $node = new MacroNode($macro, $name, $args, $modifiers, $this->macroNode, $this->htmlNode, $nPrefix);
            $node->context = $context;
            $node->startLine = $nPrefix ? $this->htmlNode->startLine : $this->getLine();
            if ($macro->nodeOpened($node) !== \false) {
                return $node;
            }
        }
        throw new CompileException('Unknown ' . ($nPrefix ? 'attribute ' . Parser::N_PREFIX . ($nPrefix === MacroNode::PREFIX_NONE ? '' : "{$nPrefix}-") . $name : 'tag {' . $name . ($args ? " {$args}" : '') . '}'));
    }
    /**
     * @param  HtmlNode|MacroNode  $node
     */
    private static function printEndTag($node) : string
    {
        return $node instanceof HtmlNode ? "</{$node->name}> for " . Parser::N_PREFIX . \implode(' and ' . Parser::N_PREFIX, \array_keys($node->macroAttrs)) : "{/{$node->name}}";
    }
    private function closeHtmlTag($token) : void
    {
        if ($this->htmlNode->macroAttrs) {
            throw new CompileException("Unexpected {$token}, expecting " . self::printEndTag($this->htmlNode));
        } elseif (\in_array($this->contentType, [self::CONTENT_HTML, self::CONTENT_XHTML], \true) && \in_array(\strtolower($this->htmlNode->name), ['script', 'style'], \true)) {
            \trigger_error("Unexpected {$token}, expecting </{$this->htmlNode->name}> (on line {$this->getLine()})", \E_USER_DEPRECATED);
        }
        $this->htmlNode = $this->htmlNode->parentNode;
    }
}
