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