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