|
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 } |