1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette\Latte;
13:
14: use Nette,
15: Nette\Utils\Strings;
16:
17:
18:
19: 20: 21: 22: 23:
24: class Compiler extends Nette\Object
25: {
26:
27: public $defaultContentType = self::CONTENT_XHTML;
28:
29:
30: private $tokens;
31:
32:
33: private $output;
34:
35:
36: private $position;
37:
38:
39: private $macros;
40:
41:
42: private $macroHandlers;
43:
44:
45: private $htmlNodes = array();
46:
47:
48: private $macroNodes = array();
49:
50:
51: private $contentType;
52:
53:
54: private $context;
55:
56:
57: private $templateId;
58:
59:
60: const CONTENT_HTML = 'html',
61: CONTENT_XHTML = 'xhtml',
62: CONTENT_XML = 'xml',
63: CONTENT_JS = 'js',
64: CONTENT_CSS = 'css',
65: CONTENT_ICAL = 'ical',
66: CONTENT_TEXT = 'text',
67:
68: CONTEXT_COMMENT = 'comment',
69: CONTEXT_SINGLE_QUOTED = "'",
70: CONTEXT_DOUBLE_QUOTED = '"';
71:
72:
73: public function __construct()
74: {
75: $this->macroHandlers = new \SplObjectStorage;
76: }
77:
78:
79:
80: 81: 82: 83: 84:
85: public function addMacro($name, IMacro $macro)
86: {
87: $this->macros[$name][] = $macro;
88: $this->macroHandlers->attach($macro);
89: return $this;
90: }
91:
92:
93:
94: 95: 96: 97: 98:
99: public function compile(array $tokens)
100: {
101: $this->templateId = Strings::random();
102: $this->tokens = $tokens;
103: $this->output = '';
104: $this->htmlNodes = $this->macroNodes = array();
105: $this->setContentType($this->defaultContentType);
106:
107: foreach ($this->macroHandlers as $handler) {
108: $handler->initialize($this);
109: }
110:
111: try {
112: foreach ($tokens as $this->position => $token) {
113: if ($token->type === Token::TEXT) {
114: $this->output .= $token->text;
115:
116: } elseif ($token->type === Token::MACRO) {
117: $isRightmost = !isset($tokens[$this->position + 1])
118: || substr($tokens[$this->position + 1]->text, 0, 1) === "\n";
119: $this->writeMacro($token->name, $token->value, $token->modifiers, $isRightmost);
120:
121: } elseif ($token->type === Token::TAG_BEGIN) {
122: $this->processTagBegin($token);
123:
124: } elseif ($token->type === Token::TAG_END) {
125: $this->processTagEnd($token);
126:
127: } elseif ($token->type === Token::ATTRIBUTE) {
128: $this->processAttribute($token);
129: }
130: }
131: } catch (ParseException $e) {
132: $e->sourceLine = $token->line;
133: throw $e;
134: }
135:
136:
137: foreach ($this->htmlNodes as $node) {
138: if (!empty($node->macroAttrs)) {
139: throw new ParseException("Missing end tag </$node->name> for macro-attribute " . Parser::N_PREFIX
140: . implode(' and ' . Parser::N_PREFIX, array_keys($node->macroAttrs)) . ".", 0, $token->line);
141: }
142: }
143:
144: $prologs = $epilogs = '';
145: foreach ($this->macroHandlers as $handler) {
146: $res = $handler->finalize();
147: $handlerName = get_class($handler);
148: $prologs .= empty($res[0]) ? '' : "<?php\n// prolog $handlerName\n$res[0]\n?>";
149: $epilogs = (empty($res[1]) ? '' : "<?php\n// epilog $handlerName\n$res[1]\n?>") . $epilogs;
150: }
151: $this->output = ($prologs ? $prologs . "<?php\n//\n// main template\n//\n?>\n" : '') . $this->output . $epilogs;
152:
153: if ($this->macroNodes) {
154: throw new ParseException("There are unclosed macros.", 0, $token->line);
155: }
156:
157: return $this->output;
158: }
159:
160:
161:
162: 163: 164:
165: public function setContentType($type)
166: {
167: $this->contentType = $type;
168: $this->context = NULL;
169: return $this;
170: }
171:
172:
173:
174: 175: 176:
177: public function getContentType()
178: {
179: return $this->contentType;
180: }
181:
182:
183:
184: 185: 186:
187: private function setContext($context, $sub = NULL)
188: {
189: $this->context = array($context, $sub);
190: return $this;
191: }
192:
193:
194:
195: 196: 197:
198: public function getContext()
199: {
200: return $this->context;
201: }
202:
203:
204:
205: 206: 207:
208: public function getTemplateId()
209: {
210: return $this->templateId;
211: }
212:
213:
214:
215: 216: 217: 218:
219: public function getLine()
220: {
221: return $this->tokens ? $this->tokens[$this->position]->line : NULL;
222: }
223:
224:
225:
226: private function processTagBegin($token)
227: {
228: if ($token->closing) {
229: do {
230: $node = array_pop($this->htmlNodes);
231: if (!$node) {
232: $node = new HtmlNode($token->name);
233: }
234: } while (strcasecmp($node->name, $token->name));
235: $this->htmlNodes[] = $node;
236: $node->closing = TRUE;
237: $node->offset = strlen($this->output);
238: $this->setContext(NULL);
239:
240: } elseif ($token->text === '<!--') {
241: $this->setContext(self::CONTEXT_COMMENT);
242:
243: } else {
244: $this->htmlNodes[] = $node = new HtmlNode($token->name);
245: $node->isEmpty = in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML))
246: && isset(Nette\Utils\Html::$emptyElements[strtolower($token->name)]);
247: $node->offset = strlen($this->output);
248: $this->setContext(NULL);
249: }
250: $this->output .= $token->text;
251: }
252:
253:
254:
255: private function processTagEnd($token)
256: {
257: if ($token->text === '-->') {
258: $this->output .= $token->text;
259: $this->setContext(NULL);
260: return;
261: }
262:
263: $node = end($this->htmlNodes);
264: $isEmpty = !$node->closing && (Strings::contains($token->text, '/') || $node->isEmpty);
265:
266: if ($isEmpty && in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML))) {
267: $token->text = preg_replace('#^.*>#', $this->contentType === self::CONTENT_XHTML ? ' />' : '>', $token->text);
268: }
269:
270: if (empty($node->macroAttrs)) {
271: $this->output .= $token->text;
272: } else {
273: $code = substr($this->output, $node->offset) . $token->text;
274: $this->output = substr($this->output, 0, $node->offset);
275: $this->writeAttrsMacro($code, $node);
276: if ($isEmpty) {
277: $node->closing = TRUE;
278: $this->writeAttrsMacro('', $node);
279: }
280: }
281:
282: if ($isEmpty) {
283: $node->closing = TRUE;
284: }
285:
286: if (!$node->closing && (strcasecmp($node->name, 'script') === 0 || strcasecmp($node->name, 'style') === 0)) {
287: $this->setContext(strcasecmp($node->name, 'style') ? self::CONTENT_JS : self::CONTENT_CSS);
288: } else {
289: $this->setContext(NULL);
290: if ($node->closing) {
291: array_pop($this->htmlNodes);
292: }
293: }
294: }
295:
296:
297:
298: private function processAttribute($token)
299: {
300: $node = end($this->htmlNodes);
301: if (Strings::startsWith($token->name, Parser::N_PREFIX)) {
302: $node->macroAttrs[substr($token->name, strlen(Parser::N_PREFIX))] = $token->value;
303: } else {
304: $node->attrs[$token->name] = TRUE;
305: $this->output .= $token->text;
306: if ($token->value) {
307: $context = NULL;
308: if (strncasecmp($token->name, 'on', 2) === 0) {
309: $context = self::CONTENT_JS;
310: } elseif ($token->name === 'style') {
311: $context = self::CONTENT_CSS;
312: }
313: $this->setContext($token->value, $context);
314: }
315: }
316: }
317:
318:
319:
320:
321:
322:
323:
324: 325: 326: 327: 328: 329: 330: 331:
332: public function writeMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE)
333: {
334: $isLeftmost = trim(substr($this->output, $leftOfs = strrpos("\n$this->output", "\n"))) === '';
335:
336: if ($name[0] === '/') {
337: $node = end($this->macroNodes);
338:
339: if (!$node || ("/$node->name" !== $name && '/' !== $name) || $modifiers
340: || ($args && $node->args && !Strings::startsWith("$node->args ", "$args "))
341: ) {
342: $name .= $args ? ' ' : '';
343: throw new ParseException("Unexpected macro {{$name}{$args}{$modifiers}}"
344: . ($node ? ", expecting {/$node->name}" . ($args && $node->args ? " or eventually {/$node->name $node->args}" : '') : ''));
345: }
346:
347: array_pop($this->macroNodes);
348: if (!$node->args) {
349: $node->setArgs($args);
350: }
351: if ($isLeftmost && $isRightmost) {
352: $this->output = substr($this->output, 0, $leftOfs);
353: }
354:
355: $code = $node->close(substr($this->output, $node->offset));
356:
357: if (!$isLeftmost && $isRightmost && substr($code, -2) === '?>') {
358: $code .= "\n";
359: }
360: $this->output = substr($this->output, 0, $node->offset) . $node->content. $code;
361:
362: } else {
363: list($node, $code) = $this->expandMacro($name, $args, $modifiers);
364: if (!$node->isEmpty) {
365: $this->macroNodes[] = $node;
366: }
367:
368: if ($isRightmost) {
369: if ($isLeftmost && substr($code, 0, 11) !== '<?php echo ') {
370: $this->output = substr($this->output, 0, $leftOfs);
371: } elseif (substr($code, -2) === '?>') {
372: $code .= "\n";
373: }
374: }
375:
376: $this->output .= $code;
377: $node->offset = strlen($this->output);
378: }
379: }
380:
381:
382:
383: 384: 385: 386: 387: 388: 389:
390: public function writeAttrsMacro($code, HtmlNode $node)
391: {
392: $attrs = $node->macroAttrs;
393: $left = $right = array();
394: foreach ($this->macros as $name => $foo) {
395: if ($name[0] === '@') {
396: $name = substr($name, 1);
397: if (!isset($attrs[$name])) {
398: continue;
399: }
400: if (!$node->closing) {
401: $pos = strrpos($code, '>');
402: if ($code[$pos-1] === '/') {
403: $pos--;
404: }
405: $this->setContext(self::CONTEXT_DOUBLE_QUOTED);
406: list(, $macroCode) = $this->expandMacro("@$name", $attrs[$name], NULL, $node);
407: $this->setContext(NULL);
408: $code = substr_replace($code, $macroCode, $pos, 0);
409: }
410: unset($attrs[$name]);
411: }
412:
413: $macro = $node->closing ? "/$name" : $name;
414: if (isset($attrs[$name])) {
415: if ($node->closing) {
416: $right[] = array($macro, '');
417: } else {
418: array_unshift($left, array($macro, $attrs[$name]));
419: }
420: }
421:
422: $innerName = "inner-$name";
423: if (isset($attrs[$innerName])) {
424: if ($node->closing) {
425: $left[] = array($macro, '');
426: } else {
427: array_unshift($right, array($macro, $attrs[$innerName]));
428: }
429: }
430:
431: $tagName = "tag-$name";
432: if (isset($attrs[$tagName])) {
433: array_unshift($left, array($name, $attrs[$tagName]));
434: $right[] = array("/$name", '');
435: }
436:
437: unset($attrs[$name], $attrs[$innerName], $attrs[$tagName]);
438: }
439:
440: if ($attrs) {
441: throw new ParseException("Unknown macro-attribute " . Parser::N_PREFIX
442: . implode(' and ' . Parser::N_PREFIX, array_keys($attrs)));
443: }
444:
445: foreach ($left as $item) {
446: $this->writeMacro($item[0], $item[1]);
447: if (substr($this->output, -2) === '?>') {
448: $this->output .= "\n";
449: }
450: }
451: $this->output .= $code;
452:
453: foreach ($right as $item) {
454: $this->writeMacro($item[0], $item[1]);
455: if (substr($this->output, -2) === '?>') {
456: $this->output .= "\n";
457: }
458: }
459: }
460:
461:
462:
463: 464: 465: 466: 467: 468: 469:
470: public function expandMacro($name, $args, $modifiers = NULL, HtmlNode $htmlNode = NULL)
471: {
472: if (empty($this->macros[$name])) {
473: throw new ParseException("Unknown macro {{$name}}");
474: }
475: foreach (array_reverse($this->macros[$name]) as $macro) {
476: $node = new MacroNode($macro, $name, $args, $modifiers, $this->macroNodes ? end($this->macroNodes) : NULL, $htmlNode);
477: $code = $macro->nodeOpened($node);
478: if ($code !== FALSE) {
479: return array($node, $code);
480: }
481: }
482: throw new ParseException("Unhandled macro {{$name}}");
483: }
484:
485: }
486: