Dotclear

source: inc/libs/twig/ExpressionParser.php @ 0:54703be25dd6

Revision 0:54703be25dd6, 13.9 KB checked in by Dsls <dsls@…>, 14 years ago (diff)

2.3 branch (trunk) first checkin

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

Sites map