4 * This file is part of Twig.
6 * (c) 2009 Fabien Potencier
7 * (c) 2009 Armin Ronacher
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
16 * This parser implements a "Precedence climbing" algorithm.
18 * @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
19 * @see http://en.wikipedia.org/wiki/Operator-precedence_parser
21 * @author Fabien Potencier <fabien@symfony.com>
23 class Twig_ExpressionParser
25 const OPERATOR_LEFT
= 1;
26 const OPERATOR_RIGHT
= 2;
29 protected $unaryOperators;
30 protected $binaryOperators;
32 public function __construct(Twig_Parser
$parser, array $unaryOperators, array $binaryOperators)
34 $this->parser
= $parser;
35 $this->unaryOperators
= $unaryOperators;
36 $this->binaryOperators
= $binaryOperators;
39 public function parseExpression($precedence = 0)
41 $expr = $this->getPrimary();
42 $token = $this->parser
->getCurrentToken();
43 while ($this->isBinary($token) && $this->binaryOperators
[$token->getValue()]['precedence'] >= $precedence) {
44 $op = $this->binaryOperators
[$token->getValue()];
45 $this->parser
->getStream()->next();
47 if (isset($op['callable'])) {
48 $expr = call_user_func($op['callable'], $this->parser
, $expr);
50 $expr1 = $this->parseExpression(self
::OPERATOR_LEFT
=== $op['associativity'] ? $op['precedence'] +
1 : $op['precedence']);
51 $class = $op['class'];
52 $expr = new $class($expr, $expr1, $token->getLine());
55 $token = $this->parser
->getCurrentToken();
58 if (0 === $precedence) {
59 return $this->parseConditionalExpression($expr);
65 protected function getPrimary()
67 $token = $this->parser
->getCurrentToken();
69 if ($this->isUnary($token)) {
70 $operator = $this->unaryOperators
[$token->getValue()];
71 $this->parser
->getStream()->next();
72 $expr = $this->parseExpression($operator['precedence']);
73 $class = $operator['class'];
75 return $this->parsePostfixExpression(new $class($expr, $token->getLine()));
76 } elseif ($token->test(Twig_Token
::PUNCTUATION_TYPE
, '(')) {
77 $this->parser
->getStream()->next();
78 $expr = $this->parseExpression();
79 $this->parser
->getStream()->expect(Twig_Token
::PUNCTUATION_TYPE
, ')', 'An opened parenthesis is not properly closed');
81 return $this->parsePostfixExpression($expr);
84 return $this->parsePrimaryExpression();
87 protected function parseConditionalExpression($expr)
89 while ($this->parser
->getStream()->test(Twig_Token
::PUNCTUATION_TYPE
, '?')) {
90 $this->parser
->getStream()->next();
91 if (!$this->parser
->getStream()->test(Twig_Token
::PUNCTUATION_TYPE
, ':')) {
92 $expr2 = $this->parseExpression();
93 if ($this->parser
->getStream()->test(Twig_Token
::PUNCTUATION_TYPE
, ':')) {
94 $this->parser
->getStream()->next();
95 $expr3 = $this->parseExpression();
97 $expr3 = new Twig_Node_Expression_Constant('', $this->parser
->getCurrentToken()->getLine());
100 $this->parser
->getStream()->next();
102 $expr3 = $this->parseExpression();
105 $expr = new Twig_Node_Expression_Conditional($expr, $expr2, $expr3, $this->parser
->getCurrentToken()->getLine());
111 protected function isUnary(Twig_Token
$token)
113 return $token->test(Twig_Token
::OPERATOR_TYPE
) && isset($this->unaryOperators
[$token->getValue()]);
116 protected function isBinary(Twig_Token
$token)
118 return $token->test(Twig_Token
::OPERATOR_TYPE
) && isset($this->binaryOperators
[$token->getValue()]);
121 public function parsePrimaryExpression()
123 $token = $this->parser
->getCurrentToken();
124 switch ($token->getType()) {
125 case Twig_Token
::NAME_TYPE
:
126 $this->parser
->getStream()->next();
127 switch ($token->getValue()) {
130 $node = new Twig_Node_Expression_Constant(true, $token->getLine());
135 $node = new Twig_Node_Expression_Constant(false, $token->getLine());
142 $node = new Twig_Node_Expression_Constant(null, $token->getLine());
146 if ('(' === $this->parser
->getCurrentToken()->getValue()) {
147 $node = $this->getFunctionNode($token->getValue(), $token->getLine());
149 $node = new Twig_Node_Expression_Name($token->getValue(), $token->getLine());
154 case Twig_Token
::NUMBER_TYPE
:
155 $this->parser
->getStream()->next();
156 $node = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
159 case Twig_Token
::STRING_TYPE
:
160 case Twig_Token
::INTERPOLATION_START_TYPE
:
161 $node = $this->parseStringExpression();
165 if ($token->test(Twig_Token
::PUNCTUATION_TYPE
, '[')) {
166 $node = $this->parseArrayExpression();
167 } elseif ($token->test(Twig_Token
::PUNCTUATION_TYPE
, '{')) {
168 $node = $this->parseHashExpression();
170 throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', Twig_Token
::typeToEnglish($token->getType(), $token->getLine()), $token->getValue()), $token->getLine(), $this->parser
->getFilename());
174 return $this->parsePostfixExpression($node);
177 public function parseStringExpression()
179 $stream = $this->parser
->getStream();
182 // a string cannot be followed by another string in a single expression
183 $nextCanBeString = true;
185 if ($stream->test(Twig_Token
::STRING_TYPE
) && $nextCanBeString) {
186 $token = $stream->next();
187 $nodes[] = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
188 $nextCanBeString = false;
189 } elseif ($stream->test(Twig_Token
::INTERPOLATION_START_TYPE
)) {
191 $nodes[] = $this->parseExpression();
192 $stream->expect(Twig_Token
::INTERPOLATION_END_TYPE
);
193 $nextCanBeString = true;
199 $expr = array_shift($nodes);
200 foreach ($nodes as $node) {
201 $expr = new Twig_Node_Expression_Binary_Concat($expr, $node, $node->getLine());
207 public function parseArrayExpression()
209 $stream = $this->parser
->getStream();
210 $stream->expect(Twig_Token
::PUNCTUATION_TYPE
, '[', 'An array element was expected');
212 $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine());
214 while (!$stream->test(Twig_Token
::PUNCTUATION_TYPE
, ']')) {
216 $stream->expect(Twig_Token
::PUNCTUATION_TYPE
, ',', 'An array element must be followed by a comma');
219 if ($stream->test(Twig_Token
::PUNCTUATION_TYPE
, ']')) {
225 $node->addElement($this->parseExpression());
227 $stream->expect(Twig_Token
::PUNCTUATION_TYPE
, ']', 'An opened array is not properly closed');
232 public function parseHashExpression()
234 $stream = $this->parser
->getStream();
235 $stream->expect(Twig_Token
::PUNCTUATION_TYPE
, '{', 'A hash element was expected');
237 $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine());
239 while (!$stream->test(Twig_Token
::PUNCTUATION_TYPE
, '}')) {
241 $stream->expect(Twig_Token
::PUNCTUATION_TYPE
, ',', 'A hash value must be followed by a comma');
244 if ($stream->test(Twig_Token
::PUNCTUATION_TYPE
, '}')) {
250 // a hash key can be:
254 // * a name, which is equivalent to a string -- a
255 // * an expression, which must be enclosed in parentheses -- (1 + 2)
256 if ($stream->test(Twig_Token
::STRING_TYPE
) || $stream->test(Twig_Token
::NAME_TYPE
) || $stream->test(Twig_Token
::NUMBER_TYPE
)) {
257 $token = $stream->next();
258 $key = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
259 } elseif ($stream->test(Twig_Token
::PUNCTUATION_TYPE
, '(')) {
260 $key = $this->parseExpression();
262 $current = $stream->getCurrent();
264 throw new Twig_Error_Syntax(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s"', Twig_Token
::typeToEnglish($current->getType(), $current->getLine()), $current->getValue()), $current->getLine(), $this->parser
->getFilename());
267 $stream->expect(Twig_Token
::PUNCTUATION_TYPE
, ':', 'A hash key must be followed by a colon (:)');
268 $value = $this->parseExpression();
270 $node->addElement($value, $key);
272 $stream->expect(Twig_Token
::PUNCTUATION_TYPE
, '}', 'An opened hash is not properly closed');
277 public function parsePostfixExpression($node)
280 $token = $this->parser
->getCurrentToken();
281 if ($token->getType() == Twig_Token
::PUNCTUATION_TYPE
) {
282 if ('.' == $token->getValue() || '[' == $token->getValue()) {
283 $node = $this->parseSubscriptExpression($node);
284 } elseif ('|' == $token->getValue()) {
285 $node = $this->parseFilterExpression($node);
297 public function getFunctionNode($name, $line)
301 $args = $this->parseArguments();
302 if (!count($this->parser
->getBlockStack())) {
303 throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line, $this->parser
->getFilename());
306 if (!$this->parser
->getParent() && !$this->parser
->hasTraits()) {
307 throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line, $this->parser
->getFilename());
310 return new Twig_Node_Expression_Parent($this->parser
->peekBlockStack(), $line);
312 return new Twig_Node_Expression_BlockReference($this->parseArguments()->getNode(0), false, $line);
314 $args = $this->parseArguments();
315 if (count($args) < 2) {
316 throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line, $this->parser
->getFilename());
319 return new Twig_Node_Expression_GetAttr($args->getNode(0), $args->getNode(1), count($args) > 2 ? $args->getNode(2) : new Twig_Node_Expression_Array(array(), $line), Twig_TemplateInterface
::ANY_CALL
, $line);
321 if (null !== $alias = $this->parser
->getImportedSymbol('function', $name)) {
322 $arguments = new Twig_Node_Expression_Array(array(), $line);
323 foreach ($this->parseArguments() as $n) {
324 $arguments->addElement($n);
327 $node = new Twig_Node_Expression_MethodCall($alias['node'], $alias['name'], $arguments, $line);
328 $node->setAttribute('safe', true);
333 $args = $this->parseArguments(true);
334 $class = $this->getFunctionNodeClass($name, $line);
336 return new $class($name, $args, $line);
340 public function parseSubscriptExpression($node)
342 $stream = $this->parser
->getStream();
343 $token = $stream->next();
344 $lineno = $token->getLine();
345 $arguments = new Twig_Node_Expression_Array(array(), $lineno);
346 $type = Twig_TemplateInterface
::ANY_CALL
;
347 if ($token->getValue() == '.') {
348 $token = $stream->next();
350 $token->getType() == Twig_Token
::NAME_TYPE
352 $token->getType() == Twig_Token
::NUMBER_TYPE
354 ($token->getType() == Twig_Token
::OPERATOR_TYPE
&& preg_match(Twig_Lexer
::REGEX_NAME
, $token->getValue()))
356 $arg = new Twig_Node_Expression_Constant($token->getValue(), $lineno);
358 if ($stream->test(Twig_Token
::PUNCTUATION_TYPE
, '(')) {
359 $type = Twig_TemplateInterface
::METHOD_CALL
;
360 foreach ($this->parseArguments() as $n) {
361 $arguments->addElement($n);
365 throw new Twig_Error_Syntax('Expected name or number', $lineno, $this->parser
->getFilename());
368 if ($node instanceof Twig_Node_Expression_Name
&& null !== $this->parser
->getImportedSymbol('template', $node->getAttribute('name'))) {
369 if (!$arg instanceof Twig_Node_Expression_Constant
) {
370 throw new Twig_Error_Syntax(sprintf('Dynamic macro names are not supported (called on "%s")', $node->getAttribute('name')), $token->getLine(), $this->parser
->getFilename());
373 $node = new Twig_Node_Expression_MethodCall($node, 'get'.$arg->getAttribute('value'), $arguments, $lineno);
374 $node->setAttribute('safe', true);
379 $type = Twig_TemplateInterface
::ARRAY_CALL
;
383 if ($stream->test(Twig_Token
::PUNCTUATION_TYPE
, ':')) {
385 $arg = new Twig_Node_Expression_Constant(0, $token->getLine());
387 $arg = $this->parseExpression();
390 if ($stream->test(Twig_Token
::PUNCTUATION_TYPE
, ':')) {
396 if ($stream->test(Twig_Token
::PUNCTUATION_TYPE
, ']')) {
397 $length = new Twig_Node_Expression_Constant(null, $token->getLine());
399 $length = $this->parseExpression();
402 $class = $this->getFilterNodeClass('slice', $token->getLine());
403 $arguments = new Twig_Node(array($arg, $length));
404 $filter = new $class($node, new Twig_Node_Expression_Constant('slice', $token->getLine()), $arguments, $token->getLine());
406 $stream->expect(Twig_Token
::PUNCTUATION_TYPE
, ']');
411 $stream->expect(Twig_Token
::PUNCTUATION_TYPE
, ']');
414 return new Twig_Node_Expression_GetAttr($node, $arg, $arguments, $type, $lineno);
417 public function parseFilterExpression($node)
419 $this->parser
->getStream()->next();
421 return $this->parseFilterExpressionRaw($node);
424 public function parseFilterExpressionRaw($node, $tag = null)
427 $token = $this->parser
->getStream()->expect(Twig_Token
::NAME_TYPE
);
429 $name = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
430 if (!$this->parser
->getStream()->test(Twig_Token
::PUNCTUATION_TYPE
, '(')) {
431 $arguments = new Twig_Node();
433 $arguments = $this->parseArguments(true);
436 $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
438 $node = new $class($node, $name, $arguments, $token->getLine(), $tag);
440 if (!$this->parser
->getStream()->test(Twig_Token
::PUNCTUATION_TYPE
, '|')) {
444 $this->parser
->getStream()->next();
453 * @param Boolean $namedArguments Whether to allow named arguments or not
454 * @param Boolean $definition Whether we are parsing arguments for a function definition
456 public function parseArguments($namedArguments = false, $definition = false)
459 $stream = $this->parser
->getStream();
461 $stream->expect(Twig_Token
::PUNCTUATION_TYPE
, '(', 'A list of arguments must begin with an opening parenthesis');
462 while (!$stream->test(Twig_Token
::PUNCTUATION_TYPE
, ')')) {
464 $stream->expect(Twig_Token
::PUNCTUATION_TYPE
, ',', 'Arguments must be separated by a comma');
468 $token = $stream->expect(Twig_Token
::NAME_TYPE
, null, 'An argument must be a name');
469 $value = new Twig_Node_Expression_Name($token->getValue(), $this->parser
->getCurrentToken()->getLine());
471 $value = $this->parseExpression();
475 if ($namedArguments && $stream->test(Twig_Token
::OPERATOR_TYPE
, '=')) {
476 $token = $stream->next();
477 if (!$value instanceof Twig_Node_Expression_Name
) {
478 throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given', get_class($value)), $token->getLine(), $this->parser
->getFilename());
480 $name = $value->getAttribute('name');
483 $value = $this->parsePrimaryExpression();
485 if (!$this->checkConstantExpression($value)) {
486 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());
489 $value = $this->parseExpression();
494 if (null === $name) {
495 $name = $value->getAttribute('name');
496 $value = new Twig_Node_Expression_Constant(null, $this->parser
->getCurrentToken()->getLine());
498 $args[$name] = $value;
500 if (null === $name) {
503 $args[$name] = $value;
507 $stream->expect(Twig_Token
::PUNCTUATION_TYPE
, ')', 'A list of arguments must be closed by a parenthesis');
509 return new Twig_Node($args);
512 public function parseAssignmentExpression()
516 $token = $this->parser
->getStream()->expect(Twig_Token
::NAME_TYPE
, null, 'Only variables can be assigned to');
517 if (in_array($token->getValue(), array('true', 'false', 'none'))) {
518 throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine(), $this->parser
->getFilename());
520 $targets[] = new Twig_Node_Expression_AssignName($token->getValue(), $token->getLine());
522 if (!$this->parser
->getStream()->test(Twig_Token
::PUNCTUATION_TYPE
, ',')) {
525 $this->parser
->getStream()->next();
528 return new Twig_Node($targets);
531 public function parseMultitargetExpression()
535 $targets[] = $this->parseExpression();
536 if (!$this->parser
->getStream()->test(Twig_Token
::PUNCTUATION_TYPE
, ',')) {
539 $this->parser
->getStream()->next();
542 return new Twig_Node($targets);
545 protected function getFunctionNodeClass($name, $line)
547 $env = $this->parser
->getEnvironment();
549 if (false === $function = $env->getFunction($name)) {
550 $message = sprintf('The function "%s" does not exist', $name);
551 if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFunctions()))) {
552 $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives));
555 throw new Twig_Error_Syntax($message, $line, $this->parser
->getFilename());
558 if ($function instanceof Twig_SimpleFunction
) {
559 return $function->getNodeClass();
562 return $function instanceof Twig_Function_Node
? $function->getClass() : 'Twig_Node_Expression_Function';
565 protected function getFilterNodeClass($name, $line)
567 $env = $this->parser
->getEnvironment();
569 if (false === $filter = $env->getFilter($name)) {
570 $message = sprintf('The filter "%s" does not exist', $name);
571 if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFilters()))) {
572 $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives));
575 throw new Twig_Error_Syntax($message, $line, $this->parser
->getFilename());
578 if ($filter instanceof Twig_SimpleFilter
) {
579 return $filter->getNodeClass();
582 return $filter instanceof Twig_Filter_Node
? $filter->getClass() : 'Twig_Node_Expression_Filter';
585 // checks that the node only contains "constant" elements
586 protected function checkConstantExpression(Twig_NodeInterface
$node)
588 if (!($node instanceof Twig_Node_Expression_Constant
|| $node instanceof Twig_Node_Expression_Array
)) {
592 foreach ($node as $n) {
593 if (!$this->checkConstantExpression($n)) {