Twig lexer : add delimiter - php

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

Related

PHP/Regex recursive tags in html template [duplicate]

This is what I got now:
/{% if(.+?) %}(.*?){% endif %}/gusi
It catches multiple if statements etc just fine.
IMG: http://image.xesau.eu/2015-02-07_23-22-11.png
But when I do nested ones, so an if in an if, it stops at the first occurence of {% endif %}
IMG: http://image.xesau.eu/2015-02-08_09-29-43.png
Is there a way catch as many {% endif %} statements as there were {% if ... %} statements, and if so, how?
Don't use regexen, use the existing Twig parser. Here's a sample of an extractor I wrote which parses for custom tags and extracts them: https://github.com/deceze/Twig-extensions/tree/master/lib/Twig/Extensions/Extension/Gettext
The job of the lexer is to turn Twig source code into objects; you can extend it if you need to hook into that process:
class My_Twig_Lexer extends Twig_Lexer {
...
/**
* Overrides lexComment by saving comment tokens into $this->commentTokens
* instead of just ignoring them.
*/
protected function lexComment() {
if (!preg_match($this->regexes['lex_comment'], $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) {
throw new Twig_Error_Syntax('Unclosed comment', $this->lineno, $this->filename);
}
$value = substr($this->code, $this->cursor, $match[0][1] - $this->cursor);
$token = new Twig_Extensions_Extension_Gettext_Token(Twig_Extensions_Extension_Gettext_Token::COMMENT, $value, $this->lineno);
$this->commentTokens[] = $token;
$this->moveCursor($value . $match[0][0]);
}
...
}
Typically Twig comment nodes are being discarded by Twig, this lexer saves them.
However, your main concern will be to work with the parser:
$twig = new Twig_Environment(new Twig_Loader_String);
$lexer = new My_Twig_Lexer($twig);
$parser = new Twig_Parser($twig);
$source = file_get_contents($file);
$tokens = $lexer->tokenize($source);
$node = $parser->parse($tokens);
processNode($node);
$node here is the root node of a tree of nodes which represent the Twig source in an object oriented fashion, all correctly parsed already. You just need to process this tree without having to worry about the exact syntax which was used to produce it:
processNode(Twig_NodeInterface $node) {
switch (true) {
case $node instanceof Twig_Node_Expression_Function :
processFunctionNode($node);
break;
case $node instanceof Twig_Node_Expression_Filter :
processFilterNode($node);
break;
}
foreach ($node as $child) {
if ($child instanceof Twig_NodeInterface) {
processNode($child);
}
}
}
Just traverse it until you find the kind of node you're looking for and get its information. Play around with it a bit. This example code may or may not be a bit outdated, you'll have to dig into the Twig parser source code anyway to understand it.
It is almost trivial to change your pattern into a recursive pattern:
{% if(.+?) %}((?>(?R)|.)*?){% endif %}
Working example: https://regex101.com/r/gX8rM0/1
However, that would be a bad idea: the pattern is missing many cases, which are really bugs in your parser. Just a few common examples:
Comments:
{% if aaa %}
123
<!-- {% endif %} -->
{% endif %}
String literals:
{% if aaa %}a = "{% endif %}"{% endif %}
{% if $x == "{% %}" %}...{% endif %}
Escaped characters (you do need escaped characters, right?):
<p>To start a condition, use <code>\{% if aaa %}</code></p>
Invalid input:
It would be nice if the parser can work relatively well on invalid input, and point to the correct position of the error.

Use custom delimiters in the current Twig template

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');
}

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

twig: create custom tag that calls a functions

SETUP:
Twig 1.13.1
PHP 5.4.3
PROBLEM:
I am needing help setting up a custom tag that calls a function that i have already built...
Current Code:
Template Code
{% set stories = get_latest_stories(2, sports) %}
{% for story in stories %}
{{ story.headline }} <br>
{% endfor %}
Controller
$function = new Twig_SimpleFunction('getViewStories', function (section, limit) {
return news_stories::getStories(section,limit);
});
$twig->addFunction($function);
$twig->render("storyList.html");
GOAL:
No with that said I would like to use a custom tag like
{% get_latest_stories 2 sports %}
to call the same function as above. The new way looks nicer and is easier to follow
Why not fetch your stories in the controller instead of the template? This does not seem like a job for the view layer...
So, something like this:
$twig->render("storyList.html", array(
'stories' => news_stories::getStories($section, $limit)
));
Then, you'll have a stories variable available in your template.
here is simple example how to write twig extension
Following code is taken from my unfinished project
function file_import($value){
//some code here
return $value;
}
$app['twig']->addFunction('file_import', new Twig_Function_Function('file_import'));
usage
{{ file_import('value') }}

FLAT FILE SITE: PHP5 Master Template Without Database

Dear folks,
Imagine a flat php site without database with hundreds of files having the same variables defined in all of them eg $read $look and $buys.
page1.php
<?
$blue= ".....";
$bell= ".....";
$beam= ".....";
?>
page2.php
<?
$blue= ".....";
$bell= ".....";
$beam= ".....";
?>
etcettera.php
Now, as soon as I invent a new variable, say $bike or $beaf then I have to go through all those template files in order to add to them $beaf = "" or else there undefined there. I miss a master template so to say... Any ideas/hints/code/suggestions are welcome. Thanks in advance.
Is there any smarter way of template management without use of database, making it easer to maintain these templates?
A templating engine like Twig might help you. Twig supports template inheritance, which allows you to define a master template that all child templates inherit from:
master.html.twig:
<html>
<head><title>{% block title %}Default Title{% endblock %}</title></head>
<body>
<h1>{% block pageHeading}{% endblock %}</h1>
{% block body}{% endblock %}
</body>
</html>
child.html.twig:
{% extends master.html.twig %}
{% block title}Child Page{% endblock %}
{% block pageHeading}Weclome!{% endblock %}
{% block body}
<p>My name is {{ name }}. Today's date is {{ today|date('n/j/Y') }}.</p>
{% endblock %}
PHP code:
$loader = new Twig_Loader_Filesystem('/path/to/templates');
$twig = new Twig_Environment($loader, array(
'cache' => '/path/to/compilation_cache',
));
$template = $twig->loadTemplate('child.html.twig');
echo $templater->render(array("name"=>"Mike", "today"=>new DateTime()));
I would suggest to create a file that has an __autoload function in it, include it at the top of your page1.php (and so on), then create some class like this:
class MyVars {
const book = "Book";
const apple = "Apple";
}
and you can use them like MyVars::book and MyVars::apple in your PHP files.
Your system with flat variables floating around is one of the thing to avoid.
Use a framework that helps you not doing such bad errors or just use Smarty
Zend

Categories