Twig: Render block multiple times: parsed, escaped, verbatim - twig

I’m building a pattern library that displays my production twig partials with some additional information. Under each pattern, I want to have two code snippets that I want to be generated from my pattern. One is the parsed HTML, the other one is my Twig code directly from the partial file, including variables and other Twig code.
Question
How do I reuse a block that I already parsed and rendered on my page, but in its unparsed form?
Files:
moduleXYZ.html (production module code), which extends…
_pattern.html (wrapper with additional informational code for pattern library), which uses…
_pattern_foot.html (in here, the block defined in _pattern.html and overwritten in moduleXYZ.html should be displayed verbatim)
moduleXYZ.html:
{% extends '_pattern.html' %}
{% block pattern %}
<h2>{{ variable|default('Some placeholder text') }}</h2>
{% endblock %}
_pattern.html:
{% block pattern %}
No pattern defined.
{% endblock %}
{% use '_pattern-foot.html' %}
{% block('patternfoot') %}
_pattern-foot.html:
{% block patternfoot %}
<h2>HTML for this pattern:</h2>
<pre><code>
{{ block('pattern')|e }} {# THIS WORKS, escaped HTML is displayed. #}
</code></pre>
<h2>Twig for this pattern:</h2>
<pre><code>
{{ block('pattern')|e }} {# THIS IS NOT WORKING, of course, because the block is already parsed. #}
</code></pre>
{% endblock %}
What didn’t work
I managed to get the result I want by wrapping the "pattern" block in moduleXYZ.html in {% verbatim %}, but then of course my logic/variables go unparsed in the module itself, too.
I understand it’s not possible to pass variables into the block() function, so I also can’t toggle verbatim conditionally for my reused block (or can I?).

Related

TWIG / GravCMS: Use loop-variable of for-loop inside modular template

Here is my current code:
{% for module in page.collection() %}
{% set index = loop.index %}
{{ module.content|raw }}
{% endfor %}
I'd like to access index inside the module.html.twig, or even better, the entire loop variable.
How do I do that?
I found it myself:
{% for module in page.collection() if not module.header.visible is same as(false) %}
{% include module.template ~ '.html.twig' with {'page':module, 'loop':loop} %}
{% endfor %}
This loop willautomatically grab the template which is linked to the modular page and pass the required variables down. Also, the loop will only include modular subpages which are not hidden. Great, isn't it?

Set a property value in an existing twig object

I would like to define a twig object that contains two properties :
The first one is a short text
The second one is a large text (so, i need to use {% set %}{% endset %} to keep readability of the template)
I first tried like this :
{% block principal %}
{% set a_form_help = {'help_toggle_label': 'See the big form field tip', 'help_content': ''} %}
{% set a_form_help.help_content %}>
<h1>This is rules for the field</h1>
<p>A looonng text that will be contained in a collapsed block</p>
<p>And several lines of tips for this field</p>
{% endset %}
{% endblock %}
But Twig complains about the use of the dot in the second {% set %} tag (for setting the large text content in the help_content property).
I've found this message (Twig - Why does it not allow us to set object / array values?) and then done this workaround by using a temporary variable:
{% block principal %}
{% set tmp_help_content %}>
<h1>This is rules for the field</h1>
<p>A looonng text that will be contained in a collapsed block</p>
{% endset %}
{% set a_form_help = {'help_toggle_label': 'See the big form field tip', 'help_content': tmp_help_content} %}
{{ dump(a_form_help) }}
{% endblock %}
I've used a a temporary variable since using the merge() filter would give the same result.
Is this workaroud a good practice or is there a better/cleaner way ?
Thanks in advance for any tip|advice ! :-)
There is no clean way to do it in twig.
Is this workaroud a good practice or is there a better/cleaner way ?
It is not a good practice. As twig is a templating language its responsability is only to display data, not to structure it. Your View Object should have been created elsewhere

"Illegal offset type in isset or empty" when using a Twig macro

Using Grav v1.3.8 (running on PHP 5.6.30), I'm currently getting a Server error ("Illegal offset type in isset or empty") when trying to render a Twig template that is using a macro.
What's interesting is that this only happens when I use the macro by itself. As soon as I append a filter, like trim, everything works as expected.
The (shortened) macro file, helpers.twig:
{% macro ascii(str) %}
{% spaceless %}
{{ str|replace({
'Á': 'A',
'À': 'A',
'Â': 'A',
'Ã': 'A',
....
'ƒ': 'f'
})
}}
{% endspaceless %}
{% endmacro ascii %}
The template (MCVE):
{% import 'macros/helpers.twig' as helpers %}
{% set img = helpers.ascii('günter-berger.jpg') %}
{% if page.media[img] is defined %}
<img src="{{ page.media[img].url }}">
{% endif %}
This will produce the error. I narrowed it down to the if line. Apparently, the macro is working fine, but the condition will throw an error if fed the output of it, unfiltered. Adding any filter, like trim or lower, will get it to work again.
In other words, these work:
{% if page.media['günter-berger.jpg'] is defined %}
{% if page.media[helpers.ascii('günter-berger.jpg')|trim] is defined %}
But this will throw an error:
{% if page.media[helpers.ascii('günter-berger.jpg')] is defined %}
However, trying the same thing on twigfiddle, all three seem to work there.
Maybe an issue with Grav? Can someone point out any possible causes?
I forgot this, but a macro does not return a string but instead returns an instance of a Twig_Markup
{% set test = macro.ascii('Ghünter.jpg') %}
{{ dump(test) }}
Output : object(Twig_Markup)#10679 (2) { ["content":protected]=> string(11) "Ghunter.jpg" ["charset":protected]=> string(5) "UTF-8" }
Because the return type is an object you get this notification as you can't use objects as index. By using a filter on this instance, the magic method __toString method will be called, causing it to return a string, thus making it useable as index for an array
The only was to bypass this, would be writing a filter instead of a macro

Prevent quotes from being converted to HTML entities in twig ternary operator

The following does not convert the quotes to HTML entities
{% for row in files %}
<tr data-id="{{ row.id }}"><td>{{ row.name }}</td></tr>
{% endfor %}
The following does convert the quotes to HTML entities
{% for row in files %}
<tr{{ row.id?' data-id="'~row.id~'"' }}><td>{{ row.name }}</td></tr>
{% endfor %}
How can I prevent quotes from being converted to HTML entities in a twig ternary operator?
You should try the |raw filter (check out the documentation).
This is because in general, everything that twig prints out will be escaped to avoid things like cross-site-scripting. An exception is made for entirely static values like {{ '<b>static value</b>' }} which will not be escaped.
In your case, the following should work:
{% for row in files %}
<tr{{ (row.id?' data-id="'~row.id~'"')|raw }}><td>{{ row.name }}</td></tr>
{% endfor %}

How to pass the current context to include in Twig?

Say I have the following headline.twig partial:
<h2>{{ headline }}</h2>
and I want to include it in two places, once as:
<% for article in teasers %>
{{ include('headline.twig') }}
<% endfor %>
And then simply:
{{ include('headline.twig') }}
Is it possible to pass an include tag or function its context, so that the include "knows" that in the first instance the headline variable is actually article.headline?
I'm looking for a systematic way to do this, not with something like
{{ include('headline.twig', {headline: article.headline}) }}
If you want headline to be in the main context of your included file in all cases, you can do something like this:
{% for article in teasers %}
{% set headline = article.headline %}
{{ include('headline.twig') }}
{% endfor %}
But this will overwrite any existing headline variable in your current context if you're re-setting it this way (and risk to repeat 2 times the last iteration of teasers.article.headline).
The best solution if you want to keep your current context AND overwrite headline variable is to use the merge filter:
{% for article in teasers %}
{{ include('headline.twig', _context|merge({headline: article.headline})) }}
{% endfor %}
By default whole context is passed to included template.
So it will work in headline.twig:
<h2>{{ article.headline }}</h2>
And then in main template you only call:
{% include('headline.twig') %}
But article variable must be defined in moment of calling include.

Resources