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