Use custom delimiters in the current Twig template - php

I use Twig to generate LaTeX documents. Twig's default delimiter syntax clashes badly with LaTeX's curly braces. Simply escaping LaTeX is no option as it makes the code completely unreadable. I know I can define custom delimiters globally, but I don't want to rewrite all of my HTML templates to use the new syntax.
I also know about verbatim sections but those make the code truly ugly:
\ihead{
{% endverbatim %}
{{ title }}
{% verbatim %}
}
Is there a way I can change the syntax for just the current template or a set of templates, something like:
{% set_delimiters({
'tag_comment' : ['<%#', '%>'],
'tag_block' : ['<%' , '%>'],
'tag_variable' : ['<%=', '%>'],
'interpolation': ['#<' , '>']
}) %}

As you can see, it's not recommanded to use this feature Customizing the Syntax
BTW here's a quick and easy example to explain how to use custom delimiters in symfony:
service.yml
services:
templating_lexer:
public: true
parent: templating.engine.twig
class: Acme\YourBundle\Twig\TwigLexerEngine
TwigLexerEngine
namespace Acme\YourBundle\Twig;
use Symfony\Bundle\TwigBundle\TwigEngine;
class TwigLexerEngine extends TwigEngine
{
public function setTwigLexer($lexer)
{
$this->environment->setLexer($lexer);
return $this;
}
}
Your controller
public function yourAction()
{
$lexer = new \Twig_Lexer($this->get('twig'), array(
'tag_comment' => array('{*', '*}'),
'tag_block' => array('{', '}'),
'tag_variable' => array('{$', '}'),
));
$templating = $this->get('templating_lexer');
$templating->setTwigLexer($lexer);
return $templating->renderResponse('YourBundle::template.html.twig');
}

Related

how to use php and volt parameter together?

I am using Volt and PHP in phalcon volt partial
I want to iterate a loop and in the loop I have php code that accept a parameter
this is my code
{% for header in headers %}
<th>
<?=gettext( {{header}} );?>
</th>
{% endfor %}
here header is a parameter that is used in php code
but I get this error
what is the right way to rewrite this code
You can configure some services in a better way to help you solve this issue.
If you define your view service like:
$di->set('view', function () {
$view = new View();
$view->setDI($this);
$view->setViewsDir(__DIR__ . '/views/');
$view->registerEngines([
'.volt' => 'voltShared'
]);
return $view;
});
You can define your voltShared service improving the volt compiler with custom functions in this way:
$di->setShared('voltShared', function ($view) use ($di) {
$config = $this->getConfig();
$volt = new VoltEngine($view, $this);
$volt->setOptions([
'autoescape' => false,
'compileAlways' => true,
'stat' => true,
'compiledSeparator' => '_',
'compiledPath' => $config->application->cacheDir.'volt/',
]);
// We add some custom functions
$compiler = $volt->getCompiler();
$compiler->addFunction('gettext', function ($resolvedArgs) {
return 'gettext('.$resolvedArgs.')';
})
return $volt;
});
I'm guessing you are trying to use a raw PHP function here, so correct me if I'm wrong. Unfortunately I forget the exact reason this is (I learned a long time), but you have to manually register PHP functions in the Volt Engine in order to use them. This can be done using the addFunction method of the Engine will allow you to add them. I thought it had been resolved that this wasn't needed anymore, but it was reported in https://github.com/phalcon/cphalcon/pull/12841.
Anyways, using addFunction: I've used the below to get around this:
foreach (get_defined_functions()['Internal'] as $functionName) {
$voltEngine->addFunction($functionName, $functionName);
}
You have to put this in the code that initializes the volt engine.
Reading documentation shows that echo command is using {{ }} instead of normal PHP tag <?= ?>:
{% for header in headers %}
<th>
{{ gettext(header) }}
</th>
{% endfor %}

Alternative to template_from_string for processing strings with calls to custom Twig functions

Is there an alternative to using template_from_string() for evaluating strings that contain calls to custom Twig functions (or any other complex Twig code for that matter)?
This is a simplified version of the code I have right now.
{% set content = {
'item_1' : {
'images' : {
'src': 'assets/images/img_1-600.jpg',
'srcset': "{{ assets('assets/images/img_1-600.jpg') }} 600w, {{ assets('assets/images/img_1-1200.jpg') }} 1200w",
}
}
# additional items follow
}
{% for data in content %}
<img src="{{ data.images.src }}" srcset="{{ include(template_from_string(data.images.srcset)) }}" alt="">
{% endfor %}
The assets() function simply returns a revisioned version of the static asset for the given path (i.e. assets('assets/images/img_1-600.jpg) renders as something like 'assets/images/img_1-600-a4c2254a6d.jpg).
The problems start with img srcset attribute which can become pretty complex and usually contains references to multiple assets that need to return revisioned values for static assets.
Now I know that I could modify the assets() function to support that kind of complex scenario but I'd like to keep things simple and let assets() only handle 1:1 transformations.
The only way to achieve this by using functionality provided by Twig seems to be template_from_string() combined with include, which in itself is not terrible but it does kind of look bulky for a simple operation as evaluating and rendering a string.
Not to mention that template_from_string requires StringLoaderExtension() to be loaded by default, which again I'd like to avoid using for performance reasons.
While the concatenation approach is not a bad idea, after much consideration I came to the conclusion that this is one of those places where having a custom Twig function makes much more sense.
The main argument in favor of using a function compared to simple concatenation is that with a function there is no need to worry about properly formating the template code (i.e. forget a space between the asset and the size descriptor in the srcset attribute value).
This approach also completely eliminates any need to use template_from_string() or additional extension dependencies.
Here is the final solution.
The template code, plain and simple, with plenty of overview.
{% set content = {
'item_1' : {
'images' : {
'src': 'assets/images/img-600.jpg',
'srcset': srcset([
[ assets('assets/images/img-600.jpg'), '600w' ],
[ assets('assets/images/img-800.jpg'), '800w' ],
[ assets('assets/images/img-1000.jpg'), '1000w' ],
[ assets('assets/images/img-1200.jpg'), '1200w' ]
]),
}
}
# additional items follow
}
{% for data in content %}
<img src="{{ data.images.src }}" srcset="{{ data.images.srcset }}" alt="">
{% endfor %}
The actual Twig function called srcset, that is used to generate a value for the srcset attribute from provided data in the above template.
public function srcset(array $srcset)
{
$output = [];
foreach ($srcset as $set) {
// Both parameters need to exist or the set isn't added to the output...
if (!(isset($set[0]) && isset($set[1]))) {
// ... just some exception handling that isn't of relevance for the question
// ...
continue;
}
$output[] = sprintf('%s %s', $set[0], $set[1]);
}
return implode(', ', $output);
}
To avoid any extra plugins, you could just concat the variables
{% set content = {
'item_1' : {
'images' : {
'src': 'assets/images/img_1-600.jpg',
'srcset': assets('assets/images/img_1-600.jpg')~' 600w,'~assets('assets/images/img_1-1200.jpg')~' 1200w',
}
}
} %}

Symfony2 translation yaml array and twig loop

I am trying to have twig convert an array from the translation file
// messages.en.yml
termsAndConditions:
title: Terms and Conditions
paragraph:
- Paragraph text...blah blah...1
- Paragraph text...blah blah...2
- Paragraph text...blah blah...3
- Paragraph text...blah blah...n
// termsandconditions.html.twig
// tried...
{% for i in range(1,termsAndConditions.paragraph|length) -%}
<p>{% trans %}termsAndConditions.paragraph. {{ i }}{% endtrans %}</p>
{%- endfor %}
// and tried...
{% for i in range(1,termsAndConditions.paragraph|length) -%}
<p>{{ 'termsAndConditions.paragraph.' ~ i ~ |trans }}</p>
{%- endfor %}
You need to use pairs of keys: values to get things to work:
// messages.en.yml
termsAndConditions:
title: Terms and Conditions
paragraph:
1: Paragraph text...blah blah...1
2: Paragraph text...blah blah...2
3: Paragraph text...blah blah...3
4: Paragraph text...blah blah...n
Also and due to the fact that you want to use a variable for your translation, access to the translation this way {{('termsAndConditions.paragraph.'~i)|trans }}.
I've hardcoded 4 instead of termsAndConditions.paragraph|length. Not really sure if you can access that from a twig template...
// termsandconditions.html.twig
{% for i in range(1,4) -%}
<p>{{('termsAndConditions.paragraph.'~i)|trans}}</p>
{%- endfor %}
It should work. Hope it helps.
UPDATE
termsAndConditions.paragraph|length makes no sense unless you've defined in the template the variable or you've injected the variable through the controller.
Solutions
Solution 1. In your controller, access the yaml and get the number of translations, then pass it to the template and that's it.
Solution 2. Create a service and inject the translator service to it. In the controller create a method that calculates the number of elements of a particular key. Injecting the translator service is better than reading the yaml directly as it caches the catalogue when it reads it.
Solution 3. Same as 2 but creating a twig filter. I'm going to go for this one as it seems kind of fun.
Solution 3
Let's start by creating the Extension:
namespace Acme\DemoBundle\Twig\Extension;
class TransLengthExtension extends \Twig_Extension
{
private $translator;
public function __construct($translator) {
$this->translator = $translator;
}
public function getFilters()
{
return array(
new \Twig_SimpleFilter('translength', array($this, 'translengthFilter')),
);
}
public function translengthFilter($id, $domain = null)
{
if (null === $domain) {
$domain = 'messages';
}
$i = 0;
$g = $this->translator->getMessages();
foreach($g["messages"] as $key => $message) {
if(substr( $key, 0, strlen($id) ) === $id ) {
$i++;
}
}
return $i;
}
public function getName()
{
return 'acme_extension';
}
}
As you can see above, it calculates the number of $id occurrences in the translation catalogue. Translator takes care of locale, loading the appropiate catalogue and so on. It also caches results which is really good in terms of performance.
Register the service injecting the translator and registering as a twig extension:
services:
acme.twig.acme_extension:
class: Acme\DemoBundle\Twig\Extension\TransLengthExtension
arguments:
- "#translator"
tags:
- { name: twig.extension }
Now, in your template:
{% set end = 'termsAndConditions.paragraph'|translength %}
{% for i in range(1,end) -%}
<p>{{('termsAndConditions.paragraph.'~i)|trans}}</p>
{%- endfor %}
Hope it helps.
There's a twig filter for this - keys
{% for keyname in yml_tree_nodes|keys %}

Twig lexer : add delimiter

I want to use Twig (v1.15.0) in my project in order to replace our homemade template engine. It uses specific delimiters to replace variables, [[...]], or to manage localized strings, [% ... %].
<table>
<tr>
<td>[%myLocalizedString%]</td>
<td>[[myVarToReplace]]</td>
</tr>
</table>
I don't want to modify all existing templates to replace each delimiter, for legacy and compatibility reasons.
For variables, it is not a big deal, I just have to set the Twig lexer's options :
$twig = new Twig_Environment();
$lexer = new Twig_Lexer($twig, array(
'tag_comment' => array('{#', '#}'),
'tag_block' => array('{%', '%}'),
'tag_variable' => array('[[', ']]'), // was array('{{', '}}')
'interpolation' => array('#{', '}'),
));
$twig->setLexer($lexer);
In the case of localization delimiters is not as simple. Initialy I wanted to do something like that :
$twig = new Twig_Environment();
$lexer = new Twig_Lexer($twig);
$lexer->addDelimiter('tag_localize', array('[%', '%]'), 'functionToCall');
But it does not seems to be implemented yet.
The ultimate solution is to extend the Lexer class and use it in my twig environnement.
But I would like to avoid that.
Is there any better solution ?
I'm sure you know this already: in Twig you do translations using the I18n extension. That extension provides a "trans" tag: see i18n docs
I see no way you can convert your translation syntax [%Hello world%] into the Twig way {% trans %}Hello world{% endtrans %} by extendling the Lexer class, because {% is a block element and trans a tag defined in the I18n extension. You could build your own translation logic, but I think it is much easier to create a preprocessor that replaces [% with {% trans %] and %} with {% endtrans %}.
I imagine it could work like this (untested):
class MYTwigEnvironment extends Twig_Environment {
public function compileSource($source, $name = null) {
/*
* code to replace '[%' with '{% trans %}' in $source
* comes here ...
*/
return parent::compileSource($source, $name = null);
}
}
As far as I understand, this way the template caching should stay intact.
regards

Print a variable that contains html and twig on a twig template

I have a variable suppose that is:
$menustr; this variable contains code html and some twig parts for example:
$menustr .= '<li><a href="{{ path("'. $actual['direccion'] .'") }}" >'. $actual['nombre'] .'</a></li>';
I need that the browser take the code html and the part of twig that in this momen is the
"{{ path(~~~~~) }}"
I make a return where i send the variable called "$menustr" and after use the expresion "raw" for the html code but this dont make effective the twig code.
This is te return:
return $this->render('::menu.html.twig', array('menu' => $menustr));
and here is the template content:
{{ menu | raw }}
Twig can't render strings containing twig. There is not something like an eval function in Twig1..
What you can do is moving the path logic to the PHP stuff. The router service can generate urls, just like the path twig function does. If you are in a controller which extends the base Controller, you can simply use generateUrl:
$menuString .= '<li>'. $actual['nombre'] .'</li>';
return $this->render('::menu.html.twig', array(
'menu' => $menuString,
));
Also, when using menu's in Symfony, I recommend to take a look at the KnpMenuBundle.
EDIT: 1. As pointed by #PepaMartinec there is a function which can do this and it is called template_from_string
You can render Twig template stored in a varible using the template_from_string function.
Check this bundle: https://github.com/LaKrue/TwigstringBundle
This Bundle adds the possibility to render strings instead of files with the Symfony2 native Twig templating engine:
$vars = array('var'=>'x');
// render example string
$vars['test'] = $this->get('twigstring')->render('v {{ var }} {% if var is defined %} y {% endif %} z', $vars);
// output
v x y z
In your case i would be:
return $this->render('::menu.html.twig', array(
'menu' => $this->get('twigstring')->render($menustr, $vars)
));

Categories