%PDF- %PDF-
| Direktori : /www/varak.net/losik.varak.net/vendor/latte/latte/src/Latte/Compiler/ |
| Current File : /www/varak.net/losik.varak.net/vendor/latte/latte/src/Latte/Compiler/Compiler.php |
<?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 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 Latte\\Runtime as LR;\n\n"
. ($comment === null ? '' : '/** ' . str_replace('*/', '* /', $comment) . " */\n")
. "final class $className extends 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;
}
}