added support for named arguments for filters, tests, and functions
authorFabien Potencier <fabien.potencier@gmail.com>
Thu, 15 Nov 2012 06:55:50 +0000 (07:55 +0100)
committerFabien Potencier <fabien.potencier@gmail.com>
Thu, 15 Nov 2012 09:23:32 +0000 (10:23 +0100)
46 files changed:
CHANGELOG
doc/filters/convert_encoding.rst
doc/filters/date.rst
doc/filters/date_modify.rst
doc/filters/default.rst
doc/filters/escape.rst
doc/filters/join.rst
doc/filters/json_encode.rst
doc/filters/number_format.rst
doc/filters/replace.rst
doc/filters/slice.rst
doc/filters/split.rst
doc/filters/trim.rst
doc/functions/cycle.rst
doc/functions/date.rst
doc/functions/dump.rst
doc/functions/random.rst
doc/functions/range.rst
doc/functions/template_from_string.rst
doc/templates.rst
lib/Twig/ExpressionParser.php
lib/Twig/Extension/Core.php
lib/Twig/Filter.php
lib/Twig/Filter/Function.php
lib/Twig/Filter/Method.php
lib/Twig/FilterCallableInterface.php [new file with mode: 0644]
lib/Twig/Function.php
lib/Twig/Function/Function.php
lib/Twig/Function/Method.php
lib/Twig/FunctionCallableInterface.php [new file with mode: 0644]
lib/Twig/Node/Expression/Call.php
lib/Twig/Node/Expression/Filter.php
lib/Twig/Node/Expression/Function.php
lib/Twig/Node/Expression/Test.php
lib/Twig/Test.php [new file with mode: 0644]
lib/Twig/Test/Function.php
lib/Twig/Test/Method.php
lib/Twig/Test/Node.php
lib/Twig/TestCallableInterface.php [new file with mode: 0644]
test/Twig/Tests/ExpressionParserTest.php
test/Twig/Tests/Fixtures/filters/date_namedargs.test [new file with mode: 0644]
test/Twig/Tests/Fixtures/filters/reverse.test
test/Twig/Tests/Fixtures/functions/date_namedargs.test [new file with mode: 0644]
test/Twig/Tests/Fixtures/functions/range.test [new file with mode: 0644]
test/Twig/Tests/Node/Expression/FilterTest.php
test/Twig/Tests/Node/Expression/FunctionTest.php

index ce2854d..6faeb42 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,6 @@
 * 1.12.0 (2012-XX-XX)
 
+ * added support for named arguments for filters, tests, and functions
  * moved filters/functions/tests syntax errors to the parser
  * added support for extended ternary operator syntaxes
 
index 2febab2..1b0eb60 100644 (file)
@@ -18,5 +18,11 @@ is the input charset:
     them must be installed. In case both are installed, `mbstring`_ is used by
     default (Twig before 1.8.1 uses `iconv`_ by default).
 
+Arguments
+---------
+
+ * ``from``: The input charset
+ * ``to``:   The output charset
+
 .. _`iconv`:    http://php.net/iconv
 .. _`mbstring`: http://php.net/mbstring
index 39ad732..8e2f31f 100644 (file)
@@ -77,6 +77,12 @@ The default timezone can also be set globally by calling ``setTimezone()``:
     $twig = new Twig_Environment($loader);
     $twig->getExtension('core')->setTimezone('Europe/Paris');
 
+Arguments
+---------
+
+ * ``format``:   The date format
+ * ``timezone``: The date timezone
+
 .. _`strtotime`:    http://www.php.net/strtotime
 .. _`DateTime`:     http://www.php.net/DateTime
 .. _`DateInterval`: http://www.php.net/DateInterval
index ae12c52..6a5c73d 100644 (file)
@@ -14,5 +14,10 @@ The ``date_modify`` filter accepts strings (it must be in a format supported
 by the `strtotime`_ function) or `DateTime`_ instances. You can easily combine
 it with the :doc:`date<date>` filter for formatting.
 
+Arguments
+---------
+
+ * ``modifier``: The modifier
+
 .. _`strtotime`: http://www.php.net/strtotime
 .. _`DateTime`:  http://www.php.net/DateTime
index 4055ead..46ed963 100644 (file)
@@ -26,3 +26,8 @@ undefined:
 
     Read the documentation for the :doc:`defined<../tests/defined>` and
     :doc:`empty<../tests/empty>` tests to learn more about their semantics.
+
+Arguments
+---------
+
+ * ``default``: The default value
index ddb2bbb..5c3bec4 100644 (file)
@@ -84,4 +84,10 @@ The ``escape`` filter supports the following escaping strategies:
             {{ var|escape(strategy)|raw }} {# won't be double-escaped #}
         {% endautoescape %}
 
+Arguments
+---------
+
+ * ``strategy``: The escaping strategy
+ * ``charset``:  The string charset
+
 .. _`htmlspecialchars`: http://php.net/htmlspecialchars
index eec2045..f495242 100644 (file)
@@ -16,3 +16,8 @@ define it with the optional first parameter:
 
     {{ [1, 2, 3]|join('|') }}
     {# returns 1|2|3 #}
+
+Arguments
+---------
+
+ * ``glue``: The separator
index c7d19b3..374f519 100644 (file)
@@ -11,4 +11,9 @@ The ``json_encode`` filter returns the JSON representation of a string:
 
     Internally, Twig uses the PHP `json_encode`_ function.
 
+Arguments
+---------
+
+ * ``options``: The options
+
 .. _`json_encode`: http://php.net/json_encode
index b591b1e..fedacd9 100644 (file)
@@ -35,4 +35,11 @@ These defaults can be easily changed through the core extension:
 The defaults set for ``number_format`` can be over-ridden upon each call using the
 additional parameters.
 
+Arguments
+---------
+
+ * ``decimal``:       The number of decimal points to display
+ * ``decimal_point``: The character(s) to use for the decimal point
+ * ``decimal_sep``:   The character(s) to use for the thousands separator
+
 .. _`number_format`: http://php.net/number_format
index cc603fa..e961f23 100644 (file)
@@ -11,4 +11,9 @@ The ``replace`` filter formats a given string by replacing the placeholders
     {# returns I like foo and bar
        if the foo parameter equals to the foo string. #}
 
+Arguments
+---------
+
+ * ``replace_pairs``: The placeholder values
+
 .. seealso:: :doc:`format<format>`
index 80a4293..0205771 100644 (file)
@@ -52,6 +52,13 @@ up until the end of the variable.
 
     It also works with objects implementing the `Traversable`_ interface.
 
+Arguments
+---------
+
+ * ``start``:         The start of the slice
+ * ``length``:        The size of the slice
+ * ``preserve_keys``: Whether to preserve key or not (when the input is an array)
+
 .. _`Traversable`: http://php.net/manual/en/class.traversable.php
 .. _`array_slice`: http://php.net/array_slice
 .. _`substr`:      http://php.net/substr
index 9b9e4e6..9108a5c 100644 (file)
@@ -43,5 +43,11 @@ chunks. Length is set by the ``limit`` argument (one character by default).
     Internally, Twig uses the PHP `explode`_ or `str_split`_ (if delimiter is
     empty) functions for string splitting.
 
+Arguments
+---------
+
+ * ``delimiter``: The delimiter
+ * ``limit``:     The limit argument
+
 .. _`explode`:   http://php.net/explode
 .. _`str_split`: http://php.net/str_split
index f1215f6..f38afd5 100644 (file)
@@ -21,4 +21,9 @@ and end of a string:
 
     Internally, Twig uses the PHP `trim`_ function.
 
+Arguments
+---------
+
+ * ``character_mask``: The characters to strip
+
 .. _`trim`: http://php.net/trim
index fe11d68..0015cae 100644 (file)
@@ -18,3 +18,8 @@ The array can contain any number of values:
     {% for i in 0..10 %}
         {{ cycle(fruits, i) }}
     {% endfor %}
+
+Arguments
+---------
+
+ * ``position``: The cycle position
index c1a011c..fd67fc5 100644 (file)
@@ -43,4 +43,10 @@ If no argument is passed, the function returns the current date:
         $twig = new Twig_Environment($loader);
         $twig->getExtension('core')->setTimezone('Europe/Paris');
 
+Arguments
+---------
+
+ * ``date``:     The date
+ * ``timezone``: The timezone
+
 .. _`date`: http://www.php.net/date
index 8ff0c05..1500b0f 100644 (file)
@@ -60,5 +60,10 @@ dumped:
 
     Internally, Twig uses the PHP `var_dump`_ function.
 
+Arguments
+---------
+
+ * ``context``: The context to dump
+
 .. _`XDebug`:   http://xdebug.org/docs/display
 .. _`var_dump`: http://php.net/var_dump
index 104493d..a5a916b 100644 (file)
@@ -21,4 +21,9 @@ parameter type:
     {{ random() }}                              {# example output: 15386094 (works as native PHP `mt_rand`_ function) #}
     {{ random(5) }}                             {# example output: 3 #}
 
+Arguments
+---------
+
+ * ``values``: The values
+
 .. _`mt_rand`: http://php.net/mt_rand
index c9bdd96..b1fa547 100644 (file)
@@ -35,4 +35,11 @@ function (with a step of 1):
 
     The ``range`` function works as the native PHP `range`_ function.
 
+Arguments
+---------
+
+ * ``low``:  The first value of the sequence.
+ * ``high``: The highest possible value of the sequence.
+ * ``step``: The increment between elements of the sequence.
+
 .. _`range`: http://php.net/range
index 0b3b0b4..6df650e 100644 (file)
@@ -25,3 +25,8 @@ The ``template_from_string`` function loads a template from a string:
     Even if you will probably always use the ``template_from_string`` function
     with the ``include`` tag, you can use it with any tag or function that
     takes a template as an argument (like the ``embed`` or ``extends`` tags).
+
+Arguments
+---------
+
+ * ``template``: The template
index 64761db..602588a 100644 (file)
@@ -190,6 +190,56 @@ progression of integers:
 Go to the :doc:`functions<functions/index>` page to learn more about the
 built-in functions.
 
+Named Arguments
+---------------
+
+.. versionadded:: 1.12
+    Support for named arguments was added in Twig 1.12.
+
+Arguments for filters and functions can also be passed as *named arguments*:
+
+.. code-block:: jinja
+
+    {% for i in range(low=1, high=10, step=2) %}
+        {{ i }},
+    {% endfor %}
+
+Using named arguments makes your templates more explicit about the meaning of
+the values you pass as arguments:
+
+.. code-block:: jinja
+
+    {{ data|convert_encoding('UTF-8', 'iso-2022-jp') }}
+
+    {# versus #}
+
+    {{ data|convert_encoding(from='iso-2022-jp', to='UTF-8') }}
+
+Named arguments also allow you to skip some arguments for which you don't want
+to change the default value::
+
+.. code-block:: jinja
+
+    {# the first argument is the date format, which defaults to the global date format if null is passed #}
+    {{ "now"|date(null, "Europe/Paris") }}
+
+    {# or skip the format value by using a named argument for the timezone #}
+    {{ "now"|date(timezone="Europe/Paris") }}
+
+You can also use both positional and named arguments in one call, which is not
+recommended as it can be confusing:
+
+.. code-block:: jinja
+
+    {# both work #}
+    {{ "now"|date('d/m/Y H:i', timezone="Europe/Paris") }}
+    {{ "now"|date(timezone="Europe/Paris", 'd/m/Y H:i') }}
+
+.. tip::
+
+    Each function and filter documentation page has a section where the names
+    of all arguments are listed when supported.
+
 Control Structure
 -----------------
 
index aa48303..b572197 100644 (file)
@@ -297,9 +297,9 @@ class Twig_ExpressionParser
 
     public function getFunctionNode($name, $line)
     {
-        $args = $this->parseArguments();
         switch ($name) {
             case 'parent':
+                $args = $this->parseArguments();
                 if (!count($this->parser->getBlockStack())) {
                     throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line, $this->parser->getFilename());
                 }
@@ -310,8 +310,9 @@ class Twig_ExpressionParser
 
                 return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line);
             case 'block':
-                return new Twig_Node_Expression_BlockReference($args->getNode(0), false, $line);
+                return new Twig_Node_Expression_BlockReference($this->parseArguments()->getNode(0), false, $line);
             case 'attribute':
+                $args = $this->parseArguments();
                 if (count($args) < 2) {
                     throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line, $this->parser->getFilename());
                 }
@@ -320,7 +321,7 @@ class Twig_ExpressionParser
             default:
                 if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
                     $arguments = new Twig_Node_Expression_Array(array(), $line);
-                    foreach ($args as $n) {
+                    foreach ($this->parseArguments() as $n) {
                         $arguments->addElement($n);
                     }
 
@@ -330,6 +331,7 @@ class Twig_ExpressionParser
                     return $node;
                 }
 
+                $args = $this->parseArguments(true);
                 $class = $this->getFunctionNodeClass($name, $line);
 
                 return new $class($name, $args, $line);
@@ -416,7 +418,7 @@ class Twig_ExpressionParser
             if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
                 $arguments = new Twig_Node();
             } else {
-                $arguments = $this->parseArguments();
+                $arguments = $this->parseArguments(true);
             }
 
             $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
@@ -433,17 +435,40 @@ class Twig_ExpressionParser
         return $node;
     }
 
-    public function parseArguments()
+    /**
+     * Parses arguments.
+     *
+     * @param Boolean $namedArguments Whether to allow named arguments or not
+     * @param Boolean $definition     Whether we are parsing arguments for a function definition
+     */
+    public function parseArguments($namedArguments = false, $definition = false)
     {
         $args = array();
         $stream = $this->parser->getStream();
 
-        $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must be opened by a parenthesis');
+        $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
         while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ')')) {
             if (!empty($args)) {
                 $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
             }
-            $args[] = $this->parseExpression();
+
+            $value = $this->parseExpression();
+
+            $name = null;
+            if ($namedArguments && $stream->test(Twig_Token::OPERATOR_TYPE, '=')) {
+                $token = $stream->next();
+                if (!$value instanceof Twig_Node_Expression_Name) {
+                    throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given', get_class($value)), $token->getLine());
+                }
+                $name = $value->getAttribute('name');
+                $value = $definition ? $this->parsePrimaryExpression() : $this->parseExpression();
+            }
+
+            if (null === $name) {
+                $args[] = $value;
+            } else {
+                $args[$name] = $value;
+            }
         }
         $stream->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
 
index d2781b6..78bcc8f 100644 (file)
@@ -267,7 +267,7 @@ class Twig_Extension_Core extends Twig_Extension
         $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue();
         $arguments = null;
         if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
-            $arguments = $parser->getExpressionParser()->parseArguments();
+            $arguments = $parser->getExpressionParser()->parseArguments(true);
         }
 
         $class = $this->getTestNodeClass($parser, $name, $node->getLine());
@@ -305,18 +305,18 @@ class Twig_Extension_Core extends Twig_Extension
 /**
  * Cycles over a value.
  *
- * @param ArrayAccess|array $values An array or an ArrayAccess instance
- * @param integer           $i      The cycle value
+ * @param ArrayAccess|array $values   An array or an ArrayAccess instance
+ * @param integer           $position The cycle position
  *
  * @return string The next value in the cycle
  */
-function twig_cycle($values, $i)
+function twig_cycle($values, $position)
 {
     if (!is_array($values) && !$values instanceof ArrayAccess) {
         return $values;
     }
 
-    return $values[$i % count($values)];
+    return $values[$position % count($values)];
 }
 
 /**
@@ -410,7 +410,7 @@ function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $
  * Returns a new date object modified
  *
  * <pre>
- *   {{ post.published_at|modify("-1day")|date("m/d/Y") }}
+ *   {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
  * </pre>
  *
  * @param Twig_Environment  $env      A Twig_Environment instance
index 1a4806c..90a62d3 100644 (file)
@@ -15,7 +15,7 @@
  * @package    twig
  * @author     Fabien Potencier <fabien@symfony.com>
  */
-abstract class Twig_Filter implements Twig_FilterInterface
+abstract class Twig_Filter implements Twig_FilterInterface, Twig_FilterCallableInterface
 {
     protected $options;
     protected $arguments = array();
@@ -27,6 +27,7 @@ abstract class Twig_Filter implements Twig_FilterInterface
             'needs_context'     => false,
             'pre_escape'        => null,
             'preserves_safety'  => null,
+            'callable'          => null,
         ), $options);
     }
 
@@ -72,4 +73,9 @@ abstract class Twig_Filter implements Twig_FilterInterface
     {
         return $this->options['pre_escape'];
     }
+
+    public function getCallable()
+    {
+        return $this->options['callable'];
+    }
 }
index 1de078b..59af50d 100644 (file)
@@ -21,6 +21,8 @@ class Twig_Filter_Function extends Twig_Filter
 
     public function __construct($function, array $options = array())
     {
+        $options['callable'] = $function;
+
         parent::__construct($options);
 
         $this->function = $function;
index a680f61..0f5b27e 100644 (file)
@@ -22,6 +22,8 @@ class Twig_Filter_Method extends Twig_Filter
 
     public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array())
     {
+        $options['callable'] = array($extension, $method);
+
         parent::__construct($options);
 
         $this->extension = $extension;
diff --git a/lib/Twig/FilterCallableInterface.php b/lib/Twig/FilterCallableInterface.php
new file mode 100644 (file)
index 0000000..86f0419
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a callable template filter.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien@symfony.com>
+ */
+interface Twig_FilterCallableInterface
+{
+    public function getCallable();
+}
index cd7643f..0a13441 100644 (file)
@@ -15,7 +15,7 @@
  * @package    twig
  * @author     Fabien Potencier <fabien@symfony.com>
  */
-abstract class Twig_Function implements Twig_FunctionInterface
+abstract class Twig_Function implements Twig_FunctionInterface, Twig_FunctionCallableInterface
 {
     protected $options;
     protected $arguments = array();
@@ -25,6 +25,7 @@ abstract class Twig_Function implements Twig_FunctionInterface
         $this->options = array_merge(array(
             'needs_environment' => false,
             'needs_context'     => false,
+            'callable'          => null,
         ), $options);
     }
 
@@ -60,4 +61,9 @@ abstract class Twig_Function implements Twig_FunctionInterface
 
         return array();
     }
+
+    public function getCallable()
+    {
+        return $this->options['callable'];
+    }
 }
index 3237d8c..e102479 100644 (file)
@@ -22,6 +22,8 @@ class Twig_Function_Function extends Twig_Function
 
     public function __construct($function, array $options = array())
     {
+        $options['callable'] = $function;
+
         parent::__construct($options);
 
         $this->function = $function;
index 8838618..d0f296d 100644 (file)
@@ -23,6 +23,8 @@ class Twig_Function_Method extends Twig_Function
 
     public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array())
     {
+        $options['callable'] = array($extension, $method);
+
         parent::__construct($options);
 
         $this->extension = $extension;
diff --git a/lib/Twig/FunctionCallableInterface.php b/lib/Twig/FunctionCallableInterface.php
new file mode 100644 (file)
index 0000000..fc54308
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a callable template function.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien@symfony.com>
+ */
+interface Twig_FunctionCallableInterface
+{
+    public function getCallable();
+}
index d24680c..0c69d51 100644 (file)
@@ -10,7 +10,7 @@
  */
 abstract class Twig_Node_Expression_Call extends Twig_Node_Expression
 {
-    public function compileArguments(Twig_Compiler $compiler)
+    protected function compileArguments(Twig_Compiler $compiler)
     {
         $compiler->raw('(');
 
@@ -48,7 +48,11 @@ abstract class Twig_Node_Expression_Call extends Twig_Node_Expression
         }
 
         if ($this->hasNode('arguments') && null !== $this->getNode('arguments')) {
-            foreach ($this->getNode('arguments') as $node) {
+            $callable = $this->hasAttribute('callable') ? $this->getAttribute('callable') : null;
+
+            $arguments = $this->getArguments($callable, $this->getNode('arguments'));
+
+            foreach ($arguments as $node) {
                 if (!$first) {
                     $compiler->raw(', ');
                 }
@@ -59,4 +63,83 @@ abstract class Twig_Node_Expression_Call extends Twig_Node_Expression
 
         $compiler->raw(')');
     }
+
+    protected function getArguments($callable, $arguments)
+    {
+        $parameters = array();
+        $named = false;
+        foreach ($arguments as $name => $node) {
+            if (!is_int($name)) {
+                $named = true;
+                $name = $this->normalizeName($name);
+            }
+            $parameters[$name] = $node;
+        }
+
+        if (!$named) {
+            return $parameters;
+        }
+
+        if (!$callable) {
+            throw new LogicException(sprintf('Named arguments are not supported for %s "%s".', $this->getAttribute('type'), $this->getAttribute('name')));
+        }
+
+        // manage named arguments
+        if (is_array($callable)) {
+            $r = new \ReflectionMethod($callable[0], $callable[1]);
+        } elseif (is_object($callable) && !$callable instanceof \Closure) {
+            $r = new \ReflectionObject($callable);
+            $r = $r->getMethod('__invoke');
+        } else {
+            $r = new \ReflectionFunction($callable);
+        }
+
+        $definition = $r->getParameters();
+        if ($this->hasNode('node')) {
+            array_shift($definition);
+        }
+        if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) {
+            array_shift($definition);
+        }
+        if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) {
+            array_shift($definition);
+        }
+        if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) {
+            foreach ($this->getAttribute('arguments') as $argument) {
+                array_shift($definition);
+            }
+        }
+
+        $arguments = array();
+        $pos = 0;
+        foreach ($definition as $param) {
+            $name = $this->normalizeName($param->name);
+
+            if (array_key_exists($name, $parameters)) {
+                $arguments[] = $parameters[$name];
+                unset($parameters[$name]);
+            } elseif (array_key_exists($pos, $parameters)) {
+                $arguments[] = $parameters[$pos];
+                unset($parameters[$pos]);
+                ++$pos;
+            } elseif ($param->isDefaultValueAvailable()) {
+                $arguments[] = new Twig_Node_Expression_Constant($param->getDefaultValue(), -1);
+            } elseif ($param->isOptional()) {
+                break;
+            } else {
+                throw new Twig_Error_Syntax(sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name')));
+            }
+        }
+
+        foreach (array_keys($parameters) as $name) {
+            throw new Twig_Error_Syntax(sprintf('Unknown argument "%s" for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name')));
+        }
+
+        return $arguments;
+    }
+
+    protected function normalizeName($name)
+    {
+        return strtolower(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1_\\2', '\\1_\\2'), $name));
+    }
 }
index 45d2c93..ea7f4a6 100644 (file)
@@ -18,13 +18,19 @@ class Twig_Node_Expression_Filter extends Twig_Node_Expression_Call
 
     public function compile(Twig_Compiler $compiler)
     {
-        $filter = $compiler->getEnvironment()->getFilter($this->getNode('filter')->getAttribute('value'));
+        $name = $this->getNode('filter')->getAttribute('value');
+        $filter = $compiler->getEnvironment()->getFilter($name);
 
         $compiler->raw($filter->compile());
 
+        $this->setAttribute('name', $name);
+        $this->setAttribute('type', 'filter');
         $this->setAttribute('needs_environment', $filter->needsEnvironment());
         $this->setAttribute('needs_context', $filter->needsContext());
         $this->setAttribute('arguments', $filter->getArguments());
+        if ($filter instanceof Twig_FilterCallableInterface) {
+            $this->setAttribute('callable', $filter->getCallable());
+        }
 
         $this->compileArguments($compiler);
     }
index 58461e4..8f2be6e 100644 (file)
@@ -17,13 +17,19 @@ class Twig_Node_Expression_Function extends Twig_Node_Expression_Call
 
     public function compile(Twig_Compiler $compiler)
     {
-        $function = $compiler->getEnvironment()->getFunction($this->getAttribute('name'));
+        $name = $this->getAttribute('name');
+        $function = $compiler->getEnvironment()->getFunction($name);
 
         $compiler->raw($function->compile());
 
+        $this->setAttribute('name', $name);
+        $this->setAttribute('type', 'function');
         $this->setAttribute('needs_environment', $function->needsEnvironment());
         $this->setAttribute('needs_context', $function->needsContext());
         $this->setAttribute('arguments', $function->getArguments());
+        if ($function instanceof Twig_FunctionCallableInterface) {
+            $this->setAttribute('callable', $function->getCallable());
+        }
 
         $this->compileArguments($compiler);
     }
index b41858c..45bb45b 100644 (file)
@@ -19,8 +19,15 @@ class Twig_Node_Expression_Test extends Twig_Node_Expression_Call
     {
         $name = $this->getAttribute('name');
         $testMap = $compiler->getEnvironment()->getTests();
+        $test = $testMap[$name];
 
-        $compiler->raw($testMap[$name]->compile());
+        $this->setAttribute('name', $name);
+        $this->setAttribute('type', 'test');
+        if ($test instanceof Twig_TestCallableInterface) {
+            $this->setAttribute('callable', $test->getCallable());
+        }
+
+        $compiler->raw($test->compile());
 
         $this->compileArguments($compiler);
     }
diff --git a/lib/Twig/Test.php b/lib/Twig/Test.php
new file mode 100644 (file)
index 0000000..06d4f12
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a template test.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien@symfony.com>
+ */
+abstract class Twig_Test implements Twig_TestInterface, Twig_TestCallableInterface
+{
+    protected $options;
+    protected $arguments = array();
+
+    public function __construct(array $options = array())
+    {
+        $this->options = array_merge(array(
+            'callable' => null,
+        ), $options);
+    }
+
+    public function getCallable()
+    {
+        return $this->options['callable'];
+    }
+}
index 1240a0f..50f561f 100644 (file)
  * @package    twig
  * @author     Fabien Potencier <fabien@symfony.com>
  */
-class Twig_Test_Function implements Twig_TestInterface
+class Twig_Test_Function extends Twig_Test
 {
     protected $function;
 
-    public function __construct($function)
+    public function __construct($function, array $options = array())
     {
+        $options['callable'] = $function;
+
+        parent::__construct($options);
+
         $this->function = $function;
     }
 
index a8d1d9b..20dbf82 100644 (file)
  * @package    twig
  * @author     Fabien Potencier <fabien@symfony.com>
  */
-class Twig_Test_Method implements Twig_TestInterface
+class Twig_Test_Method extends Twig_Test
 {
     protected $extension;
     protected $method;
 
-    public function __construct(Twig_ExtensionInterface $extension, $method)
+    public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array())
     {
+        $options['callable'] = array($extension, $method);
+
+        parent::__construct($options);
+
         $this->extension = $extension;
         $this->method = $method;
     }
index 47a978e..d9d47eb 100644 (file)
  * @package    twig
  * @author     Fabien Potencier <fabien@symfony.com>
  */
-class Twig_Test_Node implements Twig_TestInterface
+class Twig_Test_Node extends Twig_Test
 {
     protected $class;
 
-    public function __construct($class)
+    public function __construct($class, array $options = array())
     {
+        parent::__construct($options);
+
         $this->class = $class;
     }
 
diff --git a/lib/Twig/TestCallableInterface.php b/lib/Twig/TestCallableInterface.php
new file mode 100644 (file)
index 0000000..3f12290
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a callable template test.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien@symfony.com>
+ */
+interface Twig_TestCallableInterface
+{
+    public function getCallable();
+}
index 88d6d59..e3fb732 100644 (file)
@@ -216,6 +216,28 @@ class Twig_Tests_ExpressionParserTest extends PHPUnit_Framework_TestCase
     }
 
     /**
+     * @expectedException Twig_Error_Syntax
+     */
+    public function testAttributeCallDoesNotSupportNamedArguments()
+    {
+        $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+        $parser = new Twig_Parser($env);
+
+        $parser->parse($env->tokenize('{{ foo.bar(name="Foo") }}', 'index'));
+    }
+
+    /**
+     * @expectedException Twig_Error_Syntax
+     */
+    public function testMacroCallDoesNotSupportNamedArguments()
+    {
+        $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+        $parser = new Twig_Parser($env);
+
+        $parser->parse($env->tokenize('{% from _self import foo %}{% macro foo() %}{% endmacro %}{{ foo(name="Foo") }}', 'index'));
+    }
+
+    /**
      * @expectedException        Twig_Error_Syntax
      * @expectedExceptionMessage The function "cycl" does not exist. Did you mean "cycle" in "index" at line 1
      */
diff --git a/test/Twig/Tests/Fixtures/filters/date_namedargs.test b/test/Twig/Tests/Fixtures/filters/date_namedargs.test
new file mode 100644 (file)
index 0000000..6ca2049
--- /dev/null
@@ -0,0 +1,15 @@
+--TEST--
+"date" filter
+--TEMPLATE--
+{{ date|date(format='d/m/Y H:i:s P', timezone='America/Chicago') }}
+{{ date|date(timezone='America/Chicago', format='d/m/Y H:i:s P') }}
+{{ date|date(timezone='America/Chicago', 'd/m/Y H:i:s P') }}
+{{ date|date('d/m/Y H:i:s P', timezone='America/Chicago') }}
+--DATA--
+date_default_timezone_set('UTC');
+return array('date' => mktime(13, 45, 0, 10, 4, 2010))
+--EXPECT--
+04/10/2010 08:45:00 -05:00
+04/10/2010 08:45:00 -05:00
+04/10/2010 08:45:00 -05:00
+04/10/2010 08:45:00 -05:00
index 3c5f410..7948ac4 100644 (file)
@@ -4,9 +4,15 @@
 {{ [1, 2, 3, 4]|reverse|join('') }}
 {{ '1234évènement'|reverse }}
 {{ arr|reverse|join('') }}
+{{ {'a': 'c', 'b': 'a'}|reverse()|join(',') }}
+{{ {'a': 'c', 'b': 'a'}|reverse(preserveKeys=true)|join(glue=',') }}
+{{ {'a': 'c', 'b': 'a'}|reverse(preserve_keys=true)|join(glue=',') }}
 --DATA--
 return array('arr' => new ArrayObject(array(1, 2, 3, 4)))
 --EXPECT--
 4321
 tnemenèvé4321
 4321
+a,c
+a,c
+a,c
diff --git a/test/Twig/Tests/Fixtures/functions/date_namedargs.test b/test/Twig/Tests/Fixtures/functions/date_namedargs.test
new file mode 100644 (file)
index 0000000..b9dd9e3
--- /dev/null
@@ -0,0 +1,11 @@
+--TEST--
+"date" function
+--TEMPLATE--
+{{ date(date, "America/New_York")|date('d/m/Y H:i:s P', false) }}
+{{ date(timezone="America/New_York", date=date)|date('d/m/Y H:i:s P', false) }}
+--DATA--
+date_default_timezone_set('UTC');
+return array('date' => mktime(13, 45, 0, 10, 4, 2010))
+--EXPECT--
+04/10/2010 09:45:00 -04:00
+04/10/2010 09:45:00 -04:00
diff --git a/test/Twig/Tests/Fixtures/functions/range.test b/test/Twig/Tests/Fixtures/functions/range.test
new file mode 100644 (file)
index 0000000..e0377c8
--- /dev/null
@@ -0,0 +1,8 @@
+--TEST--
+"range" function
+--TEMPLATE--
+{{ range(low=0+1, high=10+0, step=2)|join(',') }}
+--DATA--
+return array()
+--EXPECT--
+1,3,5,7,9
index 48241d4..86cb834 100644 (file)
@@ -49,9 +49,65 @@ class Twig_Tests_Node_Expression_FilterTest extends Twig_Test_NodeTestCase
             $tests[] = array($node, 'twig_number_format_filter($this->env, strtoupper("foo"), 2, ".", ",")');
         }
 
+        // named arguments
+        $date = new Twig_Node_Expression_Constant(0, 1);
+        $node = $this->createFilter($date, 'date', array(
+            'timezone' => new Twig_Node_Expression_Constant('America/Chicago', 1),
+            'format'   => new Twig_Node_Expression_Constant('d/m/Y H:i:s P', 1),
+        ));
+        $tests[] = array($node, 'twig_date_format_filter($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")');
+
+        // skip an optional argument
+        $date = new Twig_Node_Expression_Constant(0, 1);
+        $node = $this->createFilter($date, 'date', array(
+            'timezone' => new Twig_Node_Expression_Constant('America/Chicago', 1),
+        ));
+        $tests[] = array($node, 'twig_date_format_filter($this->env, 0, null, "America/Chicago")');
+
+        // underscores vs camelCase for named arguments
+        $string = new Twig_Node_Expression_Constant('abc', 1);
+        $node = $this->createFilter($string, 'reverse', array(
+            'preserve_keys' => new Twig_Node_Expression_Constant(true, 1),
+        ));
+        $tests[] = array($node, 'twig_reverse_filter($this->env, "abc", true)');
+        $node = $this->createFilter($string, 'reverse', array(
+            'preserveKeys' => new Twig_Node_Expression_Constant(true, 1),
+        ));
+        $tests[] = array($node, 'twig_reverse_filter($this->env, "abc", true)');
+
         return $tests;
     }
 
+    /**
+     * @expectedException        Twig_Error_Syntax
+     * @expectedExceptionMessage Unknown argument "foobar" for filter "date".
+     */
+    public function testCompileWithWrongNamedArgumentName()
+    {
+        $date = new Twig_Node_Expression_Constant(0, 1);
+        $node = $this->createFilter($date, 'date', array(
+            'foobar' => new Twig_Node_Expression_Constant('America/Chicago', 1),
+        ));
+
+        $compiler = $this->getCompiler();
+        $compiler->compile($node);
+    }
+
+    /**
+     * @expectedException        Twig_Error_Syntax
+     * @expectedExceptionMessage Value for argument "from" is required for filter "replace".
+     */
+    public function testCompileWithMissingNamedArgument()
+    {
+        $value = new Twig_Node_Expression_Constant(0, 1);
+        $node = $this->createFilter($value, 'replace', array(
+            'to' => new Twig_Node_Expression_Constant('foo', 1),
+        ));
+
+        $compiler = $this->getCompiler();
+        $compiler->compile($node);
+    }
+
     protected function createFilter($node, $name, array $arguments = array())
     {
         $name = new Twig_Node_Expression_Constant($name, 1);
index b5ddf33..50f41f5 100644 (file)
@@ -67,6 +67,13 @@ class Twig_Tests_Node_Expression_FunctionTest extends Twig_Test_NodeTestCase
         $node = $this->createFunction('foobar', array(new Twig_Node_Expression_Constant('bar', 1)));
         $tests[] = array($node, 'foobar($this->env, $context, "bar")', $environment);
 
+        // named arguments
+        $node = $this->createFunction('date', array(
+            'timezone' => new Twig_Node_Expression_Constant('America/Chicago', 1),
+            'date'     => new Twig_Node_Expression_Constant(0, 1),
+        ));
+        $tests[] = array($node, 'twig_date_converter($this->env, 0, "America/Chicago")');
+
         return $tests;
     }