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