diff --git a/src/Bridges/Tracy/BlueScreenPanel.php b/src/Bridges/Tracy/BlueScreenPanel.php index d19e597a94..561d3942ea 100644 --- a/src/Bridges/Tracy/BlueScreenPanel.php +++ b/src/Bridges/Tracy/BlueScreenPanel.php @@ -62,6 +62,7 @@ public static function renderError(?\Throwable $e): ?array ]; } } + return null; } @@ -80,6 +81,7 @@ public static function renderUnknownMacro(?\Throwable $e): ?array 'label' => 'fix it', ]; } + return null; } } diff --git a/src/Latte/Compiler/Compiler.php b/src/Latte/Compiler/Compiler.php index b8a43ea787..f7ede2d59c 100644 --- a/src/Latte/Compiler/Compiler.php +++ b/src/Latte/Compiler/Compiler.php @@ -120,6 +120,7 @@ public function addMacro(string $name, Macro $macro, int $flags = null) } elseif ($flags && $this->flags[$name] !== $flags) { throw new \LogicException("Incompatible flags for tag '$name'."); } + $this->macros[$name][] = $macro; return $this; } @@ -188,6 +189,7 @@ private function buildClassBody(array $tokens): string )) { $this->inHead = false; } + $this->{"process$token->type"}($token); } @@ -195,6 +197,7 @@ private function buildClassBody(array $tokens): string if (!empty($this->htmlNode->macroAttrs)) { throw new CompileException('Missing ' . self::printEndTag($this->htmlNode)); } + $this->htmlNode = $this->htmlNode->parentNode; } @@ -202,9 +205,11 @@ private function buildClassBody(array $tokens): string 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); } @@ -221,6 +226,7 @@ private function buildClassBody(array $tokens): string if ($prepare) { $this->addMethod('prepare', $extractParams . "?>$preparecontentType !== self::CONTENT_HTML) { $this->addConstant('CONTENT_TYPE', $this->contentType); } @@ -229,9 +235,11 @@ private function buildClassBody(array $tokens): string 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])" @@ -390,6 +398,7 @@ private function processText(Token $token): void ) { $this->lastAttrValue = $token->text; } + $this->output .= $this->escape($token->text); } @@ -417,11 +426,13 @@ private function processMacroTag(Token $token): void && ($t->type !== Token::HTML_ATTRIBUTE_BEGIN || $t->name !== Parser::N_PREFIX . $token->name)); $token->empty = $t ? !$t->closing : true; } + $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); } } @@ -435,14 +446,18 @@ private function processHtmlTagBegin(Token $token): void if (strcasecmp($this->htmlNode->name, $token->name) === 0) { break; } + if ($this->htmlNode->macroAttrs) { throw new CompileException("Unexpected name>, expecting " . self::printEndTag($this->htmlNode)); } + $this->htmlNode = $this->htmlNode->parentNode; } + if (!$this->htmlNode) { $this->htmlNode = new HtmlNode($token->name); } + $this->htmlNode->closing = true; $this->htmlNode->endLine = $this->getLine(); $this->context = self::CONTEXT_HTML_TEXT; @@ -458,6 +473,7 @@ private function processHtmlTagBegin(Token $token): void $this->htmlNode->startLine = $this->getLine(); $this->context = self::CONTEXT_HTML_TAG; } + $this->tagOffset = strlen($this->output); $this->output .= $this->escape($token->text); } @@ -532,6 +548,7 @@ private function processHtmlAttributeBegin(Token $token): void } 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; } @@ -619,6 +636,7 @@ public function openMacro( $this->output = &$node->content; $this->output = ''; } + return $node; } @@ -672,6 +690,7 @@ public function closeMacro( 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; @@ -688,6 +707,7 @@ private function writeCode(string $code, ?bool $isReplaced, ?bool $isRightmost, 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) !== '?>') { @@ -697,6 +717,7 @@ private function writeCode(string $code, ?bool $isReplaced, ?bool $isRightmost, $code .= "\n"; // double newline to avoid newline eating by PHP } } + $this->output .= $code; } @@ -729,6 +750,7 @@ public function writeAttrsMacro(string $html): void } }); } + unset($attrs[$attrName]); } @@ -743,7 +765,6 @@ public function writeAttrsMacro(string $html): void }); } - foreach (array_reverse($this->macros) as $name => $foo) { $attrName = MacroNode::PREFIX_TAG . "-$name"; if (!isset($attrs[$attrName])) { @@ -778,6 +799,7 @@ public function writeAttrsMacro(string $html): void } }); } + unset($attrs[$name]); } } diff --git a/src/Latte/Compiler/MacroNode.php b/src/Latte/Compiler/MacroNode.php index bea5496bc6..5810f401b4 100644 --- a/src/Latte/Compiler/MacroNode.php +++ b/src/Latte/Compiler/MacroNode.php @@ -133,6 +133,7 @@ public function closest(array $names, callable $condition = null): ?self )) { $node = $node->parentNode; } + return $node; } diff --git a/src/Latte/Compiler/MacroTokens.php b/src/Latte/Compiler/MacroTokens.php index efa19de6f5..62f5a19542 100644 --- a/src/Latte/Compiler/MacroTokens.php +++ b/src/Latte/Compiler/MacroTokens.php @@ -82,6 +82,7 @@ public function append($val, int $position = null) is_array($val) ? [$val] : $this->parse($val) ); } + return $this; } @@ -96,6 +97,7 @@ public function prepend($val) if ($val != null) { // intentionally @ array_splice($this->tokens, 0, 0, is_array($val) ? [$val] : $this->parse($val)); } + return $this; } @@ -117,6 +119,7 @@ public function fetchWord(): ?string $expr .= $this->joinUntilSameDepth(','); } } + $this->nextToken(','); $this->nextAll(self::T_WHITESPACE, self::T_COMMENT); return $expr === '' ? null : $expr; @@ -136,6 +139,7 @@ public function fetchWords(): array && (($dot = $this->nextValue('.')) || $this->isPrev('.'))) { $words[0] .= $space . $dot . $this->joinUntil(','); } + $this->nextToken(','); $this->nextAll(self::T_WHITESPACE, self::T_COMMENT); return $words === [''] ? [] : $words; @@ -154,6 +158,7 @@ public function joinUntilSameDepth(...$args): string if ($this->depth === $depth) { return $res; } + $res .= $this->nextValue(); } while (true); } @@ -174,6 +179,7 @@ public function fetchWordWithModifier($modifiers): ?array ) { return [$name, $mod]; } + $this->position = $pos; $name = $this->fetchWord(); return $name === null ? null : [$name, null]; diff --git a/src/Latte/Compiler/Parser.php b/src/Latte/Compiler/Parser.php index 3f541f2fc2..cc31721de6 100644 --- a/src/Latte/Compiler/Parser.php +++ b/src/Latte/Compiler/Parser.php @@ -118,10 +118,12 @@ public function parse(string $input): array if ($this->{'context' . $this->context[0]}() === false) { break; } + while ($tokenCount < count($this->output)) { $this->filter($this->output[$tokenCount++]); } } + if ($this->context[0] === self::CONTEXT_MACRO) { throw new CompileException('Malformed tag.'); } @@ -129,6 +131,7 @@ public function parse(string $input): array if ($this->offset < strlen($input)) { $this->addToken(Token::TEXT, substr($this->input, $this->offset)); } + return $this->output; } @@ -179,6 +182,7 @@ private function contextHtmlCData(): bool if (empty($matches['tag'])) { return $this->processMacro($matches); } + // addToken(Token::HTML_TAG_BEGIN, $matches[0]); $token->name = $this->lastHtmlTag; @@ -222,6 +226,7 @@ private function contextHtmlTag(): bool $this->setContext(self::CONTEXT_HTML_ATTRIBUTE, $matches['value']); } } + return true; } else { @@ -243,6 +248,7 @@ private function contextHtmlAttribute(): bool if (empty($matches['quote'])) { return $this->processMacro($matches); } + // (attribute end) '" $this->addToken(Token::HTML_ATTRIBUTE_END, $matches[0]); $this->setContext(self::CONTEXT_HTML_TAG); @@ -263,6 +269,7 @@ private function contextHtmlComment(): bool if (empty($matches['htmlcomment'])) { return $this->processMacro($matches); } + // --> $this->addToken(Token::HTML_TAG_END, $matches[0]); $this->setContext(self::CONTEXT_HTML_TEXT); @@ -323,6 +330,7 @@ private function processMacro(array $matches): bool if (empty($matches['macro'])) { return false; } + // {macro} or {* *} $this->setContext(self::CONTEXT_MACRO, [$this->context, $matches['macro']]); return true; @@ -339,6 +347,7 @@ private function match(string $re): array if (preg_last_error()) { throw new RegexpException(null, preg_last_error()); } + return []; } @@ -346,10 +355,12 @@ private function match(string $re): array if ($value !== '') { $this->addToken(Token::TEXT, $value); } + $this->offset = $matches[0][1] + strlen($matches[0][0]); foreach ($matches as $k => $v) { $matches[$k] = $v[0]; } + return $matches; } @@ -366,6 +377,7 @@ public function setContentType(string $type) } else { $this->setContext(self::CONTEXT_NONE); } + return $this; } @@ -428,11 +440,14 @@ public function parseMacroTag(string $tag): ?array if (preg_last_error()) { throw new RegexpException(null, preg_last_error()); } + return null; } + if ($match['name'] === '') { $match['name'] = $match['shortname'] ?: ($match['closing'] ? '' : '='); } + return [$match['name'], trim($match['args']), $match['modifiers'], (bool) $match['empty'], (bool) $match['closing']]; } diff --git a/src/Latte/Compiler/PhpHelpers.php b/src/Latte/Compiler/PhpHelpers.php index 49af39a7d5..10c6c5d52d 100644 --- a/src/Latte/Compiler/PhpHelpers.php +++ b/src/Latte/Compiler/PhpHelpers.php @@ -45,6 +45,7 @@ public static function reformatCode(string $source): string } elseif (substr($next[1], -1) === "\n") { $php .= "\n" . str_repeat("\t", $level); } + $tokens->next(); } else { @@ -56,16 +57,18 @@ public static function reformatCode(string $source): string } else { $php = rtrim($php, "\t"); } + $res .= $php . $token; } + $php = ''; $lastChar = ';'; } - } elseif ($name === T_ELSE || $name === T_ELSEIF) { if ($tokens[$n + 1] === ':' && $lastChar === '}') { $php .= ';'; // semicolon needed in if(): ... if() ... else: } + $lastChar = ''; $php .= $token; @@ -80,6 +83,7 @@ public static function reformatCode(string $source): string } elseif ($prev[0] === T_OPEN_TAG) { $token = ''; } + $php .= $token; } elseif ($name === T_OBJECT_OPERATOR) { @@ -90,6 +94,7 @@ public static function reformatCode(string $source): string if (in_array($name, [T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES], true)) { $level++; } + $lastChar = ''; $php .= $token; } @@ -109,6 +114,7 @@ public static function reformatCode(string $source): string $token .= "\n" . str_repeat("\t", $level); // indent last line } } + $lastChar = $token; $php .= $token; } @@ -117,6 +123,7 @@ public static function reformatCode(string $source): string if ($php) { $res .= " ') . self::dump($v) . ",\n" : ($s === '' ? '' : ', ') . ($indexed ? '' : self::dump($k) . ' => ') . self::dump($v); } + return '[' . $s . ']'; } elseif ($value === null) { return 'null'; @@ -166,6 +174,7 @@ public static function inlineHtmlToEcho(string $source): string ) { break; } + $n++; } @@ -173,11 +182,13 @@ public static function inlineHtmlToEcho(string $source): string $res .= ""; continue; } + $res .= $token[1]; } else { $res .= $token; } } + return $res; } } diff --git a/src/Latte/Compiler/PhpWriter.php b/src/Latte/Compiler/PhpWriter.php index 38abaf52df..74c3aa5b8d 100644 --- a/src/Latte/Compiler/PhpWriter.php +++ b/src/Latte/Compiler/PhpWriter.php @@ -179,6 +179,7 @@ public function formatWord(string $s): string $s = preg_match('#\s#', $s) ? "($s)" : $s; return $this->formatArgs(new MacroTokens($s)); } + return '"' . $s . '"'; } @@ -232,9 +233,11 @@ public function validateTokens(MacroTokens $tokens): void throw new CompileException("Forbidden variable {$tokenValue}."); } } + if ($brackets) { throw new CompileException('Missing ' . array_pop($brackets)); } + $tokens->position = $pos; } @@ -263,6 +266,7 @@ public function validateKeywords(MacroTokens $tokens): void throw new CompileException("Forbidden keyword '{$tokens->currentValue()}' inside tag."); } } + $tokens->position = $pos; } @@ -276,6 +280,7 @@ public function removeCommentsPass(MacroTokens $tokens): MacroTokens while ($tokens->nextToken()) { $res->append($tokens->isCurrent($tokens::T_COMMENT) ? ' ' : $tokens->currentToken()); } + return $res; } @@ -297,11 +302,13 @@ public function replaceFunctionsPass(MacroTokens $tokens): MacroTokens 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; } @@ -331,12 +338,14 @@ public function shortTernaryPass(MacroTokens $tokens): MacroTokens $res->append(' : null'); array_pop($inTernary); } + $res->append($tokens->currentToken()); } if ($inTernary) { $res->append(' : null'); } + return $res; } @@ -403,6 +412,7 @@ public function optionalChainingPass(MacroTokens $tokens): MacroTokens $expr->append($addBraces); break; } + $expr->append($tokens->currentToken()); } elseif ($tokens->nextToken('??->')) { @@ -415,6 +425,7 @@ public function optionalChainingPass(MacroTokens $tokens): MacroTokens $expr->append($addBraces); break; } + $expr->append($tokens->currentToken()); } elseif ($tokens->nextToken('->', '::')) { @@ -423,6 +434,7 @@ public function optionalChainingPass(MacroTokens $tokens): MacroTokens $expr->append($addBraces); break; } + $expr->append($tokens->currentToken()); } elseif ($tokens->nextToken('[', '(')) { @@ -465,6 +477,7 @@ public function expandCastPass(MacroTokens $tokens): MacroTokens } else { $res->prepend('array_merge(')->append($expand ? ', [])' : '])'); } + return $res; } @@ -487,6 +500,7 @@ public function quotingPass(MacroTokens $tokens): MacroTokens : $tokens->currentToken() ); } + return $res; } @@ -510,6 +524,7 @@ public function namedArgumentsPass(MacroTokens $tokens): MacroTokens $res->append($tokens->currentToken()); } } + return $res; } @@ -539,6 +554,7 @@ public function modernArraySyntax(MacroTokens $tokens): MacroTokens $res->append($tokens->currentToken()); } } + return $res; } @@ -571,9 +587,11 @@ public function inOperatorPass(MacroTokens $tokens): MacroTokens } } } + $tokens->position = $start; } } + return $tokens->reset(); } @@ -625,12 +643,14 @@ public function sandboxPass(MacroTokens $tokens): MacroTokens 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 @@ -649,6 +669,7 @@ public function sandboxPass(MacroTokens $tokens): MacroTokens $expr->append('::class'); $static = false; } + $expr->append(', '); if ($tokens->nextToken($tokens::T_SYMBOL)) { // $obj->member or $obj::member @@ -662,7 +683,6 @@ public function sandboxPass(MacroTokens $tokens): MacroTokens } else { $expr->append($tokens->currentValue()); } - } elseif ($tokens->nextToken('{')) { // $obj->{...} $member = array_merge([$tokens->currentToken()], $this->sandboxPass($tokens)->tokens); $expr->append('(string) '); @@ -684,7 +704,6 @@ public function sandboxPass(MacroTokens $tokens): MacroTokens $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); @@ -714,6 +733,7 @@ public function inlineModifierPass(MacroTokens $tokens): MacroTokens $result->append($tokens->currentToken()); } } + return $result; } @@ -755,12 +775,14 @@ private function inlineModifierInner(MacroTokens $tokens): array } else { array_shift($result->tokens); } + return $result->tokens; } else { $current->append($tokens->currentToken()); } } + throw new CompileException('Unbalanced brackets.'); } @@ -808,6 +830,7 @@ public function modifierPass(MacroTokens $tokens, $var, bool $isContent = false) } else { $res = $this->escapePass($res); } + $tokens->nextToken('|'); } elseif (!strcasecmp($tokens->currentValue(), 'checkurl')) { $res->prepend('LR\Filters::safeUrl('); @@ -822,6 +845,7 @@ public function modifierPass(MacroTokens $tokens, $var, bool $isContent = false) if ($this->policy && !$this->policy->isFilterAllowed($name)) { throw new SecurityViolationException("Filter |$name is not allowed."); } + $name = strtolower($name); $res->prepend( $isContent @@ -834,9 +858,11 @@ public function modifierPass(MacroTokens $tokens, $var, bool $isContent = false) throw new CompileException("Filter name must be alphanumeric string, '{$tokens->currentValue()}' given."); } } + if ($inside) { $res->append(')'); } + return $res; } diff --git a/src/Latte/Compiler/TokenIterator.php b/src/Latte/Compiler/TokenIterator.php index a0a41a3d19..b7ec4638a9 100644 --- a/src/Latte/Compiler/TokenIterator.php +++ b/src/Latte/Compiler/TokenIterator.php @@ -128,6 +128,7 @@ public function isCurrent(...$args): bool if (!isset($this->tokens[$this->position])) { return false; } + $token = $this->tokens[$this->position]; return in_array($token[Tokenizer::VALUE], $args, true) || in_array($token[Tokenizer::TYPE], $args, true); @@ -164,10 +165,12 @@ public function consumeValue(...$args): string if ($token = $this->scan($args, true, true)) { // onlyFirst, advance return $token[Tokenizer::VALUE]; } + $pos = $this->position + 1; while (($next = $this->tokens[$pos] ?? null) && in_array($next[Tokenizer::TYPE], $this->ignored, true)) { $pos++; } + throw new CompileException($next ? "Unexpected token '" . $next[Tokenizer::VALUE] . "'." : 'Unexpected end.'); } @@ -228,10 +231,10 @@ protected function scan( } else { $res[] = $token; } - } elseif ($until || !in_array($token[Tokenizer::TYPE], $this->ignored, true)) { return $res; } + $pos += $prev ? -1 : 1; } while (true); } diff --git a/src/Latte/Compiler/Tokenizer.php b/src/Latte/Compiler/Tokenizer.php index 6b05beb970..22c8a5d889 100644 --- a/src/Latte/Compiler/Tokenizer.php +++ b/src/Latte/Compiler/Tokenizer.php @@ -51,6 +51,7 @@ public function tokenize(string $input): array if (preg_last_error()) { throw new RegexpException(null, preg_last_error()); } + $len = 0; $count = count($this->types); foreach ($tokens as &$match) { @@ -63,14 +64,17 @@ public function tokenize(string $input): array break; } } + $match = [self::VALUE => $match[0], self::OFFSET => $len, self::TYPE => $type]; $len += strlen($match[self::VALUE]); } + if ($len !== strlen($input)) { [$line, $col] = $this->getCoordinates($input, $len); $token = str_replace("\n", '\n', substr($input, $len, 10)); throw new CompileException("Unexpected '$token' on line $line, column $col."); } + return $tokens; } diff --git a/src/Latte/Engine.php b/src/Latte/Engine.php index 6197043853..86f08042f9 100644 --- a/src/Latte/Engine.php +++ b/src/Latte/Engine.php @@ -83,6 +83,7 @@ public function __construct() foreach ($defaults->getFilters() as $name => $callback) { $this->filters->add($name, $callback); } + foreach ($defaults->getFunctions() as $name => $callback) { $this->functions->$name = $callback; } @@ -125,6 +126,7 @@ public function createTemplate(string $name, array $params = []): Runtime\Templa if (!class_exists($class, false)) { $this->loadTemplate($name); } + $this->providers['fn'] = $this->functions; return new $class($this, $params, $this->filters, $this->providers, $name, $this->sandboxed ? $this->policy : null); } @@ -138,6 +140,7 @@ public function compile(string $name): string foreach ($this->onCompile ?: [] as $cb) { (Helpers::checkCallback($cb))($this); } + $this->onCompile = []; $source = $this->getLoader()->getContent($name); @@ -158,6 +161,7 @@ public function compile(string $name): string if (!$e instanceof CompileException) { $e = new CompileException($e instanceof SecurityViolationException ? $e->getMessage() : "Thrown exception '{$e->getMessage()}'", 0, $e); } + $line = isset($tokens) ? $this->getCompiler()->getLine() : $this->getParser()->getLine(); @@ -193,6 +197,7 @@ private function loadTemplate(string $name): void throw (new CompileException('Error in template: ' . error_get_last()['message'])) ->setSource($code, error_get_last()['line'], "$name (compiled)"); } + return; } @@ -212,6 +217,7 @@ private function loadTemplate(string $name): void if ($lock) { flock($lock, LOCK_UN); // release shared lock so we can get exclusive } + $lock = $this->acquireLock("$file.lock", LOCK_EX); // while waiting for exclusive lock, someone might have already created the cache @@ -221,6 +227,7 @@ private function loadTemplate(string $name): void @unlink("$file.tmp"); // @ - file may not exist throw new RuntimeException("Unable to create '$file'."); } + if (function_exists('opcache_invalidate')) { @opcache_invalidate($file, true); // @ can be restricted } @@ -248,6 +255,7 @@ private function acquireLock(string $file, int $mode) } elseif (!@flock($handle, $mode)) { // @ is escalated to exception throw new RuntimeException('Unable to acquire ' . ($mode & LOCK_EX ? 'exclusive' : 'shared') . " lock on file '$file'. " . error_get_last()['message']); } + return $handle; } @@ -284,6 +292,7 @@ public function addFilter(?string $name, callable $callback) if ($name !== null && !preg_match('#^[a-z]\w*$#iD', $name)) { throw new \LogicException("Invalid filter name '$name'."); } + $this->filters->add($name, $callback); return $this; } @@ -345,6 +354,7 @@ public function addFunction(string $name, callable $callback) if (!preg_match('#^[a-z]\w*$#iD', $name)) { throw new \LogicException("Invalid function name '$name'."); } + $this->functions->$name = $callback; return $this; } @@ -363,6 +373,7 @@ public function invokeFunction(string $name, array $args) : '.'; throw new \LogicException("Function '$name' is not defined$hint"); } + return ($this->functions->$name)(...$args); } @@ -377,6 +388,7 @@ public function addProvider(string $name, $value) if (!preg_match('#^[a-z]\w*$#iD', $name)) { throw new \LogicException("Invalid provider name '$name'."); } + $this->providers[$name] = $value; return $this; } @@ -462,6 +474,7 @@ public function getParser(): Parser if (!$this->parser) { $this->parser = new Parser; } + return $this->parser; } @@ -473,6 +486,7 @@ public function getCompiler(): Compiler Macros\CoreMacros::install($this->compiler); Macros\BlockMacros::install($this->compiler); } + return $this->compiler; } @@ -490,6 +504,7 @@ public function getLoader(): Loader if (!$this->loader) { $this->loader = new Loaders\FileLoader; } + return $this->loader; } diff --git a/src/Latte/Helpers.php b/src/Latte/Helpers.php index 399e64e69a..20695a6d30 100644 --- a/src/Latte/Helpers.php +++ b/src/Latte/Helpers.php @@ -32,6 +32,7 @@ public static function checkCallback($callable): callable if (!is_callable($callable, false, $text)) { throw new \InvalidArgumentException("Callback '$text' is not callable."); } + return $callable; } @@ -50,6 +51,7 @@ public static function getSuggestion(array $items, string $value): ?string $best = $item; } } + return $best; } diff --git a/src/Latte/Loaders/FileLoader.php b/src/Latte/Loaders/FileLoader.php index a920f4525c..d676126afc 100644 --- a/src/Latte/Loaders/FileLoader.php +++ b/src/Latte/Loaders/FileLoader.php @@ -46,6 +46,7 @@ public function getContent($fileName): string trigger_error("File's modification time is in the future. Cannot update it: " . error_get_last()['message'], E_USER_WARNING); } } + return file_get_contents($file); } @@ -65,6 +66,7 @@ public function getReferredName($file, $referringFile): string if ($this->baseDir || !preg_match('#/|\\\\|[a-z][a-z0-9+.-]*:#iA', $file)) { $file = $this->normalizePath($referringFile . '/../' . $file); } + return $file; } @@ -88,6 +90,7 @@ protected static function normalizePath(string $path): string $res[] = $part; } } + return implode(DIRECTORY_SEPARATOR, $res); } } diff --git a/src/Latte/Loaders/StringLoader.php b/src/Latte/Loaders/StringLoader.php index 5d9320f45e..4a983319cd 100644 --- a/src/Latte/Loaders/StringLoader.php +++ b/src/Latte/Loaders/StringLoader.php @@ -61,6 +61,7 @@ public function getReferredName($name, $referringName): string if ($this->templates === null) { throw new \LogicException("Missing template '$name'."); } + return $name; } diff --git a/src/Latte/Macros/BlockMacros.php b/src/Latte/Macros/BlockMacros.php index 764a5e8720..64dbfd877e 100644 --- a/src/Latte/Macros/BlockMacros.php +++ b/src/Latte/Macros/BlockMacros.php @@ -131,6 +131,7 @@ public function macroInclude(MacroNode $node, PhpWriter $writer) if ($node->tokenizer->isNext('=') && !$node->tokenizer->depth) { trigger_error('The assignment in the {' . $node->name . ' ' . $tmp . '= ...} looks like an error.', E_USER_NOTICE); } + $node->tokenizer->reset(); [$name, $mod] = $node->tokenizer->fetchWordWithModifier(['block', 'file']); @@ -138,12 +139,14 @@ public function macroInclude(MacroNode $node, PhpWriter $writer) if ($mod === 'file' || !$name || !preg_match('~#|[\w-]+$~DA', $name)) { return false; // {include file} } + $name = ltrim($name, '#'); } if ($name === 'parent' && $node->modifiers !== '') { throw new CompileException('Filters are not allowed in {include parent}'); } + $noEscape = Helpers::removeFilter($node->modifiers, 'noescape'); if ($node->modifiers && !$noEscape) { $node->modifiers .= '|escape'; @@ -166,6 +169,7 @@ public function macroInclude(MacroNode $node, PhpWriter $writer) if (!$item) { throw new CompileException("Cannot include $name block outside of any block."); } + $name = $item->data->name; } @@ -240,6 +244,7 @@ public function macroExtends(MacroNode $node, PhpWriter $writer): void } else { $this->extends = $writer->write('%node.word%node.args'); } + if (!$this->getCompiler()->isInHead()) { throw new CompileException($node->getNotation() . ' must be placed in template head.'); } @@ -261,6 +266,7 @@ public function macroBlock(MacroNode $node, PhpWriter $writer): string if ($node->modifiers === '') { return ''; } + $node->modifiers .= '|escape'; $node->closingCode = $writer->write( '', @@ -315,6 +321,7 @@ public function macroDefine(MacroNode $node, PhpWriter $writer): string $node->setArgs($node->args . $node->modifiers); $node->modifiers = ''; } + $node->validate(true); [$name, $local] = $node->tokenizer->fetchWordWithModifier('local'); @@ -337,6 +344,7 @@ public function macroDefine(MacroNode $node, PhpWriter $writer): string if ($tokens->nextToken($tokens::T_SYMBOL, '?', 'null', '\\')) { // type $tokens->nextAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null'); } + $param = $tokens->consumeValue($tokens::T_VARIABLE); $default = $tokens->nextToken('=') ? $tokens->joinUntilSameDepth(',') @@ -446,6 +454,7 @@ public function macroSnippet(MacroNode $node, PhpWriter $writer): string if (isset($node->htmlNode->macroAttrs['foreach'])) { throw new CompileException('Combination of n:snippet with n:foreach is invalid, use n:inner-foreach.'); } + $node->attrCode = $writer->write( "snippetAttribute}=\"' . htmlspecialchars(\$this->global->snippetDriver->getHtmlId(%var)) . '\"' ?>", $data->name @@ -475,6 +484,7 @@ private function beginDynamicSnippet(MacroNode $node, PhpWriter $writer): string $node->closingCode = $node->openingCode = ''; }; } + $node->attrCode = $writer->write( "snippetAttribute}=\"' . htmlspecialchars(\$this->global->snippetDriver->getHtmlId(\$ʟ_nm = %word)) . '\"' ?>", $data->name @@ -530,6 +540,7 @@ public function macroBlockEnd(MacroNode $node, PhpWriter $writer): string if (isset($node->data->after)) { ($node->data->after)(); } + return $node->name === 'define' ? ' ' // consume next new line : ''; @@ -561,6 +572,7 @@ private function extractMethod(MacroNode $node, Block $block, string $params = n . 'unset($ʟ_args);?>' . $node->content; } + $block->code = preg_replace('#^\n+|(?<=\n)[ \t]+$#D', '', $node->content); $node->content = substr_replace($node->content, $node->openingCode . "\n", strspn($node->content, "\n"), strlen($block->code)); $node->openingCode = ''; @@ -630,12 +642,14 @@ public function macroIfset(MacroNode $node, PhpWriter $writer) if (!preg_match('~#|\w~A', $node->args)) { return false; } + $list = []; while ([$name, $block] = $node->tokenizer->fetchWordWithModifier('block')) { $list[] = $block || preg_match('~#|\w[\w-]*$~DA', $name) ? '$this->hasBlock(' . $writer->formatWord(ltrim($name, '#')) . ')' : 'isset(' . $writer->formatArgs(new Latte\MacroTokens($name)) . ')'; } + return $writer->write(($node->name === 'elseifset' ? '} else' : '') . 'if (%raw) %node.line {', implode(' && ', $list)); } @@ -649,6 +663,7 @@ private function generateMethodName(string $blockName): string while (isset($methods[$lower . $counter])) { $counter++; } + return $name . $counter; } diff --git a/src/Latte/Macros/CoreMacros.php b/src/Latte/Macros/CoreMacros.php index c72b573ee3..e700735f5a 100644 --- a/src/Latte/Macros/CoreMacros.php +++ b/src/Latte/Macros/CoreMacros.php @@ -119,6 +119,7 @@ public function finalize() $code .= 'foreach (array_intersect_key(' . Latte\PhpHelpers::dump($vars) . ', $this->params) as $ʟ_v => $ʟ_l) { ' . 'trigger_error("Variable \$$ʟ_v overwritten in foreach on line $ʟ_l"); } '; } + $code = $code ? 'if (!$this->getReferringTemplate() || $this->getReferenceType() === "extends") { ' . $code . '}' : ''; @@ -138,6 +139,7 @@ public function macroIf(MacroNode $node, PhpWriter $writer): string if ($node->data->capture = ($node->args === '')) { return $writer->write('ob_start(function () {}) %node.line;'); } + if ($node->prefix === $node::PREFIX_TAG) { for ($id = 0, $tmp = $node->htmlNode; $tmp = $tmp->parentNode; $id++); $node->htmlNode->data->id = $node->htmlNode->data->id ?? $id; @@ -148,6 +150,7 @@ public function macroIf(MacroNode $node, PhpWriter $writer): string $node->htmlNode->data->id ); } + return $writer->write('if (%node.args) %node.line {'); } @@ -183,6 +186,7 @@ public function macroElse(MacroNode $node, PhpWriter $writer): string if ($node->args !== '' && Helpers::startsWith($node->args, 'if')) { throw new CompileException('Arguments are not allowed in {else}, did you mean {elseif}?'); } + $node->validate(false, ['if', 'ifset', 'foreach', 'ifchanged', 'try', 'first', 'last', 'sep']); $parent = $node->parentNode; @@ -207,6 +211,7 @@ public function macroElse(MacroNode $node, PhpWriter $writer): string $parent->closingCode = ''; return ''; } + return $writer->write('} else %node.line {'); } @@ -236,6 +241,7 @@ public function macroIfContent(MacroNode $node, PhpWriter $writer): void if (!$node->prefix || $node->prefix !== MacroNode::PREFIX_NONE) { throw new CompileException("Unknown {$node->getNotation()}, use n:{$node->name} attribute."); } + $node->validate(false); } @@ -302,6 +308,7 @@ public function macroRollback(MacroNode $node, PhpWriter $writer): string if (!$parent || isset($parent->data->catch)) { throw new CompileException('Tag {rollback} must be inside {try} ... {/try}.'); } + $node->validate(false); return $writer->write('throw new LR\RollbackException;'); @@ -331,6 +338,7 @@ public function macroTranslate(MacroNode $node, PhpWriter $writer): string } elseif ($node->empty = ($node->args !== '')) { return $writer->write('echo %modify(($this->filters->translate)(%node.args)) %node.line;'); } + return ''; } @@ -353,6 +361,7 @@ public function macroInclude(MacroNode $node, PhpWriter $writer): string if ($node->modifiers && !$noEscape) { $node->modifiers .= '|escape'; } + return $writer->write( '$this->createTemplate(%word, %node.array? + $this->params, %var)->renderToContentType(%raw) %node.line;', $file, @@ -394,6 +403,7 @@ public function macroCapture(MacroNode $node, PhpWriter $writer): string } elseif (!Helpers::startsWith($variable, '$')) { throw new CompileException("Invalid capture block variable '$variable'"); } + $this->checkExtraArgs($node); $node->data->variable = $variable; return $writer->write('ob_start(function () {}) %node.line;'); @@ -439,6 +449,7 @@ public function macroWhile(MacroNode $node, PhpWriter $writer): string if ($node->data->do = ($node->args === '')) { return $writer->write('do %node.line {'); } + return $writer->write('while (%node.args) %node.line {'); } @@ -452,6 +463,7 @@ public function macroEndWhile(MacroNode $node, PhpWriter $writer): string $node->validate(true); return $writer->write('} while (%node.args);'); } + return '}'; } @@ -466,6 +478,7 @@ public function macroEndForeach(MacroNode $node, PhpWriter $writer): void if ($node->modifiers) { throw new CompileException('Only modifiers |noiterator and |nocheck are allowed here.'); } + $node->validate(true); $node->openingCode = 'formatArgs(); @@ -475,6 +488,7 @@ public function macroEndForeach(MacroNode $node, PhpWriter $writer): void $this->overwrittenVars[$m[$i]][] = $node->startLine; } } + if ( !$noIterator && preg_match('#\$iterator\W|\Wget_defined_vars\W#', $this->getCompiler()->expandTokens($node->content)) @@ -497,6 +511,7 @@ public function macroIterateWhile(MacroNode $node, PhpWriter $writer): void if (!$node->closest(['foreach'])) { throw new CompileException('Tag ' . $node->getNotation() . ' must be inside {foreach} ... {/foreach}.'); } + $node->data->begin = $node->args !== ''; } @@ -540,14 +555,17 @@ public function macroBreakContinueIf(MacroNode $node, PhpWriter $writer): string $ancestors = ['for', 'foreach', 'while']; $cmd = str_replace('If', '', $node->name); } + if (!$node->closest($ancestors)) { throw new CompileException('Tag ' . $node->getNotation() . ' is unexpected here.'); } + $node->validate('condition'); if ($node->parentNode->prefix === $node::PREFIX_NONE) { return $writer->write("if (%node.args) %node.line { echo \"parentNode->htmlNode->name}>\\n\"; $cmd; }"); } + return $writer->write("if (%node.args) %node.line $cmd;"); } @@ -560,6 +578,7 @@ public function macroClass(MacroNode $node, PhpWriter $writer): string if (isset($node->htmlNode->attrs['class'])) { throw new CompileException('It is not possible to combine class with n:class.'); } + $node->validate(true); return $writer->write('echo ($ʟ_tmp = array_filter(%node.array)) ? \' class="\' . %escape(implode(" ", array_unique($ʟ_tmp))) . \'"\' : "" %node.line;'); } @@ -586,6 +605,7 @@ public function macroTag(MacroNode $node, PhpWriter $writer): void } elseif (preg_match('(style$|script$)iA', $node->htmlNode->name)) { throw new CompileException("Attribute {$node->getNotation()} is not allowed in