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 $contentType;
49:
50:
51: private $context;
52:
53:
54: private $templateId;
55:
56:
57: const CONTENT_HTML = 'html',
58: CONTENT_XHTML = 'xhtml',
59: CONTENT_XML = 'xml',
60: CONTENT_JS = 'js',
61: CONTENT_CSS = 'css',
62: CONTENT_ICAL = 'ical',
63: CONTENT_TEXT = 'text',
64:
65: CONTEXT_COMMENT = '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:
82: public function addMacro($name, IMacro $macro)
83: {
84: $this->macros[$name][] = $macro;
85: $this->macroHandlers->attach($macro);
86: return $this;
87: }
88:
89:
90:
91: 92: 93: 94: 95:
96: public function compile(array $tokens)
97: {
98: $this->templateId = NStrings::random();
99: $this->tokens = $tokens;
100: $this->output = '';
101: $this->htmlNodes = $this->macroNodes = array();
102: $this->setContentType($this->defaultContentType);
103:
104: foreach ($this->macroHandlers as $handler) {
105: $handler->initialize($this);
106: }
107:
108: try {
109: foreach ($tokens as $this->position => $token) {
110: if ($token->type === NLatteToken::TEXT) {
111: $this->output .= $token->text;
112:
113: } elseif ($token->type === NLatteToken::MACRO) {
114: $isRightmost = !isset($tokens[$this->position + 1])
115: || substr($tokens[$this->position + 1]->text, 0, 1) === "\n";
116: $this->writeMacro($token->name, $token->value, $token->modifiers, $isRightmost);
117:
118: } elseif ($token->type === NLatteToken::TAG_BEGIN) {
119: $this->processTagBegin($token);
120:
121: } elseif ($token->type === NLatteToken::TAG_END) {
122: $this->processTagEnd($token);
123:
124: } elseif ($token->type === NLatteToken::ATTRIBUTE) {
125: $this->processAttribute($token);
126: }
127: }
128: } catch (NLatteException $e) {
129: $e->sourceLine = $token->line;
130: throw $e;
131: }
132:
133:
134: foreach ($this->htmlNodes as $node) {
135: if (!empty($node->macroAttrs)) {
136: throw new NLatteException("Missing end tag </$node->name> for macro-attribute " . NParser::N_PREFIX
137: . implode(' and ' . NParser::N_PREFIX, array_keys($node->macroAttrs)) . ".", 0, $token->line);
138: }
139: }
140:
141: $prologs = $epilogs = '';
142: foreach ($this->macroHandlers as $handler) {
143: $res = $handler->finalize();
144: $handlerName = get_class($handler);
145: $prologs .= empty($res[0]) ? '' : "<?php\n// prolog $handlerName\n$res[0]\n?>";
146: $epilogs = (empty($res[1]) ? '' : "<?php\n// epilog $handlerName\n$res[1]\n?>") . $epilogs;
147: }
148: $this->output = ($prologs ? $prologs . "<?php\n//\n// main template\n//\n?>\n" : '') . $this->output . $epilogs;
149:
150: if ($this->macroNodes) {
151: throw new NLatteException("There are unclosed macros.", 0, $token->line);
152: }
153:
154: return $this->output;
155: }
156:
157:
158:
159: 160: 161:
162: public function setContentType($type)
163: {
164: $this->contentType = $type;
165: $this->context = NULL;
166: return $this;
167: }
168:
169:
170:
171: 172: 173:
174: public function getContentType()
175: {
176: return $this->contentType;
177: }
178:
179:
180:
181: 182: 183:
184: private function setContext($context, $sub = NULL)
185: {
186: $this->context = array($context, $sub);
187: return $this;
188: }
189:
190:
191:
192: 193: 194:
195: public function getContext()
196: {
197: return $this->context;
198: }
199:
200:
201:
202: 203: 204:
205: public function getTemplateId()
206: {
207: return $this->templateId;
208: }
209:
210:
211:
212: 213: 214: 215:
216: public function getLine()
217: {
218: return $this->tokens ? $this->tokens[$this->position]->line : NULL;
219: }
220:
221:
222:
223: private function processTagBegin($token)
224: {
225: if ($token->closing) {
226: do {
227: $node = array_pop($this->htmlNodes);
228: if (!$node) {
229: $node = new NHtmlNode($token->name);
230: }
231: } while (strcasecmp($node->name, $token->name));
232: $this->htmlNodes[] = $node;
233: $node->closing = TRUE;
234: $node->offset = strlen($this->output);
235: $this->setContext(NULL);
236:
237: } elseif ($token->text === '<!--') {
238: $this->setContext(self::CONTEXT_COMMENT);
239:
240: } else {
241: $this->htmlNodes[] = $node = new NHtmlNode($token->name);
242: $node->isEmpty = in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML))
243: && isset(NHtml::$emptyElements[strtolower($token->name)]);
244: $node->offset = strlen($this->output);
245: $this->setContext(NULL);
246: }
247: $this->output .= $token->text;
248: }
249:
250:
251:
252: private function processTagEnd($token)
253: {
254: if ($token->text === '-->') {
255: $this->output .= $token->text;
256: $this->setContext(NULL);
257: return;
258: }
259:
260: $node = end($this->htmlNodes);
261: $isEmpty = !$node->closing && (NStrings::contains($token->text, '/') || $node->isEmpty);
262:
263: if ($isEmpty && in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML))) {
264: $token->text = preg_replace('#^.*>#', $this->contentType === self::CONTENT_XHTML ? ' />' : '>', $token->text);
265: }
266:
267: if (empty($node->macroAttrs)) {
268: $this->output .= $token->text;
269: } else {
270: $code = substr($this->output, $node->offset) . $token->text;
271: $this->output = substr($this->output, 0, $node->offset);
272: $this->writeAttrsMacro($code, $node);
273: if ($isEmpty) {
274: $node->closing = TRUE;
275: $this->writeAttrsMacro('', $node);
276: }
277: }
278:
279: if ($isEmpty) {
280: $node->closing = TRUE;
281: }
282:
283: if (!$node->closing && (strcasecmp($node->name, 'script') === 0 || strcasecmp($node->name, 'style') === 0)) {
284: $this->setContext(strcasecmp($node->name, 'style') ? self::CONTENT_JS : self::CONTENT_CSS);
285: } else {
286: $this->setContext(NULL);
287: if ($node->closing) {
288: array_pop($this->htmlNodes);
289: }
290: }
291: }
292:
293:
294:
295: private function processAttribute($token)
296: {
297: $node = end($this->htmlNodes);
298: if (NStrings::startsWith($token->name, NParser::N_PREFIX)) {
299: $node->macroAttrs[substr($token->name, strlen(NParser::N_PREFIX))] = $token->value;
300: } else {
301: $node->attrs[$token->name] = TRUE;
302: $this->output .= $token->text;
303: if ($token->value) {
304: $context = NULL;
305: if (strncasecmp($token->name, 'on', 2) === 0) {
306: $context = self::CONTENT_JS;
307: } elseif ($token->name === 'style') {
308: $context = self::CONTENT_CSS;
309: }
310: $this->setContext($token->value, $context);
311: }
312: }
313: }
314:
315:
316:
317:
318:
319:
320:
321: 322: 323: 324: 325: 326: 327: 328:
329: public function writeMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE)
330: {
331: $isLeftmost = trim(substr($this->output, $leftOfs = strrpos("\n$this->output", "\n"))) === '';
332:
333: if ($name[0] === '/') {
334: $node = end($this->macroNodes);
335:
336: if (!$node || ("/$node->name" !== $name && '/' !== $name) || $modifiers
337: || ($args && $node->args && !NStrings::startsWith("$node->args ", "$args "))
338: ) {
339: $name .= $args ? ' ' : '';
340: throw new NLatteException("Unexpected macro {{$name}{$args}{$modifiers}}"
341: . ($node ? ", expecting {/$node->name}" . ($args && $node->args ? " or eventually {/$node->name $node->args}" : '') : ''));
342: }
343:
344: array_pop($this->macroNodes);
345: if (!$node->args) {
346: $node->setArgs($args);
347: }
348: if ($isLeftmost && $isRightmost) {
349: $this->output = substr($this->output, 0, $leftOfs);
350: }
351:
352: $code = $node->close(substr($this->output, $node->offset));
353:
354: if (!$isLeftmost && $isRightmost && substr($code, -2) === '?>') {
355: $code .= "\n";
356: }
357: $this->output = substr($this->output, 0, $node->offset) . $node->content. $code;
358:
359: } else {
360: list($node, $code) = $this->expandMacro($name, $args, $modifiers);
361: if (!$node->isEmpty) {
362: $this->macroNodes[] = $node;
363: }
364:
365: if ($isRightmost) {
366: if ($isLeftmost && substr($code, 0, 11) !== '<?php echo ') {
367: $this->output = substr($this->output, 0, $leftOfs);
368: } elseif (substr($code, -2) === '?>') {
369: $code .= "\n";
370: }
371: }
372:
373: $this->output .= $code;
374: $node->offset = strlen($this->output);
375: }
376: }
377:
378:
379:
380: 381: 382: 383: 384: 385: 386:
387: public function writeAttrsMacro($code, NHtmlNode $node)
388: {
389: $attrs = $node->macroAttrs;
390: $left = $right = array();
391: foreach ($this->macros as $name => $foo) {
392: if ($name[0] === '@') {
393: $name = substr($name, 1);
394: if (!isset($attrs[$name])) {
395: continue;
396: }
397: if (!$node->closing) {
398: $pos = strrpos($code, '>');
399: if ($code[$pos-1] === '/') {
400: $pos--;
401: }
402: $this->setContext(self::CONTEXT_DOUBLE_QUOTED);
403: list(, $macroCode) = $this->expandMacro("@$name", $attrs[$name], NULL, $node);
404: $this->setContext(NULL);
405: $code = substr_replace($code, $macroCode, $pos, 0);
406: }
407: unset($attrs[$name]);
408: }
409:
410: $macro = $node->closing ? "/$name" : $name;
411: if (isset($attrs[$name])) {
412: if ($node->closing) {
413: $right[] = array($macro, '');
414: } else {
415: array_unshift($left, array($macro, $attrs[$name]));
416: }
417: }
418:
419: $innerName = "inner-$name";
420: if (isset($attrs[$innerName])) {
421: if ($node->closing) {
422: $left[] = array($macro, '');
423: } else {
424: array_unshift($right, array($macro, $attrs[$innerName]));
425: }
426: }
427:
428: $tagName = "tag-$name";
429: if (isset($attrs[$tagName])) {
430: array_unshift($left, array($name, $attrs[$tagName]));
431: $right[] = array("/$name", '');
432: }
433:
434: unset($attrs[$name], $attrs[$innerName], $attrs[$tagName]);
435: }
436:
437: if ($attrs) {
438: throw new NLatteException("Unknown macro-attribute " . NParser::N_PREFIX
439: . implode(' and ' . NParser::N_PREFIX, array_keys($attrs)));
440: }
441:
442: foreach ($left as $item) {
443: $this->writeMacro($item[0], $item[1]);
444: if (substr($this->output, -2) === '?>') {
445: $this->output .= "\n";
446: }
447: }
448: $this->output .= $code;
449:
450: foreach ($right as $item) {
451: $this->writeMacro($item[0], $item[1]);
452: if (substr($this->output, -2) === '?>') {
453: $this->output .= "\n";
454: }
455: }
456: }
457:
458:
459:
460: 461: 462: 463: 464: 465: 466:
467: public function expandMacro($name, $args, $modifiers = NULL, NHtmlNode $htmlNode = NULL)
468: {
469: if (empty($this->macros[$name])) {
470: throw new NLatteException("Unknown macro {{$name}}");
471: }
472: foreach (array_reverse($this->macros[$name]) as $macro) {
473: $node = new NMacroNode($macro, $name, $args, $modifiers, $this->macroNodes ? end($this->macroNodes) : NULL, $htmlNode);
474: $code = $macro->nodeOpened($node);
475: if ($code !== FALSE) {
476: return array($node, $code);
477: }
478: }
479: throw new NLatteException("Unhandled macro {{$name}}");
480: }
481:
482: }
483: