1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13:
14:
15: 16: 17: 18: 19: 20:
21: class NLatteCompiler extends NObject
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: public function addMacro($name, IMacro $macro)
86: {
87: $this->macros[$name][] = $macro;
88: $this->macroHandlers->attach($macro);
89: return $this;
90: }
91:
92:
93: 94: 95: 96: 97:
98: public function compile(array $tokens)
99: {
100: $this->templateId = NStrings::random();
101: $this->tokens = $tokens;
102: $output = '';
103: $this->output = & $output;
104: $this->htmlNodes = $this->macroNodes = array();
105: $this->setContentType($this->defaultContentType);
106:
107: foreach ($this->macroHandlers as $handler) {
108: $handler->initialize($this);
109: }
110:
111: try {
112: foreach ($tokens as $this->position => $token) {
113: if ($token->type === NLatteToken::TEXT) {
114: $this->output .= $token->text;
115:
116: } elseif ($token->type === NLatteToken::MACRO_TAG) {
117: $isRightmost = !isset($tokens[$this->position + 1])
118: || substr($tokens[$this->position + 1]->text, 0, 1) === "\n";
119: $this->writeMacro($token->name, $token->value, $token->modifiers, $isRightmost);
120:
121: } elseif ($token->type === NLatteToken::HTML_TAG_BEGIN) {
122: $this->processHtmlTagBegin($token);
123:
124: } elseif ($token->type === NLatteToken::HTML_TAG_END) {
125: $this->processHtmlTagEnd($token);
126:
127: } elseif ($token->type === NLatteToken::HTML_ATTRIBUTE) {
128: $this->processHtmlAttribute($token);
129:
130: } elseif ($token->type === NLatteToken::COMMENT) {
131: $this->processComment($token);
132: }
133: }
134: } catch (NCompileException $e) {
135: $e->sourceLine = $token->line;
136: throw $e;
137: }
138:
139:
140: foreach ($this->htmlNodes as $htmlNode) {
141: if (!empty($htmlNode->macroAttrs)) {
142: throw new NCompileException("Missing end tag </$htmlNode->name> for macro-attribute " . NParser::N_PREFIX
143: . implode(' and ' . NParser::N_PREFIX, array_keys($htmlNode->macroAttrs)) . ".", 0, $token->line);
144: }
145: }
146:
147: $prologs = $epilogs = '';
148: foreach ($this->macroHandlers as $handler) {
149: $res = $handler->finalize();
150: $handlerName = get_class($handler);
151: $prologs .= empty($res[0]) ? '' : "<?php\n// prolog $handlerName\n$res[0]\n?>";
152: $epilogs = (empty($res[1]) ? '' : "<?php\n// epilog $handlerName\n$res[1]\n?>") . $epilogs;
153: }
154: $output = ($prologs ? $prologs . "<?php\n//\n// main template\n//\n?>\n" : '') . $output . $epilogs;
155:
156: if ($this->macroNodes) {
157: throw new NCompileException("There are unclosed macros.", 0, $token->line);
158: }
159:
160: $output = $this->expandTokens($output);
161: return $output;
162: }
163:
164:
165: 166: 167:
168: public function setContentType($type)
169: {
170: $this->contentType = $type;
171: $this->context = NULL;
172: return $this;
173: }
174:
175:
176: 177: 178:
179: public function getContentType()
180: {
181: return $this->contentType;
182: }
183:
184:
185: 186: 187:
188: public function setContext($context, $sub = NULL)
189: {
190: $this->context = array($context, $sub);
191: return $this;
192: }
193:
194:
195: 196: 197:
198: public function getContext()
199: {
200: return $this->context;
201: }
202:
203:
204: 205: 206:
207: public function getTemplateId()
208: {
209: return $this->templateId;
210: }
211:
212:
213: 214: 215: 216:
217: public function getLine()
218: {
219: return $this->tokens ? $this->tokens[$this->position]->line : NULL;
220: }
221:
222:
223: public function expandTokens($s)
224: {
225: return strtr($s, $this->attrCodes);
226: }
227:
228:
229: private function processHtmlTagBegin(NLatteToken $token)
230: {
231: if ($token->closing) {
232: do {
233: $htmlNode = array_pop($this->htmlNodes);
234: if (!$htmlNode) {
235: $htmlNode = new NHtmlNode($token->name);
236: }
237: if (strcasecmp($htmlNode->name, $token->name) === 0) {
238: break;
239: }
240: if ($htmlNode->macroAttrs) {
241: throw new NCompileException("Unexpected </$token->name>.", 0, $token->line);
242: }
243: } while (TRUE);
244: $this->htmlNodes[] = $htmlNode;
245: $htmlNode->closing = TRUE;
246: $htmlNode->offset = strlen($this->output);
247: $this->setContext(NULL);
248:
249: } elseif ($token->text === '<!--') {
250: $this->setContext(self::CONTEXT_COMMENT);
251:
252: } else {
253: $this->htmlNodes[] = $htmlNode = new NHtmlNode($token->name);
254: $htmlNode->isEmpty = in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML))
255: && isset(NHtml::$emptyElements[strtolower($token->name)]);
256: $htmlNode->offset = strlen($this->output);
257: $this->setContext(NULL);
258: }
259: $this->output .= $token->text;
260: }
261:
262:
263: private function processHtmlTagEnd(NLatteToken $token)
264: {
265: if ($token->text === '-->') {
266: $this->output .= $token->text;
267: $this->setContext(NULL);
268: return;
269: }
270:
271: $htmlNode = end($this->htmlNodes);
272: $isEmpty = !$htmlNode->closing && (NStrings::contains($token->text, '/') || $htmlNode->isEmpty);
273:
274: if ($isEmpty && in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML))) {
275: $token->text = preg_replace('#^.*>#', $this->contentType === self::CONTENT_XHTML ? ' />' : '>', $token->text);
276: }
277:
278: if (empty($htmlNode->macroAttrs)) {
279: $this->output .= $token->text;
280: } else {
281: $code = substr($this->output, $htmlNode->offset) . $token->text;
282: $this->output = substr($this->output, 0, $htmlNode->offset);
283: $this->writeAttrsMacro($code, $htmlNode);
284: if ($isEmpty) {
285: $htmlNode->closing = TRUE;
286: $this->writeAttrsMacro('', $htmlNode);
287: }
288: }
289:
290: if ($isEmpty) {
291: $htmlNode->closing = TRUE;
292: }
293:
294: if (!$htmlNode->closing && (strcasecmp($htmlNode->name, 'script') === 0 || strcasecmp($htmlNode->name, 'style') === 0)) {
295: $this->setContext(strcasecmp($htmlNode->name, 'style') ? self::CONTENT_JS : self::CONTENT_CSS);
296: } else {
297: $this->setContext(NULL);
298: if ($htmlNode->closing) {
299: array_pop($this->htmlNodes);
300: }
301: }
302: }
303:
304:
305: private function processHtmlAttribute(NLatteToken $token)
306: {
307: $htmlNode = end($this->htmlNodes);
308: if (NStrings::startsWith($token->name, NParser::N_PREFIX)) {
309: $name = substr($token->name, strlen(NParser::N_PREFIX));
310: if (isset($htmlNode->macroAttrs[$name])) {
311: throw new NCompileException("Found multiple macro-attributes $token->name.", 0, $token->line);
312: }
313: $htmlNode->macroAttrs[$name] = $token->value;
314:
315: } else {
316: $htmlNode->attrs[$token->name] = TRUE;
317: $this->output .= $token->text;
318: if ($token->value) {
319: $context = NULL;
320: if (strncasecmp($token->name, 'on', 2) === 0) {
321: $context = self::CONTENT_JS;
322: } elseif ($token->name === 'style') {
323: $context = self::CONTENT_CSS;
324: }
325: $this->setContext($token->value, $context);
326: }
327: }
328: }
329:
330:
331: private function (NLatteToken $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, NHtmlNode $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 && !NStrings::startsWith("$node->args ", "$args "))
358: ) {
359: $name .= $args ? ' ' : '';
360: throw new NCompileException("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, NHtmlNode $htmlNode)
415: {
416: $attrs = $htmlNode->macroAttrs;
417: $left = $right = array();
418: $attrCode = '';
419:
420: foreach ($this->macros as $name => $foo) {
421: $attrName = NMacroNode::PREFIX_INNER . "-$name";
422: if (isset($attrs[$attrName])) {
423: if ($htmlNode->closing) {
424: $left[] = array("/$name", '', NMacroNode::PREFIX_INNER);
425: } else {
426: array_unshift($right, array($name, $attrs[$attrName], NMacroNode::PREFIX_INNER));
427: }
428: unset($attrs[$attrName]);
429: }
430: }
431:
432: foreach (array_reverse($this->macros) as $name => $foo) {
433: $attrName = NMacroNode::PREFIX_TAG . "-$name";
434: if (isset($attrs[$attrName])) {
435: $left[] = array($name, $attrs[$attrName], NMacroNode::PREFIX_TAG);
436: array_unshift($right, array("/$name", '', NMacroNode::PREFIX_TAG));
437: unset($attrs[$attrName]);
438: }
439: }
440:
441: foreach ($this->macros as $name => $foo) {
442: if (isset($attrs[$name])) {
443: if ($htmlNode->closing) {
444: $right[] = array("/$name", '', NULL);
445: } else {
446: array_unshift($left, array($name, $attrs[$name], NULL));
447: }
448: unset($attrs[$name]);
449: }
450: }
451:
452: if ($attrs) {
453: throw new NCompileException("Unknown macro-attribute " . NParser::N_PREFIX
454: . implode(' and ' . NParser::N_PREFIX, array_keys($attrs)));
455: }
456:
457: if (!$htmlNode->closing) {
458: $htmlNode->attrCode = & $this->attrCodes[$uniq = ' n:' . NStrings::random()];
459: $code = substr_replace($code, $uniq, ($tmp=strrpos($code, '/>')) ? $tmp : strrpos($code, '>'), 0);
460: }
461:
462: foreach ($left as $item) {
463: $node = $this->writeMacro($item[0], $item[1], NULL, NULL, $htmlNode, $item[2]);
464: if ($node->closing || $node->isEmpty) {
465: $htmlNode->attrCode .= $node->attrCode;
466: if ($node->isEmpty) {
467: unset($htmlNode->macroAttrs[$node->name]);
468: }
469: }
470: }
471:
472: $this->output .= $code;
473:
474: foreach ($right as $item) {
475: $node = $this->writeMacro($item[0], $item[1], NULL, NULL, $htmlNode);
476: if ($node->closing) {
477: $htmlNode->attrCode .= $node->attrCode;
478: }
479: }
480:
481: if ($right && substr($this->output, -2) === '?>') {
482: $this->output .= "\n";
483: }
484: }
485:
486:
487: 488: 489: 490: 491: 492: 493:
494: public function expandMacro($name, $args, $modifiers = NULL, NHtmlNode $htmlNode = NULL, $prefix = NULL)
495: {
496: if (empty($this->macros[$name])) {
497: $cdata = $this->htmlNodes && in_array(strtolower(end($this->htmlNodes)->name), array('script', 'style'));
498: throw new NCompileException("Unknown macro {{$name}}" . ($cdata ? " (in JavaScript or CSS, try to put a space after bracket.)" : ''));
499: }
500: foreach (array_reverse($this->macros[$name]) as $macro) {
501: $node = new NMacroNode($macro, $name, $args, $modifiers, $this->macroNodes ? end($this->macroNodes) : NULL, $htmlNode, $prefix);
502: if ($macro->nodeOpened($node) !== FALSE) {
503: return $node;
504: }
505: }
506: throw new NCompileException("Unhandled macro {{$name}}");
507: }
508:
509: }
510: