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