1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Latte;
9:
10: use Nette;
11: use Nette\Utils\Strings;
12:
13:
14: 15: 16: 17: 18:
19: class Compiler extends Nette\Object
20: {
21:
22: public $defaultContentType = self::CONTENT_HTML;
23:
24:
25: private $tokens;
26:
27:
28: private $output;
29:
30:
31: private $position;
32:
33:
34: private $macros;
35:
36:
37: private $macroHandlers;
38:
39:
40: private $htmlNode;
41:
42:
43: private $macroNode;
44:
45:
46: private $attrCodes = array();
47:
48:
49: private $contentType;
50:
51:
52: private $context;
53:
54:
55: private $templateId;
56:
57:
58: const CONTENT_HTML = 'html',
59: CONTENT_XHTML = 'xhtml',
60: CONTENT_XML = 'xml',
61: CONTENT_JS = 'js',
62: CONTENT_CSS = 'css',
63: CONTENT_URL = 'url',
64: CONTENT_ICAL = 'ical',
65: CONTENT_TEXT = 'text';
66:
67:
68: const = 'comment',
69: CONTEXT_SINGLE_QUOTED_ATTR = "'",
70: CONTEXT_DOUBLE_QUOTED_ATTR = '"',
71: CONTEXT_UNQUOTED_ATTR = '=';
72:
73:
74: public function __construct()
75: {
76: $this->macroHandlers = new \SplObjectStorage;
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: public function compile(array $tokens)
99: {
100: $this->templateId = Strings::random();
101: $this->tokens = $tokens;
102: $output = '';
103: $this->output = & $output;
104: $this->htmlNode = $this->macroNode = NULL;
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: $this->{"process$token->type"}($token);
114: }
115: } catch (CompileException $e) {
116: $e->sourceLine = $token->line;
117: throw $e;
118: }
119:
120: while ($this->htmlNode) {
121: if (!empty($this->htmlNode->macroAttrs)) {
122: throw new CompileException('Missing ' . self::printEndTag($this->macroNode), 0, $token->line);
123: }
124: $this->htmlNode = $this->htmlNode->parentNode;
125: }
126:
127: $prologs = $epilogs = '';
128: foreach ($this->macroHandlers as $handler) {
129: $res = $handler->finalize();
130: $handlerName = get_class($handler);
131: $prologs .= empty($res[0]) ? '' : "<?php\n// prolog $handlerName\n$res[0]\n?>";
132: $epilogs = (empty($res[1]) ? '' : "<?php\n// epilog $handlerName\n$res[1]\n?>") . $epilogs;
133: }
134: $output = ($prologs ? $prologs . "<?php\n//\n// main template\n//\n?>\n" : '') . $output . $epilogs;
135:
136: if ($this->macroNode) {
137: throw new CompileException('Missing ' . self::printEndTag($this->macroNode), 0, $token->line);
138: }
139:
140: $output = $this->expandTokens($output);
141: return $output;
142: }
143:
144:
145: 146: 147:
148: public function setContentType($type)
149: {
150: $this->contentType = $type;
151: $this->context = NULL;
152: return $this;
153: }
154:
155:
156: 157: 158:
159: public function getContentType()
160: {
161: return $this->contentType;
162: }
163:
164:
165: 166: 167:
168: public function setContext($context, $sub = NULL)
169: {
170: $this->context = array($context, $sub);
171: return $this;
172: }
173:
174:
175: 176: 177:
178: public function getContext()
179: {
180: return $this->context;
181: }
182:
183:
184: 185: 186:
187: public function getTemplateId()
188: {
189: return $this->templateId;
190: }
191:
192:
193: 194: 195:
196: public function getMacroNode()
197: {
198: return $this->macroNode;
199: }
200:
201:
202: 203: 204: 205:
206: public function getLine()
207: {
208: return $this->tokens ? $this->tokens[$this->position]->line : NULL;
209: }
210:
211:
212: public function expandTokens($s)
213: {
214: return strtr($s, $this->attrCodes);
215: }
216:
217:
218: private function processText(Token $token)
219: {
220: if (($this->context[0] === self::CONTEXT_SINGLE_QUOTED_ATTR || $this->context[0] === self::CONTEXT_DOUBLE_QUOTED_ATTR)
221: && $token->text === $this->context[0]
222: ) {
223: $this->setContext(self::CONTEXT_UNQUOTED_ATTR);
224: }
225: $this->output .= $token->text;
226: }
227:
228:
229: private function processMacroTag(Token $token)
230: {
231: $isRightmost = !isset($this->tokens[$this->position + 1])
232: || substr($this->tokens[$this->position + 1]->text, 0, 1) === "\n";
233:
234: if ($token->name[0] === '/') {
235: $this->closeMacro((string) substr($token->name, 1), $token->value, $token->modifiers, $isRightmost);
236: } else {
237: $this->openMacro($token->name, $token->value, $token->modifiers, $isRightmost && !$token->empty);
238: if ($token->empty) {
239: $this->closeMacro($token->name, NULL, NULL, $isRightmost);
240: }
241: }
242: }
243:
244:
245: private function processHtmlTagBegin(Token $token)
246: {
247: if ($token->closing) {
248: while ($this->htmlNode) {
249: if (strcasecmp($this->htmlNode->name, $token->name) === 0) {
250: break;
251: }
252: if ($this->htmlNode->macroAttrs) {
253: throw new CompileException("Unexpected </$token->name>, expecting " . self::printEndTag($this->macroNode));
254: }
255: $this->htmlNode = $this->htmlNode->parentNode;
256: }
257: if (!$this->htmlNode) {
258: $this->htmlNode = new HtmlNode($token->name);
259: }
260: $this->htmlNode->closing = TRUE;
261: $this->htmlNode->offset = strlen($this->output);
262: $this->setContext(NULL);
263:
264: } elseif ($token->text === '<!--') {
265: $this->setContext(self::CONTEXT_COMMENT);
266:
267: } else {
268: $this->htmlNode = new HtmlNode($token->name, $this->htmlNode);
269: $this->htmlNode->isEmpty = in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML), TRUE)
270: && isset(Nette\Utils\Html::$emptyElements[strtolower($token->name)]);
271: $this->htmlNode->offset = strlen($this->output);
272: $this->setContext(self::CONTEXT_UNQUOTED_ATTR);
273: }
274: $this->output .= $token->text;
275: }
276:
277:
278: private function processHtmlTagEnd(Token $token)
279: {
280: if ($token->text === '-->') {
281: $this->output .= $token->text;
282: $this->setContext(NULL);
283: return;
284: }
285:
286: $htmlNode = $this->htmlNode;
287: $isEmpty = !$htmlNode->closing && (Strings::contains($token->text, '/') || $htmlNode->isEmpty);
288: $end = '';
289:
290: if ($isEmpty && in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML), TRUE)) {
291: $space = substr(strstr($token->text, '>'), 1);
292: $token->text = $htmlNode->isEmpty && $this->contentType === self::CONTENT_XHTML ? ' />' : '>';
293: if ($htmlNode->isEmpty) {
294: $token->text .= $space;
295: } else {
296: $end = "</$htmlNode->name>" . $space;
297: }
298: }
299:
300: if (empty($htmlNode->macroAttrs)) {
301: $this->output .= $token->text . $end;
302: } else {
303: $code = substr($this->output, $htmlNode->offset) . $token->text;
304: $this->output = substr($this->output, 0, $htmlNode->offset);
305: $this->writeAttrsMacro($code);
306: if ($isEmpty) {
307: $htmlNode->closing = TRUE;
308: $this->writeAttrsMacro($end);
309: }
310: }
311:
312: if ($isEmpty) {
313: $htmlNode->closing = TRUE;
314: }
315:
316: $lower = strtolower($htmlNode->name);
317: if (!$htmlNode->closing && ($lower === 'script' || $lower === 'style')) {
318: $this->setContext($lower === 'script' ? self::CONTENT_JS : self::CONTENT_CSS);
319: } else {
320: $this->setContext(NULL);
321: if ($htmlNode->closing) {
322: $this->htmlNode = $this->htmlNode->parentNode;
323: }
324: }
325: }
326:
327:
328: private function processHtmlAttribute(Token $token)
329: {
330: if (Strings::startsWith($token->name, Parser::N_PREFIX)) {
331: $name = substr($token->name, strlen(Parser::N_PREFIX));
332: if (isset($this->htmlNode->macroAttrs[$name])) {
333: throw new CompileException("Found multiple macro-attributes $token->name.");
334:
335: } elseif ($this->macroNode && $this->macroNode->htmlNode === $this->htmlNode) {
336: throw new CompileException("Macro-attributes must not appear inside macro; found $token->name inside {{$this->macroNode->name}}.");
337: }
338: $this->htmlNode->macroAttrs[$name] = $token->value;
339: return;
340: }
341:
342: $this->htmlNode->attrs[$token->name] = TRUE;
343: $this->output .= $token->text;
344:
345: $contextMain = in_array($token->value, array(self::CONTEXT_SINGLE_QUOTED_ATTR, self::CONTEXT_DOUBLE_QUOTED_ATTR), TRUE)
346: ? $token->value
347: : self::CONTEXT_UNQUOTED_ATTR;
348:
349: $context = NULL;
350: if (in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML), TRUE)) {
351: $lower = strtolower($token->name);
352: if (substr($lower, 0, 2) === 'on') {
353: $context = self::CONTENT_JS;
354: } elseif ($lower === 'style') {
355: $context = self::CONTENT_CSS;
356: } elseif (in_array($lower, array('href', 'src', 'action', 'formaction'), TRUE)
357: || ($lower === 'data' && strtolower($this->htmlNode->name) === 'object')
358: ) {
359: $context = self::CONTENT_URL;
360: }
361: }
362:
363: $this->setContext($contextMain, $context);
364: }
365:
366:
367: private function (Token $token)
368: {
369: $isLeftmost = trim(substr($this->output, strrpos("\n$this->output", "\n"))) === '';
370: if (!$isLeftmost) {
371: $this->output .= substr($token->text, strlen(rtrim($token->text, "\n")));
372: }
373: }
374:
375:
376:
377:
378:
379: 380: 381: 382: 383: 384: 385: 386:
387: public function openMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE, $nPrefix = NULL)
388: {
389: $node = $this->expandMacro($name, $args, $modifiers, $nPrefix);
390: if ($node->isEmpty) {
391: $this->writeCode($node->openingCode, $this->output, $isRightmost);
392: } else {
393: $this->macroNode = $node;
394: $node->saved = array(& $this->output, $isRightmost);
395: $this->output = & $node->content;
396: }
397: return $node;
398: }
399:
400:
401: 402: 403: 404: 405: 406: 407: 408:
409: public function closeMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE, $nPrefix = NULL)
410: {
411: $node = $this->macroNode;
412:
413: if (!$node || ($node->name !== $name && '' !== $name) || $modifiers
414: || ($args && $node->args && !Strings::startsWith("$node->args ", "$args "))
415: || $nPrefix !== $node->prefix
416: ) {
417: $name = $nPrefix
418: ? "</{$this->htmlNode->name}> for macro-attribute " . Parser::N_PREFIX . implode(' and ' . Parser::N_PREFIX, array_keys($this->htmlNode->macroAttrs))
419: : '{/' . $name . ($args ? ' ' . $args : '') . $modifiers . '}';
420: throw new CompileException("Unexpected $name" . ($node ? ', expecting ' . self::printEndTag($node) : ''));
421: }
422:
423: $this->macroNode = $node->parentNode;
424: if (!$node->args) {
425: $node->setArgs($args);
426: }
427:
428: $isLeftmost = $node->content ? trim(substr($this->output, strrpos("\n$this->output", "\n"))) === '' : FALSE;
429:
430: $node->closing = TRUE;
431: $node->macro->nodeClosed($node);
432:
433: $this->output = & $node->saved[0];
434: $this->writeCode($node->openingCode, $this->output, $node->saved[1]);
435: $this->writeCode($node->closingCode, $node->content, $isRightmost, $isLeftmost);
436: $this->output .= $node->content;
437: return $node;
438: }
439:
440:
441: private function writeCode($code, & $output, $isRightmost, $isLeftmost = NULL)
442: {
443: if ($isRightmost) {
444: $leftOfs = strrpos("\n$output", "\n");
445: $isLeftmost = $isLeftmost === NULL ? trim(substr($output, $leftOfs)) === '' : $isLeftmost;
446: if ($isLeftmost && substr($code, 0, 11) !== '<?php echo ') {
447: $output = substr($output, 0, $leftOfs);
448: } elseif (substr($code, -2) === '?>') {
449: $code .= "\n";
450: }
451: }
452: $output .= $code;
453: }
454:
455:
456: 457: 458: 459: 460:
461: public function writeAttrsMacro($code)
462: {
463: $attrs = $this->htmlNode->macroAttrs;
464: $left = $right = array();
465:
466: foreach ($this->macros as $name => $foo) {
467: $attrName = MacroNode::PREFIX_INNER . "-$name";
468: if (isset($attrs[$attrName])) {
469: if ($this->htmlNode->closing) {
470: $left[] = array('closeMacro', $name, '', MacroNode::PREFIX_INNER);
471: } else {
472: array_unshift($right, array('openMacro', $name, $attrs[$attrName], MacroNode::PREFIX_INNER));
473: }
474: unset($attrs[$attrName]);
475: }
476: }
477:
478: foreach (array_reverse($this->macros) as $name => $foo) {
479: $attrName = MacroNode::PREFIX_TAG . "-$name";
480: if (isset($attrs[$attrName])) {
481: $left[] = array('openMacro', $name, $attrs[$attrName], MacroNode::PREFIX_TAG);
482: array_unshift($right, array('closeMacro', $name, '', MacroNode::PREFIX_TAG));
483: unset($attrs[$attrName]);
484: }
485: }
486:
487: foreach ($this->macros as $name => $foo) {
488: if (isset($attrs[$name])) {
489: if ($this->htmlNode->closing) {
490: $right[] = array('closeMacro', $name, '', MacroNode::PREFIX_NONE);
491: } else {
492: array_unshift($left, array('openMacro', $name, $attrs[$name], MacroNode::PREFIX_NONE));
493: }
494: unset($attrs[$name]);
495: }
496: }
497:
498: if ($attrs) {
499: throw new CompileException('Unknown macro-attribute ' . Parser::N_PREFIX
500: . implode(' and ' . Parser::N_PREFIX, array_keys($attrs)));
501: }
502:
503: if (!$this->htmlNode->closing) {
504: $this->htmlNode->attrCode = & $this->attrCodes[$uniq = ' n:' . Nette\Utils\Strings::random()];
505: $code = substr_replace($code, $uniq, strrpos($code, '/>') ?: strrpos($code, '>'), 0);
506: }
507:
508: foreach ($left as $item) {
509: $node = $this->{$item[0]}($item[1], $item[2], NULL, NULL, $item[3]);
510: if ($node->closing || $node->isEmpty) {
511: $this->htmlNode->attrCode .= $node->attrCode;
512: if ($node->isEmpty) {
513: unset($this->htmlNode->macroAttrs[$node->name]);
514: }
515: }
516: }
517:
518: $this->output .= $code;
519:
520: foreach ($right as $item) {
521: $node = $this->{$item[0]}($item[1], $item[2], NULL, NULL, $item[3]);
522: if ($node->closing) {
523: $this->htmlNode->attrCode .= $node->attrCode;
524: }
525: }
526:
527: if ($right && substr($this->output, -2) === '?>') {
528: $this->output .= "\n";
529: }
530: }
531:
532:
533: 534: 535: 536: 537: 538: 539:
540: public function expandMacro($name, $args, $modifiers = NULL, $nPrefix = NULL)
541: {
542: $inScript = in_array($this->context[0], array(self::CONTENT_JS, self::CONTENT_CSS), TRUE);
543:
544: if (empty($this->macros[$name])) {
545: throw new CompileException("Unknown macro {{$name}}" . ($inScript ? ' (in JavaScript or CSS, try to put a space after bracket.)' : ''));
546: }
547:
548: if ($this->context[1] === self::CONTENT_URL) {
549: $modifiers = preg_replace('#\|nosafeurl\s?(?=\||\z)#i', '', $modifiers, -1, $found);
550: if (!$found && !preg_match('#\|datastream(?=\s|\||\z)#i', $modifiers)) {
551: $modifiers .= '|safeurl';
552: }
553: }
554:
555: $modifiers = preg_replace('#\|noescape\s?(?=\||\z)#i', '', $modifiers, -1, $found);
556: if (!$found && strpbrk($name, '=~%^&_')) {
557: $modifiers .= '|escape';
558: }
559:
560: if (!$found && $inScript && $name === '=' && preg_match('#["\'] *\z#', $this->tokens[$this->position - 1]->text)) {
561: throw new CompileException("Do not place {$this->tokens[$this->position]->text} inside quotes.");
562: }
563:
564: foreach (array_reverse($this->macros[$name]) as $macro) {
565: $node = new MacroNode($macro, $name, $args, $modifiers, $this->macroNode, $this->htmlNode, $nPrefix);
566: if ($macro->nodeOpened($node) !== FALSE) {
567: return $node;
568: }
569: }
570:
571: throw new CompileException($nPrefix ? 'Unknown macro-attribute ' . Parser::N_PREFIX . "$nPrefix-$name" : "Unhandled macro {{$name}}");
572: }
573:
574:
575: private static function printEndTag(MacroNode $node)
576: {
577: if ($node->prefix) {
578: return "</{$node->htmlNode->name}> for macro-attribute " . Parser::N_PREFIX
579: . implode(' and ' . Parser::N_PREFIX, array_keys($node->htmlNode->macroAttrs));
580: } else {
581: return "{/$node->name}";
582: }
583: }
584:
585: }
586: