added macro support

git-svn-id: http://svn.twig-project.org/trunk@65 93ef8e89-cb99-4229-a87c-7fa0fa45744b
This commit is contained in:
fabien
2009-10-16 07:51:42 +00:00
parent 49117dd863
commit abc9c5d5ca
14 changed files with 389 additions and 26 deletions
+2
View File
@@ -1,5 +1,7 @@
* 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
+82 -1
View File
@@ -345,7 +345,7 @@ When automatic escaping is enabled everything is escaped by default except for
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`
@@ -423,6 +423,45 @@ more complex `expressions` there too:
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
@@ -464,6 +503,48 @@ An included file can be evaluated in the sandbox environment by appending
[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
-----------
+2
View File
@@ -37,6 +37,8 @@ class Twig_Extension_Core extends Twig_Extension
new Twig_TokenParser_Parent(),
new Twig_TokenParser_Display(),
new Twig_TokenParser_Filter(),
new Twig_TokenParser_Macro(),
new Twig_TokenParser_Import(),
);
}
+29
View File
@@ -0,0 +1,29 @@
<?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;
}
}
+19
View File
@@ -0,0 +1,19 @@
<?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();
}
+66
View File
@@ -0,0 +1,66 @@
<?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;
}
}
+49 -7
View File
@@ -20,17 +20,29 @@ class Twig_Node_Macro extends Twig_Node implements Twig_NodeListInterface
{
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()
@@ -43,13 +55,43 @@ class Twig_Node_Macro extends Twig_Node implements Twig_NodeListInterface
$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")
;
}
}
+61 -13
View File
@@ -22,17 +22,19 @@ class Twig_Node_Module extends Twig_Node implements Twig_NodeListInterface
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();
@@ -83,6 +85,12 @@ class Twig_Node_Module extends Twig_Node implements Twig_NodeListInterface
}
public function compile($compiler)
{
$this->compileTemplate($compiler);
$this->compileMacros($compiler);
}
protected function compileTemplate($compiler)
{
$sandboxed = $compiler->getEnvironment()->hasExtension('sandbox');
@@ -102,24 +110,38 @@ class Twig_Node_Module extends Twig_Node implements Twig_NodeListInterface
->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");
@@ -175,4 +197,30 @@ class Twig_Node_Module extends Twig_Node implements Twig_NodeListInterface
->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")
;
}
}
+13 -1
View File
@@ -18,6 +18,7 @@ class Twig_Parser
protected $expressionParser;
protected $blocks;
protected $currentBlock;
protected $macros;
protected $env;
public function __construct(Twig_Environment $env = null)
@@ -61,6 +62,7 @@ class Twig_Parser
$this->stream = $stream;
$this->extends = null;
$this->blocks = array();
$this->macros = array();
$this->currentBlock = null;
try
@@ -85,7 +87,7 @@ class Twig_Parser
}
}
$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);
@@ -186,6 +188,16 @@ class Twig_Parser
$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;
+27
View File
@@ -0,0 +1,27 @@
<?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';
}
}
+4 -3
View File
@@ -15,14 +15,15 @@ class Twig_TokenParser_Macro extends Twig_TokenParser
$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)
+17
View File
@@ -0,0 +1,17 @@
--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">
+17
View File
@@ -0,0 +1,17 @@
--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">
+1 -1
View File
@@ -35,7 +35,7 @@ class Foo
}
}
$t = new LimeTest(44);
$t = new LimeTest(46);
$fixturesDir = realpath(dirname(__FILE__).'/../fixtures/');
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($fixturesDir), RecursiveIteratorIterator::LEAVES_ONLY) as $file)