1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13:
14:
15: 16: 17: 18: 19: 20:
21: class LatteCompiler extends Object
22: {
23:
24: public $defaultContentType = self::CONTENT_XHTML;
25:
26:
27: private $tokens;
28:
29:
30: private $output;
31:
32:
33: private $position;
34:
35:
36: private $macros;
37:
38:
39: private $macroHandlers;
40:
41:
42: private $htmlNodes = array();
43:
44:
45: private $macroNodes = array();
46:
47:
48: private $attrCodes = 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:
69: const = 'comment',
70: CONTEXT_SINGLE_QUOTED = "'",
71: CONTEXT_DOUBLE_QUOTED = '"';
72:
73:
74: public function __construct()
75: {
76: $this->macroHandlers = new SplObjectStorage;
77: }
78:
79:
80:
81: 82: 83: 84: 85:
86: public function addMacro($name, IMacro $macro)
87: {
88: $this->macros[$name][] = $macro;
89: $this->macroHandlers->attach($macro);
90: return $this;
91: }
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 === LatteToken::TEXT) {
116: $this->output .= $token->text;
117:
118: } elseif ($token->type === LatteToken::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 === LatteToken::HTML_TAG_BEGIN) {
124: $this->processHtmlTagBegin($token);
125:
126: } elseif ($token->type === LatteToken::HTML_TAG_END) {
127: $this->processHtmlTagEnd($token);
128:
129: } elseif ($token->type === LatteToken::HTML_ATTRIBUTE) {
130: $this->processHtmlAttribute($token);
131:
132: } elseif ($token->type === LatteToken::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:
171: public function setContentType($type)
172: {
173: $this->contentType = $type;
174: $this->context = NULL;
175: return $this;
176: }
177:
178:
179:
180: 181: 182:
183: public function getContentType()
184: {
185: return $this->contentType;
186: }
187:
188:
189:
190: 191: 192:
193: public function setContext($context, $sub = NULL)
194: {
195: $this->context = array($context, $sub);
196: return $this;
197: }
198:
199:
200:
201: 202: 203:
204: public function getContext()
205: {
206: return $this->context;
207: }
208:
209:
210:
211: 212: 213:
214: public function getTemplateId()
215: {
216: return $this->templateId;
217: }
218:
219:
220:
221: 222: 223: 224:
225: public function getLine()
226: {
227: return $this->tokens ? $this->tokens[$this->position]->line : NULL;
228: }
229:
230:
231:
232: public function expandTokens($s)
233: {
234: return strtr($s, $this->attrCodes);
235: }
236:
237:
238:
239: private function processHtmlTagBegin(LatteToken $token)
240: {
241: if ($token->closing) {
242: do {
243: $htmlNode = array_pop($this->htmlNodes);
244: if (!$htmlNode) {
245: $htmlNode = new HtmlNode($token->name);
246: }
247: if (strcasecmp($htmlNode->name, $token->name) === 0) {
248: break;
249: }
250: if ($htmlNode->macroAttrs) {
251: throw new CompileException("Unexpected </$token->name>.", 0, $token->line);
252: }
253: } while (TRUE);
254: $this->htmlNodes[] = $htmlNode;
255: $htmlNode->closing = TRUE;
256: $htmlNode->offset = strlen($this->output);
257: $this->setContext(NULL);
258:
259: } elseif ($token->text === '<!--') {
260: $this->setContext(self::CONTEXT_COMMENT);
261:
262: } else {
263: $this->htmlNodes[] = $htmlNode = new HtmlNode($token->name);
264: $htmlNode->isEmpty = in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML))
265: && isset(Html::$emptyElements[strtolower($token->name)]);
266: $htmlNode->offset = strlen($this->output);
267: $this->setContext(NULL);
268: }
269: $this->output .= $token->text;
270: }
271:
272:
273:
274: private function processHtmlTagEnd(LatteToken $token)
275: {
276: if ($token->text === '-->') {
277: $this->output .= $token->text;
278: $this->setContext(NULL);
279: return;
280: }
281:
282: $htmlNode = end($this->htmlNodes);
283: $isEmpty = !$htmlNode->closing && (Strings::contains($token->text, '/') || $htmlNode->isEmpty);
284:
285: if ($isEmpty && in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML))) {
286: $token->text = preg_replace('#^.*>#', $this->contentType === self::CONTENT_XHTML ? ' />' : '>', $token->text);
287: }
288:
289: if (empty($htmlNode->macroAttrs)) {
290: $this->output .= $token->text;
291: } else {
292: $code = substr($this->output, $htmlNode->offset) . $token->text;
293: $this->output = substr($this->output, 0, $htmlNode->offset);
294: $this->writeAttrsMacro($code, $htmlNode);
295: if ($isEmpty) {
296: $htmlNode->closing = TRUE;
297: $this->writeAttrsMacro('', $htmlNode);
298: }
299: }
300:
301: if ($isEmpty) {
302: $htmlNode->closing = TRUE;
303: }
304:
305: if (!$htmlNode->closing && (strcasecmp($htmlNode->name, 'script') === 0 || strcasecmp($htmlNode->name, 'style') === 0)) {
306: $this->setContext(strcasecmp($htmlNode->name, 'style') ? self::CONTENT_JS : self::CONTENT_CSS);
307: } else {
308: $this->setContext(NULL);
309: if ($htmlNode->closing) {
310: array_pop($this->htmlNodes);
311: }
312: }
313: }
314:
315:
316:
317: private function processHtmlAttribute(LatteToken $token)
318: {
319: $htmlNode = end($this->htmlNodes);
320: if (Strings::startsWith($token->name, Parser::N_PREFIX)) {
321: $name = substr($token->name, strlen(Parser::N_PREFIX));
322: if (isset($htmlNode->macroAttrs[$name])) {
323: throw new CompileException("Found multiple macro-attributes $token->name.", 0, $token->line);
324: }
325: $htmlNode->macroAttrs[$name] = $token->value;
326:
327: } else {
328: $htmlNode->attrs[$token->name] = TRUE;
329: $this->output .= $token->text;
330: if ($token->value) {
331: $context = NULL;
332: if (strncasecmp($token->name, 'on', 2) === 0) {
333: $context = self::CONTENT_JS;
334: } elseif ($token->name === 'style') {
335: $context = self::CONTENT_CSS;
336: }
337: $this->setContext($token->value, $context);
338: }
339: }
340: }
341:
342:
343:
344: private function (LatteToken $token)
345: {
346: $isLeftmost = trim(substr($this->output, strrpos("\n$this->output", "\n"))) === '';
347: if (!$isLeftmost) {
348: $this->output .= substr($token->text, strlen(rtrim($token->text, "\n")));
349: }
350: }
351:
352:
353:
354:
355:
356:
357:
358: 359: 360: 361: 362: 363: 364: 365:
366: public function writeMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE, HtmlNode $htmlNode = NULL, $prefix = NULL)
367: {
368: if ($name[0] === '/') {
369: $node = end($this->macroNodes);
370:
371: if (!$node || ("/$node->name" !== $name && '/' !== $name) || $modifiers
372: || ($args && $node->args && !Strings::startsWith("$node->args ", "$args "))
373: ) {
374: $name .= $args ? ' ' : '';
375: throw new CompileException("Unexpected macro {{$name}{$args}{$modifiers}}"
376: . ($node ? ", expecting {/$node->name}" . ($args && $node->args ? " or eventually {/$node->name $node->args}" : '') : ''));
377: }
378:
379: array_pop($this->macroNodes);
380: if (!$node->args) {
381: $node->setArgs($args);
382: }
383:
384: $isLeftmost = $node->content ? trim(substr($this->output, strrpos("\n$this->output", "\n"))) === '' : FALSE;
385:
386: $node->closing = TRUE;
387: $node->macro->nodeClosed($node);
388:
389: $this->output = & $node->saved[0];
390: $this->writeCode($node->openingCode, $this->output, $node->saved[1]);
391: $this->writeCode($node->closingCode, $node->content, $isRightmost, $isLeftmost);
392: $this->output .= $node->content;
393:
394: } else {
395: $node = $this->expandMacro($name, $args, $modifiers, $htmlNode, $prefix);
396: if ($node->isEmpty) {
397: $this->writeCode($node->openingCode, $this->output, $isRightmost);
398:
399: } else {
400: $this->macroNodes[] = $node;
401: $node->saved = array(& $this->output, $isRightmost);
402: $this->output = & $node->content;
403: }
404: }
405: return $node;
406: }
407:
408:
409:
410: private function writeCode($code, & $output, $isRightmost, $isLeftmost = NULL)
411: {
412: if ($isRightmost) {
413: $leftOfs = strrpos("\n$output", "\n");
414: $isLeftmost = $isLeftmost === NULL ? trim(substr($output, $leftOfs)) === '' : $isLeftmost;
415: if ($isLeftmost && substr($code, 0, 11) !== '<?php echo ') {
416: $output = substr($output, 0, $leftOfs);
417: } elseif (substr($code, -2) === '?>') {
418: $code .= "\n";
419: }
420: }
421: $output .= $code;
422: }
423:
424:
425:
426: 427: 428: 429: 430:
431: public function writeAttrsMacro($code, HtmlNode $htmlNode)
432: {
433: $attrs = $htmlNode->macroAttrs;
434: $left = $right = array();
435: $attrCode = '';
436:
437: foreach ($this->macros as $name => $foo) {
438: $attrName = MacroNode::PREFIX_INNER . "-$name";
439: if (isset($attrs[$attrName])) {
440: if ($htmlNode->closing) {
441: $left[] = array("/$name", '', MacroNode::PREFIX_INNER);
442: } else {
443: array_unshift($right, array($name, $attrs[$attrName], MacroNode::PREFIX_INNER));
444: }
445: unset($attrs[$attrName]);
446: }
447: }
448:
449: foreach (array_reverse($this->macros) as $name => $foo) {
450: $attrName = MacroNode::PREFIX_TAG . "-$name";
451: if (isset($attrs[$attrName])) {
452: $left[] = array($name, $attrs[$attrName], MacroNode::PREFIX_TAG);
453: array_unshift($right, array("/$name", '', MacroNode::PREFIX_TAG));
454: unset($attrs[$attrName]);
455: }
456: }
457:
458: foreach ($this->macros as $name => $foo) {
459: if (isset($attrs[$name])) {
460: if ($htmlNode->closing) {
461: $right[] = array("/$name", '', NULL);
462: } else {
463: array_unshift($left, array($name, $attrs[$name], NULL));
464: }
465: unset($attrs[$name]);
466: }
467: }
468:
469: if ($attrs) {
470: throw new CompileException("Unknown macro-attribute " . Parser::N_PREFIX
471: . implode(' and ' . Parser::N_PREFIX, array_keys($attrs)));
472: }
473:
474: if (!$htmlNode->closing) {
475: $htmlNode->attrCode = & $this->attrCodes[$uniq = ' n:' . Strings::random()];
476: $code = substr_replace($code, $uniq, ($tmp=strrpos($code, '/>')) ? $tmp : strrpos($code, '>'), 0);
477: }
478:
479: foreach ($left as $item) {
480: $node = $this->writeMacro($item[0], $item[1], NULL, NULL, $htmlNode, $item[2]);
481: if ($node->closing || $node->isEmpty) {
482: $htmlNode->attrCode .= $node->attrCode;
483: if ($node->isEmpty) {
484: unset($htmlNode->macroAttrs[$node->name]);
485: }
486: }
487: }
488:
489: $this->output .= $code;
490:
491: foreach ($right as $item) {
492: $node = $this->writeMacro($item[0], $item[1], NULL, NULL, $htmlNode);
493: if ($node->closing) {
494: $htmlNode->attrCode .= $node->attrCode;
495: }
496: }
497:
498: if ($right && substr($this->output, -2) === '?>') {
499: $this->output .= "\n";
500: }
501: }
502:
503:
504:
505: 506: 507: 508: 509: 510: 511:
512: public function expandMacro($name, $args, $modifiers = NULL, HtmlNode $htmlNode = NULL, $prefix = NULL)
513: {
514: if (empty($this->macros[$name])) {
515: $cdata = $this->htmlNodes && in_array(strtolower(end($this->htmlNodes)->name), array('script', 'style'));
516: throw new CompileException("Unknown macro {{$name}}" . ($cdata ? " (in JavaScript or CSS, try to put a space after bracket.)" : ''));
517: }
518: foreach (array_reverse($this->macros[$name]) as $macro) {
519: $node = new MacroNode($macro, $name, $args, $modifiers, $this->macroNodes ? end($this->macroNodes) : NULL, $htmlNode, $prefix);
520: if ($macro->nodeOpened($node) !== FALSE) {
521: return $node;
522: }
523: }
524: throw new CompileException("Unhandled macro {{$name}}");
525: }
526:
527: }
528: