How to wrap HTML code without including new files in Twig?
I have CSS divs that represent fields, they wrap html code.
<div class="box indent"> <div class="padding"> my code here </div> </div> I created a "layoutbundle" where each HTML wrapper (e.g. boxes, tabs, grids, etc.) is placed in separate twig files. Thus, representations of other packages can be implemented with other layouts.
But I'm tired of turning on. Every little html wrapper needs to be included, and I wonder if there is an easier way to wrap HTML code.
We give an example with a simple field. In fact, I created several files:
A box.html.twig , which contains a field and includes the contents:
<div class="box indent"> <div class="padding"> {% include content %} </div> </div> Several box-content.html.twig files containing the contents of my boxes.
And finally, I create a window in the view by doing:
{% include 'AcmeDemoBundle:layout:box.html.twig' with { 'content': 'ReusableBundle:feature:xxx.html.twig' } %} Is there a way to create wrappers, for example:
a) I declare a new shell:
{% wrapperheader "box" %} <div class="box indent"> <div class="padding"> {% endwrapperheader %} {% wrapperfooter "box" %} </div> </div> {% endwrapperfooter %} b) And then on my pages I use:
{% wrapper "box" %} {# here my content #} {% endwrapper %} It seems to me that I need to add new tag extensions to Twig, but first I want to know if something like this is possible. Maybe,
Block method
This method was proposed by Sebastiaan Stok on GitHub.
This idea uses the block> function . It writes the given contents of the block and can be called several times.
Wrapper File:
{# src/Fuz/LayoutBundle/Resources/views/Default/wrappers.html.twig #} {% block box_head %} <div class="box indent"> <div class="padding"> {% enblock %} {% block box_foot %} </div> </div> {% enblock %} Feature Page:
{{ block('box_head') }} Some content {{ block('box_foot') }} Macro Wrapper Extension
This idea was proposed by Charles on GitHub.
First, you declare a macro in macro.html.twig .
{% macro box(content) %} <div class="box indent"> <div class="padding"> {{ content | raw }} </div> </div> {% endmacro %} Amd, instead of calling {{ macros.box('my content') }} (see doc, you create the tag {% wrap %} that will handle the macro call, with which between [% wrap %} and {% endwrap %} as a parameter.
This extension was easy to develop. I thought it might be difficult to access macros, but in fact they are stored in context as objects, and calls can be easily compiled.
Just some changes: we will use the following syntax:
{# to access a macro from an object #} {% wrap macro_object macro_name %} my content here {% endwrap %} {# to access a macro declared in the same file #} {% wrap macro_name %} macro {% endwrap %} In the following code, remember to change the namespaces if you want it to work!
First add the extension to your services.yml:
parameters: fuz_tools.twig.wrap_extension.class: Fuz\ToolsBundle\Twig\Extension\WrapExtension services: fuz_tools.twig.wrap_extension: class: '%fuz_tools.twig.wrap_extension.class%' tags: - { name: twig.extension } Create a Twig directory inside your package.
Add an extension, it will return a new TokenParser (in English: it will declare a new tag).
Twig / Extension / WrapExtension.php:
<?php // src/Fuz/ToolsBundle/Twig/Extension/WrapExtension.php namespace Fuz\ToolsBundle\Twig\Extension; use Fuz\ToolsBundle\Twig\TokenParser\WrapHeaderTokenParser; use Fuz\ToolsBundle\Twig\TokenParser\WrapFooterTokenParser; use Fuz\ToolsBundle\Twig\TokenParser\WrapTokenParser; class WrapExtension extends \Twig_Extension { public function getTokenParsers() { return array ( new WrapTokenParser(), ); } public function getName() { return 'wrap'; } } Then add TokenParser itself, it will be executed when the parser finds the tag {% wrap %} . This TokenParser will check if the tag is called correctly (for our example, it has 2 parameters), save these parameters and get the contents between {% wrap %} and {% endwrap%} `.
Twig / TokenParser / WrapTokenParser.php:
<?php // src/Fuz/ToolsBundle/Twig/TokenParser/WrapTokenParser.php namespace Fuz\ToolsBundle\Twig\TokenParser; use Fuz\ToolsBundle\Twig\Node\WrapNode; class WrapTokenParser extends \Twig_TokenParser { public function parse(\Twig_Token $token) { $lineno = $token->getLine(); $stream = $this->parser->getStream(); $object = null; $name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue(); if ($stream->test(\Twig_Token::BLOCK_END_TYPE)) { if (!$this->parser->hasMacro($name)) { throw new \Twig_Error_Syntax("The macro '$name' does not exist", $lineno); } } else { $object = $name; $name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue(); } $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array ($this, 'decideWrapEnd'), true); $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); return new WrapNode($object, $name, $body, $token->getLine(), $this->getTag()); } public function decideWrapEnd(\Twig_Token $token) { return $token->test('endwrap'); } public function getTag() { return 'wrap'; } } Next, we need a compiler (a Node in the branch dialect), it will generate the PHP code associated with our tag {% wrap %} .
This tag is an alias of {{ macro_object.box(content) }} , so I wrote this line in the template and looked at the resulting code in the resulting generated php file (stored in your app/cache/dev/twig ). I got:
echo $this->getAttribute($this->getContext($context, "(macro object name)"), "(name)", array("(body)"), "method"); So my compiler has become:
Twig / Node / WrapNode.php:
<?php // src/Fuz/ToolsBundle/Twig/Node/WrapNode.php namespace Fuz\ToolsBundle\Twig\Node; class WrapNode extends \Twig_Node { public function __construct($object, $name, $body, $lineno = 0, $tag = null) { parent::__construct(array ('body' => $body), array ('object' => $object, 'name' => $name), $lineno, $tag); } public function compile(\Twig_Compiler $compiler) { $compiler ->addDebugInfo($this) ->write('ob_start();'); $compiler ->addDebugInfo($this) ->subcompile($this->getNode('body')); if (is_null($this->getAttribute('object'))) { $compiler ->write(sprintf('echo $this->get%s(ob_get_clean());', $this->getAttribute('name')) . "\n"); } else { $compiler ->write('echo $this->getAttribute($this->getContext($context, ') ->repr($this->getAttribute('object')) ->raw('), ') ->repr($this->getAttribute('name')) ->raw(', array(ob_get_clean()), "method");') ->raw("\n"); } } } Note: to find out how the subparagraph / subcompilation works, I read the source code of the spaceless extension.
It's all! We get an alias that allows the use of macros with a large body. To try:
macros.html.twig:
{% macro box(content) %} <div class="box indent"> <div class="padding"> {{ content | raw }} {# Don't forget the raw filter! #} </div> </div> {% endmacro %} some layout.html.twig:
{% import "FuzLayoutBundle:Default:macros.html.twig" as macros %} {% wrap macros box %} test {% endwrap %} {% macro test(content) %} some {{ content | raw }} in the same file {% endmacro %} {% wrap test %} macro {% endwrap %} Outputs:
<div class="box indent"> <div class="padding"> test </div> </div> some macro in the same file Browser wrapper, wrapperfooter, shell extension
This method is the one I will talk about in my question. You can read / implement it if you want to train using parsers-parsers, but functionnaly, which is less nice than the previous method.
In the wrapper.html.twig file wrapper.html.twig you declare all the wrappers:
{% wrapperheader box %} <div class="box"> {% endwrapper %} {% wrapperfooter box %} </div> {% endwrapperfooter %} In your twig file functions you use your wrappers:
{% wrapper box %} This is my content {% endwrapper %} The following extension has 3 problems:
There is no way to store data (such as context variables) in a Twig Environnement. Therefore, when you define
{% wrapperheader NAME %}, you donโt have in principle a clean way to check if the header forNAMEhas already been defined (in this extension I use static properties).When you
includea twig file, it is parsed at run time, and not immediately (I mean, the included branch template is parsed, and the generated file is executed, and not when theincludetag is parsed). So itโs impossible to find out if a shell exists in a previously included file when parsing the{% wrapper NAME %}tag. If your shell does not exist, this extension simply shows that between{% wrapper %}and{% endwrapper %}without any notifications.The idea behind this extension is: when the parser encounters a
wrapperheadertag and awrapperfooter, the compiler stores the contents of the tag somewhere for later use with thewrappertag. But the twig context is passed to{% include %}as a copy, not by reference. So itโs impossible to store the information{% wrapperheader %}and{% wrapperfooter %}inside this context for use at the top level (in files containing files). I also needed to use a global context.
Here is the code, take care to change the namespace.
First we need to create an extension that will add new token parsers to Twig.
Inside the services.yml package, add the following lines to activate the extension:
parameters: fuz_tools.twig.wrapper_extension.class: Fuz\ToolsBundle\Twig\Extension\WrapperExtension services: fuz_tools.twig.wrapper_extension: class: '%fuz_tools.twig.wrapper_extension.class%' tags: - { name: twig.extension } Create a Twig directory inside your package.
Create the following Twig \ Extension \ WrapperExtension.php file:
<?php // src/Fuz/ToolsBundle/Twig/Extension/WrapperExtension.php namespace Fuz\ToolsBundle\Twig\Extension; use Fuz\ToolsBundle\Twig\TokenParser\WrapperHeaderTokenParser; use Fuz\ToolsBundle\Twig\TokenParser\WrapperFooterTokenParser; use Fuz\ToolsBundle\Twig\TokenParser\WrapperTokenParser; class WrapperExtension extends \Twig_Extension { public function getTokenParsers() { return array( new WrapperHeaderTokenParser(), new WrapperFooterTokenParser(), new WrapperTokenParser(), ); } public function getName() { return 'wrapper'; } } Now we need to add syntax parsers: our syntax {% wrapper NAME %} ... {% endwrapper %} the same with wrapperheader and wrapperfooter . Therefore, those parser markers are used to declare tags, to extract the NAME shell, and to extract the body (between wrapper and endwrapper`).
Shell Token Analyzer: Twig \ TokenParser \ WrapperTokenParser.php:
<?php // src/Fuz/ToolsBundle/Twig/TokenParser/WrapperTokenParser.php namespace Fuz\ToolsBundle\Twig\TokenParser; use Fuz\ToolsBundle\Twig\Node\WrapperNode; class WrapperTokenParser extends \Twig_TokenParser { public function parse(\Twig_Token $token) { $stream = $this->parser->getStream(); $name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue(); $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideWrapperEnd'), true); $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); return new WrapperNode($name, $body, $token->getLine(), $this->getTag()); } public function decideWrapperEnd(\Twig_Token $token) { return $token->test('endwrapper'); } public function getTag() { return 'wrapper'; } } Token analyzer for wrapperheader: Twig \ TokenParser \ WrapperHeaderTokenParser.php:
<?php // src/Fuz/ToolsBundle/Twig/TokenParser/WrapperHeaderTokenParser.php namespace Fuz\ToolsBundle\Twig\TokenParser; use Fuz\ToolsBundle\Twig\Node\WrapperHeaderNode; class WrapperHeaderTokenParser extends \Twig_TokenParser { static public $wrappers = array (); public function parse(\Twig_Token $token) { $lineno = $token->getLine(); $stream = $this->parser->getStream(); $name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue(); if (in_array($name, self::$wrappers)) { throw new \Twig_Error_Syntax("The wrapper '$name' header has already been defined.", $lineno); } self::$wrappers[] = $name; $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideWrapperHeaderEnd'), true); $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); return new WrapperHeaderNode($name, $body, $token->getLine(), $this->getTag()); } public function decideWrapperHeaderEnd(\Twig_Token $token) { return $token->test('endwrapperheader'); } public function getTag() { return 'wrapperheader'; } } Token analyzer for wrapperfooter: Twig \ TokenParser \ WrapperFooterTokenParser.php:
<?php // src/Fuz/ToolsBundle/Twig/TokenParser/WrapperFooterTokenParser.php namespace Fuz\ToolsBundle\Twig\TokenParser; use Fuz\ToolsBundle\Twig\Node\WrapperFooterNode; class WrapperFooterTokenParser extends \Twig_TokenParser { static public $wrappers = array (); public function parse(\Twig_Token $token) { $lineno = $token->getLine(); $stream = $this->parser->getStream(); $name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue(); if (in_array($name, self::$wrappers)) { throw new \Twig_Error_Syntax("The wrapper '$name' footer has already been defined.", $lineno); } self::$wrappers[] = $name; $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideWrapperFooterEnd'), true); $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); return new WrapperFooterNode($name, $body, $token->getLine(), $this->getTag()); } public function decideWrapperFooterEnd(\Twig_Token $token) { return $token->test('endwrapperfooter'); } public function getTag() { return 'wrapperfooter'; } } Token analyzers extract all the necessary information, now we need to compile this information in PHP. This PHP code will be generated by the twig engine inside the Twig_Template implementation (you can find the generated classes in your cache directory). It generates code in the method, and the context of the included files is not available (since the array of contexts is not specified by reference). Thus, it is not possible to access what is inside the included file without a global context. That's why I use static attributes here ... It's not at all nice, but I donโt know how to avoid them (if you have ideas, let me know!).
Compiler for the wrapper tag: Twig \ Nodes \ WrapperNode.php
<?php // src/Fuz/ToolsBundle/Twig/Node/WrapperNode.php namespace Fuz\ToolsBundle\Twig\Node; class WrapperNode extends \Twig_Node { public function __construct($name, $body, $lineno = 0, $tag = null) { parent::__construct(array ('body' => $body), array ('name' => $name), $lineno, $tag); } public function compile(\Twig_Compiler $compiler) { $compiler ->addDebugInfo($this) ->write('if (isset(\\') ->raw(__NAMESPACE__) ->raw('\WrapperHeaderNode::$headers[') ->repr($this->getAttribute('name')) ->raw('])) {') ->raw("\n") ->indent() ->write('echo \\') ->raw(__NAMESPACE__) ->raw('\WrapperHeaderNode::$headers[') ->repr($this->getAttribute('name')) ->raw('];') ->raw("\n") ->outdent() ->write('}') ->raw("\n"); $compiler ->addDebugInfo($this) ->subcompile($this->getNode('body')); $compiler ->addDebugInfo($this) ->write('if (isset(\\') ->raw(__NAMESPACE__) ->raw('\WrapperFooterNode::$footers[') ->repr($this->getAttribute('name')) ->raw('])) {') ->raw("\n") ->indent() ->write('echo \\') ->raw(__NAMESPACE__) ->raw('\WrapperFooterNode::$footers[') ->repr($this->getAttribute('name')) ->raw('];') ->raw("\n") ->outdent() ->write('}') ->raw("\n"); } } Compiler for wrapperheader tag: Twig \ Nodes \ WrapperHeaderNode.php
<?php // src/Fuz/ToolsBundle/Twig/Node/WrapperHeaderNode.php namespace Fuz\ToolsBundle\Twig\Node; /** * @author alain tiemblo */ class WrapperHeaderNode extends \Twig_Node { static public $headers = array(); public function __construct($name, $body, $lineno = 0, $tag = null) { parent::__construct(array ('body' => $body), array ('name' => $name), $lineno, $tag); } public function compile(\Twig_Compiler $compiler) { $compiler ->write("ob_start();") ->raw("\n") ->subcompile($this->getNode('body')) ->write(__CLASS__) ->raw('::$headers[') ->repr($this->getAttribute('name')) ->raw('] = ob_get_clean();') ->raw("\n"); } } Compiler for wrapperfooter tag: Twig \ Nodes \ WrapperFooterNode.php
<?php // src/Fuz/ToolsBundle/Twig/Node/WrapperFooterNode.php namespace Fuz\ToolsBundle\Twig\Node; class WrapperFooterNode extends \Twig_Node { static public $footers = array(); public function __construct($name, $body, $lineno = 0, $tag = null) { parent::__construct(array ('body' => $body), array ('name' => $name), $lineno, $tag); } public function compile(\Twig_Compiler $compiler) { $compiler ->write("ob_start();") ->raw("\n") ->subcompile($this->getNode('body')) ->write(__CLASS__) ->raw('::$footers[') ->repr($this->getAttribute('name')) ->raw('] = ob_get_clean();') ->raw("\n"); } } The implementation is now in order. Give it a try!
Create a view called wrappers.html.twig:
{# src/Fuz/LayoutBundle/Resources/views/Default/wrappers.html.twig #} {% wrapperheader demo %} HEAD {% endwrapperheader %} {% wrapperfooter demo %} FOOT {% endwrapperfooter %} Create a view called what want.html.twig:
{# src/Fuz/HomeBundle/Resources/views/Default/index.html.twig #} {% include 'FuzLayoutBundle:Default:wrappers.html.twig' %} {% wrapper demo %} O YEAH {% endwrapper %} It shows:
HEAD ABOUT THE JEHOVEN FOOT
There is a fairly simple method with Twig variables and macros.
<div class="box indent"> <div class="padding"> my code here </div> </div> Create a macro:
{% macro box(content) %} <div class="box indent"> <div class="padding"> {{ content }} </div> </div> {% endmacro %} And name it as follows:
{% set content %} my code here {% endset %} {{ _self.box(content) }} Not the destruction of the earth, but a slightly different solution.