From 114aebae7cdcd6286386e9f87f97e24f398d7e73 Mon Sep 17 00:00:00 2001 From: Andy Miller Date: Fri, 21 Nov 2025 12:25:58 +0000 Subject: [PATCH] more robust deferred logic + deprecated fixes Signed-off-by: Andy Miller --- composer.lock | 8 +- .../Grav/Common/Twig/Node/TwigNodeSwitch.php | 2 +- .../Twig/TokenParser/TwigTokenParserCache.php | 4 +- .../Twig/TokenParser/TwigTokenParserLink.php | 8 +- .../TokenParser/TwigTokenParserRender.php | 6 +- .../TokenParser/TwigTokenParserScript.php | 8 +- .../Twig/TokenParser/TwigTokenParserStyle.php | 8 +- .../TokenParser/TwigTokenParserSwitch.php | 20 +++-- .../Twig/TokenParser/TwigTokenParserThrow.php | 2 +- .../src/Grav/Common/Twig/TwigEnvironment.php | 36 ++++++++ .../DeferredExtension/DeferredTokenParser.php | 90 ++++++++++--------- 11 files changed, 117 insertions(+), 75 deletions(-) diff --git a/composer.lock b/composer.lock index d2ec8b938..845c23d97 100644 --- a/composer.lock +++ b/composer.lock @@ -4228,12 +4228,12 @@ "source": { "type": "git", "url": "https://github.com/getgrav/Twig.git", - "reference": "b987134b5cc7553fcdc41840799df5318504e0c7" + "reference": "5a01e60e351f4c8d41765970c412d2500288339b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getgrav/Twig/zipball/b987134b5cc7553fcdc41840799df5318504e0c7", - "reference": "b987134b5cc7553fcdc41840799df5318504e0c7", + "url": "https://api.github.com/repos/getgrav/Twig/zipball/5a01e60e351f4c8d41765970c412d2500288339b", + "reference": "5a01e60e351f4c8d41765970c412d2500288339b", "shasum": "" }, "require": { @@ -4293,7 +4293,7 @@ "support": { "source": "https://github.com/getgrav/Twig/tree/3.x" }, - "time": "2025-11-04T11:37:42+00:00" + "time": "2025-11-21T11:38:05+00:00" }, { "name": "willdurand/negotiation", diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php b/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php index 273b7b509..c15e77bd5 100644 --- a/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php @@ -31,7 +31,7 @@ class TwigNodeSwitch extends Node $nodes = ['value' => $value, 'cases' => $cases, 'default' => $default]; $nodes = array_filter($nodes); - parent::__construct($nodes, [], $lineno, $tag); + parent::__construct($nodes, [], $lineno); } /** diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserCache.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserCache.php index 0a12263f3..06012f778 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserCache.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserCache.php @@ -44,9 +44,9 @@ class TwigTokenParserCache extends AbstractTokenParser $lifetime = null; while (!$stream->test(Token::BLOCK_END_TYPE)) { if ($stream->test(Token::STRING_TYPE)) { - $key = $this->parser->getExpressionParser()->parseExpression(); + $key = $this->parser->parseExpression(); } elseif ($stream->test(Token::NUMBER_TYPE)) { - $lifetime = $this->parser->getExpressionParser()->parseExpression(); + $lifetime = $this->parser->parseExpression(); } else { throw new \Twig\Error\SyntaxError("Unexpected token type in cache tag.", $token->getLine(), $stream->getSourceContext()); } diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserLink.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserLink.php index 77cd5a677..754cba741 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserLink.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserLink.php @@ -73,23 +73,23 @@ class TwigTokenParserLink extends AbstractTokenParser $file = null; if (!$stream->test(Token::NAME_TYPE) && !$stream->test(Token::BLOCK_END_TYPE)) { - $file = $this->parser->getExpressionParser()->parseExpression(); + $file = $this->parser->parseExpression(); } $group = null; if ($stream->nextIf(Token::NAME_TYPE, 'at')) { - $group = $this->parser->getExpressionParser()->parseExpression(); + $group = $this->parser->parseExpression(); } $priority = null; if ($stream->nextIf(Token::NAME_TYPE, 'priority')) { $stream->expect(Token::PUNCTUATION_TYPE, ':'); - $priority = $this->parser->getExpressionParser()->parseExpression(); + $priority = $this->parser->parseExpression(); } $attributes = null; if ($stream->nextIf(Token::NAME_TYPE, 'with')) { - $attributes = $this->parser->getExpressionParser()->parseExpression(); + $attributes = $this->parser->parseExpression(); } $stream->expect(Token::BLOCK_END_TYPE); diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserRender.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserRender.php index 5e73e9142..b56fa6fb7 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserRender.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserRender.php @@ -44,17 +44,17 @@ class TwigTokenParserRender extends AbstractTokenParser { $stream = $this->parser->getStream(); - $object = $this->parser->getExpressionParser()->parseExpression(); + $object = $this->parser->parseExpression(); $layout = null; if ($stream->nextIf(Token::NAME_TYPE, 'layout')) { $stream->expect(Token::PUNCTUATION_TYPE, ':'); - $layout = $this->parser->getExpressionParser()->parseExpression(); + $layout = $this->parser->parseExpression(); } $context = null; if ($stream->nextIf(Token::NAME_TYPE, 'with')) { - $context = $this->parser->getExpressionParser()->parseExpression(); + $context = $this->parser->parseExpression(); } $stream->expect(Token::BLOCK_END_TYPE); diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php index faf8ef908..4e6d46466 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php @@ -87,23 +87,23 @@ class TwigTokenParserScript extends AbstractTokenParser $file = null; if (!$stream->test(Token::NAME_TYPE) && !$stream->test(Token::OPERATOR_TYPE, 'in') && !$stream->test(Token::BLOCK_END_TYPE)) { - $file = $this->parser->getExpressionParser()->parseExpression(); + $file = $this->parser->parseExpression(); } $group = null; if ($stream->nextIf(Token::NAME_TYPE, 'at') || $stream->nextIf(Token::OPERATOR_TYPE, 'in')) { - $group = $this->parser->getExpressionParser()->parseExpression(); + $group = $this->parser->parseExpression(); } $priority = null; if ($stream->nextIf(Token::NAME_TYPE, 'priority')) { $stream->expect(Token::PUNCTUATION_TYPE, ':'); - $priority = $this->parser->getExpressionParser()->parseExpression(); + $priority = $this->parser->parseExpression(); } $attributes = null; if ($stream->nextIf(Token::NAME_TYPE, 'with')) { - $attributes = $this->parser->getExpressionParser()->parseExpression(); + $attributes = $this->parser->parseExpression(); } $stream->expect(Token::BLOCK_END_TYPE); diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php index 7495b799b..3e74f8d73 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php @@ -74,23 +74,23 @@ class TwigTokenParserStyle extends AbstractTokenParser $file = null; if (!$stream->test(Token::NAME_TYPE) && !$stream->test(Token::OPERATOR_TYPE, 'in') && !$stream->test(Token::BLOCK_END_TYPE)) { - $file = $this->parser->getExpressionParser()->parseExpression(); + $file = $this->parser->parseExpression(); } $group = null; if ($stream->nextIf(Token::NAME_TYPE, 'at') || $stream->nextIf(Token::OPERATOR_TYPE, 'in')) { - $group = $this->parser->getExpressionParser()->parseExpression(); + $group = $this->parser->parseExpression(); } $priority = null; if ($stream->nextIf(Token::NAME_TYPE, 'priority')) { $stream->expect(Token::PUNCTUATION_TYPE, ':'); - $priority = $this->parser->getExpressionParser()->parseExpression(); + $priority = $this->parser->parseExpression(); } $attributes = null; if ($stream->nextIf(Token::NAME_TYPE, 'with')) { - $attributes = $this->parser->getExpressionParser()->parseExpression(); + $attributes = $this->parser->parseExpression(); } $stream->expect(Token::BLOCK_END_TYPE); diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php index 9f30b0237..3db575727 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php @@ -13,6 +13,7 @@ namespace Grav\Common\Twig\TokenParser; use Grav\Common\Twig\Node\TwigNodeSwitch; use Twig\Error\SyntaxError; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Token; use Twig\TokenParser\AbstractTokenParser; @@ -40,21 +41,22 @@ class TwigTokenParserSwitch extends AbstractTokenParser $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $name = $this->parser->getExpressionParser()->parseExpression(); + $name = $this->parser->parseExpression(); $stream->expect(Token::BLOCK_END_TYPE); // There can be some whitespace between the {% switch %} and first {% case %} tag. - while ($stream->getCurrent()->getType() === Token::TEXT_TYPE && trim((string) $stream->getCurrent()->getValue()) === '') { + while ($stream->getCurrent()->test(Token::TEXT_TYPE) && trim((string) $stream->getCurrent()->getValue()) === '') { $stream->next(); } $stream->expect(Token::BLOCK_START_TYPE); - $expressionParser = $this->parser->getExpressionParser(); - $default = null; $cases = []; $end = false; + + // 'or' operator precedence is 10. We want to stop parsing if we encounter it. + $orPrecedence = 10; while (!$end) { $next = $stream->next(); @@ -64,7 +66,7 @@ class TwigTokenParserSwitch extends AbstractTokenParser $values = []; while (true) { - $values[] = $expressionParser->parsePrimaryExpression(); + $values[] = $this->parser->parseExpression($orPrecedence + 1); // Multiple allowed values? if ($stream->test(Token::OPERATOR_TYPE, 'or')) { $stream->next(); @@ -75,10 +77,10 @@ class TwigTokenParserSwitch extends AbstractTokenParser $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse($this->decideIfFork(...)); - $cases[] = new Node([ - 'values' => new Node($values), + $cases[] = new class([ + 'values' => new Nodes($values), 'body' => $body - ]); + ]) extends Node {}; break; case 'default': @@ -97,7 +99,7 @@ class TwigTokenParserSwitch extends AbstractTokenParser $stream->expect(Token::BLOCK_END_TYPE); - return new TwigNodeSwitch($name, new Node($cases), $default, $lineno, $this->getTag()); + return new TwigNodeSwitch($name, new Nodes($cases), $default, $lineno, $this->getTag()); } /** diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserThrow.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserThrow.php index 63527cafe..992342908 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserThrow.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserThrow.php @@ -37,7 +37,7 @@ class TwigTokenParserThrow extends AbstractTokenParser $stream = $this->parser->getStream(); $code = $stream->expect(Token::NUMBER_TYPE)->getValue(); - $message = $this->parser->getExpressionParser()->parseExpression(); + $message = $this->parser->parseExpression(); $stream->expect(Token::BLOCK_END_TYPE); return new TwigNodeThrow((int)$code, $message, $lineno, $this->getTag()); diff --git a/system/src/Grav/Common/Twig/TwigEnvironment.php b/system/src/Grav/Common/Twig/TwigEnvironment.php index 5afa9389c..f5ee39a36 100644 --- a/system/src/Grav/Common/Twig/TwigEnvironment.php +++ b/system/src/Grav/Common/Twig/TwigEnvironment.php @@ -11,8 +11,11 @@ namespace Grav\Common\Twig; use Twig\Environment; use Twig\Error\LoaderError; +use Twig\Extension\EscaperExtension; +use Twig\Extension\ExtensionInterface; use Twig\Loader\ExistsLoaderInterface; use Twig\Loader\LoaderInterface; +use Twig\Runtime\EscaperRuntime; use Twig\Template; use Twig\TemplateWrapper; @@ -22,6 +25,39 @@ use Twig\TemplateWrapper; */ class TwigEnvironment extends Environment { + /** + * @inheritDoc + */ + public function getExtension(string $name): ExtensionInterface + { + $extension = parent::getExtension($name); + + if ($name === EscaperExtension::class && class_exists(EscaperRuntime::class)) { + return new class($extension, $this) extends EscaperExtension { + private $original; + private $env; + + public function __construct($original, $env) + { + $this->original = $original; + $this->env = $env; + } + + public function setEscaper($strategy, $callable) + { + $this->env->getRuntime(EscaperRuntime::class)->setEscaper($strategy, $callable); + } + + public function getDefaultStrategy($filename) + { + return $this->original->getDefaultStrategy($filename); + } + }; + } + + return $extension; + } + /** * @inheritDoc * diff --git a/system/src/Twig/DeferredExtension/DeferredTokenParser.php b/system/src/Twig/DeferredExtension/DeferredTokenParser.php index 9ae3cc0e6..1be3e1264 100644 --- a/system/src/Twig/DeferredExtension/DeferredTokenParser.php +++ b/system/src/Twig/DeferredExtension/DeferredTokenParser.php @@ -13,66 +13,70 @@ declare(strict_types=1); namespace Twig\DeferredExtension; +use Twig\Error\SyntaxError; use Twig\Node\BlockNode; +use Twig\Node\BlockReferenceNode; +use Twig\Node\EmptyNode; use Twig\Node\Node; -use Twig\Parser; +use Twig\Node\Nodes; +use Twig\Node\PrintNode; use Twig\Token; use Twig\TokenParser\AbstractTokenParser; -use Twig\TokenParser\BlockTokenParser; final class DeferredTokenParser extends AbstractTokenParser { - private $blockTokenParser; - - public function setParser(Parser $parser) : void - { - parent::setParser($parser); - - $this->blockTokenParser = new BlockTokenParser(); - $this->blockTokenParser->setParser($parser); - } - public function parse(Token $token) : Node { + $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $nameToken = $stream->next(); - $deferredToken = $stream->nextIf(Token::NAME_TYPE, 'deferred'); - $stream->injectTokens([$nameToken]); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); + + $deferred = $stream->nextIf(Token::NAME_TYPE, 'deferred'); - $node = $this->blockTokenParser->parse($token); - - if ($deferredToken) { - $this->replaceBlockNode($nameToken->getValue()); + if ($this->parser->hasBlock($name)) { + throw new SyntaxError(\sprintf("The block '%s' has already been defined line %d.", $name, $this->parser->getBlock($name)->getTemplateLine()), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } - return $node; + if ($deferred) { + $block = new DeferredBlockNode($name, new EmptyNode(), $lineno); + } else { + $block = new BlockNode($name, new EmptyNode(), $lineno); + } + + $this->parser->setBlock($name, $block); + $this->parser->pushLocalScope(); + $this->parser->pushBlockStack($name); + + if ($stream->nextIf(Token::BLOCK_END_TYPE)) { + $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); + if ($token = $stream->nextIf(Token::NAME_TYPE)) { + $value = $token->getValue(); + + if ($value != $name) { + throw new SyntaxError(\sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + } + } else { + $body = new Nodes([ + new PrintNode($this->parser->parseExpression(), $lineno), + ]); + } + $stream->expect(Token::BLOCK_END_TYPE); + + $block->setNode('body', $body); + $this->parser->popBlockStack(); + $this->parser->popLocalScope(); + + return new BlockReferenceNode($name, $lineno, $this->getTag()); + } + + public function decideBlockEnd(Token $token): bool + { + return $token->test('endblock'); } public function getTag() : string { return 'block'; } - - private function replaceBlockNode(string $name) : void - { - $blockContainer = $this->parser->getBlock($name); - $block = $blockContainer->getNode('0'); - $blockContainer->setNode('0', $this->createDeferredBlockNode($block)); - } - - private function createDeferredBlockNode(BlockNode $block) : DeferredBlockNode - { - $name = $block->getAttribute('name'); - $deferredBlock = new DeferredBlockNode($name, new Node([]), $block->getTemplateLine()); - - foreach ($block as $nodeName => $node) { - $deferredBlock->setNode($nodeName, $node); - } - - if ($sourceContext = $block->getSourceContext()) { - $deferredBlock->setSourceContext($sourceContext); - } - - return $deferredBlock; - } }