Dotclear

source: inc/libs/Twig/ExpressionParser.php @ 1149:1657e862089c

Revision 1149:1657e862089c, 23.2 KB checked in by dsls, 12 years ago (diff)

Fixed unix case-sensitive twig directory

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            if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
93                $expr2 = $this->parseExpression();
94                if ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
95                    $this->parser->getStream()->next();
96                    $expr3 = $this->parseExpression();
97                } else {
98                    $expr3 = new Twig_Node_Expression_Constant('', $this->parser->getCurrentToken()->getLine());
99                }
100            } else {
101                $this->parser->getStream()->next();
102                $expr2 = $expr;
103                $expr3 = $this->parseExpression();
104            }
105
106            $expr = new Twig_Node_Expression_Conditional($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
107        }
108
109        return $expr;
110    }
111
112    protected function isUnary(Twig_Token $token)
113    {
114        return $token->test(Twig_Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]);
115    }
116
117    protected function isBinary(Twig_Token $token)
118    {
119        return $token->test(Twig_Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]);
120    }
121
122    public function parsePrimaryExpression()
123    {
124        $token = $this->parser->getCurrentToken();
125        switch ($token->getType()) {
126            case Twig_Token::NAME_TYPE:
127                $this->parser->getStream()->next();
128                switch ($token->getValue()) {
129                    case 'true':
130                    case 'TRUE':
131                        $node = new Twig_Node_Expression_Constant(true, $token->getLine());
132                        break;
133
134                    case 'false':
135                    case 'FALSE':
136                        $node = new Twig_Node_Expression_Constant(false, $token->getLine());
137                        break;
138
139                    case 'none':
140                    case 'NONE':
141                    case 'null':
142                    case 'NULL':
143                        $node = new Twig_Node_Expression_Constant(null, $token->getLine());
144                        break;
145
146                    default:
147                        if ('(' === $this->parser->getCurrentToken()->getValue()) {
148                            $node = $this->getFunctionNode($token->getValue(), $token->getLine());
149                        } else {
150                            $node = new Twig_Node_Expression_Name($token->getValue(), $token->getLine());
151                        }
152                }
153                break;
154
155            case Twig_Token::NUMBER_TYPE:
156                $this->parser->getStream()->next();
157                $node = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
158                break;
159
160            case Twig_Token::STRING_TYPE:
161            case Twig_Token::INTERPOLATION_START_TYPE:
162                $node = $this->parseStringExpression();
163                break;
164
165            default:
166                if ($token->test(Twig_Token::PUNCTUATION_TYPE, '[')) {
167                    $node = $this->parseArrayExpression();
168                } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) {
169                    $node = $this->parseHashExpression();
170                } else {
171                    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());
172                }
173        }
174
175        return $this->parsePostfixExpression($node);
176    }
177
178    public function parseStringExpression()
179    {
180        $stream = $this->parser->getStream();
181
182        $nodes = array();
183        // a string cannot be followed by another string in a single expression
184        $nextCanBeString = true;
185        while (true) {
186            if ($stream->test(Twig_Token::STRING_TYPE) && $nextCanBeString) {
187                $token = $stream->next();
188                $nodes[] = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
189                $nextCanBeString = false;
190            } elseif ($stream->test(Twig_Token::INTERPOLATION_START_TYPE)) {
191                $stream->next();
192                $nodes[] = $this->parseExpression();
193                $stream->expect(Twig_Token::INTERPOLATION_END_TYPE);
194                $nextCanBeString = true;
195            } else {
196                break;
197            }
198        }
199
200        $expr = array_shift($nodes);
201        foreach ($nodes as $node) {
202            $expr = new Twig_Node_Expression_Binary_Concat($expr, $node, $node->getLine());
203        }
204
205        return $expr;
206    }
207
208    public function parseArrayExpression()
209    {
210        $stream = $this->parser->getStream();
211        $stream->expect(Twig_Token::PUNCTUATION_TYPE, '[', 'An array element was expected');
212
213        $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine());
214        $first = true;
215        while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
216            if (!$first) {
217                $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma');
218
219                // trailing ,?
220                if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
221                    break;
222                }
223            }
224            $first = false;
225
226            $node->addElement($this->parseExpression());
227        }
228        $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed');
229
230        return $node;
231    }
232
233    public function parseHashExpression()
234    {
235        $stream = $this->parser->getStream();
236        $stream->expect(Twig_Token::PUNCTUATION_TYPE, '{', 'A hash element was expected');
237
238        $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine());
239        $first = true;
240        while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) {
241            if (!$first) {
242                $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma');
243
244                // trailing ,?
245                if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) {
246                    break;
247                }
248            }
249            $first = false;
250
251            // a hash key can be:
252            //
253            //  * a number -- 12
254            //  * a string -- 'a'
255            //  * a name, which is equivalent to a string -- a
256            //  * an expression, which must be enclosed in parentheses -- (1 + 2)
257            if ($stream->test(Twig_Token::STRING_TYPE) || $stream->test(Twig_Token::NAME_TYPE) || $stream->test(Twig_Token::NUMBER_TYPE)) {
258                $token = $stream->next();
259                $key = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
260            } elseif ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
261                $key = $this->parseExpression();
262            } else {
263                $current = $stream->getCurrent();
264
265                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());
266            }
267
268            $stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
269            $value = $this->parseExpression();
270
271            $node->addElement($value, $key);
272        }
273        $stream->expect(Twig_Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed');
274
275        return $node;
276    }
277
278    public function parsePostfixExpression($node)
279    {
280        while (true) {
281            $token = $this->parser->getCurrentToken();
282            if ($token->getType() == Twig_Token::PUNCTUATION_TYPE) {
283                if ('.' == $token->getValue() || '[' == $token->getValue()) {
284                    $node = $this->parseSubscriptExpression($node);
285                } elseif ('|' == $token->getValue()) {
286                    $node = $this->parseFilterExpression($node);
287                } else {
288                    break;
289                }
290            } else {
291                break;
292            }
293        }
294
295        return $node;
296    }
297
298    public function getFunctionNode($name, $line)
299    {
300        switch ($name) {
301            case 'parent':
302                $args = $this->parseArguments();
303                if (!count($this->parser->getBlockStack())) {
304                    throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line, $this->parser->getFilename());
305                }
306
307                if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
308                    throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line, $this->parser->getFilename());
309                }
310
311                return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line);
312            case 'block':
313                return new Twig_Node_Expression_BlockReference($this->parseArguments()->getNode(0), false, $line);
314            case 'attribute':
315                $args = $this->parseArguments();
316                if (count($args) < 2) {
317                    throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line, $this->parser->getFilename());
318                }
319
320                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            default:
322                if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
323                    $arguments = new Twig_Node_Expression_Array(array(), $line);
324                    foreach ($this->parseArguments() as $n) {
325                        $arguments->addElement($n);
326                    }
327
328                    $node = new Twig_Node_Expression_MethodCall($alias['node'], $alias['name'], $arguments, $line);
329                    $node->setAttribute('safe', true);
330
331                    return $node;
332                }
333
334                $args = $this->parseArguments(true);
335                $class = $this->getFunctionNodeClass($name, $line);
336
337                return new $class($name, $args, $line);
338        }
339    }
340
341    public function parseSubscriptExpression($node)
342    {
343        $stream = $this->parser->getStream();
344        $token = $stream->next();
345        $lineno = $token->getLine();
346        $arguments = new Twig_Node_Expression_Array(array(), $lineno);
347        $type = Twig_TemplateInterface::ANY_CALL;
348        if ($token->getValue() == '.') {
349            $token = $stream->next();
350            if (
351                $token->getType() == Twig_Token::NAME_TYPE
352                ||
353                $token->getType() == Twig_Token::NUMBER_TYPE
354                ||
355                ($token->getType() == Twig_Token::OPERATOR_TYPE && preg_match(Twig_Lexer::REGEX_NAME, $token->getValue()))
356            ) {
357                $arg = new Twig_Node_Expression_Constant($token->getValue(), $lineno);
358
359                if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
360                    $type = Twig_TemplateInterface::METHOD_CALL;
361                    foreach ($this->parseArguments() as $n) {
362                        $arguments->addElement($n);
363                    }
364                }
365            } else {
366                throw new Twig_Error_Syntax('Expected name or number', $lineno, $this->parser->getFilename());
367            }
368
369            if ($node instanceof Twig_Node_Expression_Name && null !== $alias = $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
370                if (!$arg instanceof Twig_Node_Expression_Constant) {
371                    throw new Twig_Error_Syntax(sprintf('Dynamic macro names are not supported (called on "%s")', $node->getAttribute('name')), $token->getLine(), $this->parser->getFilename());
372                }
373
374                $node = new Twig_Node_Expression_MethodCall($node, 'get'.$arg->getAttribute('value'), $arguments, $lineno);
375                $node->setAttribute('safe', true);
376
377                return $node;
378            }
379        } else {
380            $type = Twig_TemplateInterface::ARRAY_CALL;
381
382            // slice?
383            $slice = false;
384            if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
385                $slice = true;
386                $arg = new Twig_Node_Expression_Constant(0, $token->getLine());
387            } else {
388                $arg = $this->parseExpression();
389            }
390
391            if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
392                $slice = true;
393                $stream->next();
394            }
395
396            if ($slice) {
397                if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
398                    $length = new Twig_Node_Expression_Constant(null, $token->getLine());
399                } else {
400                    $length = $this->parseExpression();
401                }
402
403                $class = $this->getFilterNodeClass('slice', $token->getLine());
404                $arguments = new Twig_Node(array($arg, $length));
405                $filter = new $class($node, new Twig_Node_Expression_Constant('slice', $token->getLine()), $arguments, $token->getLine());
406
407                $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']');
408
409                return $filter;
410            }
411
412            $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']');
413        }
414
415        return new Twig_Node_Expression_GetAttr($node, $arg, $arguments, $type, $lineno);
416    }
417
418    public function parseFilterExpression($node)
419    {
420        $this->parser->getStream()->next();
421
422        return $this->parseFilterExpressionRaw($node);
423    }
424
425    public function parseFilterExpressionRaw($node, $tag = null)
426    {
427        while (true) {
428            $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE);
429
430            $name = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
431            if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
432                $arguments = new Twig_Node();
433            } else {
434                $arguments = $this->parseArguments(true);
435            }
436
437            $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
438
439            $node = new $class($node, $name, $arguments, $token->getLine(), $tag);
440
441            if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '|')) {
442                break;
443            }
444
445            $this->parser->getStream()->next();
446        }
447
448        return $node;
449    }
450
451    /**
452     * Parses arguments.
453     *
454     * @param Boolean $namedArguments Whether to allow named arguments or not
455     * @param Boolean $definition     Whether we are parsing arguments for a function definition
456     */
457    public function parseArguments($namedArguments = false, $definition = false)
458    {
459        $args = array();
460        $stream = $this->parser->getStream();
461
462        $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
463        while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ')')) {
464            if (!empty($args)) {
465                $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
466            }
467
468            if ($definition) {
469                $token = $stream->expect(Twig_Token::NAME_TYPE, null, 'An argument must be a name');
470                $value = new Twig_Node_Expression_Name($token->getValue(), $this->parser->getCurrentToken()->getLine());
471            } else {
472                $value = $this->parseExpression();
473            }
474
475            $name = null;
476            if ($namedArguments && $stream->test(Twig_Token::OPERATOR_TYPE, '=')) {
477                $token = $stream->next();
478                if (!$value instanceof Twig_Node_Expression_Name) {
479                    throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given', get_class($value)), $token->getLine(), $this->parser->getFilename());
480                }
481                $name = $value->getAttribute('name');
482
483                if ($definition) {
484                    $value = $this->parsePrimaryExpression();
485
486                    if (!$this->checkConstantExpression($value)) {
487                        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());
488                    }
489                } else {
490                    $value = $this->parseExpression();
491                }
492            }
493
494            if ($definition) {
495                if (null === $name) {
496                    $name = $value->getAttribute('name');
497                    $value = new Twig_Node_Expression_Constant(null, $this->parser->getCurrentToken()->getLine());
498                }
499                $args[$name] = $value;
500            } else {
501                if (null === $name) {
502                    $args[] = $value;
503                } else {
504                    $args[$name] = $value;
505                }
506            }
507        }
508        $stream->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
509
510        return new Twig_Node($args);
511    }
512
513    public function parseAssignmentExpression()
514    {
515        $targets = array();
516        while (true) {
517            $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, null, 'Only variables can be assigned to');
518            if (in_array($token->getValue(), array('true', 'false', 'none'))) {
519                throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine(), $this->parser->getFilename());
520            }
521            $targets[] = new Twig_Node_Expression_AssignName($token->getValue(), $token->getLine());
522
523            if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ',')) {
524                break;
525            }
526            $this->parser->getStream()->next();
527        }
528
529        return new Twig_Node($targets);
530    }
531
532    public function parseMultitargetExpression()
533    {
534        $targets = array();
535        while (true) {
536            $targets[] = $this->parseExpression();
537            if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ',')) {
538                break;
539            }
540            $this->parser->getStream()->next();
541        }
542
543        return new Twig_Node($targets);
544    }
545
546    protected function getFunctionNodeClass($name, $line)
547    {
548        $env = $this->parser->getEnvironment();
549
550        if (false === $function = $env->getFunction($name)) {
551            $message = sprintf('The function "%s" does not exist', $name);
552            if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFunctions()))) {
553                $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives));
554            }
555
556            throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename());
557        }
558
559        if ($function instanceof Twig_SimpleFunction) {
560            return $function->getNodeClass();
561        }
562
563        return $function instanceof Twig_Function_Node ? $function->getClass() : 'Twig_Node_Expression_Function';
564    }
565
566    protected function getFilterNodeClass($name, $line)
567    {
568        $env = $this->parser->getEnvironment();
569
570        if (false === $filter = $env->getFilter($name)) {
571            $message = sprintf('The filter "%s" does not exist', $name);
572            if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFilters()))) {
573                $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives));
574            }
575
576            throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename());
577        }
578
579        if ($filter instanceof Twig_SimpleFilter) {
580            return $filter->getNodeClass();
581        }
582
583        return $filter instanceof Twig_Filter_Node ? $filter->getClass() : 'Twig_Node_Expression_Filter';
584    }
585
586    // checks that the node only contains "constant" elements
587    protected function checkConstantExpression(Twig_NodeInterface $node)
588    {
589        if (!($node instanceof Twig_Node_Expression_Constant || $node instanceof Twig_Node_Expression_Array)) {
590            return false;
591        }
592
593        foreach ($node as $n) {
594            if (!$this->checkConstantExpression($n)) {
595                return false;
596            }
597        }
598
599        return true;
600    }
601}
Note: See TracBrowser for help on using the repository browser.

Sites map