From 4647913e500839e7d2d72f48385c23ddc3715b92 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 15 Nov 2012 10:41:23 +0100 Subject: [PATCH] added the ability to set default values for macro arguments (closes #447) --- CHANGELOG | 1 + doc/templates.rst | 12 ++++ lib/Twig/ExpressionParser.php | 50 +++++++++++++++-- lib/Twig/Node/Macro.php | 29 +++++++--- lib/Twig/TokenParser/Macro.php | 2 +- test/Twig/Tests/ExpressionParserTest.php | 57 ++++++++++++++++++++ .../Twig/Tests/Fixtures/macros/default_values.test | 16 ++++++ test/Twig/Tests/Node/MacroTest.php | 8 ++- 8 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 test/Twig/Tests/Fixtures/macros/default_values.test diff --git a/CHANGELOG b/CHANGELOG index 6faeb42..fffef7e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ * 1.12.0 (2012-XX-XX) + * added the ability to set default values for macro arguments * 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 diff --git a/doc/templates.rst b/doc/templates.rst index 602588a..1bcb8ee 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -493,6 +493,9 @@ For bigger sections it makes sense to mark a block :doc:`raw`. Macros ------ +.. versionadded:: 1.12 + Support for default argument values was added in Twig 1.12. + Macros are comparable with functions in regular programming languages. They are useful to reuse often used HTML fragments to not repeat yourself. @@ -528,6 +531,15 @@ current namespace via the :doc:`from` tag and optionally alias them:
{{ input_field('password', '', 'password') }}
+A default value can also be defined for macro arguments when not provided in a +macro call: + +.. code-block:: jinja + + {% macro input(name, value = "", type = "text", size = 20) %} + + {% endmacro %} + Expressions ----------- diff --git a/lib/Twig/ExpressionParser.php b/lib/Twig/ExpressionParser.php index b572197..6a555e1 100644 --- a/lib/Twig/ExpressionParser.php +++ b/lib/Twig/ExpressionParser.php @@ -452,22 +452,44 @@ class Twig_ExpressionParser $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); } - $value = $this->parseExpression(); + if ($definition) { + $token = $stream->expect(Twig_Token::NAME_TYPE, null, 'An argument must be a name'); + $value = new Twig_Node_Expression_Name($token->getValue(), $this->parser->getCurrentToken()->getLine()); + } else { + $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()); + throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given', get_class($value)), $token->getLine(), $this->parser->getFilename()); } $name = $value->getAttribute('name'); - $value = $definition ? $this->parsePrimaryExpression() : $this->parseExpression(); + + if ($definition) { + $value = $this->parsePrimaryExpression(); + + if (!$this->checkConstantExpression($value)) { + throw new Twig_Error_Syntax(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $this->parser->getFilename()); + } + } else { + $value = $this->parseExpression(); + } } - if (null === $name) { - $args[] = $value; - } else { + if ($definition) { + if (null === $name) { + $name = $value->getAttribute('name'); + $value = new Twig_Node_Expression_Constant(null, $this->parser->getCurrentToken()->getLine()); + } $args[$name] = $value; + } else { + 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'); @@ -539,4 +561,20 @@ class Twig_ExpressionParser return $filter instanceof Twig_Filter_Node ? $filter->getClass() : 'Twig_Node_Expression_Filter'; } + + // checks that the node only contains "constant" elements + protected function checkConstantExpression(Twig_NodeInterface $node) + { + if (!($node instanceof Twig_Node_Expression_Constant || $node instanceof Twig_Node_Expression_Array)) { + return false; + } + + foreach ($node as $n) { + if (!$this->checkConstantExpression($n)) { + return false; + } + } + + return true; + } } diff --git a/lib/Twig/Node/Macro.php b/lib/Twig/Node/Macro.php index 8bb5d9d..347e4b2 100644 --- a/lib/Twig/Node/Macro.php +++ b/lib/Twig/Node/Macro.php @@ -29,14 +29,27 @@ class Twig_Node_Macro extends Twig_Node */ public function compile(Twig_Compiler $compiler) { - $arguments = array(); - foreach ($this->getNode('arguments') as $argument) { - $arguments[] = '$_'.$argument->getAttribute('name').' = null'; + $compiler + ->addDebugInfo($this) + ->write(sprintf("public function get%s(", $this->getAttribute('name'))) + ; + + $count = count($this->getNode('arguments')); + $pos = 0; + foreach ($this->getNode('arguments') as $name => $default) { + $compiler + ->raw('$_'.$name.' = ') + ->subcompile($default) + ; + + if (++$pos < $count) { + $compiler->raw(', '); + } } $compiler - ->addDebugInfo($this) - ->write(sprintf("public function get%s(%s)\n", $this->getAttribute('name'), implode(', ', $arguments)), "{\n") + ->raw(")\n") + ->write("{\n") ->indent() ; @@ -48,11 +61,11 @@ class Twig_Node_Macro extends Twig_Node ->indent() ; - foreach ($this->getNode('arguments') as $argument) { + foreach ($this->getNode('arguments') as $name => $default) { $compiler ->write('') - ->string($argument->getAttribute('name')) - ->raw(' => $_'.$argument->getAttribute('name')) + ->string($name) + ->raw(' => $_'.$name) ->raw(",\n") ; } diff --git a/lib/Twig/TokenParser/Macro.php b/lib/Twig/TokenParser/Macro.php index de10059..c2a0336 100644 --- a/lib/Twig/TokenParser/Macro.php +++ b/lib/Twig/TokenParser/Macro.php @@ -33,7 +33,7 @@ class Twig_TokenParser_Macro extends Twig_TokenParser $stream = $this->parser->getStream(); $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); - $arguments = $this->parser->getExpressionParser()->parseArguments(); + $arguments = $this->parser->getExpressionParser()->parseArguments(true, true); $stream->expect(Twig_Token::BLOCK_END_TYPE); $this->parser->pushLocalScope(); diff --git a/test/Twig/Tests/ExpressionParserTest.php b/test/Twig/Tests/ExpressionParserTest.php index e3fb732..8ec6537 100644 --- a/test/Twig/Tests/ExpressionParserTest.php +++ b/test/Twig/Tests/ExpressionParserTest.php @@ -239,6 +239,63 @@ class Twig_Tests_ExpressionParserTest extends PHPUnit_Framework_TestCase /** * @expectedException Twig_Error_Syntax + * @expectedExceptionMessage An argument must be a name. Unexpected token "string" of value "a" ("name" expected) in "index" at line 1 + */ + public function testMacroDefinitionDoesNotSupportNonNameVariableName() + { + $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false)); + $parser = new Twig_Parser($env); + + $parser->parse($env->tokenize('{% macro foo("a") %}{% endmacro %}', 'index')); + } + + /** + * @expectedException Twig_Error_Syntax + * @expectedExceptionMessage A default value for an argument must be a constant (a boolean, a string, a number, or an array) in "index" at line 1 + * @dataProvider getMacroDefinitionDoesNotSupportNonConstantDefaultValues + */ + public function testMacroDefinitionDoesNotSupportNonConstantDefaultValues($template) + { + $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false)); + $parser = new Twig_Parser($env); + + $parser->parse($env->tokenize($template, 'index')); + } + + public function getMacroDefinitionDoesNotSupportNonConstantDefaultValues() + { + return array( + array('{% macro foo(name = "a #{foo} a") %}{% endmacro %}'), + array('{% macro foo(name = [["b", "a #{foo} a"]]) %}{% endmacro %}'), + ); + } + + /** + * @dataProvider getMacroDefinitionSupportsConstantDefaultValues + */ + public function testMacroDefinitionSupportsConstantDefaultValues($template) + { + $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false)); + $parser = new Twig_Parser($env); + + $parser->parse($env->tokenize($template, 'index')); + } + + public function getMacroDefinitionSupportsConstantDefaultValues() + { + return array( + array('{% macro foo(name = "aa") %}{% endmacro %}'), + array('{% macro foo(name = 12) %}{% endmacro %}'), + array('{% macro foo(name = true) %}{% endmacro %}'), + array('{% macro foo(name = ["a"]) %}{% endmacro %}'), + array('{% macro foo(name = [["a"]]) %}{% endmacro %}'), + array('{% macro foo(name = {a: "a"}) %}{% endmacro %}'), + array('{% macro foo(name = {a: {b: "a"}}) %}{% endmacro %}'), + ); + } + + /** + * @expectedException Twig_Error_Syntax * @expectedExceptionMessage The function "cycl" does not exist. Did you mean "cycle" in "index" at line 1 */ public function testUnknownFunction() diff --git a/test/Twig/Tests/Fixtures/macros/default_values.test b/test/Twig/Tests/Fixtures/macros/default_values.test new file mode 100644 index 0000000..4ccff7b --- /dev/null +++ b/test/Twig/Tests/Fixtures/macros/default_values.test @@ -0,0 +1,16 @@ +--TEST-- +macro +--TEMPLATE-- +{% from _self import test %} + +{% macro test(a, b = 'bar') -%} +{{ a }}{{ b }} +{%- endmacro %} + +{{ test('foo') }} +{{ test('bar', 'foo') }} +--DATA-- +return array(); +--EXPECT-- +foobar +barfoo diff --git a/test/Twig/Tests/Node/MacroTest.php b/test/Twig/Tests/Node/MacroTest.php index 39e8131..4d2f641 100644 --- a/test/Twig/Tests/Node/MacroTest.php +++ b/test/Twig/Tests/Node/MacroTest.php @@ -37,16 +37,20 @@ class Twig_Tests_Node_MacroTest extends Twig_Test_NodeTestCase public function getTests() { $body = new Twig_Node_Text('foo', 1); - $arguments = new Twig_Node(array(new Twig_Node_Expression_Name('foo', 1)), array(), 1); + $arguments = new Twig_Node(array( + 'foo' => new Twig_Node_Expression_Constant(null, 1), + 'bar' => new Twig_Node_Expression_Constant('Foo', 1), + ), array(), 1); $node = new Twig_Node_Macro('foo', $body, $arguments, 1); return array( array($node, <<env->mergeGlobals(array( "foo" => \$_foo, + "bar" => \$_bar, )); \$blocks = array(); -- 1.7.2.5