added macro support
authorfabien <fabien@93ef8e89-cb99-4229-a87c-7fa0fa45744b>
Fri, 16 Oct 2009 07:51:42 +0000 (07:51 +0000)
committerfabien <fabien@93ef8e89-cb99-4229-a87c-7fa0fa45744b>
Fri, 16 Oct 2009 07:51:42 +0000 (07:51 +0000)
git-svn-id: http://svn.twig-project.org/trunk@65 93ef8e89-cb99-4229-a87c-7fa0fa45744b

14 files changed:
CHANGELOG
doc/02-Twig-for-Template-Designers.markdown
lib/Twig/Extension/Core.php
lib/Twig/Macro.php [new file with mode: 0644]
lib/Twig/MacroInterface.php [new file with mode: 0644]
lib/Twig/Node/Import.php [new file with mode: 0644]
lib/Twig/Node/Macro.php
lib/Twig/Node/Module.php
lib/Twig/Parser.php
lib/Twig/TokenParser/Import.php [new file with mode: 0644]
lib/Twig/TokenParser/Macro.php
test/fixtures/tags/macro/basic.test [new file with mode: 0644]
test/fixtures/tags/macro/external.test [new file with mode: 0644]
test/unit/integrationTest.php

index 3bb1721..6ade7f7 100644 (file)
--- 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
index 71444b0..33c08ee 100644 (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
 -----------
 
index 1b6eeca..0be3e20 100644 (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(),
     );
   }
 
diff --git a/lib/Twig/Macro.php b/lib/Twig/Macro.php
new file mode 100644 (file)
index 0000000..e9d2469
--- /dev/null
@@ -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;
+  }
+}
diff --git a/lib/Twig/MacroInterface.php b/lib/Twig/MacroInterface.php
new file mode 100644 (file)
index 0000000..c29ab1b
--- /dev/null
@@ -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();
+}
diff --git a/lib/Twig/Node/Import.php b/lib/Twig/Node/Import.php
new file mode 100644 (file)
index 0000000..e0b0dc9
--- /dev/null
@@ -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;
+  }
+}
index c5f67f6..eccc918 100644 (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")
+    ;
   }
 }
index 86ba64e..f48e49d 100644 (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();
@@ -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("<?php\n\n");
@@ -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")
+    ;
+  }
 }
index def516d..4eea96d 100644 (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;
diff --git a/lib/Twig/TokenParser/Import.php b/lib/Twig/TokenParser/Import.php
new file mode 100644 (file)
index 0000000..e3a3c2c
--- /dev/null
@@ -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';
+  }
+}
index 45d6567..3f1efc6 100644 (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)
diff --git a/test/fixtures/tags/macro/basic.test b/test/fixtures/tags/macro/basic.test
new file mode 100644 (file)
index 0000000..be2a20b
--- /dev/null
@@ -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">
diff --git a/test/fixtures/tags/macro/external.test b/test/fixtures/tags/macro/external.test
new file mode 100644 (file)
index 0000000..d364170
--- /dev/null
@@ -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">
index ee2f098..83639d7 100644 (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)