From 5d6e7e2e0126b9f44bd9e3e7bd3911cd697b9b95 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 13 Aug 2010 16:15:33 +0200 Subject: [PATCH] added 'test' feature via the 'is' operator (closes #88) --- CHANGELOG | 3 + doc/02-Twig-for-Template-Designers.markdown | 84 +++++++++++++++++++++------ doc/04-Extending-Twig.markdown | 7 ++ lib/Twig/Environment.php | 22 +++++++ lib/Twig/ExpressionParser.php | 32 ++++++++++ lib/Twig/Extension.php | 10 +++ lib/Twig/Extension/Core.php | 56 +++++++++++++----- lib/Twig/ExtensionInterface.php | 7 ++ lib/Twig/Lexer.php | 2 +- lib/Twig/Node/Expression/Test.php | 45 ++++++++++++++ lib/Twig/Test/Function.php | 32 ++++++++++ lib/Twig/TestInterface.php | 22 +++++++ 12 files changed, 288 insertions(+), 34 deletions(-) create mode 100644 lib/Twig/Node/Expression/Test.php create mode 100644 lib/Twig/Test/Function.php create mode 100644 lib/Twig/TestInterface.php diff --git a/CHANGELOG b/CHANGELOG index 3d2e64b..295252e 100644 --- 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 diff --git a/doc/02-Twig-for-Template-Designers.markdown b/doc/02-Twig-for-Template-Designers.markdown index 85667a2..fa646db 100644 --- a/doc/02-Twig-for-Template-Designers.markdown +++ b/doc/02-Twig-for-Template-Designers.markdown @@ -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 ---------- diff --git a/doc/04-Extending-Twig.markdown b/doc/04-Extending-Twig.markdown index a8ad8d3..eca706d 100644 --- a/doc/04-Extending-Twig.markdown +++ b/doc/04-Extending-Twig.markdown @@ -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 diff --git a/lib/Twig/Environment.php b/lib/Twig/Environment.php index 08b295c..dd8fc68 100644 --- a/lib/Twig/Environment.php +++ b/lib/Twig/Environment.php @@ -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)); diff --git a/lib/Twig/ExpressionParser.php b/lib/Twig/ExpressionParser.php index 3c85cdc..19e24a1 100644 --- a/lib/Twig/ExpressionParser.php +++ b/lib/Twig/ExpressionParser.php @@ -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(); diff --git a/lib/Twig/Extension.php b/lib/Twig/Extension.php index 00bc6d7..a0eeb5b 100644 --- a/lib/Twig/Extension.php +++ b/lib/Twig/Extension.php @@ -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(); + } } diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php index f5d6683..60b51dc 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -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; +} diff --git a/lib/Twig/ExtensionInterface.php b/lib/Twig/ExtensionInterface.php index d0a7add..756e953 100644 --- a/lib/Twig/ExtensionInterface.php +++ b/lib/Twig/ExtensionInterface.php @@ -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 diff --git a/lib/Twig/Lexer.php b/lib/Twig/Lexer.php index f1c304b..be0c55c 100644 --- a/lib/Twig/Lexer.php +++ b/lib/Twig/Lexer.php @@ -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 index 0000000..95e3689 --- /dev/null +++ b/lib/Twig/Node/Expression/Test.php @@ -0,0 +1,45 @@ + $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 index 0000000..0214a3d --- /dev/null +++ b/lib/Twig/Test/Function.php @@ -0,0 +1,32 @@ + + * @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 index 0000000..46bb3c9 --- /dev/null +++ b/lib/Twig/TestInterface.php @@ -0,0 +1,22 @@ + + * @version SVN: $Id$ + */ +interface Twig_TestInterface +{ + public function compile(); +} -- 1.7.2.5