Parser.php (12410B)
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 * Default parser implementation. 15 * 16 * @author Fabien Potencier <fabien@symfony.com> 17 */ 18 class Twig_Parser implements Twig_ParserInterface 19 { 20 protected $stack = array(); 21 protected $stream; 22 protected $parent; 23 protected $handlers; 24 protected $visitors; 25 protected $expressionParser; 26 protected $blocks; 27 protected $blockStack; 28 protected $macros; 29 protected $env; 30 protected $reservedMacroNames; 31 protected $importedSymbols; 32 protected $traits; 33 protected $embeddedTemplates = array(); 34 35 /** 36 * Constructor. 37 * 38 * @param Twig_Environment $env A Twig_Environment instance 39 */ 40 public function __construct(Twig_Environment $env) 41 { 42 $this->env = $env; 43 } 44 45 public function getEnvironment() 46 { 47 return $this->env; 48 } 49 50 public function getVarName() 51 { 52 return sprintf('__internal_%s', hash('sha256', uniqid(mt_rand(), true), false)); 53 } 54 55 public function getFilename() 56 { 57 return $this->stream->getFilename(); 58 } 59 60 /** 61 * {@inheritdoc} 62 */ 63 public function parse(Twig_TokenStream $stream, $test = null, $dropNeedle = false) 64 { 65 // push all variables into the stack to keep the current state of the parser 66 // using get_object_vars() instead of foreach would lead to https://bugs.php.net/71336 67 $vars = array(); 68 foreach ($this as $k => $v) { 69 $vars[$k] = $v; 70 } 71 72 unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames']); 73 $this->stack[] = $vars; 74 75 // tag handlers 76 if (null === $this->handlers) { 77 $this->handlers = $this->env->getTokenParsers(); 78 $this->handlers->setParser($this); 79 } 80 81 // node visitors 82 if (null === $this->visitors) { 83 $this->visitors = $this->env->getNodeVisitors(); 84 } 85 86 if (null === $this->expressionParser) { 87 $this->expressionParser = new Twig_ExpressionParser($this, $this->env->getUnaryOperators(), $this->env->getBinaryOperators()); 88 } 89 90 $this->stream = $stream; 91 $this->parent = null; 92 $this->blocks = array(); 93 $this->macros = array(); 94 $this->traits = array(); 95 $this->blockStack = array(); 96 $this->importedSymbols = array(array()); 97 $this->embeddedTemplates = array(); 98 99 try { 100 $body = $this->subparse($test, $dropNeedle); 101 102 if (null !== $this->parent && null === $body = $this->filterBodyNodes($body)) { 103 $body = new Twig_Node(); 104 } 105 } catch (Twig_Error_Syntax $e) { 106 if (!$e->getTemplateFile()) { 107 $e->setTemplateFile($this->getFilename()); 108 } 109 110 if (!$e->getTemplateLine()) { 111 $e->setTemplateLine($this->stream->getCurrent()->getLine()); 112 } 113 114 throw $e; 115 } 116 117 $node = new Twig_Node_Module(new Twig_Node_Body(array($body)), $this->parent, new Twig_Node($this->blocks), new Twig_Node($this->macros), new Twig_Node($this->traits), $this->embeddedTemplates, $this->getFilename()); 118 119 $traverser = new Twig_NodeTraverser($this->env, $this->visitors); 120 121 $node = $traverser->traverse($node); 122 123 // restore previous stack so previous parse() call can resume working 124 foreach (array_pop($this->stack) as $key => $val) { 125 $this->$key = $val; 126 } 127 128 return $node; 129 } 130 131 public function subparse($test, $dropNeedle = false) 132 { 133 $lineno = $this->getCurrentToken()->getLine(); 134 $rv = array(); 135 while (!$this->stream->isEOF()) { 136 switch ($this->getCurrentToken()->getType()) { 137 case Twig_Token::TEXT_TYPE: 138 $token = $this->stream->next(); 139 $rv[] = new Twig_Node_Text($token->getValue(), $token->getLine()); 140 break; 141 142 case Twig_Token::VAR_START_TYPE: 143 $token = $this->stream->next(); 144 $expr = $this->expressionParser->parseExpression(); 145 $this->stream->expect(Twig_Token::VAR_END_TYPE); 146 $rv[] = new Twig_Node_Print($expr, $token->getLine()); 147 break; 148 149 case Twig_Token::BLOCK_START_TYPE: 150 $this->stream->next(); 151 $token = $this->getCurrentToken(); 152 153 if ($token->getType() !== Twig_Token::NAME_TYPE) { 154 throw new Twig_Error_Syntax('A block must start with a tag name.', $token->getLine(), $this->getFilename()); 155 } 156 157 if (null !== $test && call_user_func($test, $token)) { 158 if ($dropNeedle) { 159 $this->stream->next(); 160 } 161 162 if (1 === count($rv)) { 163 return $rv[0]; 164 } 165 166 return new Twig_Node($rv, array(), $lineno); 167 } 168 169 $subparser = $this->handlers->getTokenParser($token->getValue()); 170 if (null === $subparser) { 171 if (null !== $test) { 172 $e = new Twig_Error_Syntax(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->getFilename()); 173 174 if (is_array($test) && isset($test[0]) && $test[0] instanceof Twig_TokenParserInterface) { 175 $e->appendMessage(sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno)); 176 } 177 } else { 178 $e = new Twig_Error_Syntax(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->getFilename()); 179 $e->addSuggestions($token->getValue(), array_keys($this->env->getTags())); 180 } 181 182 throw $e; 183 } 184 185 $this->stream->next(); 186 187 $node = $subparser->parse($token); 188 if (null !== $node) { 189 $rv[] = $node; 190 } 191 break; 192 193 default: 194 throw new Twig_Error_Syntax('Lexer or parser ended up in unsupported state.', 0, $this->getFilename()); 195 } 196 } 197 198 if (1 === count($rv)) { 199 return $rv[0]; 200 } 201 202 return new Twig_Node($rv, array(), $lineno); 203 } 204 205 public function addHandler($name, $class) 206 { 207 $this->handlers[$name] = $class; 208 } 209 210 public function addNodeVisitor(Twig_NodeVisitorInterface $visitor) 211 { 212 $this->visitors[] = $visitor; 213 } 214 215 public function getBlockStack() 216 { 217 return $this->blockStack; 218 } 219 220 public function peekBlockStack() 221 { 222 return $this->blockStack[count($this->blockStack) - 1]; 223 } 224 225 public function popBlockStack() 226 { 227 array_pop($this->blockStack); 228 } 229 230 public function pushBlockStack($name) 231 { 232 $this->blockStack[] = $name; 233 } 234 235 public function hasBlock($name) 236 { 237 return isset($this->blocks[$name]); 238 } 239 240 public function getBlock($name) 241 { 242 return $this->blocks[$name]; 243 } 244 245 public function setBlock($name, Twig_Node_Block $value) 246 { 247 $this->blocks[$name] = new Twig_Node_Body(array($value), array(), $value->getLine()); 248 } 249 250 public function hasMacro($name) 251 { 252 return isset($this->macros[$name]); 253 } 254 255 public function setMacro($name, Twig_Node_Macro $node) 256 { 257 if ($this->isReservedMacroName($name)) { 258 throw new Twig_Error_Syntax(sprintf('"%s" cannot be used as a macro name as it is a reserved keyword.', $name), $node->getLine(), $this->getFilename()); 259 } 260 261 $this->macros[$name] = $node; 262 } 263 264 public function isReservedMacroName($name) 265 { 266 if (null === $this->reservedMacroNames) { 267 $this->reservedMacroNames = array(); 268 $r = new ReflectionClass($this->env->getBaseTemplateClass()); 269 foreach ($r->getMethods() as $method) { 270 $methodName = strtolower($method->getName()); 271 272 if ('get' === substr($methodName, 0, 3) && isset($methodName[3])) { 273 $this->reservedMacroNames[] = substr($methodName, 3); 274 } 275 } 276 } 277 278 return in_array(strtolower($name), $this->reservedMacroNames); 279 } 280 281 public function addTrait($trait) 282 { 283 $this->traits[] = $trait; 284 } 285 286 public function hasTraits() 287 { 288 return count($this->traits) > 0; 289 } 290 291 public function embedTemplate(Twig_Node_Module $template) 292 { 293 $template->setIndex(mt_rand()); 294 295 $this->embeddedTemplates[] = $template; 296 } 297 298 public function addImportedSymbol($type, $alias, $name = null, Twig_Node_Expression $node = null) 299 { 300 $this->importedSymbols[0][$type][$alias] = array('name' => $name, 'node' => $node); 301 } 302 303 public function getImportedSymbol($type, $alias) 304 { 305 foreach ($this->importedSymbols as $functions) { 306 if (isset($functions[$type][$alias])) { 307 return $functions[$type][$alias]; 308 } 309 } 310 } 311 312 public function isMainScope() 313 { 314 return 1 === count($this->importedSymbols); 315 } 316 317 public function pushLocalScope() 318 { 319 array_unshift($this->importedSymbols, array()); 320 } 321 322 public function popLocalScope() 323 { 324 array_shift($this->importedSymbols); 325 } 326 327 /** 328 * Gets the expression parser. 329 * 330 * @return Twig_ExpressionParser The expression parser 331 */ 332 public function getExpressionParser() 333 { 334 return $this->expressionParser; 335 } 336 337 public function getParent() 338 { 339 return $this->parent; 340 } 341 342 public function setParent($parent) 343 { 344 $this->parent = $parent; 345 } 346 347 /** 348 * Gets the token stream. 349 * 350 * @return Twig_TokenStream The token stream 351 */ 352 public function getStream() 353 { 354 return $this->stream; 355 } 356 357 /** 358 * Gets the current token. 359 * 360 * @return Twig_Token The current token 361 */ 362 public function getCurrentToken() 363 { 364 return $this->stream->getCurrent(); 365 } 366 367 protected function filterBodyNodes(Twig_NodeInterface $node) 368 { 369 // check that the body does not contain non-empty output nodes 370 if ( 371 ($node instanceof Twig_Node_Text && !ctype_space($node->getAttribute('data'))) 372 || 373 (!$node instanceof Twig_Node_Text && !$node instanceof Twig_Node_BlockReference && $node instanceof Twig_NodeOutputInterface) 374 ) { 375 if (false !== strpos((string) $node, chr(0xEF).chr(0xBB).chr(0xBF))) { 376 throw new Twig_Error_Syntax('A template that extends another one cannot have a body but a byte order mark (BOM) has been detected; it must be removed.', $node->getLine(), $this->getFilename()); 377 } 378 379 throw new Twig_Error_Syntax('A template that extends another one cannot have a body.', $node->getLine(), $this->getFilename()); 380 } 381 382 // bypass "set" nodes as they "capture" the output 383 if ($node instanceof Twig_Node_Set) { 384 return $node; 385 } 386 387 if ($node instanceof Twig_NodeOutputInterface) { 388 return; 389 } 390 391 foreach ($node as $k => $n) { 392 if (null !== $n && null === $this->filterBodyNodes($n)) { 393 $node->removeNode($k); 394 } 395 } 396 397 return $node; 398 } 399 }