Skip to content

Commit

Permalink
Version 5.0.1 - twig 3.12 fixes (Brandon)
Browse files Browse the repository at this point in the history
- added twig test code
- Brandon’s twig 3.12 breaking {% return %} fix
  • Loading branch information
marionnewlevant committed Nov 28, 2024
1 parent 23985a4 commit 4db6047
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 13 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Twig Perversion Changelog

## 5.0.1 - 2024.11.28

###

- Merged in Brandon's fix for twig 3.12 breaking `{% return %}`

## 5.0.0 - 2024.04.29

### Changed
Expand Down
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
# Twig Perversion plugin for Craft CMS

## Obituary

Twig Perversions tags no longer work as of twig 3.12 (Craft version 5.4). Twig used to compile its macros into fairly straightforward functions, which Twig Perversion was able to hack. They are now using generator functions, and the old hacks no longer work.

## Intro

Making twig do things it really shouldn't. Twig is not intended to be a general purpose programming language, and there are some things that really don't belong in the language. This plugin adds a few of those things anyway.

- `{% while %}`, `{% break %}`, `{% continue %}`, and `{% return %}` tags
Expand All @@ -15,7 +9,7 @@ Making twig do things it really shouldn't. Twig is not intended to be a gene

## Requirements

This plugin requires Craft CMS 3.1.29 or later.
This plugin requires Craft CMS 4.12 or later.

## Installation

Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "marionnewlevant/twig-perversion",
"description": "Making twig do things it really shouldn't",
"version": "5.0.0",
"version": "5.0.1",
"type": "craft-plugin",
"keywords": [
"craft",
Expand All @@ -23,7 +23,7 @@
},
"require": {
"php": ">=8.2",
"craftcms/cms": "5.0 - 5.3"
"craftcms/cms": "^5.0"
},
"autoload": {
"psr-4": {
Expand Down
33 changes: 33 additions & 0 deletions src/Plugin.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,41 @@
<?php
namespace marionnewlevant\twigperversion;

use Twig\Markup;

class Plugin extends \craft\base\Plugin
{
/**
* @var array Values being returned by macros via `{% return %}` tags.
* @since 2.3.0
*/
public static $returnValues = [];

/**
* Processes a macro response, searching for return markers left by `{% return %}` tags.
*
* If one is found, the value passed to the `{% return %}` tag will be returned instead.
*
* @param mixed $response
* @return mixed
* @since 2.3.0
*/
public static function processMacroResponse(mixed $response): mixed
{
if ($response instanceof Markup) {
$markup = (string)$response;
$markerPos = strpos($markup, '[RETURN_MARKER:');
if ($markerPos !== false) {
$endPos = strpos($markup, ']', $markerPos);
$marker = substr($markup, $markerPos, $endPos - $markerPos + 1);
if (isset(self::$returnValues[$marker])) {
return self::$returnValues[$marker];
}
}
}
return $response;
}

public function init()
{
parent::init();
Expand Down
36 changes: 36 additions & 0 deletions src/twigextensions/MacroProcessor_Node.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
namespace marionnewlevant\twigperversion\twigextensions;

use marionnewlevant\twigperversion\Plugin;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;

/**
* Twig Perversion
*
* @package TwigPerversion
* @author Marion Newlevant
* @copyright Copyright (c) 2024, Marion Newlevant
* @license MIT
* @link https://github.com/marionnewlevant/craft-twig_perversion
* @since 2.3.0
*/

class MacroProcessor_Node extends AbstractExpression
{
public function compile(Compiler $compiler): void
{
$compiler
->raw("\n")
->indent()
->write(sprintf("%s::processMacroResponse(\n", Plugin::class))
->indent()
->write('')
->subcompile($this->getNode('methodCallExpression'))
->raw("\n")
->outdent()
->write(")\n")
->outdent()
->write('');
}
}
17 changes: 14 additions & 3 deletions src/twigextensions/Return_Node.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?php
namespace marionnewlevant\twigperversion\twigextensions;

use marionnewlevant\twigperversion\Plugin;

/**
* Twig Perversion
*
Expand All @@ -20,8 +22,13 @@ class Return_Node extends \Twig\Node\Node
public function compile(\Twig\Compiler $compiler)
{
$compiler
->addDebugInfo($this)
->write('return ');
->addDebugInfo($this)
->write("if (!isset(\$returnedMarker)) {\n")
->indent()
->write("\$returnedMarker = true;\n")
->write("\$marker = sprintf('[RETURN_MARKER:%s]', mt_rand());\n")
->write(sprintf("%s::\$returnValues[\$marker] = ", Plugin::class));

// check for an expression to return.
if ($this->hasNode('expr')) {
$compiler->subcompile($this->getNode('expr'));
Expand All @@ -31,6 +38,10 @@ public function compile(\Twig\Compiler $compiler)
$compiler->raw('""');
}

$compiler->raw(";\n");
$compiler
->raw(";\n")
->write("yield \$marker;\n")
->outdent()
->write("}\n");
}
}
42 changes: 42 additions & 0 deletions src/twigextensions/Return_NodeVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
namespace marionnewlevant\twigperversion\twigextensions;

use Twig\Environment;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Node;
use Twig\NodeVisitor\NodeVisitorInterface;

/**
* Twig Perversion
*
* @package TwigPerversion
* @author Marion Newlevant
* @copyright Copyright (c) 2024, Marion Newlevant
* @license MIT
* @link https://github.com/marionnewlevant/craft-twig_perversion
* @since 2.3.0
*/
class Return_NodeVisitor implements NodeVisitorInterface
{
public function enterNode(Node $node, Environment $env): Node
{
return $node;
}

public function leaveNode(Node $node, Environment $env): ?Node
{
if ($node instanceof MethodCallExpression && !$node->getAttribute('is_defined_test')) {
return new MacroProcessor_Node([
'methodCallExpression' => $node,
], [
'is_generator' => $node->hasAttribute('is_generator') ? $node->getAttribute('is_generator') : false,
]);
}
return $node;
}

public function getPriority()
{
return 0;
}
}
9 changes: 8 additions & 1 deletion src/twigextensions/TwigPerversionTwigExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,14 @@ public function getOperators()
];
}

/**
public function getNodeVisitors()
{
return [
new Return_NodeVisitor(),
];
}

/**
* Cast value as a string.
*
* @param mixed $subject Value to be cast.
Expand Down
137 changes: 137 additions & 0 deletions twig-perversion-tests.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<h1> Twig-perversion tests</h1>
<p>twig code for testing twig-perversion</p>

<h2>return</h2>
{% macro testMacro() %}
{% return "test" %}
ignore me...
{% endmacro %}

{% set x = _self.testMacro() %}

x = {{x}}
{# x = test #}
<hr>
{% macro t2(v) %}
{% if v < 0 %}
{% return "negative" %}
ignore me
{% else %}
{% return "positive" %}
{% endif %}
ignore me
{% endmacro %}

negative: {{ _self.t2(-4)}}
positive: {{ _self.t2(4)}}

<hr>
{% macro double(n) %}
{% return n * 2 %}
ignore me
{% endmacro %}
double(4): {{_self.double(4)}}

{% macro triple(n) %}
{% return n + _self.double(n) %}
ignore me
{% endmacro %}
triple(4): {{_self.triple(4)}}
<hr>
{% macro recur(n) %}
{{n}}
{% if n > 1 %}
{{ _self.recur(n-1)}}
{% endif %}
{% endmacro %}

{{ _self.recur(5)}}
<hr>
{% macro fact(n) %}
{% if n == 1 %}
{% return 1 %}
{% endif %}
{% return n * _self.fact(n-1) %}
{% endmacro %}

1!: {{ _self.fact(1)}}
{% set fact5 = _self.fact(5) %}
5! = {{fact5}}

<h2>while loops</h2>
while loop indexes<br>
{% set x = 0 %}
{% while x < 5 %}
{{x}} loop: {{loop.index}} {{loop.index0}} {{loop.first ? 'first' : 'not first'}}<br>
{% set x = x + 1 %}
{% endwhile %}
<hr>
while with loop and loop.parent indexes<br>
{% set x = 0 %}
{% while x < 5 %}
{% set y = 0 %}
{% while y < 3 %}
loop: {{loop.index}} {{loop.index0}} {{loop.first ? 'first' : 'not first'}}
parent: {{loop.parent.loop.index}} {{loop.parent.loop.index0}} {{loop.parent.loop.first ? 'first': 'not'}}<br>
{% set y = y + 1 %}
{% endwhile %}
{% set x = x + 1 %}
{% endwhile %}
<hr>
while w/ continue on even:
{% set x = 0 %}
{% while x < 5 %}
{% set x = x + 1 %}
{{x}}
{% if x % 2 == 0 %} {% continue %} {% endif %}
odd
{% endwhile %}
<hr>
while w/ break on 3:
{% set x = 0 %}
{% while x < 5 %}
{{x}}
{% set x = x + 1 %}
{% if x == 3 %} {% break %} {% endif %}
..
{% endwhile %}
<hr>

<h4>numeric</h4>
12 ? {{ 12 is numeric ? 'Yes' : 'No' }}
{# Yes #}

-1.3 ? {{ '-1.3' is numeric ? 'Yes' : 'No' }}
{# Yes #}

0x539 ? {{ '0x539' is numeric ? 'Yes' : 'No'}}
{# No #}

<h4>return types</h4>
{% macro foo() %}
{# ... calculate someValue ... #}
{% set someValue = 'our return value' %}
{% return someValue %}
{% endmacro %}

{% macro bar() %}
{% set x = 0 %}
{% while x < 5 %}
x = {{x}}
{% set x = x + 1 %}
{% endwhile %}
{% return %}
{% endmacro %}

{% macro baz() %}
{% return 23 %}
{% endmacro %}

{{ _self.foo() }}
{{ _self.foo() is string ? 'foo is string' : 'foo is not string' }}<br>

{{ _self.bar() }}
{{ _self.bar() is string ? 'bar is string' : 'bar is not string' }}<br>

{{ _self.baz() }}
{{ _self.baz() is string ? 'baz is string' : 'baz is not string' }}<br>

0 comments on commit 4db6047

Please sign in to comment.