added 'test' feature via the 'is' operator (closes #88)
authorFabien Potencier <fabien.potencier@gmail.com>
Fri, 13 Aug 2010 14:15:33 +0000 (16:15 +0200)
committerFabien Potencier <fabien.potencier@gmail.com>
Fri, 13 Aug 2010 14:22:54 +0000 (16:22 +0200)
12 files changed:
CHANGELOG
doc/02-Twig-for-Template-Designers.markdown
doc/04-Extending-Twig.markdown
lib/Twig/Environment.php
lib/Twig/ExpressionParser.php
lib/Twig/Extension.php
lib/Twig/Extension/Core.php
lib/Twig/ExtensionInterface.php
lib/Twig/Lexer.php
lib/Twig/Node/Expression/Test.php [new file with mode: 0644]
lib/Twig/Test/Function.php [new file with mode: 0644]
lib/Twig/TestInterface.php [new file with mode: 0644]

index 3d2e64b..295252e 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,7 +2,10 @@
 
 Backward incompatibilities:
  * the self special variable has been renamed to _self
+ * the odd and even filters are now tests:
+     {{ foo|odd }} must now be written {{ foo is(odd) }}
 
+ * added "test" feature (accessible via the "is" operator)
  * removed the debug tag (should be done in an extension)
  * fixed trans tag when no vars are used in plural form
  * fixed race condition when writing template cache
index 85667a2..fa646db 100644 (file)
@@ -122,7 +122,28 @@ applied to the next.
 around the arguments, like a function call. This example will join a list by
 commas: `{{ list|join(', ') }}`.
 
-The builtin filters section below describes all the builtin filters.
+The built-in filters section below describes all the built-in filters.
+
+Tests (new in Twig 0.9.9)
+-------------------------
+
+Beside filters, there are also so called "tests" available. Tests can be used
+to test a variable against a common expression. To test a variable or
+expression you add `is` plus the name of the test after the variable. For
+example to find out if a variable is odd, you can do `name is odd` which will
+then return `true` or `false` depending on if `name` is odd or not.
+
+Tests can accept arguments too:
+
+    [twig]
+    {% if loop.index is divisibleby(3) %}
+
+Tests can be negated by prepending them with `not`:
+
+    [twig]
+    {% if loop.index is not divisibleby(3) %}
+
+The built-in tests section below describes all the built-in tests.
 
 Comments
 --------
@@ -980,8 +1001,8 @@ two categories:
         [twig]
         {{ foo ? 'yes' : 'no' }}
 
-List of Builtin Filters
------------------------
+List of built-in Filters
+------------------------
 
 ### `date`
 
@@ -1004,22 +1025,6 @@ The `format` filter formats a given string by replacing the placeholders:
     {{ string|format(foo, "bar") }}
     {# returns I like foo and bar. (if the foo parameter equals to the foo string) #}
 
-### `even`
-
-The `even` filter returns `true` if the given number is even, `false`
-otherwise:
-
-    [twig]
-    {{ var|even ? 'even' : 'odd' }}
-
-### `odd`
-
-The `odd` filter returns `true` if the given number is odd, `false`
-otherwise:
-
-    [twig]
-    {{ var|odd ? 'odd' : 'even' }}
-
 ### `cycle`
 
 The `cycle` filter can be used to cycle between an array of values:
@@ -1187,6 +1192,47 @@ with automatic escaping enabled this variable will not be escaped.
       {{ var|safe }} {# var won't be escaped #}
     {% autoescape off %}
 
+List of built-in Tests (new in Twig 0.9.9)
+------------------------------------------
+
+### `divisibleby`
+
+`divisibleby` checks if a variable is divisible by a number:
+
+    [twig]
+    {% if loop.index is divisibleby(3) %}
+
+### `none`
+
+`none` returns `true` if the variable is `none`:
+
+    [twig]
+    {{ var is none }}
+
+### `even`
+
+`even` returns `true` if the given number is even:
+
+    [twig]
+    {{ var is even }}
+
+### `odd`
+
+`odd` returns `true` if the given number is odd:
+
+    [twig]
+    {{ var is odd }}
+
+### `sameas`
+
+`sameas` checks if a variable points to the same memory address than another
+variable:
+
+    [twig]
+    {% if foo.attribute is sameas(false) %}
+        the foo attribute really is the `false` PHP value
+    {% endif %}
+
 Extensions
 ----------
 
index a8ad8d3..eca706d 100644 (file)
@@ -61,6 +61,13 @@ An extension is a class that implements the following interface:
       public function getFilters();
 
       /**
+       * Returns a list of tests to add to the existing list.
+       *
+       * @return array An array of tests
+       */
+      public function getTests();
+
+      /**
        * Returns the name of the extension.
        *
        * @return string The extension name
index 08b295c..dd8fc68 100644 (file)
@@ -27,6 +27,7 @@ class Twig_Environment
     protected $parsers;
     protected $visitors;
     protected $filters;
+    protected $tests;
     protected $runtimeInitialized;
     protected $loadedTemplates;
     protected $strictVariables;
@@ -404,6 +405,27 @@ class Twig_Environment
         return $this->filters;
     }
 
+    public function addTest($name, Twig_TestInterface $test)
+    {
+        if (null === $this->tests) {
+            $this->getTests();
+        }
+
+        $this->tests[$name] = $test;
+    }
+
+    public function getTests()
+    {
+        if (null === $this->tests) {
+            $this->tests = array();
+            foreach ($this->getExtensions() as $extension) {
+                $this->tests = array_merge($this->tests, $extension->getTests());
+            }
+        }
+
+        return $this->tests;
+    }
+
     protected function writeCacheFile($file, $content)
     {
         $tmpFile = tempnam(dirname($file), basename($file));
index 3c85cdc..19e24a1 100644 (file)
@@ -347,6 +347,11 @@ class Twig_ExpressionParser
                     $node = $this->parseFilterExpression($node);
                     break;
 
+                case 'is':
+                    $stop = true;
+                    $node = $this->parseTestExpression($node);
+                    break;
+
                 default:
                     $stop = true;
                     break;
@@ -356,6 +361,33 @@ class Twig_ExpressionParser
         return $node;
     }
 
+    public function parseTestExpression($node)
+    {
+        $stream = $this->parser->getStream();
+        $token = $stream->next();
+        $lineno = $token->getLine();
+
+        $negated = false;
+        if ($stream->test('not')) {
+            $stream->next();
+            $negated = true;
+        }
+
+        $name = $stream->expect(Twig_Token::NAME_TYPE);
+
+        $arguments = null;
+        if ($stream->test(Twig_Token::OPERATOR_TYPE, '(')) {
+            $arguments = $this->parseArguments($node);
+        }
+        $test = new Twig_Node_Expression_Test($node, $name->getValue(), $arguments, $lineno);
+
+        if ($negated) {
+            $test = new Twig_Node_Expression_Unary_Not($test, $lineno);
+        }
+
+        return $test;
+    }
+
     public function parseRangeExpression($node)
     {
         $token = $this->parser->getStream()->next();
index 00bc6d7..a0eeb5b 100644 (file)
@@ -48,4 +48,14 @@ abstract class Twig_Extension implements Twig_ExtensionInterface
     {
         return array();
     }
+
+    /**
+     * Returns a list of tests to add to the existing list.
+     *
+     * @return array An array of tests
+     */
+    public function getTests()
+    {
+        return array();
+    }
 }
index f5d6683..60b51dc 100644 (file)
@@ -44,10 +44,6 @@ class Twig_Extension_Core extends Twig_Extension
             'date'   => new Twig_Filter_Function('twig_date_format_filter'),
             'format' => new Twig_Filter_Function('sprintf'),
 
-            // numbers
-            'even'  => new Twig_Filter_Function('twig_is_even_filter'),
-            'odd'   => new Twig_Filter_Function('twig_is_odd_filter'),
-
             // encoding
             'urlencode' => new Twig_Filter_Function('twig_urlencode_filter', array('is_escaper' => true)),
 
@@ -86,6 +82,23 @@ class Twig_Extension_Core extends Twig_Extension
     }
 
     /**
+     * Returns a list of filters to add to the existing list.
+     *
+     * @return array An array of filters
+     */
+    public function getTests()
+    {
+        return array(
+            'even'        => new Twig_Test_Function('twig_test_even'),
+            'odd'         => new Twig_Test_Function('twig_test_odd'),
+            //'defined'     => new Twig_Test_Function(),
+            'sameas'      => new Twig_Test_Function('twig_test_sameas'),
+            'none'        => new Twig_Test_Function('twig_test_none'),
+            'divisibleby' => new Twig_Test_Function('twig_test_divisibleby'),
+        );
+    }
+
+    /**
      * Returns the name of the extension.
      *
      * @return string The extension name
@@ -150,16 +163,6 @@ function twig_reverse_filter($array)
     return array_reverse($array);
 }
 
-function twig_is_even_filter($value)
-{
-    return $value % 2 == 0;
-}
-
-function twig_is_odd_filter($value)
-{
-    return $value % 2 == 1;
-}
-
 function twig_sort_filter($array)
 {
     asort($array);
@@ -304,3 +307,28 @@ function twig_get_array_items_filter($array)
     // noop
     return $array;
 }
+
+function twig_test_sameas($value, $test)
+{
+    return $value === $test;
+}
+
+function twig_test_none($value)
+{
+    return null === $value;
+}
+
+function twig_test_divisibleby($value, $num)
+{
+    return 0 == $value % $num;
+}
+
+function twig_test_even($value)
+{
+    return $value % 2 == 0;
+}
+
+function twig_test_odd($value)
+{
+    return $value % 2 == 1;
+}
index d0a7add..756e953 100644 (file)
@@ -47,6 +47,13 @@ interface Twig_ExtensionInterface
     public function getFilters();
 
     /**
+     * Returns a list of tests to add to the existing list.
+     *
+     * @return array An array of tests
+     */
+    public function getTests();
+
+    /**
      * Returns the name of the extension.
      *
      * @return string The extension name
index f1c304b..be0c55c 100644 (file)
@@ -36,7 +36,7 @@ class Twig_Lexer implements Twig_LexerInterface
     const REGEX_NAME     = '/[A-Za-z_][A-Za-z0-9_]*/A';
     const REGEX_NUMBER   = '/[0-9]+(?:\.[0-9]+)?/A';
     const REGEX_STRING   = '/(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')/Asm';
-    const REGEX_OPERATOR = '/<=? | >=? | [!=]= | = | \/\/ | \.\. | [(){}.,%*\/+~|-] | \[ | \] | \? | \:/Ax';
+    const REGEX_OPERATOR = '/<=? | >=? | [!=]= | = | \/\/ | \.\. | is | [(){}.,%*\/+~|-] | \[ | \] | \? | \:/Ax';
 
     public function __construct(Twig_Environment $env = null, array $options = array())
     {
diff --git a/lib/Twig/Node/Expression/Test.php b/lib/Twig/Node/Expression/Test.php
new file mode 100644 (file)
index 0000000..95e3689
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Test extends Twig_Node_Expression
+{
+    public function __construct(Twig_NodeInterface $node, $name, Twig_NodeInterface $arguments = null, $lineno)
+    {
+        parent::__construct(array('node' => $node, 'arguments' => $arguments), array('name' => $name), $lineno);
+    }
+
+    public function compile($compiler)
+    {
+        $testMap = $compiler->getEnvironment()->getTests();
+        if (!isset($testMap[$this['name']])) {
+            throw new Twig_SyntaxError(sprintf('The test "%s" does not exist', $this['name']), $this->getLine());
+        }
+
+        $compiler
+            ->raw($testMap[$this['name']]->compile().'(')
+            ->subcompile($this->node)
+        ;
+
+        if (null !== $this->arguments) {
+            $compiler->raw(', ');
+
+            $max = count($this->arguments) - 1;
+            foreach ($this->arguments as $i => $node) {
+                $compiler->subcompile($node);
+
+                if ($i != $max) {
+                    $compiler->raw(', ');
+                }
+            }
+        }
+
+        $compiler->raw(')');
+    }
+}
diff --git a/lib/Twig/Test/Function.php b/lib/Twig/Test/Function.php
new file mode 100644 (file)
index 0000000..0214a3d
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a function template test.
+ *
+ * @package    twig
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+class Twig_Test_Function implements Twig_TestInterface
+{
+    protected $function;
+
+    public function __construct($function)
+    {
+        $this->function = $function;
+    }
+
+    public function compile()
+    {
+        return $this->function;
+    }
+}
diff --git a/lib/Twig/TestInterface.php b/lib/Twig/TestInterface.php
new file mode 100644 (file)
index 0000000..46bb3c9
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 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.potencier@symfony-project.com>
+ * @version    SVN: $Id$
+ */
+interface Twig_TestInterface
+{
+    public function compile();
+}