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 CONTEXT_COMMENT = '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 = NStrings::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 === NLatteToken::TEXT) {
116: $this->output .= $token->text;
117:
118: } elseif ($token->type === NLatteToken::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 === NLatteToken::HTML_TAG_BEGIN) {
124: $this->processHtmlTagBegin($token);
125:
126: } elseif ($token->type === NLatteToken::HTML_TAG_END) {
127: $this->processHtmlTagEnd($token);
128:
129: } elseif ($token->type === NLatteToken::HTML_ATTRIBUTE) {
130: $this->processHtmlAttribute($token);
131: }
132: }
133: } catch (NCompileException $e) {
134: $e->sourceLine = $token->line;
135: throw $e;
136: }
137:
138:
139: foreach ($this->htmlNodes as $htmlNode) {
140: if (!empty($htmlNode->macroAttrs)) {
141: throw new NCompileException("Missing end tag </$htmlNode->name> for macro-attribute " . NParser::N_PREFIX
142: . implode(' and ' . NParser::N_PREFIX, array_keys($htmlNode->macroAttrs)) . ".", 0, $token->line);
143: }
144: }
145:
146: $prologs = $epilogs = '';
147: foreach ($this->macroHandlers as $handler) {
148: $res = $handler->finalize();
149: $handlerName = get_class($handler);
150: $prologs .= empty($res[0]) ? '' : "<?php\n// prolog $handlerName\n$res[0]\n?>";
151: $epilogs = (empty($res[1]) ? '' : "<?php\n// epilog $handlerName\n$res[1]\n?>") . $epilogs;
152: }
153: $output = ($prologs ? $prologs . "<?php\n//\n// main template\n//\n?>\n" : '') . $output . $epilogs;
154:
155: if ($this->macroNodes) {
156: throw new NCompileException("There are unclosed macros.", 0, $token->line);
157: }
158:
159: $output = $this->expandTokens($output);
160: return $output;
161: }
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:
180: public function getContentType()
181: {
182: return $this->contentType;
183: }
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:
201: public function getContext()
202: {
203: return $this->context;
204: }
205:
206:
207:
208: 209: 210:
211: public function getTemplateId()
212: {
213: return $this->templateId;
214: }
215:
216:
217:
218: 219: 220: 221:
222: public function getLine()
223: {
224: return $this->tokens ? $this->tokens[$this->position]->line : NULL;
225: }
226:
227:
228:
229: public function expandTokens($s)
230: {
231: return strtr($s, $this->attrCodes);
232: }
233:
234:
235:
236: private function processHtmlTagBegin($token)
237: {
238: if ($token->closing) {
239: do {
240: $htmlNode = array_pop($this->htmlNodes);
241: if (!$htmlNode) {
242: $htmlNode = new NHtmlNode($token->name);
243: }
244: } while (strcasecmp($htmlNode->name, $token->name));
245: $this->htmlNodes[] = $htmlNode;
246: $htmlNode->closing = TRUE;
247: $htmlNode->offset = strlen($this->output);
248: $this->setContext(NULL);
249:
250: } elseif ($token->text === '<!--') {
251: $this->setContext(self::CONTEXT_COMMENT);
252:
253: } else {
254: $this->htmlNodes[] = $htmlNode = new NHtmlNode($token->name);
255: $htmlNode->isEmpty = in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML))
256: && isset(NHtml::$emptyElements[strtolower($token->name)]);
257: $htmlNode->offset = strlen($this->output);
258: $this->setContext(NULL);
259: }
260: $this->output .= $token->text;
261: }
262:
263:
264:
265: private function processHtmlTagEnd($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 && (NStrings::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:
308: private function processHtmlAttribute($token)
309: {
310: $htmlNode = end($this->htmlNodes);
311: if (NStrings::startsWith($token->name, NParser::N_PREFIX)) {
312: $htmlNode->macroAttrs[substr($token->name, strlen(NParser::N_PREFIX))] = $token->value;
313: } else {
314: $htmlNode->attrs[$token->name] = TRUE;
315: $this->output .= $token->text;
316: if ($token->value) {
317: $context = NULL;
318: if (strncasecmp($token->name, 'on', 2) === 0) {
319: $context = self::CONTENT_JS;
320: } elseif ($token->name === 'style') {
321: $context = self::CONTENT_CSS;
322: }
323: $this->setContext($token->value, $context);
324: }
325: }
326: }
327:
328:
329:
330:
331:
332:
333:
334: 335: 336: 337: 338: 339: 340: 341:
342: public function writeMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE, NHtmlNode $htmlNode = NULL, $prefix = NULL)
343: {
344: if ($name[0] === '/') {
345: $node = end($this->macroNodes);
346:
347: if (!$node || ("/$node->name" !== $name && '/' !== $name) || $modifiers
348: || ($args && $node->args && !NStrings::startsWith("$node->args ", "$args "))
349: ) {
350: $name .= $args ? ' ' : '';
351: throw new NCompileException("Unexpected macro {{$name}{$args}{$modifiers}}"
352: . ($node ? ", expecting {/$node->name}" . ($args && $node->args ? " or eventually {/$node->name $node->args}" : '') : ''));
353: }
354:
355: array_pop($this->macroNodes);
356: if (!$node->args) {
357: $node->setArgs($args);
358: }
359:
360: $isLeftmost = $node->content ? trim(substr($this->output, strrpos("\n$this->output", "\n"))) === '' : FALSE;
361:
362: $node->closing = TRUE;
363: $node->macro->nodeClosed($node);
364:
365: $this->output = & $node->saved[0];
366: $this->writeCode($node->openingCode, $this->output, $node->saved[1]);
367: $this->writeCode($node->closingCode, $node->content, $isRightmost, $isLeftmost);
368: $this->output .= $node->content;
369:
370: } else {
371: $node = $this->expandMacro($name, $args, $modifiers, $htmlNode, $prefix);
372: if ($node->isEmpty) {
373: $this->writeCode($node->openingCode, $this->output, $isRightmost);
374:
375: } else {
376: $this->macroNodes[] = $node;
377: $node->saved = array(& $this->output, $isRightmost);
378: $this->output = & $node->content;
379: }
380: }
381: return $node;
382: }
383:
384:
385:
386: private function writeCode($code, & $output, $isRightmost, $isLeftmost = NULL)
387: {
388: if ($isRightmost) {
389: $leftOfs = strrpos("\n$output", "\n");
390: $isLeftmost = $isLeftmost === NULL ? trim(substr($output, $leftOfs)) === '' : $isLeftmost;
391: if ($isLeftmost && substr($code, 0, 11) !== '<?php echo ') {
392: $output = substr($output, 0, $leftOfs);
393: } elseif (substr($code, -2) === '?>') {
394: $code .= "\n";
395: }
396: }
397: $output .= $code;
398: }
399:
400:
401:
402: 403: 404: 405: 406:
407: public function writeAttrsMacro($code, NHtmlNode $htmlNode)
408: {
409: $attrs = $htmlNode->macroAttrs;
410: $left = $right = array();
411: $attrCode = '';
412:
413: foreach ($this->macros as $name => $foo) {
414: $attrName = NMacroNode::PREFIX_INNER . "-$name";
415: if (isset($attrs[$attrName])) {
416: if ($htmlNode->closing) {
417: $left[] = array("/$name", '', NMacroNode::PREFIX_INNER);
418: } else {
419: array_unshift($right, array($name, $attrs[$attrName], NMacroNode::PREFIX_INNER));
420: }
421: unset($attrs[$attrName]);
422: }
423: }
424:
425: foreach (array_reverse($this->macros) as $name => $foo) {
426: $attrName = NMacroNode::PREFIX_TAG . "-$name";
427: if (isset($attrs[$attrName])) {
428: $left[] = array($name, $attrs[$attrName], NMacroNode::PREFIX_TAG);
429: array_unshift($right, array("/$name", '', NMacroNode::PREFIX_TAG));
430: unset($attrs[$attrName]);
431: }
432: }
433:
434: foreach ($this->macros as $name => $foo) {
435: if (isset($attrs[$name])) {
436: if ($htmlNode->closing) {
437: $right[] = array("/$name", '', NULL);
438: } else {
439: array_unshift($left, array($name, $attrs[$name], NULL));
440: }
441: unset($attrs[$name]);
442: }
443: }
444:
445: if ($attrs) {
446: throw new NCompileException("Unknown macro-attribute " . NParser::N_PREFIX
447: . implode(' and ' . NParser::N_PREFIX, array_keys($attrs)));
448: }
449:
450: if (!$htmlNode->closing) {
451: $htmlNode->attrCode = & $this->attrCodes[$uniq = ' n:' . NStrings::random()];
452: $code = substr_replace($code, $uniq, ($tmp=strrpos($code, '/>')) ? $tmp : strrpos($code, '>'), 0);
453: }
454:
455: foreach ($left as $item) {
456: $node = $this->writeMacro($item[0], $item[1], NULL, NULL, $htmlNode, $item[2]);
457: if ($node->closing || $node->isEmpty) {
458: $htmlNode->attrCode .= $node->attrCode;
459: if ($node->isEmpty) {
460: unset($htmlNode->macroAttrs[$node->name]);
461: }
462: }
463: }
464:
465: $this->output .= $code;
466:
467: foreach ($right as $item) {
468: $node = $this->writeMacro($item[0], $item[1], NULL, NULL, $htmlNode);
469: if ($node->closing) {
470: $htmlNode->attrCode .= $node->attrCode;
471: }
472: }
473:
474: if ($right && substr($this->output, -2) === '?>') {
475: $this->output .= "\n";
476: }
477: }
478:
479:
480:
481: 482: 483: 484: 485: 486: 487:
488: public function expandMacro($name, $args, $modifiers = NULL, NHtmlNode $htmlNode = NULL, $prefix = NULL)
489: {
490: if (empty($this->macros[$name])) {
491: $cdata = $this->htmlNodes && in_array(strtolower(end($this->htmlNodes)->name), array('script', 'style'));
492: throw new NCompileException("Unknown macro {{$name}}" . ($cdata ? " (in JavaScript or CSS, try to put a space after bracket.)" : ''));
493: }
494: foreach (array_reverse($this->macros[$name]) as $macro) {
495: $node = new NMacroNode($macro, $name, $args, $modifiers, $this->macroNodes ? end($this->macroNodes) : NULL, $htmlNode, $prefix);
496: if ($macro->nodeOpened($node) !== FALSE) {
497: return $node;
498: }
499: }
500: throw new NCompileException("Unhandled macro {{$name}}");
501: }
502:
503: }
504: