4 * This file is part of Twig.
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
14 * Default parser implementation.
16 * @author Fabien Potencier <fabien@symfony.com>
18 class Twig_Parser implements Twig_ParserInterface
20 protected $stack = array();
25 protected $expressionParser;
27 protected $blockStack;
30 protected $reservedMacroNames;
31 protected $importedSymbols;
33 protected $embeddedTemplates = array();
35 public function __construct(Twig_Environment $env)
41 * @deprecated since 1.27 (to be removed in 2.0)
43 public function getEnvironment()
45 @trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0.', E_USER_DEPRECATED);
50 public function getVarName()
52 return sprintf('__internal_%s', hash('sha256', uniqid(mt_rand(), true), false));
56 * @deprecated since 1.27 (to be removed in 2.0). Use $parser->getStream()->getSourceContext()->getPath() instead.
58 public function getFilename()
60 @trigger_error(sprintf('The "%s" method is deprecated since version 1.27 and will be removed in 2.0. Use $parser->getStream()->getSourceContext()->getPath() instead.', __METHOD__), E_USER_DEPRECATED);
62 return $this->stream->getSourceContext()->getName();
65 public function parse(Twig_TokenStream $stream, $test = null, $dropNeedle = false)
67 // push all variables into the stack to keep the current state of the parser
68 // using get_object_vars() instead of foreach would lead to https://bugs.php.net/71336
69 // This hack can be removed when min version if PHP 7.0
71 foreach ($this as $k => $v) {
75 unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames']);
76 $this->stack[] = $vars;
79 if (null === $this->handlers) {
80 $this->handlers = $this->env->getTokenParsers();
81 $this->handlers->setParser($this);
85 if (null === $this->visitors) {
86 $this->visitors = $this->env->getNodeVisitors();
89 if (null === $this->expressionParser) {
90 $this->expressionParser = new Twig_ExpressionParser($this, $this->env);
93 $this->stream = $stream;
95 $this->blocks = array();
96 $this->macros = array();
97 $this->traits = array();
98 $this->blockStack = array();
99 $this->importedSymbols = array(array());
100 $this->embeddedTemplates = array();
103 $body = $this->subparse($test, $dropNeedle);
105 if (null !== $this->parent && null === $body = $this->filterBodyNodes($body)) {
106 $body = new Twig_Node();
108 } catch (Twig_Error_Syntax $e) {
109 if (!$e->getSourceContext()) {
110 $e->setSourceContext($this->stream->getSourceContext());
113 if (!$e->getTemplateLine()) {
114 $e->setTemplateLine($this->stream->getCurrent()->getLine());
120 $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, $stream->getSourceContext());
122 $traverser = new Twig_NodeTraverser($this->env, $this->visitors);
124 $node = $traverser->traverse($node);
126 // restore previous stack so previous parse() call can resume working
127 foreach (array_pop($this->stack) as $key => $val) {
134 public function subparse($test, $dropNeedle = false)
136 $lineno = $this->getCurrentToken()->getLine();
138 while (!$this->stream->isEOF()) {
139 switch ($this->getCurrentToken()->getType()) {
140 case Twig_Token::TEXT_TYPE:
141 $token = $this->stream->next();
142 $rv[] = new Twig_Node_Text($token->getValue(), $token->getLine());
145 case Twig_Token::VAR_START_TYPE:
146 $token = $this->stream->next();
147 $expr = $this->expressionParser->parseExpression();
148 $this->stream->expect(Twig_Token::VAR_END_TYPE);
149 $rv[] = new Twig_Node_Print($expr, $token->getLine());
152 case Twig_Token::BLOCK_START_TYPE:
153 $this->stream->next();
154 $token = $this->getCurrentToken();
156 if ($token->getType() !== Twig_Token::NAME_TYPE) {
157 throw new Twig_Error_Syntax('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext());
160 if (null !== $test && call_user_func($test, $token)) {
162 $this->stream->next();
165 if (1 === count($rv)) {
169 return new Twig_Node($rv, array(), $lineno);
172 $subparser = $this->handlers->getTokenParser($token->getValue());
173 if (null === $subparser) {
174 if (null !== $test) {
175 $e = new Twig_Error_Syntax(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
177 if (is_array($test) && isset($test[0]) && $test[0] instanceof Twig_TokenParserInterface) {
178 $e->appendMessage(sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno));
181 $e = new Twig_Error_Syntax(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
182 $e->addSuggestions($token->getValue(), array_keys($this->env->getTags()));
188 $this->stream->next();
190 $node = $subparser->parse($token);
191 if (null !== $node) {
197 throw new Twig_Error_Syntax('Lexer or parser ended up in unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext());
201 if (1 === count($rv)) {
205 return new Twig_Node($rv, array(), $lineno);
209 * @deprecated since 1.27 (to be removed in 2.0)
211 public function addHandler($name, $class)
213 @trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0.', E_USER_DEPRECATED);
215 $this->handlers[$name] = $class;
219 * @deprecated since 1.27 (to be removed in 2.0)
221 public function addNodeVisitor(Twig_NodeVisitorInterface $visitor)
223 @trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0.', E_USER_DEPRECATED);
225 $this->visitors[] = $visitor;
228 public function getBlockStack()
230 return $this->blockStack;
233 public function peekBlockStack()
235 return $this->blockStack[count($this->blockStack) - 1];
238 public function popBlockStack()
240 array_pop($this->blockStack);
243 public function pushBlockStack($name)
245 $this->blockStack[] = $name;
248 public function hasBlock($name)
250 return isset($this->blocks[$name]);
253 public function getBlock($name)
255 return $this->blocks[$name];
258 public function setBlock($name, Twig_Node_Block $value)
260 $this->blocks[$name] = new Twig_Node_Body(array($value), array(), $value->getTemplateLine());
263 public function hasMacro($name)
265 return isset($this->macros[$name]);
268 public function setMacro($name, Twig_Node_Macro $node)
270 if ($this->isReservedMacroName($name)) {
271 throw new Twig_Error_Syntax(sprintf('"%s" cannot be used as a macro name as it is a reserved keyword.', $name), $node->getTemplateLine(), $this->stream->getSourceContext());
274 $this->macros[$name] = $node;
277 public function isReservedMacroName($name)
279 if (null === $this->reservedMacroNames) {
280 $this->reservedMacroNames = array();
281 $r = new ReflectionClass($this->env->getBaseTemplateClass());
282 foreach ($r->getMethods() as $method) {
283 $methodName = strtolower($method->getName());
285 if ('get' === substr($methodName, 0, 3) && isset($methodName[3])) {
286 $this->reservedMacroNames[] = substr($methodName, 3);
291 return in_array(strtolower($name), $this->reservedMacroNames);
294 public function addTrait($trait)
296 $this->traits[] = $trait;
299 public function hasTraits()
301 return count($this->traits) > 0;
304 public function embedTemplate(Twig_Node_Module $template)
306 $template->setIndex(mt_rand());
308 $this->embeddedTemplates[] = $template;
311 public function addImportedSymbol($type, $alias, $name = null, Twig_Node_Expression $node = null)
313 $this->importedSymbols[0][$type][$alias] = array('name' => $name, 'node' => $node);
316 public function getImportedSymbol($type, $alias)
318 foreach ($this->importedSymbols as $functions) {
319 if (isset($functions[$type][$alias])) {
320 return $functions[$type][$alias];
325 public function isMainScope()
327 return 1 === count($this->importedSymbols);
330 public function pushLocalScope()
332 array_unshift($this->importedSymbols, array());
335 public function popLocalScope()
337 array_shift($this->importedSymbols);
341 * @return Twig_ExpressionParser
343 public function getExpressionParser()
345 return $this->expressionParser;
348 public function getParent()
350 return $this->parent;
353 public function setParent($parent)
355 $this->parent = $parent;
359 * @return Twig_TokenStream
361 public function getStream()
363 return $this->stream;
369 public function getCurrentToken()
371 return $this->stream->getCurrent();
374 protected function filterBodyNodes(Twig_NodeInterface $node)
376 // check that the body does not contain non-empty output nodes
378 ($node instanceof Twig_Node_Text && !ctype_space($node->getAttribute('data')))
380 (!$node instanceof Twig_Node_Text && !$node instanceof Twig_Node_BlockReference && $node instanceof Twig_NodeOutputInterface)
382 if (false !== strpos((string) $node, chr(0xEF).chr(0xBB).chr(0xBF))) {
383 throw new Twig_Error_Syntax('A template that extends another one cannot start with a byte order mark (BOM); it must be removed.', $node->getTemplateLine(), $this->stream->getSourceContext());
386 throw new Twig_Error_Syntax('A template that extends another one cannot include contents outside Twig blocks. Did you forget to put the contents inside a {% block %} tag?', $node->getTemplateLine(), $this->stream->getSourceContext());
389 // bypass nodes that will "capture" the output
390 if ($node instanceof Twig_NodeCaptureInterface) {
394 if ($node instanceof Twig_NodeOutputInterface) {
398 foreach ($node as $k => $n) {
399 if (null !== $n && null === $this->filterBodyNodes($n)) {
400 $node->removeNode($k);