From 7fd614f8b6b51b2a0eae26cd7ad1d382aba86c56 Mon Sep 17 00:00:00 2001 From: Andy Miller Date: Tue, 18 Nov 2025 17:46:20 +0000 Subject: [PATCH] Add Twig 3 compatibility transformations for raw, divisibleby, and none --- .../Twig3CompatibilityTransformer.php | 57 ++++++++++++++++++- .../Twig3CompatibilityTransformerTest.php | 49 ++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/system/src/Grav/Common/Twig/Compatibility/Twig3CompatibilityTransformer.php b/system/src/Grav/Common/Twig/Compatibility/Twig3CompatibilityTransformer.php index caa07cbac..9ef45d5bf 100644 --- a/system/src/Grav/Common/Twig/Compatibility/Twig3CompatibilityTransformer.php +++ b/system/src/Grav/Common/Twig/Compatibility/Twig3CompatibilityTransformer.php @@ -18,7 +18,10 @@ class Twig3CompatibilityTransformer $code = $this->rewriteSpacelessBlocks($code); $code = $this->rewriteFilterBlocks($code); $code = $this->rewriteSameAsTests($code); + $code = $this->rewriteDivisibleByTests($code); + $code = $this->rewriteNoneTests($code); $code = $this->rewriteReplaceFilterSignatures($code); + $code = $this->rewriteRawBlocks($code); return $code; } @@ -135,12 +138,12 @@ class Twig3CompatibilityTransformer private function rewriteSameAsTests(string $code): string { - $pattern = '/([\'"])(?:\\\\.|(?!\\1).)*\\1|\\bis\\s+sameas\\b/is'; + $pattern = '/([\'"])(?:\\\\.|(?!\\1).)*\\1|\\bis\\s+(?:not\\s+)?sameas\\b/is'; return (string) preg_replace_callback($pattern, static function ($matches) { // If group 1 is not set, it means 'is sameas' was matched. if (!isset($matches[1])) { - return str_ireplace('is sameas', 'is same as', $matches[0]); + return str_ireplace('sameas', 'same as', $matches[0]); } // Otherwise, it's a quoted string, so return it as is. @@ -163,6 +166,56 @@ class Twig3CompatibilityTransformer return $code; } + private function rewriteRawBlocks(string $code): string + { + $openPattern = '/\{%(\-?)\s*raw\s*(\-?)%\}/i'; + $code = (string) preg_replace_callback($openPattern, static function (array $matches): string { + $leading = $matches[1] === '-' ? '-' : ''; + $trailing = $matches[2] === '-' ? '-' : ''; + + return '{%' . $leading . ' verbatim ' . $trailing . '%}'; + }, $code); + + $closePattern = '/\{%(\-?)\s*endraw\s*(\-?)%\}/i'; + + return (string) preg_replace_callback($closePattern, static function (array $matches): string { + $leading = $matches[1] === '-' ? '-' : ''; + $trailing = $matches[2] === '-' ? '-' : ''; + + return '{%' . $leading . ' endverbatim ' . $trailing . '%}'; + }, $code); + } + + private function rewriteDivisibleByTests(string $code): string + { + $pattern = '/([\'"])(?:\\\\.|(?!\\1).)*\\1|\\bis\\s+(?:not\\s+)?divisibleby\\b/is'; + + return (string) preg_replace_callback($pattern, static function ($matches) { + // If group 1 is not set, it means 'is divisibleby' was matched. + if (!isset($matches[1])) { + return str_ireplace('divisibleby', 'divisible by', $matches[0]); + } + + // Otherwise, it's a quoted string, so return it as is. + return $matches[0]; + }, $code); + } + + private function rewriteNoneTests(string $code): string + { + $pattern = '/([\'"])(?:\\\\.|(?!\\1).)*\\1|\\bis\\s+(?:not\\s+)?none\\b/is'; + + return (string) preg_replace_callback($pattern, static function ($matches) { + // If group 1 is not set, it means 'is none' was matched. + if (!isset($matches[1])) { + return str_ireplace('none', 'null', $matches[0]); + } + + // Otherwise, it's a quoted string, so return it as is. + return $matches[0]; + }, $code); + } + private function ensureWrapped(string $expression): string { $trimmed = trim($expression); diff --git a/tests/unit/Grav/Common/Twig/Compatibility/Twig3CompatibilityTransformerTest.php b/tests/unit/Grav/Common/Twig/Compatibility/Twig3CompatibilityTransformerTest.php index 632170c4e..237cabfdb 100644 --- a/tests/unit/Grav/Common/Twig/Compatibility/Twig3CompatibilityTransformerTest.php +++ b/tests/unit/Grav/Common/Twig/Compatibility/Twig3CompatibilityTransformerTest.php @@ -30,6 +30,11 @@ class Twig3CompatibilityTransformerTest extends \PHPUnit\Framework\TestCase $this->transformer->transform('{% if foo is sameas bar %}') ); + $this->assertSame( + '{% if foo is not same as bar %}', + $this->transformer->transform('{% if foo is not sameas bar %}') + ); + $this->assertSame( "'foo is sameas(bar)'", $this->transformer->transform("'foo is sameas(bar)'") @@ -102,4 +107,48 @@ class Twig3CompatibilityTransformerTest extends \PHPUnit\Framework\TestCase $this->transformer->transform('{{ "hello world"|replace("hello", "goodbye") }}') ); } + + public function testRewriteRawBlocks(): void + { + $this->assertSame( + '{% verbatim %}foo{% endverbatim %}', + $this->transformer->transform('{% raw %}foo{% endraw %}') + ); + + $this->assertSame( + '{%- verbatim -%}foo{%- endverbatim -%}', + $this->transformer->transform('{%- raw -%}foo{%- endraw -%}') + ); + } + + public function testRewriteDivisibleBy(): void + { + $this->assertSame( + '{% if loop.index is divisible by(3) %}', + $this->transformer->transform('{% if loop.index is divisibleby(3) %}') + ); + + $this->assertSame( + '{% if loop.index is divisible by (3) %}', + $this->transformer->transform('{% if loop.index is divisibleby (3) %}') + ); + + $this->assertSame( + '{% if loop.index is not divisible by(3) %}', + $this->transformer->transform('{% if loop.index is not divisibleby(3) %}') + ); + } + + public function testRewriteNoneTest(): void + { + $this->assertSame( + '{% if var is null %}', + $this->transformer->transform('{% if var is none %}') + ); + + $this->assertSame( + '{% if var is not null %}', + $this->transformer->transform('{% if var is not none %}') + ); + } } \ No newline at end of file