more robust deferred logic + deprecated fixes

Signed-off-by: Andy Miller <rhuk@mac.com>
This commit is contained in:
Andy Miller
2025-11-21 12:25:58 +00:00
parent 370dfd6016
commit 114aebae7c
11 changed files with 117 additions and 75 deletions

8
composer.lock generated
View File

@@ -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",

View File

@@ -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);
}
/**

View File

@@ -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());
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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());
}
/**

View File

@@ -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());

View File

@@ -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
*

View File

@@ -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;
}
}