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