Dotclear

source: inc/libs/twig/ExpressionParser.php @ 991:e42f791e0975

Revision 991:e42f791e0975, 18.6 KB checked in by Dsls <dsls@…>, 13 years ago (diff)

New twig branch. 1st step : add twig 1.11.1

Line 
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 */
24class 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                $this->parser->getStream()->next();
147                $node = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
148                break;
149
150            case Twig_Token::STRING_TYPE:
151            case Twig_Token::INTERPOLATION_START_TYPE:
152                $node = $this->parseStringExpression();
153                break;
154
155            default:
156                if ($token->test(Twig_Token::PUNCTUATION_TYPE, '[')) {
157                    $node = $this->parseArrayExpression();
158                } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) {
159                    $node = $this->parseHashExpression();
160                } else {
161                    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());
162                }
163        }
164
165        return $this->parsePostfixExpression($node);
166    }
167
168    public function parseStringExpression()
169    {
170        $stream = $this->parser->getStream();
171
172        $nodes = array();
173        // a string cannot be followed by another string in a single expression
174        $nextCanBeString = true;
175        while (true) {
176            if ($stream->test(Twig_Token::STRING_TYPE) && $nextCanBeString) {
177                $token = $stream->next();
178                $nodes[] = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
179                $nextCanBeString = false;
180            } elseif ($stream->test(Twig_Token::INTERPOLATION_START_TYPE)) {
181                $stream->next();
182                $nodes[] = $this->parseExpression();
183                $stream->expect(Twig_Token::INTERPOLATION_END_TYPE);
184                $nextCanBeString = true;
185            } else {
186                break;
187            }
188        }
189
190        $expr = array_shift($nodes);
191        foreach ($nodes as $node) {
192            $expr = new Twig_Node_Expression_Binary_Concat($expr, $node, $node->getLine());
193        }
194
195        return $expr;
196    }
197
198    public function parseArrayExpression()
199    {
200        $stream = $this->parser->getStream();
201        $stream->expect(Twig_Token::PUNCTUATION_TYPE, '[', 'An array element was expected');
202
203        $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine());
204        $first = true;
205        while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
206            if (!$first) {
207                $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma');
208
209                // trailing ,?
210                if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
211                    break;
212                }
213            }
214            $first = false;
215
216            $node->addElement($this->parseExpression());
217        }
218        $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed');
219
220        return $node;
221    }
222
223    public function parseHashExpression()
224    {
225        $stream = $this->parser->getStream();
226        $stream->expect(Twig_Token::PUNCTUATION_TYPE, '{', 'A hash element was expected');
227
228        $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine());
229        $first = true;
230        while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) {
231            if (!$first) {
232                $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma');
233
234                // trailing ,?
235                if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) {
236                    break;
237                }
238            }
239            $first = false;
240
241            // a hash key can be:
242            //
243            //  * a number -- 12
244            //  * a string -- 'a'
245            //  * a name, which is equivalent to a string -- a
246            //  * an expression, which must be enclosed in parentheses -- (1 + 2)
247            if ($stream->test(Twig_Token::STRING_TYPE) || $stream->test(Twig_Token::NAME_TYPE) || $stream->test(Twig_Token::NUMBER_TYPE)) {
248                $token = $stream->next();
249                $key = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
250            } elseif ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
251                $key = $this->parseExpression();
252            } else {
253                $current = $stream->getCurrent();
254
255                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());
256            }
257
258            $stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
259            $value = $this->parseExpression();
260
261            $node->addElement($value, $key);
262        }
263        $stream->expect(Twig_Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed');
264
265        return $node;
266    }
267
268    public function parsePostfixExpression($node)
269    {
270        while (true) {
271            $token = $this->parser->getCurrentToken();
272            if ($token->getType() == Twig_Token::PUNCTUATION_TYPE) {
273                if ('.' == $token->getValue() || '[' == $token->getValue()) {
274                    $node = $this->parseSubscriptExpression($node);
275                } elseif ('|' == $token->getValue()) {
276                    $node = $this->parseFilterExpression($node);
277                } else {
278                    break;
279                }
280            } else {
281                break;
282            }
283        }
284
285        return $node;
286    }
287
288    public function getFunctionNode($name, $line)
289    {
290        $args = $this->parseArguments();
291        switch ($name) {
292            case 'parent':
293                if (!count($this->parser->getBlockStack())) {
294                    throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line, $this->parser->getFilename());
295                }
296
297                if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
298                    throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line, $this->parser->getFilename());
299                }
300
301                return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line);
302            case 'block':
303                return new Twig_Node_Expression_BlockReference($args->getNode(0), false, $line);
304            case 'attribute':
305                if (count($args) < 2) {
306                    throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line, $this->parser->getFilename());
307                }
308
309                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);
310            default:
311                if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
312                    $arguments = new Twig_Node_Expression_Array(array(), $line);
313                    foreach ($args as $n) {
314                        $arguments->addElement($n);
315                    }
316
317                    $node = new Twig_Node_Expression_MethodCall($alias['node'], $alias['name'], $arguments, $line);
318                    $node->setAttribute('safe', true);
319
320                    return $node;
321                }
322
323                $class = $this->getFunctionNodeClass($name);
324
325                return new $class($name, $args, $line);
326        }
327    }
328
329    public function parseSubscriptExpression($node)
330    {
331        $stream = $this->parser->getStream();
332        $token = $stream->next();
333        $lineno = $token->getLine();
334        $arguments = new Twig_Node_Expression_Array(array(), $lineno);
335        $type = Twig_TemplateInterface::ANY_CALL;
336        if ($token->getValue() == '.') {
337            $token = $stream->next();
338            if (
339                $token->getType() == Twig_Token::NAME_TYPE
340                ||
341                $token->getType() == Twig_Token::NUMBER_TYPE
342                ||
343                ($token->getType() == Twig_Token::OPERATOR_TYPE && preg_match(Twig_Lexer::REGEX_NAME, $token->getValue()))
344            ) {
345                $arg = new Twig_Node_Expression_Constant($token->getValue(), $lineno);
346
347                if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
348                    $type = Twig_TemplateInterface::METHOD_CALL;
349                    foreach ($this->parseArguments() as $n) {
350                        $arguments->addElement($n);
351                    }
352                }
353            } else {
354                throw new Twig_Error_Syntax('Expected name or number', $lineno, $this->parser->getFilename());
355            }
356        } else {
357            $type = Twig_TemplateInterface::ARRAY_CALL;
358
359            $arg = $this->parseExpression();
360
361            // slice?
362            if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
363                $stream->next();
364
365                if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
366                    $length = new Twig_Node_Expression_Constant(null, $token->getLine());
367                } else {
368                    $length = $this->parseExpression();
369                }
370
371                $class = $this->getFilterNodeClass('slice');
372                $arguments = new Twig_Node(array($arg, $length));
373                $filter = new $class($node, new Twig_Node_Expression_Constant('slice', $token->getLine()), $arguments, $token->getLine());
374
375                $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']');
376
377                return $filter;
378            }
379
380            $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']');
381        }
382
383        if ($node instanceof Twig_Node_Expression_Name && null !== $alias = $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
384            $node = new Twig_Node_Expression_MethodCall($node, 'get'.$arg->getAttribute('value'), $arguments, $lineno);
385            $node->setAttribute('safe', true);
386
387            return $node;
388        }
389
390        return new Twig_Node_Expression_GetAttr($node, $arg, $arguments, $type, $lineno);
391    }
392
393    public function parseFilterExpression($node)
394    {
395        $this->parser->getStream()->next();
396
397        return $this->parseFilterExpressionRaw($node);
398    }
399
400    public function parseFilterExpressionRaw($node, $tag = null)
401    {
402        while (true) {
403            $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE);
404
405            $name = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
406            if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
407                $arguments = new Twig_Node();
408            } else {
409                $arguments = $this->parseArguments();
410            }
411
412            $class = $this->getFilterNodeClass($name->getAttribute('value'));
413
414            $node = new $class($node, $name, $arguments, $token->getLine(), $tag);
415
416            if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '|')) {
417                break;
418            }
419
420            $this->parser->getStream()->next();
421        }
422
423        return $node;
424    }
425
426    public function parseArguments()
427    {
428        $args = array();
429        $stream = $this->parser->getStream();
430
431        $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must be opened by a parenthesis');
432        while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ')')) {
433            if (!empty($args)) {
434                $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
435            }
436            $args[] = $this->parseExpression();
437        }
438        $stream->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
439
440        return new Twig_Node($args);
441    }
442
443    public function parseAssignmentExpression()
444    {
445        $targets = array();
446        while (true) {
447            $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, null, 'Only variables can be assigned to');
448            if (in_array($token->getValue(), array('true', 'false', 'none'))) {
449                throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine(), $this->parser->getFilename());
450            }
451            $targets[] = new Twig_Node_Expression_AssignName($token->getValue(), $token->getLine());
452
453            if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ',')) {
454                break;
455            }
456            $this->parser->getStream()->next();
457        }
458
459        return new Twig_Node($targets);
460    }
461
462    public function parseMultitargetExpression()
463    {
464        $targets = array();
465        while (true) {
466            $targets[] = $this->parseExpression();
467            if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ',')) {
468                break;
469            }
470            $this->parser->getStream()->next();
471        }
472
473        return new Twig_Node($targets);
474    }
475
476    protected function getFunctionNodeClass($name)
477    {
478        $functionMap = $this->parser->getEnvironment()->getFunctions();
479        if (isset($functionMap[$name]) && $functionMap[$name] instanceof Twig_Function_Node) {
480            return $functionMap[$name]->getClass();
481        }
482
483        return 'Twig_Node_Expression_Function';
484    }
485
486    protected function getFilterNodeClass($name)
487    {
488        $filterMap = $this->parser->getEnvironment()->getFilters();
489        if (isset($filterMap[$name]) && $filterMap[$name] instanceof Twig_Filter_Node) {
490            return $filterMap[$name]->getClass();
491        }
492
493        return 'Twig_Node_Expression_Filter';
494    }
495}
Note: See TracBrowser for help on using the repository browser.

Sites map