From b66181d1c56c5c8166194b4a9694f64084de79b0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 29 Sep 2013 09:40:36 +0200 Subject: [PATCH] added the starts with, ends with, and matches operators --- CHANGELOG | 1 + doc/templates.rst | 32 ++++++++++-- lib/Twig/Extension/Core.php | 51 ++++++++++--------- lib/Twig/Node/Expression/Binary/EndsWith.php | 30 ++++++++++++ lib/Twig/Node/Expression/Binary/Matches.php | 28 +++++++++++ lib/Twig/Node/Expression/Binary/StartsWith.php | 28 +++++++++++ .../Twig/Tests/Fixtures/expressions/ends_with.test | 12 +++++ test/Twig/Tests/Fixtures/expressions/matches.test | 12 +++++ .../Tests/Fixtures/expressions/starts_with.test | 12 +++++ 9 files changed, 177 insertions(+), 29 deletions(-) create mode 100644 lib/Twig/Node/Expression/Binary/EndsWith.php create mode 100644 lib/Twig/Node/Expression/Binary/Matches.php create mode 100644 lib/Twig/Node/Expression/Binary/StartsWith.php create mode 100644 test/Twig/Tests/Fixtures/expressions/ends_with.test create mode 100644 test/Twig/Tests/Fixtures/expressions/matches.test create mode 100644 test/Twig/Tests/Fixtures/expressions/starts_with.test diff --git a/CHANGELOG b/CHANGELOG index ab39662..1c03184 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ * 1.14.0 (2013-XX-XX) + * added new operators: ends with, starts with, and matches * fixed some compatibility issues with HHVM * added a way to add custom escaping strategies * fixed the C extension compilation on Windows diff --git a/doc/templates.rst b/doc/templates.rst index cad1fff..1894928 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -586,9 +586,9 @@ even if you're not working with PHP you should feel comfortable with it. The operator precedence is as follows, with the lowest-precedence operators listed first: ``b-and``, ``b-xor``, ``b-or``, ``or``, ``and``, - ``==``, ``!=``, ``<``, ``>``, ``>=``, ``<=``, ``in``, ``..``, ``+``, - ``-``, ``~``, ``*``, ``/``, ``//``, ``%``, ``is``, ``**``, ``|``, ``[]``, - and ``.``: + ``==``, ``!=``, ``<``, ``>``, ``>=``, ``<=``, ``in``, ``matches``, + ``starts with``, ``ends with``, ``..``, ``+``, ``-``, ``~``, ``*``, ``/``, + ``//``, ``%``, ``is``, ``**``, ``|``, ``[]``, and ``.``: .. code-block:: jinja @@ -707,6 +707,27 @@ Comparisons The following comparison operators are supported in any expression: ``==``, ``!=``, ``<``, ``>``, ``>=``, and ``<=``. +You can also check if a string ``starts with`` or ``ends with`` another +string: + +.. code-block:: jinja + + {% if 'Fabien' starts with 'F' %} + {% endif %} + + {% if 'Fabien' ends with 'n' %} + {% endif %} + +.. note:: + + For complex string comparisons, the ``matches`` operator allows you to use + `regular expressions`_: + + .. code-block:: jinja + + {% if phone matches '{^[\d\.]+$}' %} + {% endif %} + Containment Operator ~~~~~~~~~~~~~~~~~~~~ @@ -794,8 +815,8 @@ categories: {{ foo ? 'yes' : 'no' }} {# as of Twig 1.12.0 #} - {{ foo ?: 'no' }} == {{ foo ? foo : 'no' }} - {{ foo ? 'yes' }} == {{ foo ? 'yes' : '' }} + {{ foo ?: 'no' }} is the same as {{ foo ? foo : 'no' }} + {{ foo ? 'yes' }} is the same as {{ foo ? 'yes' : '' }} String Interpolation ~~~~~~~~~~~~~~~~~~~~ @@ -882,3 +903,4 @@ Extension` chapter. .. _`other Twig syntax mode`: https://github.com/muxx/Twig-HTML.mode .. _`Notepad++ Twig Highlighter`: https://github.com/Banane9/notepadplusplus-twig .. _`web-mode.el`: http://web-mode.org/ +.. _`regular expressions`: http://php.net/manual/en/pcre.pattern.php diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php index f63a249..e03bb3c 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -253,30 +253,33 @@ class Twig_Extension_Core extends Twig_Extension '+' => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Pos'), ), array( - 'or' => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - 'and' => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - 'b-or' => array('precedence' => 16, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - 'b-xor' => array('precedence' => 17, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - 'b-and' => array('precedence' => 18, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '==' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Equal', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '!=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '<' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Less', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '>' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Greater', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '>=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_GreaterEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '<=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_LessEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - 'not in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotIn', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - 'in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_In', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '..' => array('precedence' => 25, 'class' => 'Twig_Node_Expression_Binary_Range', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '+' => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Add', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '-' => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Sub', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '~' => array('precedence' => 40, 'class' => 'Twig_Node_Expression_Binary_Concat', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '*' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mul', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '/' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Div', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '//' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_FloorDiv', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '%' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mod', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - 'is' => array('precedence' => 100, 'callable' => array($this, 'parseTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - 'is not' => array('precedence' => 100, 'callable' => array($this, 'parseNotTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), - '**' => array('precedence' => 200, 'class' => 'Twig_Node_Expression_Binary_Power', 'associativity' => Twig_ExpressionParser::OPERATOR_RIGHT), + 'or' => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'and' => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'b-or' => array('precedence' => 16, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'b-xor' => array('precedence' => 17, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'b-and' => array('precedence' => 18, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '==' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Equal', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '!=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '<' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Less', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '>' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Greater', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '>=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_GreaterEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '<=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_LessEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'not in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotIn', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_In', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'matches' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Matches', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'starts with' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_StartsWith', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'ends with' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_EndsWith', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '..' => array('precedence' => 25, 'class' => 'Twig_Node_Expression_Binary_Range', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '+' => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Add', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '-' => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Sub', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '~' => array('precedence' => 40, 'class' => 'Twig_Node_Expression_Binary_Concat', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '*' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mul', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '/' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Div', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '//' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_FloorDiv', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '%' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mod', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'is' => array('precedence' => 100, 'callable' => array($this, 'parseTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'is not' => array('precedence' => 100, 'callable' => array($this, 'parseNotTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '**' => array('precedence' => 200, 'class' => 'Twig_Node_Expression_Binary_Power', 'associativity' => Twig_ExpressionParser::OPERATOR_RIGHT), ), ); } diff --git a/lib/Twig/Node/Expression/Binary/EndsWith.php b/lib/Twig/Node/Expression/Binary/EndsWith.php new file mode 100644 index 0000000..5de6c72 --- /dev/null +++ b/lib/Twig/Node/Expression/Binary/EndsWith.php @@ -0,0 +1,30 @@ +raw('(0 === substr_compare(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(', -strlen(') + ->subcompile($this->getNode('right')) + ->raw(')))') + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw(''); + } +} diff --git a/lib/Twig/Node/Expression/Binary/Matches.php b/lib/Twig/Node/Expression/Binary/Matches.php new file mode 100644 index 0000000..93bb292 --- /dev/null +++ b/lib/Twig/Node/Expression/Binary/Matches.php @@ -0,0 +1,28 @@ +raw('preg_match(') + ->subcompile($this->getNode('right')) + ->raw(', ') + ->subcompile($this->getNode('left')) + ->raw(')') + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw(''); + } +} diff --git a/lib/Twig/Node/Expression/Binary/StartsWith.php b/lib/Twig/Node/Expression/Binary/StartsWith.php new file mode 100644 index 0000000..eb8c107 --- /dev/null +++ b/lib/Twig/Node/Expression/Binary/StartsWith.php @@ -0,0 +1,28 @@ +raw('(0 === strpos(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw(''); + } +} diff --git a/test/Twig/Tests/Fixtures/expressions/ends_with.test b/test/Twig/Tests/Fixtures/expressions/ends_with.test new file mode 100644 index 0000000..d259d11 --- /dev/null +++ b/test/Twig/Tests/Fixtures/expressions/ends_with.test @@ -0,0 +1,12 @@ +--TEST-- +Twig supports the "ends with" operator +--TEMPLATE-- +{{ 'foo' ends with 'o' ? 'OK' : 'KO' }} +{{ not ('foo' ends with 'f') ? 'OK' : 'KO' }} +{{ not ('foo' ends with 'foowaytoolong') ? 'OK' : 'KO' }} +--DATA-- +return array() +--EXPECT-- +OK +OK +OK diff --git a/test/Twig/Tests/Fixtures/expressions/matches.test b/test/Twig/Tests/Fixtures/expressions/matches.test new file mode 100644 index 0000000..b6c7716 --- /dev/null +++ b/test/Twig/Tests/Fixtures/expressions/matches.test @@ -0,0 +1,12 @@ +--TEST-- +Twig supports the "matches" operator +--TEMPLATE-- +{{ 'foo' matches '/o/' ? 'OK' : 'KO' }} +{{ 'foo' matches '/^fo/' ? 'OK' : 'KO' }} +{{ 'foo' matches '/O/i' ? 'OK' : 'KO' }} +--DATA-- +return array() +--EXPECT-- +OK +OK +OK diff --git a/test/Twig/Tests/Fixtures/expressions/starts_with.test b/test/Twig/Tests/Fixtures/expressions/starts_with.test new file mode 100644 index 0000000..84004ac --- /dev/null +++ b/test/Twig/Tests/Fixtures/expressions/starts_with.test @@ -0,0 +1,12 @@ +--TEST-- +Twig supports the "starts with" operator +--TEMPLATE-- +{{ 'foo' starts with 'f' ? 'OK' : 'KO' }} +{{ not ('foo' starts with 'oo') ? 'OK' : 'KO' }} +{{ not ('foo' starts with 'foowaytoolong') ? 'OK' : 'KO' }} +--DATA-- +return array() +--EXPECT-- +OK +OK +OK -- 1.7.2.5