* 0.9.2-DEV
+ * added macro support
+ * changed filters first optional argument to be a Twig_Environment instance instead of a Twig_Template instance
* made Twig_Autoloader::autoload() a static method
* avoid writing template file if an error occurs
* added $ escaping when outputting raw strings
values explicitly marked as safe. Those can be marked in the template by using
the `|safe` filter.
-Functions returning template data (like `parent`) return safe markup always.
+Functions returning template data (like macros and `parent`) return safe markup always.
>**NOTE**
>Twig is smart enough to not escape an already escaped value by the `escape`
Kenny looks okay --- so far
{% endif %}
+### Macros
+
+Macros are comparable with functions in regular programming languages. They
+are useful to put often used HTML idioms into reusable functions to not repeat
+yourself.
+
+Here a small example of a macro that renders a form element:
+
+ [twig]
+ {% macro input(name, value, type, size) %}
+ <input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value|e }}" size="{{ size|default(20) }}" />
+ {% endmacro %}
+
+Macros differs from native PHP functions in a few ways:
+
+ * Default argument values are defined by using the `default` filter in the
+ macro body;
+
+ * Arguments of a macro are always optional.
+
+But as PHP functions, macros don't have access to the current template
+variables.
+
+Macros can be defined in any template, and always need to be "imported" before
+being used (see the Import section for more information):
+
+ [twig]
+ {% import "forms.html" as forms %}
+
+The above `import` call imports the "forms.html" file (which can contain only
+macros, or a template and some macros), and import the functions as items of
+the `forms` variable.
+
+The macro can then be called at will:
+
+ [twig]
+ <p>{{ forms.input('username') }}</p>
+ <p>{{ forms.input('password', null, 'password') }}</p>
+
### Filters
Filter sections allow you to apply regular Twig filters on a block of template
[twig]
{% include 'user.html' sandboxed %}
+### Import
+
+Twig supports putting often used code into macros. These macros can go into
+different templates and get imported from there.
+
+Imagine we have a helper module that renders forms (called `forms.html`):
+
+ [twig]
+ {% macro input(name, value, type, size) %}
+ <input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value|e }}" size="{{ size|default(20) }}" />
+ {% endmacro %}
+
+ {% macro textarea(name, value, rows) %}
+ <textarea name="{{ name }}" rows="{{ rows|default(10) }}" cols="{{ cols|default(40) }}">{{ value|e }}</textarea>
+ {% endmacro %}
+
+Importing these macros in a template is as easy as using the `import` tag:
+
+ [twig]
+ {% import 'forms.html' as forms %}
+ <dl>
+ <dt>Username</dt>
+ <dd>{{ forms.input('username') }}</dd>
+ <dt>Password</dt>
+ <dd>{{ forms.input('password', null, 'password') }}</dd>
+ </dl>
+ <p>{{ forms.textarea('comment') }}</p>
+
+Even if the macros are defined in the same template as the one where you want
+to use them, they still need to be imported:
+
+ [twig]
+ {# index.html template #}
+
+ {% macro textarea(name, value, rows) %}
+ <textarea name="{{ name }}" rows="{{ rows|default(10) }}" cols="{{ cols|default(40) }}">{{ value|e }}</textarea>
+ {% endmacro %}
+
+ {% import "index.html" as forms %}
+
+ <p>{{ forms.textarea('comment') }}</p>
+
Expressions
-----------
new Twig_TokenParser_Parent(),
new Twig_TokenParser_Display(),
new Twig_TokenParser_Filter(),
+ new Twig_TokenParser_Macro(),
+ new Twig_TokenParser_Import(),
);
}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+abstract class Twig_Macro implements Twig_MacroInterface
+{
+ protected $env;
+
+ public function __construct(Twig_Environment $env)
+ {
+ $this->env = $env;
+ }
+
+ /**
+ * Returns the bound environment for this template.
+ *
+ * @return Twig_Environment The current environment
+ */
+ public function getEnvironment()
+ {
+ return $this->env;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+interface Twig_MacroInterface
+{
+ /**
+ * Returns the bound environment for this template.
+ *
+ * @return Twig_Environment The current environment
+ */
+ public function getEnvironment();
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents an import node.
+ *
+ * @package twig
+ * @author Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version SVN: $Id$
+ */
+class Twig_Node_Import extends Twig_Node
+{
+ protected $macro;
+ protected $var;
+
+ public function __construct($macro, $var, $lineno, $tag = null)
+ {
+ parent::__construct($lineno, $tag);
+ $this->macro = $macro;
+ $this->var = $var;
+ }
+
+ public function __toString()
+ {
+ return get_class($this).'('.$this->macro.', '.$this->var.')';
+ }
+
+ public function compile($compiler)
+ {
+ $compiler
+ ->addDebugInfo($this)
+ ->write('$this->env->getLoader()->load(')
+ ->string($this->macro)
+ ->raw(");\n\n")
+ ->write("if (!class_exists(")
+ ->string('__TwigMacro_'.md5($this->macro))
+ ->raw("))\n")
+ ->write("{\n")
+ ->indent()
+ ->write(sprintf("throw new InvalidArgumentException('There is no defined macros in template \"%s\".');\n", $this->macro))
+ ->outdent()
+ ->write("}\n")
+ ->write(sprintf("\$context["))
+ ->string($this->var)
+ ->raw(sprintf("] = new __TwigMacro_%s(\$this->env);\n", md5($this->macro)))
+ ;
+ }
+
+ public function getMacro()
+ {
+ return $this->macro;
+ }
+
+ public function getVar()
+ {
+ return $this->var;
+ }
+}
{
protected $name;
protected $body;
+ protected $arguments;
- public function __construct($name, Twig_NodeList $body, $lineno, $tag = null)
+ public function __construct($name, Twig_NodeList $body, $arguments, $lineno, $parent = null, $tag = null)
{
parent::__construct($lineno, $tag);
$this->name = $name;
- $this->body = $body;
+ $this->body = $body;
+ $this->arguments = $arguments;
}
public function __toString()
{
- return get_class($this).'('.$this->name.')';
+ $repr = array(get_class($this).' '.$this->name.'(');
+ foreach ($this->body->getNodes() as $node)
+ {
+ foreach (explode("\n", $node->__toString()) as $line)
+ {
+ $repr[] = ' '.$line;
+ }
+ }
+ $repr[] = ')';
+
+ return implode("\n", $repr);
}
public function getNodes()
$this->body = new Twig_NodeList($nodes, $this->lineno);
}
- public function compile($compiler)
+ public function replace($other)
{
- $compiler->subcompile($this->body);
+ $this->body = $other->body;
}
- public function getName()
+ public function compile($compiler)
{
- return $this->name;
+ $arguments = array();
+ foreach ($this->arguments as $argument)
+ {
+ $arguments[] = '$'.$argument->getName().' = null';
+ }
+
+ $compiler
+ ->addDebugInfo($this)
+ ->write(sprintf("public function get%s(%s)\n", $this->name, implode(', ', $arguments)), "{\n")
+ ->indent()
+ ->write("\$context = array(\n")
+ ->indent()
+ ;
+
+ foreach ($this->arguments as $argument)
+ {
+ $compiler
+ ->write('')
+ ->string($argument->getName())
+ ->raw(' => $'.$argument->getName())
+ ->raw(",\n")
+ ;
+ }
+
+ $compiler
+ ->outdent()
+ ->write(");\n\n")
+ ->subcompile($this->body)
+ ->outdent()
+ ->write("}\n\n")
+ ;
}
}
protected $body;
protected $extends;
protected $blocks;
+ protected $macros;
protected $filename;
protected $usedFilters;
protected $usedTags;
- public function __construct(Twig_NodeList $body, $extends, $blocks, $filename)
+ public function __construct(Twig_NodeList $body, $extends, array $blocks, array $macros, $filename)
{
parent::__construct(1);
$this->body = $body;
$this->extends = $extends;
$this->blocks = array_values($blocks);
+ $this->macros = $macros;
$this->filename = $filename;
$this->usedFilters = array();
$this->usedTags = array();
public function compile($compiler)
{
+ $this->compileTemplate($compiler);
+ $this->compileMacros($compiler);
+ }
+
+ protected function compileTemplate($compiler)
+ {
$sandboxed = $compiler->getEnvironment()->hasExtension('sandbox');
$compiler->write("<?php\n\n");
->write('class __TwigTemplate_'.md5($this->filename))
;
- if (!is_null($this->extends))
+ $parent = null === $this->extends ? $compiler->getEnvironment()->getBaseTemplateClass() : '__TwigTemplate_'.md5($this->extends);
+
+ $compiler
+ ->raw(" extends $parent\n")
+ ->write("{\n")
+ ->indent()
+ ->write("public function display(array \$context)\n", "{\n")
+ ->indent()
+ ;
+
+ if (null !== $this->extends)
{
- $parent = md5($this->extends);
+ // remove all but import nodes
+ $nodes = array();
+ foreach ($this->body->getNodes() as $node)
+ {
+ if ($node instanceof Twig_Node_Import)
+ {
+ $nodes[] = $node;
+ }
+ }
+
$compiler
- ->raw(" extends __TwigTemplate_$parent\n")
- ->write("{\n")
- ->indent()
+ ->subcompile(new Twig_NodeList($nodes))
+ ->write("\n")
+ ->write("parent::display(\$context);\n")
+ ->outdent()
+ ->write("}\n\n")
;
}
else
{
- $compiler
- ->write(" extends ".$compiler->getEnvironment()->getBaseTemplateClass()."\n", "{\n")
- ->indent()
- ->write("public function display(array \$context)\n", "{\n")
- ->indent()
- ;
-
if ($sandboxed)
{
$compiler->write("\$this->checkSecurity();\n");
->write("}\n")
;
}
+
+ protected function compileMacros($compiler)
+ {
+ if (!$this->macros)
+ {
+ return;
+ }
+
+ $compiler
+ ->write("\n")
+ ->write('class __TwigMacro_'.md5($this->filename).' extends Twig_Macro'."\n")
+ ->write("{\n")
+ ->indent()
+ ;
+
+ // macros
+ foreach ($this->macros as $node)
+ {
+ $compiler->subcompile($node);
+ }
+
+ $compiler
+ ->outdent()
+ ->write("}\n")
+ ;
+ }
}
protected $expressionParser;
protected $blocks;
protected $currentBlock;
+ protected $macros;
protected $env;
public function __construct(Twig_Environment $env = null)
$this->stream = $stream;
$this->extends = null;
$this->blocks = array();
+ $this->macros = array();
$this->currentBlock = null;
try
}
}
- $node = new Twig_Node_Module($body, $this->extends, $this->blocks, $this->stream->getFilename());
+ $node = new Twig_Node_Module($body, $this->extends, $this->blocks, $this->macros, $this->stream->getFilename());
$transformer = new Twig_NodeTransformer_Chain($this->transformers);
$transformer->setEnvironment($this->env);
$this->blocks[$name] = $value;
}
+ public function hasMacro($name)
+ {
+ return isset($this->macros[$name]);
+ }
+
+ public function setMacro($name, $value)
+ {
+ $this->macros[$name] = $value;
+ }
+
public function getExpressionParser()
{
return $this->expressionParser;
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_TokenParser_Import extends Twig_TokenParser
+{
+ public function parse(Twig_Token $token)
+ {
+ $macro = $this->parser->getStream()->expect(Twig_Token::STRING_TYPE)->getValue();
+ $this->parser->getStream()->expect('as');
+ $var = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
+ $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+ return new Twig_Node_Import($macro, $var, $token->getLine(), $this->getTag());
+ }
+
+ public function getTag()
+ {
+ return 'import';
+ }
+}
$lineno = $token->getLine();
$name = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
- // arguments
-
+ $arguments = $this->parser->getExpressionParser()->parseArguments();
$this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideBlockEnd'), true);
$this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
- return new Twig_Node_Macro($name, $body, $lineno, $this->getTag());
+ $this->parser->setMacro($name, new Twig_Node_Macro($name, $body, $arguments, $lineno, $this->getTag()));
+
+ return null;
}
public function decideBlockEnd($token)
--- /dev/null
+--TEST--
+"macro" tag
+--TEMPLATE--
+{% import '%prefix%index.twig' as forms %}
+
+{{ forms.input('username') }}
+{{ forms.input('password', null, 'password', 1) }}
+
+{% macro input(name, value, type, size) %}
+ <input type="{{ type|default("text") }}" name="{{ name }}" value="{{ value|e|default('') }}" size="{{ size|default(20) }}">
+{% endmacro %}
+--DATA--
+return array()
+--EXPECT--
+ <input type="text" name="username" value="" size="20">
+
+ <input type="password" name="password" value="" size="1">
--- /dev/null
+--TEST--
+"macro" tag
+--TEMPLATE--
+{% import '%prefix%forms.twig' as forms %}
+
+{{ forms.input('username') }}
+{{ forms.input('password', null, 'password', 1) }}
+--TEMPLATE(forms.twig)--
+{% macro input(name, value, type, size) %}
+ <input type="{{ type|default("text") }}" name="{{ name }}" value="{{ value|e|default('') }}" size="{{ size|default(20) }}">
+{% endmacro %}
+--DATA--
+return array()
+--EXPECT--
+ <input type="text" name="username" value="" size="20">
+
+ <input type="password" name="password" value="" size="1">
}
}
-$t = new LimeTest(44);
+$t = new LimeTest(46);
$fixturesDir = realpath(dirname(__FILE__).'/../fixtures/');
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($fixturesDir), RecursiveIteratorIterator::LEAVES_ONLY) as $file)