|
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 * @package twig |
|
17 * @author Fabien Potencier <fabien@symfony.com> |
|
18 */ |
|
19 class Twig_Parser implements Twig_ParserInterface |
|
20 { |
|
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 $importedFunctions; |
|
32 protected $tmpVarCount; |
|
33 protected $traits; |
|
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 getVarName() |
|
46 { |
|
47 return sprintf('__internal_%s_%d', substr($this->env->getTemplateClass($this->stream->getFilename()), strlen($this->env->getTemplateClassPrefix())), ++$this->tmpVarCount); |
|
48 } |
|
49 |
|
50 /** |
|
51 * Converts a token stream to a node tree. |
|
52 * |
|
53 * @param Twig_TokenStream $stream A token stream instance |
|
54 * |
|
55 * @return Twig_Node_Module A node tree |
|
56 */ |
|
57 public function parse(Twig_TokenStream $stream) |
|
58 { |
|
59 $this->tmpVarCount = 0; |
|
60 |
|
61 // tag handlers |
|
62 $this->handlers = $this->env->getTokenParsers(); |
|
63 $this->handlers->setParser($this); |
|
64 |
|
65 // node visitors |
|
66 $this->visitors = $this->env->getNodeVisitors(); |
|
67 |
|
68 if (null === $this->expressionParser) { |
|
69 $this->expressionParser = new Twig_ExpressionParser($this, $this->env->getUnaryOperators(), $this->env->getBinaryOperators()); |
|
70 } |
|
71 |
|
72 $this->stream = $stream; |
|
73 $this->parent = null; |
|
74 $this->blocks = array(); |
|
75 $this->macros = array(); |
|
76 $this->traits = array(); |
|
77 $this->blockStack = array(); |
|
78 $this->importedFunctions = array(array()); |
|
79 |
|
80 try { |
|
81 $body = $this->subparse(null); |
|
82 |
|
83 if (null !== $this->parent) { |
|
84 if (null === $body = $this->filterBodyNodes($body)) { |
|
85 $body = new Twig_Node(); |
|
86 } |
|
87 } |
|
88 } catch (Twig_Error_Syntax $e) { |
|
89 if (null === $e->getTemplateFile()) { |
|
90 $e->setTemplateFile($this->stream->getFilename()); |
|
91 } |
|
92 |
|
93 throw $e; |
|
94 } |
|
95 |
|
96 $node = new Twig_Node_Module($body, $this->parent, new Twig_Node($this->blocks), new Twig_Node($this->macros), new Twig_Node($this->traits), $this->stream->getFilename()); |
|
97 |
|
98 $traverser = new Twig_NodeTraverser($this->env, $this->visitors); |
|
99 |
|
100 return $traverser->traverse($node); |
|
101 } |
|
102 |
|
103 public function subparse($test, $dropNeedle = false) |
|
104 { |
|
105 $lineno = $this->getCurrentToken()->getLine(); |
|
106 $rv = array(); |
|
107 while (!$this->stream->isEOF()) { |
|
108 switch ($this->getCurrentToken()->getType()) { |
|
109 case Twig_Token::TEXT_TYPE: |
|
110 $token = $this->stream->next(); |
|
111 $rv[] = new Twig_Node_Text($token->getValue(), $token->getLine()); |
|
112 break; |
|
113 |
|
114 case Twig_Token::VAR_START_TYPE: |
|
115 $token = $this->stream->next(); |
|
116 $expr = $this->expressionParser->parseExpression(); |
|
117 $this->stream->expect(Twig_Token::VAR_END_TYPE); |
|
118 $rv[] = new Twig_Node_Print($expr, $token->getLine()); |
|
119 break; |
|
120 |
|
121 case Twig_Token::BLOCK_START_TYPE: |
|
122 $this->stream->next(); |
|
123 $token = $this->getCurrentToken(); |
|
124 |
|
125 if ($token->getType() !== Twig_Token::NAME_TYPE) { |
|
126 throw new Twig_Error_Syntax('A block must start with a tag name', $token->getLine(), $this->stream->getFilename()); |
|
127 } |
|
128 |
|
129 if (null !== $test && call_user_func($test, $token)) { |
|
130 if ($dropNeedle) { |
|
131 $this->stream->next(); |
|
132 } |
|
133 |
|
134 if (1 === count($rv)) { |
|
135 return $rv[0]; |
|
136 } |
|
137 |
|
138 return new Twig_Node($rv, array(), $lineno); |
|
139 } |
|
140 |
|
141 $subparser = $this->handlers->getTokenParser($token->getValue()); |
|
142 if (null === $subparser) { |
|
143 if (null !== $test) { |
|
144 throw new Twig_Error_Syntax(sprintf('Unexpected tag name "%s" (expecting closing tag for the "%s" tag defined near line %s)', $token->getValue(), $test[0]->getTag(), $lineno), $token->getLine(), $this->stream->getFilename()); |
|
145 } |
|
146 |
|
147 throw new Twig_Error_Syntax(sprintf('Unknown tag name "%s"', $token->getValue()), $token->getLine(), $this->stream->getFilename()); |
|
148 } |
|
149 |
|
150 $this->stream->next(); |
|
151 |
|
152 $node = $subparser->parse($token); |
|
153 if (null !== $node) { |
|
154 $rv[] = $node; |
|
155 } |
|
156 break; |
|
157 |
|
158 default: |
|
159 throw new Twig_Error_Syntax('Lexer or parser ended up in unsupported state.', -1, $this->stream->getFilename()); |
|
160 } |
|
161 } |
|
162 |
|
163 if (1 === count($rv)) { |
|
164 return $rv[0]; |
|
165 } |
|
166 |
|
167 return new Twig_Node($rv, array(), $lineno); |
|
168 } |
|
169 |
|
170 public function addHandler($name, $class) |
|
171 { |
|
172 $this->handlers[$name] = $class; |
|
173 } |
|
174 |
|
175 public function addNodeVisitor(Twig_NodeVisitorInterface $visitor) |
|
176 { |
|
177 $this->visitors[] = $visitor; |
|
178 } |
|
179 |
|
180 public function getBlockStack() |
|
181 { |
|
182 return $this->blockStack; |
|
183 } |
|
184 |
|
185 public function peekBlockStack() |
|
186 { |
|
187 return $this->blockStack[count($this->blockStack) - 1]; |
|
188 } |
|
189 |
|
190 public function popBlockStack() |
|
191 { |
|
192 array_pop($this->blockStack); |
|
193 } |
|
194 |
|
195 public function pushBlockStack($name) |
|
196 { |
|
197 $this->blockStack[] = $name; |
|
198 } |
|
199 |
|
200 public function hasBlock($name) |
|
201 { |
|
202 return isset($this->blocks[$name]); |
|
203 } |
|
204 |
|
205 public function setBlock($name, $value) |
|
206 { |
|
207 $this->blocks[$name] = $value; |
|
208 } |
|
209 |
|
210 public function hasMacro($name) |
|
211 { |
|
212 return isset($this->macros[$name]); |
|
213 } |
|
214 |
|
215 public function setMacro($name, Twig_Node_Macro $node) |
|
216 { |
|
217 if (null === $this->reservedMacroNames) { |
|
218 $this->reservedMacroNames = array(); |
|
219 $r = new ReflectionClass($this->env->getBaseTemplateClass()); |
|
220 foreach ($r->getMethods() as $method) { |
|
221 $this->reservedMacroNames[] = $method->getName(); |
|
222 } |
|
223 } |
|
224 |
|
225 if (in_array($name, $this->reservedMacroNames)) { |
|
226 throw new Twig_Error_Syntax(sprintf('"%s" cannot be used as a macro name as it is a reserved keyword', $name), $node->getLine()); |
|
227 } |
|
228 |
|
229 $this->macros[$name] = $node; |
|
230 } |
|
231 |
|
232 public function addTrait($trait) |
|
233 { |
|
234 $this->traits[] = $trait; |
|
235 } |
|
236 |
|
237 public function hasTraits() |
|
238 { |
|
239 return count($this->traits) > 0; |
|
240 } |
|
241 |
|
242 public function addImportedFunction($alias, $name, Twig_Node_Expression $node) |
|
243 { |
|
244 $this->importedFunctions[0][$alias] = array('name' => $name, 'node' => $node); |
|
245 } |
|
246 |
|
247 public function getImportedFunction($alias) |
|
248 { |
|
249 foreach ($this->importedFunctions as $functions) { |
|
250 if (isset($functions[$alias])) { |
|
251 return $functions[$alias]; |
|
252 } |
|
253 } |
|
254 } |
|
255 |
|
256 public function isMainScope() |
|
257 { |
|
258 return 1 === count($this->importedFunctions); |
|
259 } |
|
260 |
|
261 public function pushLocalScope() |
|
262 { |
|
263 array_unshift($this->importedFunctions, array()); |
|
264 } |
|
265 |
|
266 public function popLocalScope() |
|
267 { |
|
268 array_shift($this->importedFunctions); |
|
269 } |
|
270 |
|
271 /** |
|
272 * Gets the expression parser. |
|
273 * |
|
274 * @return Twig_ExpressionParser The expression parser |
|
275 */ |
|
276 public function getExpressionParser() |
|
277 { |
|
278 return $this->expressionParser; |
|
279 } |
|
280 |
|
281 public function getParent() |
|
282 { |
|
283 return $this->parent; |
|
284 } |
|
285 |
|
286 public function setParent($parent) |
|
287 { |
|
288 $this->parent = $parent; |
|
289 } |
|
290 |
|
291 /** |
|
292 * Gets the token stream. |
|
293 * |
|
294 * @return Twig_TokenStream The token stream |
|
295 */ |
|
296 public function getStream() |
|
297 { |
|
298 return $this->stream; |
|
299 } |
|
300 |
|
301 /** |
|
302 * Gets the current token. |
|
303 * |
|
304 * @return Twig_Token The current token |
|
305 */ |
|
306 public function getCurrentToken() |
|
307 { |
|
308 return $this->stream->getCurrent(); |
|
309 } |
|
310 |
|
311 protected function filterBodyNodes(Twig_NodeInterface $node) |
|
312 { |
|
313 // check that the body does not contain non-empty output nodes |
|
314 if ( |
|
315 ($node instanceof Twig_Node_Text && !ctype_space($node->getAttribute('data'))) |
|
316 || |
|
317 (!$node instanceof Twig_Node_Text && !$node instanceof Twig_Node_BlockReference && $node instanceof Twig_NodeOutputInterface) |
|
318 ) { |
|
319 throw new Twig_Error_Syntax('A template that extends another one cannot have a body.', $node->getLine(), $this->stream->getFilename()); |
|
320 } |
|
321 |
|
322 // bypass "set" nodes as they "capture" the output |
|
323 if ($node instanceof Twig_Node_Set) { |
|
324 return $node; |
|
325 } |
|
326 |
|
327 if ($node instanceof Twig_NodeOutputInterface) { |
|
328 return; |
|
329 } |
|
330 |
|
331 foreach ($node as $k => $n) { |
|
332 if (null !== $n && null === $n = $this->filterBodyNodes($n)) { |
|
333 $node->removeNode($k); |
|
334 } |
|
335 } |
|
336 |
|
337 return $node; |
|
338 } |
|
339 } |