vendor/twig/twig/src/Parser.php line 184

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of Twig.
  4. *
  5. * (c) Fabien Potencier
  6. * (c) Armin Ronacher
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Twig;
  12. use Twig\Error\SyntaxError;
  13. use Twig\Node\BlockNode;
  14. use Twig\Node\BlockReferenceNode;
  15. use Twig\Node\BodyNode;
  16. use Twig\Node\Expression\AbstractExpression;
  17. use Twig\Node\MacroNode;
  18. use Twig\Node\ModuleNode;
  19. use Twig\Node\Node;
  20. use Twig\Node\NodeCaptureInterface;
  21. use Twig\Node\NodeOutputInterface;
  22. use Twig\Node\PrintNode;
  23. use Twig\Node\SpacelessNode;
  24. use Twig\Node\TextNode;
  25. use Twig\TokenParser\TokenParserInterface;
  26. /**
  27. * Default parser implementation.
  28. *
  29. * @author Fabien Potencier <fabien@symfony.com>
  30. */
  31. class Parser
  32. {
  33. private $stack = [];
  34. private $stream;
  35. private $parent;
  36. private $handlers;
  37. private $visitors;
  38. private $expressionParser;
  39. private $blocks;
  40. private $blockStack;
  41. private $macros;
  42. private $env;
  43. private $importedSymbols;
  44. private $traits;
  45. private $embeddedTemplates = [];
  46. private $varNameSalt = 0;
  47. public function __construct(Environment $env)
  48. {
  49. $this->env = $env;
  50. }
  51. public function getVarName()
  52. {
  53. return sprintf('__internal_parse_%d', $this->varNameSalt++);
  54. }
  55. public function parse(TokenStream $stream, $test = null, $dropNeedle = false)
  56. {
  57. $vars = get_object_vars($this);
  58. unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames'], $vars['varNameSalt']);
  59. $this->stack[] = $vars;
  60. // tag handlers
  61. if (null === $this->handlers) {
  62. $this->handlers = [];
  63. foreach ($this->env->getTokenParsers() as $handler) {
  64. $handler->setParser($this);
  65. $this->handlers[$handler->getTag()] = $handler;
  66. }
  67. }
  68. // node visitors
  69. if (null === $this->visitors) {
  70. $this->visitors = $this->env->getNodeVisitors();
  71. }
  72. if (null === $this->expressionParser) {
  73. $this->expressionParser = new ExpressionParser($this, $this->env);
  74. }
  75. $this->stream = $stream;
  76. $this->parent = null;
  77. $this->blocks = [];
  78. $this->macros = [];
  79. $this->traits = [];
  80. $this->blockStack = [];
  81. $this->importedSymbols = [[]];
  82. $this->embeddedTemplates = [];
  83. try {
  84. $body = $this->subparse($test, $dropNeedle);
  85. if (null !== $this->parent && null === $body = $this->filterBodyNodes($body)) {
  86. $body = new Node();
  87. }
  88. } catch (SyntaxError $e) {
  89. if (!$e->getSourceContext()) {
  90. $e->setSourceContext($this->stream->getSourceContext());
  91. }
  92. if (!$e->getTemplateLine()) {
  93. $e->setTemplateLine($this->stream->getCurrent()->getLine());
  94. }
  95. throw $e;
  96. }
  97. $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Node($this->blocks), new Node($this->macros), new Node($this->traits), $this->embeddedTemplates, $stream->getSourceContext());
  98. $traverser = new NodeTraverser($this->env, $this->visitors);
  99. $node = $traverser->traverse($node);
  100. // restore previous stack so previous parse() call can resume working
  101. foreach (array_pop($this->stack) as $key => $val) {
  102. $this->$key = $val;
  103. }
  104. return $node;
  105. }
  106. public function subparse($test, $dropNeedle = false)
  107. {
  108. $lineno = $this->getCurrentToken()->getLine();
  109. $rv = [];
  110. while (!$this->stream->isEOF()) {
  111. switch ($this->getCurrentToken()->getType()) {
  112. case /* Token::TEXT_TYPE */ 0:
  113. $token = $this->stream->next();
  114. $rv[] = new TextNode($token->getValue(), $token->getLine());
  115. break;
  116. case /* Token::VAR_START_TYPE */ 2:
  117. $token = $this->stream->next();
  118. $expr = $this->expressionParser->parseExpression();
  119. $this->stream->expect(/* Token::VAR_END_TYPE */ 4);
  120. $rv[] = new PrintNode($expr, $token->getLine());
  121. break;
  122. case /* Token::BLOCK_START_TYPE */ 1:
  123. $this->stream->next();
  124. $token = $this->getCurrentToken();
  125. if (/* Token::NAME_TYPE */ 5 !== $token->getType()) {
  126. throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext());
  127. }
  128. if (null !== $test && $test($token)) {
  129. if ($dropNeedle) {
  130. $this->stream->next();
  131. }
  132. if (1 === \count($rv)) {
  133. return $rv[0];
  134. }
  135. return new Node($rv, [], $lineno);
  136. }
  137. if (!isset($this->handlers[$token->getValue()])) {
  138. if (null !== $test) {
  139. $e = new SyntaxError(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
  140. if (\is_array($test) && isset($test[0]) && $test[0] instanceof TokenParserInterface) {
  141. $e->appendMessage(sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno));
  142. }
  143. } else {
  144. $e = new SyntaxError(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
  145. $e->addSuggestions($token->getValue(), array_keys($this->env->getTags()));
  146. }
  147. throw $e;
  148. }
  149. $this->stream->next();
  150. $subparser = $this->handlers[$token->getValue()];
  151. $node = $subparser->parse($token);
  152. if (null !== $node) {
  153. $rv[] = $node;
  154. }
  155. break;
  156. default:
  157. throw new SyntaxError('Lexer or parser ended up in unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext());
  158. }
  159. }
  160. if (1 === \count($rv)) {
  161. return $rv[0];
  162. }
  163. return new Node($rv, [], $lineno);
  164. }
  165. public function getBlockStack()
  166. {
  167. return $this->blockStack;
  168. }
  169. public function peekBlockStack()
  170. {
  171. return isset($this->blockStack[\count($this->blockStack) - 1]) ? $this->blockStack[\count($this->blockStack) - 1] : null;
  172. }
  173. public function popBlockStack()
  174. {
  175. array_pop($this->blockStack);
  176. }
  177. public function pushBlockStack($name)
  178. {
  179. $this->blockStack[] = $name;
  180. }
  181. public function hasBlock($name)
  182. {
  183. return isset($this->blocks[$name]);
  184. }
  185. public function getBlock($name)
  186. {
  187. return $this->blocks[$name];
  188. }
  189. public function setBlock($name, BlockNode $value)
  190. {
  191. $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine());
  192. }
  193. public function hasMacro($name)
  194. {
  195. return isset($this->macros[$name]);
  196. }
  197. public function setMacro($name, MacroNode $node)
  198. {
  199. $this->macros[$name] = $node;
  200. }
  201. /**
  202. * @deprecated since Twig 2.7 as there are no reserved macro names anymore, will be removed in 3.0.
  203. */
  204. public function isReservedMacroName($name)
  205. {
  206. @trigger_error(sprintf('The "%s" method is deprecated since Twig 2.7 and will be removed in 3.0.', __METHOD__), \E_USER_DEPRECATED);
  207. return false;
  208. }
  209. public function addTrait($trait)
  210. {
  211. $this->traits[] = $trait;
  212. }
  213. public function hasTraits()
  214. {
  215. return \count($this->traits) > 0;
  216. }
  217. public function embedTemplate(ModuleNode $template)
  218. {
  219. $template->setIndex(mt_rand());
  220. $this->embeddedTemplates[] = $template;
  221. }
  222. public function addImportedSymbol($type, $alias, $name = null, AbstractExpression $node = null)
  223. {
  224. $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $node];
  225. }
  226. public function getImportedSymbol($type, $alias)
  227. {
  228. // if the symbol does not exist in the current scope (0), try in the main/global scope (last index)
  229. return $this->importedSymbols[0][$type][$alias] ?? ($this->importedSymbols[\count($this->importedSymbols) - 1][$type][$alias] ?? null);
  230. }
  231. public function isMainScope()
  232. {
  233. return 1 === \count($this->importedSymbols);
  234. }
  235. public function pushLocalScope()
  236. {
  237. array_unshift($this->importedSymbols, []);
  238. }
  239. public function popLocalScope()
  240. {
  241. array_shift($this->importedSymbols);
  242. }
  243. /**
  244. * @return ExpressionParser
  245. */
  246. public function getExpressionParser()
  247. {
  248. return $this->expressionParser;
  249. }
  250. public function getParent()
  251. {
  252. return $this->parent;
  253. }
  254. public function setParent($parent)
  255. {
  256. $this->parent = $parent;
  257. }
  258. /**
  259. * @return TokenStream
  260. */
  261. public function getStream()
  262. {
  263. return $this->stream;
  264. }
  265. /**
  266. * @return Token
  267. */
  268. public function getCurrentToken()
  269. {
  270. return $this->stream->getCurrent();
  271. }
  272. private function filterBodyNodes(Node $node, bool $nested = false)
  273. {
  274. // check that the body does not contain non-empty output nodes
  275. if (
  276. ($node instanceof TextNode && !ctype_space($node->getAttribute('data')))
  277. ||
  278. // the "&& !$node instanceof SpacelessNode" part of the condition must be removed in 3.0
  279. (!$node instanceof TextNode && !$node instanceof BlockReferenceNode && ($node instanceof NodeOutputInterface && !$node instanceof SpacelessNode))
  280. ) {
  281. if (false !== strpos((string) $node, \chr(0xEF).\chr(0xBB).\chr(0xBF))) {
  282. $t = substr($node->getAttribute('data'), 3);
  283. if ('' === $t || ctype_space($t)) {
  284. // bypass empty nodes starting with a BOM
  285. return;
  286. }
  287. }
  288. throw new SyntaxError('A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?', $node->getTemplateLine(), $this->stream->getSourceContext());
  289. }
  290. // bypass nodes that "capture" the output
  291. if ($node instanceof NodeCaptureInterface) {
  292. // a "block" tag in such a node will serve as a block definition AND be displayed in place as well
  293. return $node;
  294. }
  295. // to be removed completely in Twig 3.0
  296. if (!$nested && $node instanceof SpacelessNode) {
  297. @trigger_error(sprintf('Using the spaceless tag at the root level of a child template in "%s" at line %d is deprecated since Twig 2.5.0 and will become a syntax error in 3.0.', $this->stream->getSourceContext()->getName(), $node->getTemplateLine()), \E_USER_DEPRECATED);
  298. }
  299. // "block" tags that are not captured (see above) are only used for defining
  300. // the content of the block. In such a case, nesting it does not work as
  301. // expected as the definition is not part of the default template code flow.
  302. if ($nested && ($node instanceof BlockReferenceNode || $node instanceof \Twig_Node_BlockReference)) {
  303. //throw new SyntaxError('A block definition cannot be nested under non-capturing nodes.', $node->getTemplateLine(), $this->stream->getSourceContext());
  304. @trigger_error(sprintf('Nesting a block definition under a non-capturing node in "%s" at line %d is deprecated since Twig 2.5.0 and will become a syntax error in 3.0.', $this->stream->getSourceContext()->getName(), $node->getTemplateLine()), \E_USER_DEPRECATED);
  305. return;
  306. }
  307. // the "&& !$node instanceof SpacelessNode" part of the condition must be removed in 3.0
  308. if ($node instanceof NodeOutputInterface && !$node instanceof SpacelessNode) {
  309. return;
  310. }
  311. // here, $nested means "being at the root level of a child template"
  312. // we need to discard the wrapping "Twig_Node" for the "body" node
  313. $nested = $nested || ('Twig_Node' !== \get_class($node) && Node::class !== \get_class($node));
  314. foreach ($node as $k => $n) {
  315. if (null !== $n && null === $this->filterBodyNodes($n, $nested)) {
  316. $node->removeNode($k);
  317. }
  318. }
  319. return $node;
  320. }
  321. }
  322. class_alias('Twig\Parser', 'Twig_Parser');