Просмотр файла engine/classes/lib/Twig/ExpressionParser.php

Размер файла: 23.19Kb
  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()->nextIf(Twig_Token::PUNCTUATION_TYPE, '?')) {
  90. if (!$this->parser->getStream()->nextIf(Twig_Token::PUNCTUATION_TYPE, ':')) {
  91. $expr2 = $this->parseExpression();
  92. if ($this->parser->getStream()->nextIf(Twig_Token::PUNCTUATION_TYPE, ':')) {
  93. $expr3 = $this->parseExpression();
  94. } else {
  95. $expr3 = new Twig_Node_Expression_Constant('', $this->parser->getCurrentToken()->getLine());
  96. }
  97. } else {
  98. $expr2 = $expr;
  99. $expr3 = $this->parseExpression();
  100. }
  101.  
  102. $expr = new Twig_Node_Expression_Conditional($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
  103. }
  104.  
  105. return $expr;
  106. }
  107.  
  108. protected function isUnary(Twig_Token $token)
  109. {
  110. return $token->test(Twig_Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]);
  111. }
  112.  
  113. protected function isBinary(Twig_Token $token)
  114. {
  115. return $token->test(Twig_Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]);
  116. }
  117.  
  118. public function parsePrimaryExpression()
  119. {
  120. $token = $this->parser->getCurrentToken();
  121. switch ($token->getType()) {
  122. case Twig_Token::NAME_TYPE:
  123. $this->parser->getStream()->next();
  124. switch ($token->getValue()) {
  125. case 'true':
  126. case 'TRUE':
  127. $node = new Twig_Node_Expression_Constant(true, $token->getLine());
  128. break;
  129.  
  130. case 'false':
  131. case 'FALSE':
  132. $node = new Twig_Node_Expression_Constant(false, $token->getLine());
  133. break;
  134.  
  135. case 'none':
  136. case 'NONE':
  137. case 'null':
  138. case 'NULL':
  139. $node = new Twig_Node_Expression_Constant(null, $token->getLine());
  140. break;
  141.  
  142. default:
  143. if ('(' === $this->parser->getCurrentToken()->getValue()) {
  144. $node = $this->getFunctionNode($token->getValue(), $token->getLine());
  145. } else {
  146. $node = new Twig_Node_Expression_Name($token->getValue(), $token->getLine());
  147. }
  148. }
  149. break;
  150.  
  151. case Twig_Token::NUMBER_TYPE:
  152. $this->parser->getStream()->next();
  153. $node = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
  154. break;
  155.  
  156. case Twig_Token::STRING_TYPE:
  157. case Twig_Token::INTERPOLATION_START_TYPE:
  158. $node = $this->parseStringExpression();
  159. break;
  160.  
  161. case Twig_Token::OPERATOR_TYPE:
  162. if (preg_match(Twig_Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
  163. // in this context, string operators are variable names
  164. $this->parser->getStream()->next();
  165. $node = new Twig_Node_Expression_Name($token->getValue(), $token->getLine());
  166. break;
  167. }
  168.  
  169. default:
  170. if ($token->test(Twig_Token::PUNCTUATION_TYPE, '[')) {
  171. $node = $this->parseArrayExpression();
  172. } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) {
  173. $node = $this->parseHashExpression();
  174. } else {
  175. 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());
  176. }
  177. }
  178.  
  179. return $this->parsePostfixExpression($node);
  180. }
  181.  
  182. public function parseStringExpression()
  183. {
  184. $stream = $this->parser->getStream();
  185.  
  186. $nodes = array();
  187. // a string cannot be followed by another string in a single expression
  188. $nextCanBeString = true;
  189. while (true) {
  190. if ($nextCanBeString && $token = $stream->nextIf(Twig_Token::STRING_TYPE)) {
  191. $nodes[] = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
  192. $nextCanBeString = false;
  193. } elseif ($stream->nextIf(Twig_Token::INTERPOLATION_START_TYPE)) {
  194. $nodes[] = $this->parseExpression();
  195. $stream->expect(Twig_Token::INTERPOLATION_END_TYPE);
  196. $nextCanBeString = true;
  197. } else {
  198. break;
  199. }
  200. }
  201.  
  202. $expr = array_shift($nodes);
  203. foreach ($nodes as $node) {
  204. $expr = new Twig_Node_Expression_Binary_Concat($expr, $node, $node->getLine());
  205. }
  206.  
  207. return $expr;
  208. }
  209.  
  210. public function parseArrayExpression()
  211. {
  212. $stream = $this->parser->getStream();
  213. $stream->expect(Twig_Token::PUNCTUATION_TYPE, '[', 'An array element was expected');
  214.  
  215. $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine());
  216. $first = true;
  217. while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
  218. if (!$first) {
  219. $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma');
  220.  
  221. // trailing ,?
  222. if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
  223. break;
  224. }
  225. }
  226. $first = false;
  227.  
  228. $node->addElement($this->parseExpression());
  229. }
  230. $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed');
  231.  
  232. return $node;
  233. }
  234.  
  235. public function parseHashExpression()
  236. {
  237. $stream = $this->parser->getStream();
  238. $stream->expect(Twig_Token::PUNCTUATION_TYPE, '{', 'A hash element was expected');
  239.  
  240. $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine());
  241. $first = true;
  242. while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) {
  243. if (!$first) {
  244. $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma');
  245.  
  246. // trailing ,?
  247. if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) {
  248. break;
  249. }
  250. }
  251. $first = false;
  252.  
  253. // a hash key can be:
  254. //
  255. // * a number -- 12
  256. // * a string -- 'a'
  257. // * a name, which is equivalent to a string -- a
  258. // * an expression, which must be enclosed in parentheses -- (1 + 2)
  259. if (($token = $stream->nextIf(Twig_Token::STRING_TYPE)) || ($token = $stream->nextIf(Twig_Token::NAME_TYPE)) || $token = $stream->nextIf(Twig_Token::NUMBER_TYPE)) {
  260. $key = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
  261. } elseif ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
  262. $key = $this->parseExpression();
  263. } else {
  264. $current = $stream->getCurrent();
  265.  
  266. 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());
  267. }
  268.  
  269. $stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
  270. $value = $this->parseExpression();
  271.  
  272. $node->addElement($value, $key);
  273. }
  274. $stream->expect(Twig_Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed');
  275.  
  276. return $node;
  277. }
  278.  
  279. public function parsePostfixExpression($node)
  280. {
  281. while (true) {
  282. $token = $this->parser->getCurrentToken();
  283. if ($token->getType() == Twig_Token::PUNCTUATION_TYPE) {
  284. if ('.' == $token->getValue() || '[' == $token->getValue()) {
  285. $node = $this->parseSubscriptExpression($node);
  286. } elseif ('|' == $token->getValue()) {
  287. $node = $this->parseFilterExpression($node);
  288. } else {
  289. break;
  290. }
  291. } else {
  292. break;
  293. }
  294. }
  295.  
  296. return $node;
  297. }
  298.  
  299. public function getFunctionNode($name, $line)
  300. {
  301. switch ($name) {
  302. case 'parent':
  303. $args = $this->parseArguments();
  304. if (!count($this->parser->getBlockStack())) {
  305. throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line, $this->parser->getFilename());
  306. }
  307.  
  308. if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
  309. throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line, $this->parser->getFilename());
  310. }
  311.  
  312. return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line);
  313. case 'block':
  314. return new Twig_Node_Expression_BlockReference($this->parseArguments()->getNode(0), false, $line);
  315. case 'attribute':
  316. $args = $this->parseArguments();
  317. if (count($args) < 2) {
  318. throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line, $this->parser->getFilename());
  319. }
  320.  
  321. 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_Template::ANY_CALL, $line);
  322. default:
  323. if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
  324. $arguments = new Twig_Node_Expression_Array(array(), $line);
  325. foreach ($this->parseArguments() as $n) {
  326. $arguments->addElement($n);
  327. }
  328.  
  329. $node = new Twig_Node_Expression_MethodCall($alias['node'], $alias['name'], $arguments, $line);
  330. $node->setAttribute('safe', true);
  331.  
  332. return $node;
  333. }
  334.  
  335. $args = $this->parseArguments(true);
  336. $class = $this->getFunctionNodeClass($name, $line);
  337.  
  338. return new $class($name, $args, $line);
  339. }
  340. }
  341.  
  342. public function parseSubscriptExpression($node)
  343. {
  344. $stream = $this->parser->getStream();
  345. $token = $stream->next();
  346. $lineno = $token->getLine();
  347. $arguments = new Twig_Node_Expression_Array(array(), $lineno);
  348. $type = Twig_Template::ANY_CALL;
  349. if ($token->getValue() == '.') {
  350. $token = $stream->next();
  351. if (
  352. $token->getType() == Twig_Token::NAME_TYPE
  353. ||
  354. $token->getType() == Twig_Token::NUMBER_TYPE
  355. ||
  356. ($token->getType() == Twig_Token::OPERATOR_TYPE && preg_match(Twig_Lexer::REGEX_NAME, $token->getValue()))
  357. ) {
  358. $arg = new Twig_Node_Expression_Constant($token->getValue(), $lineno);
  359.  
  360. if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
  361. $type = Twig_TemplateInterface::METHOD_CALL;
  362. foreach ($this->parseArguments() as $n) {
  363. $arguments->addElement($n);
  364. }
  365. }
  366. } else {
  367. throw new Twig_Error_Syntax('Expected name or number', $lineno, $this->parser->getFilename());
  368. }
  369.  
  370. if ($node instanceof Twig_Node_Expression_Name && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
  371. if (!$arg instanceof Twig_Node_Expression_Constant) {
  372. throw new Twig_Error_Syntax(sprintf('Dynamic macro names are not supported (called on "%s")', $node->getAttribute('name')), $token->getLine(), $this->parser->getFilename());
  373. }
  374.  
  375. $node = new Twig_Node_Expression_MethodCall($node, 'get'.$arg->getAttribute('value'), $arguments, $lineno);
  376. $node->setAttribute('safe', true);
  377.  
  378. return $node;
  379. }
  380. } else {
  381. $type = Twig_Template::ARRAY_CALL;
  382.  
  383. // slice?
  384. $slice = false;
  385. if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
  386. $slice = true;
  387. $arg = new Twig_Node_Expression_Constant(0, $token->getLine());
  388. } else {
  389. $arg = $this->parseExpression();
  390. }
  391.  
  392. if ($stream->nextIf(Twig_Token::PUNCTUATION_TYPE, ':')) {
  393. $slice = true;
  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 && $token = $stream->nextIf(Twig_Token::OPERATOR_TYPE, '=')) {
  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()->nextIf(Twig_Token::PUNCTUATION_TYPE, ',')) {
  523. break;
  524. }
  525. }
  526.  
  527. return new Twig_Node($targets);
  528. }
  529.  
  530. public function parseMultitargetExpression()
  531. {
  532. $targets = array();
  533. while (true) {
  534. $targets[] = $this->parseExpression();
  535. if (!$this->parser->getStream()->nextIf(Twig_Token::PUNCTUATION_TYPE, ',')) {
  536. break;
  537. }
  538. }
  539.  
  540. return new Twig_Node($targets);
  541. }
  542.  
  543. protected function getFunctionNodeClass($name, $line)
  544. {
  545. $env = $this->parser->getEnvironment();
  546.  
  547. if (false === $function = $env->getFunction($name)) {
  548. $message = sprintf('The function "%s" does not exist', $name);
  549. if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFunctions()))) {
  550. $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives));
  551. }
  552.  
  553. throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename());
  554. }
  555.  
  556. if ($function instanceof Twig_SimpleFunction) {
  557. return $function->getNodeClass();
  558. }
  559.  
  560. return $function instanceof Twig_Function_Node ? $function->getClass() : 'Twig_Node_Expression_Function';
  561. }
  562.  
  563. protected function getFilterNodeClass($name, $line)
  564. {
  565. $env = $this->parser->getEnvironment();
  566.  
  567. if (false === $filter = $env->getFilter($name)) {
  568. $message = sprintf('The filter "%s" does not exist', $name);
  569. if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFilters()))) {
  570. $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives));
  571. }
  572.  
  573. throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename());
  574. }
  575.  
  576. if ($filter instanceof Twig_SimpleFilter) {
  577. return $filter->getNodeClass();
  578. }
  579.  
  580. return $filter instanceof Twig_Filter_Node ? $filter->getClass() : 'Twig_Node_Expression_Filter';
  581. }
  582.  
  583. // checks that the node only contains "constant" elements
  584. protected function checkConstantExpression(Twig_NodeInterface $node)
  585. {
  586. if (!($node instanceof Twig_Node_Expression_Constant || $node instanceof Twig_Node_Expression_Array)) {
  587. return false;
  588. }
  589.  
  590. foreach ($node as $n) {
  591. if (!$this->checkConstantExpression($n)) {
  592. return false;
  593. }
  594. }
  595.  
  596. return true;
  597. }
  598. }