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