vendor/twig/lib/Twig/ExpressionParser.php
changeset 0 7f95f8617b0b
equal deleted inserted replaced
-1:000000000000 0:7f95f8617b0b
       
     1 <?php
       
     2 
       
     3 /*
       
     4  * This file is part of Twig.
       
     5  *
       
     6  * (c) 2009 Fabien Potencier
       
     7  * (c) 2009 Armin Ronacher
       
     8  *
       
     9  * For the full copyright and license information, please view the LICENSE
       
    10  * file that was distributed with this source code.
       
    11  */
       
    12 
       
    13 /**
       
    14  * Parses expressions.
       
    15  *
       
    16  * This parser implements a "Precedence climbing" algorithm.
       
    17  *
       
    18  * @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
       
    19  * @see http://en.wikipedia.org/wiki/Operator-precedence_parser
       
    20  *
       
    21  * @package    twig
       
    22  * @author     Fabien Potencier <fabien@symfony.com>
       
    23  */
       
    24 class Twig_ExpressionParser
       
    25 {
       
    26     const OPERATOR_LEFT = 1;
       
    27     const OPERATOR_RIGHT = 2;
       
    28 
       
    29     protected $parser;
       
    30     protected $unaryOperators;
       
    31     protected $binaryOperators;
       
    32 
       
    33     public function __construct(Twig_Parser $parser, array $unaryOperators, array $binaryOperators)
       
    34     {
       
    35         $this->parser = $parser;
       
    36         $this->unaryOperators = $unaryOperators;
       
    37         $this->binaryOperators = $binaryOperators;
       
    38     }
       
    39 
       
    40     public function parseExpression($precedence = 0)
       
    41     {
       
    42         $expr = $this->getPrimary();
       
    43         $token = $this->parser->getCurrentToken();
       
    44         while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) {
       
    45             $op = $this->binaryOperators[$token->getValue()];
       
    46             $this->parser->getStream()->next();
       
    47 
       
    48             if (isset($op['callable'])) {
       
    49                 $expr = call_user_func($op['callable'], $this->parser, $expr);
       
    50             } else {
       
    51                 $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']);
       
    52                 $class = $op['class'];
       
    53                 $expr = new $class($expr, $expr1, $token->getLine());
       
    54             }
       
    55 
       
    56             $token = $this->parser->getCurrentToken();
       
    57         }
       
    58 
       
    59         if (0 === $precedence) {
       
    60             return $this->parseConditionalExpression($expr);
       
    61         }
       
    62 
       
    63         return $expr;
       
    64     }
       
    65 
       
    66     protected function getPrimary()
       
    67     {
       
    68         $token = $this->parser->getCurrentToken();
       
    69 
       
    70         if ($this->isUnary($token)) {
       
    71             $operator = $this->unaryOperators[$token->getValue()];
       
    72             $this->parser->getStream()->next();
       
    73             $expr = $this->parseExpression($operator['precedence']);
       
    74             $class = $operator['class'];
       
    75 
       
    76             return $this->parsePostfixExpression(new $class($expr, $token->getLine()));
       
    77         } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
       
    78             $this->parser->getStream()->next();
       
    79             $expr = $this->parseExpression();
       
    80             $this->parser->getStream()->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed');
       
    81 
       
    82             return $this->parsePostfixExpression($expr);
       
    83         }
       
    84 
       
    85         return $this->parsePrimaryExpression();
       
    86     }
       
    87 
       
    88     protected function parseConditionalExpression($expr)
       
    89     {
       
    90         while ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '?')) {
       
    91             $this->parser->getStream()->next();
       
    92             $expr2 = $this->parseExpression();
       
    93             $this->parser->getStream()->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'The ternary operator must have a default value');
       
    94             $expr3 = $this->parseExpression();
       
    95 
       
    96             $expr = new Twig_Node_Expression_Conditional($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
       
    97         }
       
    98 
       
    99         return $expr;
       
   100     }
       
   101 
       
   102     protected function isUnary(Twig_Token $token)
       
   103     {
       
   104         return $token->test(Twig_Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]);
       
   105     }
       
   106 
       
   107     protected function isBinary(Twig_Token $token)
       
   108     {
       
   109         return $token->test(Twig_Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]);
       
   110     }
       
   111 
       
   112     public function parsePrimaryExpression()
       
   113     {
       
   114         $token = $this->parser->getCurrentToken();
       
   115         switch ($token->getType()) {
       
   116             case Twig_Token::NAME_TYPE:
       
   117                 $this->parser->getStream()->next();
       
   118                 switch ($token->getValue()) {
       
   119                     case 'true':
       
   120                     case 'TRUE':
       
   121                         $node = new Twig_Node_Expression_Constant(true, $token->getLine());
       
   122                         break;
       
   123 
       
   124                     case 'false':
       
   125                     case 'FALSE':
       
   126                         $node = new Twig_Node_Expression_Constant(false, $token->getLine());
       
   127                         break;
       
   128 
       
   129                     case 'none':
       
   130                     case 'NONE':
       
   131                     case 'null':
       
   132                     case 'NULL':
       
   133                         $node = new Twig_Node_Expression_Constant(null, $token->getLine());
       
   134                         break;
       
   135 
       
   136                     default:
       
   137                         $node = new Twig_Node_Expression_Name($token->getValue(), $token->getLine());
       
   138                 }
       
   139                 break;
       
   140 
       
   141             case Twig_Token::NUMBER_TYPE:
       
   142             case Twig_Token::STRING_TYPE:
       
   143                 $this->parser->getStream()->next();
       
   144                 $node = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
       
   145                 break;
       
   146 
       
   147             default:
       
   148                 if ($token->test(Twig_Token::PUNCTUATION_TYPE, '[')) {
       
   149                     $node = $this->parseArrayExpression();
       
   150                 } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) {
       
   151                     $node = $this->parseHashExpression();
       
   152                 } else {
       
   153                     throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($token->getType(), $token->getLine()), $token->getValue()), $token->getLine());
       
   154                 }
       
   155         }
       
   156 
       
   157         return $this->parsePostfixExpression($node);
       
   158     }
       
   159 
       
   160     public function parseArrayExpression()
       
   161     {
       
   162         $stream = $this->parser->getStream();
       
   163         $stream->expect(Twig_Token::PUNCTUATION_TYPE, '[', 'An array element was expected');
       
   164         $elements = array();
       
   165         while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
       
   166             if (!empty($elements)) {
       
   167                 $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma');
       
   168 
       
   169                 // trailing ,?
       
   170                 if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
       
   171                     break;
       
   172                 }
       
   173             }
       
   174 
       
   175             $elements[] = $this->parseExpression();
       
   176         }
       
   177         $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed');
       
   178 
       
   179         return new Twig_Node_Expression_Array($elements, $stream->getCurrent()->getLine());
       
   180     }
       
   181 
       
   182     public function parseHashExpression()
       
   183     {
       
   184         $stream = $this->parser->getStream();
       
   185         $stream->expect(Twig_Token::PUNCTUATION_TYPE, '{', 'A hash element was expected');
       
   186         $elements = array();
       
   187         while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) {
       
   188             if (!empty($elements)) {
       
   189                 $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma');
       
   190 
       
   191                 // trailing ,?
       
   192                 if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) {
       
   193                     break;
       
   194                 }
       
   195             }
       
   196 
       
   197             if (!$stream->test(Twig_Token::STRING_TYPE) && !$stream->test(Twig_Token::NUMBER_TYPE)) {
       
   198                 $current = $stream->getCurrent();
       
   199                 throw new Twig_Error_Syntax(sprintf('A hash key must be a quoted string or a number (unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($current->getType(), $current->getLine()), $current->getValue()), $current->getLine());
       
   200             }
       
   201 
       
   202             $key = $stream->next()->getValue();
       
   203             $stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
       
   204             $elements[$key] = $this->parseExpression();
       
   205         }
       
   206         $stream->expect(Twig_Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed');
       
   207 
       
   208         return new Twig_Node_Expression_Array($elements, $stream->getCurrent()->getLine());
       
   209     }
       
   210 
       
   211     public function parsePostfixExpression($node)
       
   212     {
       
   213         $firstPass = true;
       
   214         while (true) {
       
   215             $token = $this->parser->getCurrentToken();
       
   216             if ($token->getType() == Twig_Token::PUNCTUATION_TYPE) {
       
   217                 if ('.' == $token->getValue() || '[' == $token->getValue()) {
       
   218                     $node = $this->parseSubscriptExpression($node);
       
   219                 } elseif ('|' == $token->getValue()) {
       
   220                     $node = $this->parseFilterExpression($node);
       
   221                 } elseif ($firstPass && $node instanceof Twig_Node_Expression_Name && '(' == $token->getValue()) {
       
   222                     $node = $this->getFunctionNode($node);
       
   223                 } else {
       
   224                     break;
       
   225                 }
       
   226             } else {
       
   227                 break;
       
   228             }
       
   229 
       
   230             $firstPass = false;
       
   231         }
       
   232 
       
   233         return $node;
       
   234     }
       
   235 
       
   236     public function getFunctionNode(Twig_Node_Expression_Name $node)
       
   237     {
       
   238         $args = $this->parseArguments();
       
   239 
       
   240         if ('parent' === $node->getAttribute('name')) {
       
   241             if (!count($this->parser->getBlockStack())) {
       
   242                 throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $node->getLine());
       
   243             }
       
   244 
       
   245             if (!$this->parser->getParent()) {
       
   246                 throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend another one is forbidden', $node->getLine());
       
   247             }
       
   248 
       
   249             return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $node->getLine());
       
   250         }
       
   251 
       
   252         if ('block' === $node->getAttribute('name')) {
       
   253             return new Twig_Node_Expression_BlockReference($args->getNode(0), false, $node->getLine());
       
   254         }
       
   255 
       
   256         if (null !== $alias = $this->parser->getImportedFunction($node->getAttribute('name'))) {
       
   257             return new Twig_Node_Expression_GetAttr($alias['node'], new Twig_Node_Expression_Constant($alias['name'], $node->getLine()), $args, Twig_TemplateInterface::METHOD_CALL, $node->getLine());
       
   258         }
       
   259 
       
   260         return new Twig_Node_Expression_Function($node, $args, $node->getLine());
       
   261     }
       
   262 
       
   263     public function parseSubscriptExpression($node)
       
   264     {
       
   265         $token = $this->parser->getStream()->next();
       
   266         $lineno = $token->getLine();
       
   267         $arguments = new Twig_Node();
       
   268         $type = Twig_TemplateInterface::ANY_CALL;
       
   269         if ($token->getValue() == '.') {
       
   270             $token = $this->parser->getStream()->next();
       
   271             if (
       
   272                 $token->getType() == Twig_Token::NAME_TYPE
       
   273                 ||
       
   274                 $token->getType() == Twig_Token::NUMBER_TYPE
       
   275                 ||
       
   276                 ($token->getType() == Twig_Token::OPERATOR_TYPE && preg_match(Twig_Lexer::REGEX_NAME, $token->getValue()))
       
   277             ) {
       
   278                 $arg = new Twig_Node_Expression_Constant($token->getValue(), $lineno);
       
   279 
       
   280                 if ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
       
   281                     $type = Twig_TemplateInterface::METHOD_CALL;
       
   282                     $arguments = $this->parseArguments();
       
   283                 } else {
       
   284                     $arguments = new Twig_Node();
       
   285                 }
       
   286             } else {
       
   287                 throw new Twig_Error_Syntax('Expected name or number', $lineno);
       
   288             }
       
   289         } else {
       
   290             $type = Twig_TemplateInterface::ARRAY_CALL;
       
   291 
       
   292             $arg = $this->parseExpression();
       
   293             $this->parser->getStream()->expect(Twig_Token::PUNCTUATION_TYPE, ']');
       
   294         }
       
   295 
       
   296         return new Twig_Node_Expression_GetAttr($node, $arg, $arguments, $type, $lineno);
       
   297     }
       
   298 
       
   299     public function parseFilterExpression($node)
       
   300     {
       
   301         $this->parser->getStream()->next();
       
   302 
       
   303         return $this->parseFilterExpressionRaw($node);
       
   304     }
       
   305 
       
   306     public function parseFilterExpressionRaw($node, $tag = null)
       
   307     {
       
   308         while (true) {
       
   309             $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE);
       
   310 
       
   311             $name = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
       
   312             if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
       
   313                 $arguments = new Twig_Node();
       
   314             } else {
       
   315                 $arguments = $this->parseArguments();
       
   316             }
       
   317 
       
   318             $node = new Twig_Node_Expression_Filter($node, $name, $arguments, $token->getLine(), $tag);
       
   319 
       
   320             if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '|')) {
       
   321                 break;
       
   322             }
       
   323 
       
   324             $this->parser->getStream()->next();
       
   325         }
       
   326 
       
   327         return $node;
       
   328     }
       
   329 
       
   330     public function parseArguments()
       
   331     {
       
   332         $args = array();
       
   333         $stream = $this->parser->getStream();
       
   334 
       
   335         $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must be opened by a parenthesis');
       
   336         while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ')')) {
       
   337             if (!empty($args)) {
       
   338                 $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
       
   339             }
       
   340             $args[] = $this->parseExpression();
       
   341         }
       
   342         $stream->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
       
   343 
       
   344         return new Twig_Node($args);
       
   345     }
       
   346 
       
   347     public function parseAssignmentExpression()
       
   348     {
       
   349         $targets = array();
       
   350         while (true) {
       
   351             $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, null, 'Only variables can be assigned to');
       
   352             if (in_array($token->getValue(), array('true', 'false', 'none'))) {
       
   353                 throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine());
       
   354             }
       
   355             $targets[] = new Twig_Node_Expression_AssignName($token->getValue(), $token->getLine());
       
   356 
       
   357             if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ',')) {
       
   358                 break;
       
   359             }
       
   360             $this->parser->getStream()->next();
       
   361         }
       
   362 
       
   363         return new Twig_Node($targets);
       
   364     }
       
   365 
       
   366     public function parseMultitargetExpression()
       
   367     {
       
   368         $targets = array();
       
   369         while (true) {
       
   370             $targets[] = $this->parseExpression();
       
   371             if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ',')) {
       
   372                 break;
       
   373             }
       
   374             $this->parser->getStream()->next();
       
   375         }
       
   376 
       
   377         return new Twig_Node($targets);
       
   378     }
       
   379 }