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