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