From: fabien Date: Fri, 16 Oct 2009 07:51:42 +0000 (+0000) Subject: added macro support X-Git-Url: http://git.silmor.de/gitweb/?a=commitdiff_plain;h=abc9c5d5cabd7ae0ba7140f626ae4933eb56b7e4;p=konrad%2Ftwig.git added macro support git-svn-id: http://svn.twig-project.org/trunk@65 93ef8e89-cb99-4229-a87c-7fa0fa45744b --- diff --git a/CHANGELOG b/CHANGELOG index 3bb1721..6ade7f7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/doc/02-Twig-for-Template-Designers.markdown b/doc/02-Twig-for-Template-Designers.markdown index 71444b0..33c08ee 100644 --- a/doc/02-Twig-for-Template-Designers.markdown +++ b/doc/02-Twig-for-Template-Designers.markdown @@ -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) %} + + {% 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] +

{{ forms.input('username') }}

+

{{ forms.input('password', null, 'password') }}

+ ### 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) %} + + {% endmacro %} + + {% macro textarea(name, value, rows) %} + + {% endmacro %} + +Importing these macros in a template is as easy as using the `import` tag: + + [twig] + {% import 'forms.html' as forms %} +
+
Username
+
{{ forms.input('username') }}
+
Password
+
{{ forms.input('password', null, 'password') }}
+
+

{{ forms.textarea('comment') }}

+ +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) %} + + {% endmacro %} + + {% import "index.html" as forms %} + +

{{ forms.textarea('comment') }}

+ Expressions ----------- diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php index 1b6eeca..0be3e20 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -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(), ); } diff --git a/lib/Twig/Macro.php b/lib/Twig/Macro.php new file mode 100644 index 0000000..e9d2469 --- /dev/null +++ b/lib/Twig/Macro.php @@ -0,0 +1,29 @@ +env = $env; + } + + /** + * Returns the bound environment for this template. + * + * @return Twig_Environment The current environment + */ + public function getEnvironment() + { + return $this->env; + } +} diff --git a/lib/Twig/MacroInterface.php b/lib/Twig/MacroInterface.php new file mode 100644 index 0000000..c29ab1b --- /dev/null +++ b/lib/Twig/MacroInterface.php @@ -0,0 +1,19 @@ + + * @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; + } +} diff --git a/lib/Twig/Node/Macro.php b/lib/Twig/Node/Macro.php index c5f67f6..eccc918 100644 --- a/lib/Twig/Node/Macro.php +++ b/lib/Twig/Node/Macro.php @@ -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") + ; } } diff --git a/lib/Twig/Node/Module.php b/lib/Twig/Node/Module.php index 86ba64e..f48e49d 100644 --- a/lib/Twig/Node/Module.php +++ b/lib/Twig/Node/Module.php @@ -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(); @@ -84,6 +86,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'); $compiler->write("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") + ; + } } diff --git a/lib/Twig/Parser.php b/lib/Twig/Parser.php index def516d..4eea96d 100644 --- a/lib/Twig/Parser.php +++ b/lib/Twig/Parser.php @@ -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; diff --git a/lib/Twig/TokenParser/Import.php b/lib/Twig/TokenParser/Import.php new file mode 100644 index 0000000..e3a3c2c --- /dev/null +++ b/lib/Twig/TokenParser/Import.php @@ -0,0 +1,27 @@ +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'; + } +} diff --git a/lib/Twig/TokenParser/Macro.php b/lib/Twig/TokenParser/Macro.php index 45d6567..3f1efc6 100644 --- a/lib/Twig/TokenParser/Macro.php +++ b/lib/Twig/TokenParser/Macro.php @@ -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) diff --git a/test/fixtures/tags/macro/basic.test b/test/fixtures/tags/macro/basic.test new file mode 100644 index 0000000..be2a20b --- /dev/null +++ b/test/fixtures/tags/macro/basic.test @@ -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) %} + +{% endmacro %} +--DATA-- +return array() +--EXPECT-- + + + diff --git a/test/fixtures/tags/macro/external.test b/test/fixtures/tags/macro/external.test new file mode 100644 index 0000000..d364170 --- /dev/null +++ b/test/fixtures/tags/macro/external.test @@ -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) %} + +{% endmacro %} +--DATA-- +return array() +--EXPECT-- + + + diff --git a/test/unit/integrationTest.php b/test/unit/integrationTest.php index ee2f098..83639d7 100644 --- a/test/unit/integrationTest.php +++ b/test/unit/integrationTest.php @@ -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)