Twig-1.3.0/lib/Twig/ExpressionParser.php
changeset 4 9a001a04b634
equal deleted inserted replaced
3:6d109e3804ac 4:9a001a04b634
       
     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                         if ('(' === $this->parser->getCurrentToken()->getValue()) {
       
   138                             $node = $this->getFunctionNode($token->getValue(), $token->getLine());
       
   139                         } else {
       
   140                             $node = new Twig_Node_Expression_Name($token->getValue(), $token->getLine());
       
   141                         }
       
   142                 }
       
   143                 break;
       
   144 
       
   145             case Twig_Token::NUMBER_TYPE:
       
   146             case Twig_Token::STRING_TYPE:
       
   147                 $this->parser->getStream()->next();
       
   148                 $node = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
       
   149                 break;
       
   150 
       
   151             default:
       
   152                 if ($token->test(Twig_Token::PUNCTUATION_TYPE, '[')) {
       
   153                     $node = $this->parseArrayExpression();
       
   154                 } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) {
       
   155                     $node = $this->parseHashExpression();
       
   156                 } else {
       
   157                     throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($token->getType(), $token->getLine()), $token->getValue()), $token->getLine());
       
   158                 }
       
   159         }
       
   160 
       
   161         return $this->parsePostfixExpression($node);
       
   162     }
       
   163 
       
   164     public function parseArrayExpression()
       
   165     {
       
   166         $stream = $this->parser->getStream();
       
   167         $stream->expect(Twig_Token::PUNCTUATION_TYPE, '[', 'An array element was expected');
       
   168         $elements = array();
       
   169         while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
       
   170             if (!empty($elements)) {
       
   171                 $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma');
       
   172 
       
   173                 // trailing ,?
       
   174                 if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
       
   175                     break;
       
   176                 }
       
   177             }
       
   178 
       
   179             $elements[] = $this->parseExpression();
       
   180         }
       
   181         $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed');
       
   182 
       
   183         return new Twig_Node_Expression_Array($elements, $stream->getCurrent()->getLine());
       
   184     }
       
   185 
       
   186     public function parseHashExpression()
       
   187     {
       
   188         $stream = $this->parser->getStream();
       
   189         $stream->expect(Twig_Token::PUNCTUATION_TYPE, '{', 'A hash element was expected');
       
   190         $elements = array();
       
   191         while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) {
       
   192             if (!empty($elements)) {
       
   193                 $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma');
       
   194 
       
   195                 // trailing ,?
       
   196                 if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) {
       
   197                     break;
       
   198                 }
       
   199             }
       
   200 
       
   201             if (!$stream->test(Twig_Token::STRING_TYPE) && !$stream->test(Twig_Token::NUMBER_TYPE)) {
       
   202                 $current = $stream->getCurrent();
       
   203                 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());
       
   204             }
       
   205 
       
   206             $key = $stream->next()->getValue();
       
   207             $stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
       
   208             $elements[$key] = $this->parseExpression();
       
   209         }
       
   210         $stream->expect(Twig_Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed');
       
   211 
       
   212         return new Twig_Node_Expression_Array($elements, $stream->getCurrent()->getLine());
       
   213     }
       
   214 
       
   215     public function parsePostfixExpression($node)
       
   216     {
       
   217         while (true) {
       
   218             $token = $this->parser->getCurrentToken();
       
   219             if ($token->getType() == Twig_Token::PUNCTUATION_TYPE) {
       
   220                 if ('.' == $token->getValue() || '[' == $token->getValue()) {
       
   221                     $node = $this->parseSubscriptExpression($node);
       
   222                 } elseif ('|' == $token->getValue()) {
       
   223                     $node = $this->parseFilterExpression($node);
       
   224                 } else {
       
   225                     break;
       
   226                 }
       
   227             } else {
       
   228                 break;
       
   229             }
       
   230         }
       
   231 
       
   232         return $node;
       
   233     }
       
   234 
       
   235     public function getFunctionNode($name, $line)
       
   236     {
       
   237         $args = $this->parseArguments();
       
   238         switch ($name) {
       
   239             case 'parent':
       
   240                 if (!count($this->parser->getBlockStack())) {
       
   241                     throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line);
       
   242                 }
       
   243 
       
   244                 if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
       
   245                     throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line);
       
   246                 }
       
   247 
       
   248                 return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line);
       
   249             case 'block':
       
   250                 return new Twig_Node_Expression_BlockReference($args->getNode(0), false, $line);
       
   251             case 'attribute':
       
   252                 if (count($args) < 2) {
       
   253                     throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attribute)', $line);
       
   254                 }
       
   255 
       
   256                 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);
       
   257             default:
       
   258                 if (null !== $alias = $this->parser->getImportedFunction($name)) {
       
   259                     return new Twig_Node_Expression_GetAttr($alias['node'], new Twig_Node_Expression_Constant($alias['name'], $line), $args, Twig_TemplateInterface::METHOD_CALL, $line);
       
   260                 }
       
   261 
       
   262                 return new Twig_Node_Expression_Function($name, $args, $line);
       
   263         }
       
   264     }
       
   265 
       
   266     public function parseSubscriptExpression($node)
       
   267     {
       
   268         $token = $this->parser->getStream()->next();
       
   269         $lineno = $token->getLine();
       
   270         $arguments = new Twig_Node();
       
   271         $type = Twig_TemplateInterface::ANY_CALL;
       
   272         if ($token->getValue() == '.') {
       
   273             $token = $this->parser->getStream()->next();
       
   274             if (
       
   275                 $token->getType() == Twig_Token::NAME_TYPE
       
   276                 ||
       
   277                 $token->getType() == Twig_Token::NUMBER_TYPE
       
   278                 ||
       
   279                 ($token->getType() == Twig_Token::OPERATOR_TYPE && preg_match(Twig_Lexer::REGEX_NAME, $token->getValue()))
       
   280             ) {
       
   281                 $arg = new Twig_Node_Expression_Constant($token->getValue(), $lineno);
       
   282 
       
   283                 if ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
       
   284                     $type = Twig_TemplateInterface::METHOD_CALL;
       
   285                     $arguments = $this->parseArguments();
       
   286                 } else {
       
   287                     $arguments = new Twig_Node();
       
   288                 }
       
   289             } else {
       
   290                 throw new Twig_Error_Syntax('Expected name or number', $lineno);
       
   291             }
       
   292         } else {
       
   293             $type = Twig_TemplateInterface::ARRAY_CALL;
       
   294 
       
   295             $arg = $this->parseExpression();
       
   296             $this->parser->getStream()->expect(Twig_Token::PUNCTUATION_TYPE, ']');
       
   297         }
       
   298 
       
   299         return new Twig_Node_Expression_GetAttr($node, $arg, $arguments, $type, $lineno);
       
   300     }
       
   301 
       
   302     public function parseFilterExpression($node)
       
   303     {
       
   304         $this->parser->getStream()->next();
       
   305 
       
   306         return $this->parseFilterExpressionRaw($node);
       
   307     }
       
   308 
       
   309     public function parseFilterExpressionRaw($node, $tag = null)
       
   310     {
       
   311         while (true) {
       
   312             $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE);
       
   313 
       
   314             $name = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
       
   315             if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
       
   316                 $arguments = new Twig_Node();
       
   317             } else {
       
   318                 $arguments = $this->parseArguments();
       
   319             }
       
   320 
       
   321             $node = new Twig_Node_Expression_Filter($node, $name, $arguments, $token->getLine(), $tag);
       
   322 
       
   323             if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '|')) {
       
   324                 break;
       
   325             }
       
   326 
       
   327             $this->parser->getStream()->next();
       
   328         }
       
   329 
       
   330         return $node;
       
   331     }
       
   332 
       
   333     public function parseArguments()
       
   334     {
       
   335         $args = array();
       
   336         $stream = $this->parser->getStream();
       
   337 
       
   338         $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must be opened by a parenthesis');
       
   339         while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ')')) {
       
   340             if (!empty($args)) {
       
   341                 $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
       
   342             }
       
   343             $args[] = $this->parseExpression();
       
   344         }
       
   345         $stream->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
       
   346 
       
   347         return new Twig_Node($args);
       
   348     }
       
   349 
       
   350     public function parseAssignmentExpression()
       
   351     {
       
   352         $targets = array();
       
   353         while (true) {
       
   354             $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, null, 'Only variables can be assigned to');
       
   355             if (in_array($token->getValue(), array('true', 'false', 'none'))) {
       
   356                 throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine());
       
   357             }
       
   358             $targets[] = new Twig_Node_Expression_AssignName($token->getValue(), $token->getLine());
       
   359 
       
   360             if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ',')) {
       
   361                 break;
       
   362             }
       
   363             $this->parser->getStream()->next();
       
   364         }
       
   365 
       
   366         return new Twig_Node($targets);
       
   367     }
       
   368 
       
   369     public function parseMultitargetExpression()
       
   370     {
       
   371         $targets = array();
       
   372         while (true) {
       
   373             $targets[] = $this->parseExpression();
       
   374             if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ',')) {
       
   375                 break;
       
   376             }
       
   377             $this->parser->getStream()->next();
       
   378         }
       
   379 
       
   380         return new Twig_Node($targets);
       
   381     }
       
   382 }