1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Latte;
9:
10:
11: 12: 13: 14: 15:
16: class Compiler extends Object
17: {
18:
19: private $tokens;
20:
21:
22: private $output;
23:
24:
25: private $position;
26:
27:
28: private $macros;
29:
30:
31: private $macroHandlers;
32:
33:
34: private $htmlNode;
35:
36:
37: private $macroNode;
38:
39:
40: private $attrCodes = array();
41:
42:
43: private $contentType;
44:
45:
46: private $context;
47:
48:
49: private $templateId;
50:
51:
52: private $lastAttrValue;
53:
54:
55: const CONTENT_HTML = Engine::CONTENT_HTML,
56: CONTENT_XHTML = Engine::CONTENT_XHTML,
57: CONTENT_XML = Engine::CONTENT_XML,
58: CONTENT_JS = Engine::CONTENT_JS,
59: CONTENT_CSS = Engine::CONTENT_CSS,
60: CONTENT_URL = Engine::CONTENT_URL,
61: CONTENT_ICAL = Engine::CONTENT_ICAL,
62: CONTENT_TEXT = Engine::CONTENT_TEXT;
63:
64:
65: const = 'comment',
66: CONTEXT_SINGLE_QUOTED_ATTR = "'",
67: CONTEXT_DOUBLE_QUOTED_ATTR = '"',
68: CONTEXT_UNQUOTED_ATTR = '=';
69:
70:
71: public function __construct()
72: {
73: $this->macroHandlers = new \SplObjectStorage;
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: public function compile(array $tokens, $className)
96: {
97: $this->templateId = substr(md5($className), 0, 10);
98: $this->tokens = $tokens;
99: $output = '';
100: $this->output = & $output;
101: $this->htmlNode = $this->macroNode = $this->context = NULL;
102:
103: foreach ($this->macroHandlers as $handler) {
104: $handler->initialize($this);
105: }
106:
107: foreach ($tokens as $this->position => $token) {
108: $this->{"process$token->type"}($token);
109: }
110:
111: while ($this->htmlNode) {
112: if (!empty($this->htmlNode->macroAttrs)) {
113: throw new CompileException('Missing ' . self::printEndTag($this->macroNode));
114: }
115: $this->htmlNode = $this->htmlNode->parentNode;
116: }
117:
118: $prologs = $epilogs = '';
119: foreach ($this->macroHandlers as $handler) {
120: $res = $handler->finalize();
121: $handlerName = get_class($handler);
122: $prologs .= empty($res[0]) ? '' : "<?php\n// prolog $handlerName\n$res[0]\n?>";
123: $epilogs = (empty($res[1]) ? '' : "<?php\n// epilog $handlerName\n$res[1]\n?>") . $epilogs;
124: }
125: $output = ($prologs ? $prologs . "<?php\n//\n// main template\n//\n?>\n" : '') . $output . $epilogs;
126:
127: if ($this->macroNode) {
128: throw new CompileException('Missing ' . self::printEndTag($this->macroNode));
129: }
130:
131: $output = $this->expandTokens($output);
132: $output = "<?php\n"
133: . "class $className extends Latte\\Template {\n"
134: . "function render() {\n"
135: . 'foreach ($this->params as $__k => $__v) $$__k = $__v; unset($__k, $__v);'
136: . '?>' . $output . "<?php\n}}";
137:
138: return $output;
139: }
140:
141:
142: 143: 144:
145: public function setContentType($type)
146: {
147: $this->contentType = $type;
148: $this->context = NULL;
149: return $this;
150: }
151:
152:
153: 154: 155:
156: public function getContentType()
157: {
158: return $this->contentType;
159: }
160:
161:
162: 163: 164:
165: public function setContext($context, $sub = NULL)
166: {
167: $this->context = array($context, $sub);
168: return $this;
169: }
170:
171:
172: 173: 174:
175: public function getContext()
176: {
177: return $this->context;
178: }
179:
180:
181: 182: 183:
184: public function getTemplateId()
185: {
186: return $this->templateId;
187: }
188:
189:
190: 191: 192:
193: public function getMacroNode()
194: {
195: return $this->macroNode;
196: }
197:
198:
199: 200: 201: 202:
203: public function getLine()
204: {
205: return $this->tokens ? $this->tokens[$this->position]->line : NULL;
206: }
207:
208:
209:
210: public function expandTokens($s)
211: {
212: return strtr($s, $this->attrCodes);
213: }
214:
215:
216: private function processText(Token $token)
217: {
218: if (in_array($this->context[0], array(self::CONTEXT_SINGLE_QUOTED_ATTR, self::CONTEXT_DOUBLE_QUOTED_ATTR), TRUE)) {
219: if ($token->text === $this->context[0]) {
220: $this->setContext(self::CONTEXT_UNQUOTED_ATTR);
221: } elseif ($this->lastAttrValue === '') {
222: $this->lastAttrValue = $token->text;
223: }
224: }
225: $this->output .= $token->text;
226: }
227:
228:
229: private function processMacroTag(Token $token)
230: {
231: if (in_array($this->context[0], array(self::CONTEXT_SINGLE_QUOTED_ATTR, self::CONTEXT_DOUBLE_QUOTED_ATTR, self::CONTEXT_UNQUOTED_ATTR), TRUE)) {
232: $this->lastAttrValue = TRUE;
233: }
234:
235: $isRightmost = !isset($this->tokens[$this->position + 1])
236: || substr($this->tokens[$this->position + 1]->text, 0, 1) === "\n";
237:
238: if ($token->name[0] === '/') {
239: $this->closeMacro((string) substr($token->name, 1), $token->value, $token->modifiers, $isRightmost);
240: } else {
241: $this->openMacro($token->name, $token->value, $token->modifiers, $isRightmost && !$token->empty);
242: if ($token->empty) {
243: $this->closeMacro($token->name, NULL, NULL, $isRightmost);
244: }
245: }
246: }
247:
248:
249: private function processHtmlTagBegin(Token $token)
250: {
251: if ($token->closing) {
252: while ($this->htmlNode) {
253: if (strcasecmp($this->htmlNode->name, $token->name) === 0) {
254: break;
255: }
256: if ($this->htmlNode->macroAttrs) {
257: throw new CompileException("Unexpected </$token->name>, expecting " . self::printEndTag($this->macroNode));
258: }
259: $this->htmlNode = $this->htmlNode->parentNode;
260: }
261: if (!$this->htmlNode) {
262: $this->htmlNode = new HtmlNode($token->name);
263: }
264: $this->htmlNode->closing = TRUE;
265: $this->htmlNode->offset = strlen($this->output);
266: $this->setContext(NULL);
267:
268: } elseif ($token->text === '<!--') {
269: $this->setContext(self::CONTEXT_COMMENT);
270:
271: } else {
272: $this->htmlNode = new HtmlNode($token->name, $this->htmlNode);
273: $this->htmlNode->isEmpty = in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML), TRUE)
274: && isset(Helpers::$emptyElements[strtolower($token->name)]);
275: $this->htmlNode->offset = strlen($this->output);
276: $this->setContext(self::CONTEXT_UNQUOTED_ATTR);
277: }
278: $this->output .= $token->text;
279: }
280:
281:
282: private function processHtmlTagEnd(Token $token)
283: {
284: if ($token->text === '-->') {
285: $this->output .= $token->text;
286: $this->setContext(NULL);
287: return;
288: }
289:
290: $htmlNode = $this->htmlNode;
291: $isEmpty = !$htmlNode->closing && (strpos($token->text, '/') !== FALSE || $htmlNode->isEmpty);
292: $end = '';
293:
294: if ($isEmpty && in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML), TRUE)) {
295: $space = substr(strstr($token->text, '>'), 1);
296: $token->text = $htmlNode->isEmpty && $this->contentType === self::CONTENT_XHTML ? ' />' : '>';
297: if ($htmlNode->isEmpty) {
298: $token->text .= $space;
299: } else {
300: $end = "</$htmlNode->name>" . $space;
301: }
302: }
303:
304: if (empty($htmlNode->macroAttrs)) {
305: $this->output .= $token->text . $end;
306: } else {
307: $code = substr($this->output, $htmlNode->offset) . $token->text;
308: $this->output = substr($this->output, 0, $htmlNode->offset);
309: $this->writeAttrsMacro($code);
310: if ($isEmpty) {
311: $htmlNode->closing = TRUE;
312: $this->writeAttrsMacro($end);
313: }
314: }
315:
316: if ($isEmpty) {
317: $htmlNode->closing = TRUE;
318: }
319:
320: $this->setContext(NULL);
321:
322: if ($htmlNode->closing) {
323: $this->htmlNode = $this->htmlNode->parentNode;
324:
325: } elseif ((($lower = strtolower($htmlNode->name)) === 'script' || $lower === 'style')
326: && (!isset($htmlNode->attrs['type']) || preg_match('#(java|j|ecma|live)script|json|css#i', $htmlNode->attrs['type']))
327: ) {
328: $this->setContext($lower === 'script' ? self::CONTENT_JS : self::CONTENT_CSS);
329: }
330: }
331:
332:
333: private function processHtmlAttribute(Token $token)
334: {
335: if (strncmp($token->name, Parser::N_PREFIX, strlen(Parser::N_PREFIX)) === 0) {
336: $name = substr($token->name, strlen(Parser::N_PREFIX));
337: if (isset($this->htmlNode->macroAttrs[$name])) {
338: throw new CompileException("Found multiple attributes $token->name.");
339:
340: } elseif ($this->macroNode && $this->macroNode->htmlNode === $this->htmlNode) {
341: throw new CompileException("n:attributes must not appear inside macro; found $token->name inside {{$this->macroNode->name}}.");
342: }
343: $this->htmlNode->macroAttrs[$name] = $token->value;
344: return;
345: }
346:
347: $this->lastAttrValue = & $this->htmlNode->attrs[$token->name];
348: $this->output .= $token->text;
349:
350: if (in_array($token->value, array(self::CONTEXT_SINGLE_QUOTED_ATTR, self::CONTEXT_DOUBLE_QUOTED_ATTR), TRUE)) {
351: $this->lastAttrValue = '';
352: $contextMain = $token->value;
353: } else {
354: $this->lastAttrValue = $token->value;
355: $contextMain = self::CONTEXT_UNQUOTED_ATTR;
356: }
357:
358: $context = NULL;
359: if (in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML), TRUE)) {
360: $lower = strtolower($token->name);
361: if (substr($lower, 0, 2) === 'on') {
362: $context = self::CONTENT_JS;
363: } elseif ($lower === 'style') {
364: $context = self::CONTENT_CSS;
365: } elseif (in_array($lower, array('href', 'src', 'action', 'formaction'), TRUE)
366: || ($lower === 'data' && strtolower($this->htmlNode->name) === 'object')
367: ) {
368: $context = self::CONTENT_URL;
369: }
370: }
371:
372: $this->setContext($contextMain, $context);
373: }
374:
375:
376: private function (Token $token)
377: {
378: $isLeftmost = trim(substr($this->output, strrpos("\n$this->output", "\n"))) === '';
379: if (!$isLeftmost) {
380: $this->output .= substr($token->text, strlen(rtrim($token->text, "\n")));
381: }
382: }
383:
384:
385:
386:
387:
388: 389: 390: 391: 392: 393: 394: 395: 396:
397: public function openMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE, $nPrefix = NULL)
398: {
399: $node = $this->expandMacro($name, $args, $modifiers, $nPrefix);
400: if ($node->isEmpty) {
401: $this->writeCode($node->openingCode, $this->output, $node->replaced, $isRightmost);
402: } else {
403: $this->macroNode = $node;
404: $node->saved = array(& $this->output, $isRightmost);
405: $this->output = & $node->content;
406: }
407: return $node;
408: }
409:
410:
411: 412: 413: 414: 415: 416: 417: 418: 419:
420: public function closeMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE, $nPrefix = NULL)
421: {
422: $node = $this->macroNode;
423:
424: if (!$node || ($node->name !== $name && '' !== $name) || $modifiers
425: || ($args && $node->args && strncmp("$node->args ", "$args ", strlen($args) + 1))
426: || $nPrefix !== $node->prefix
427: ) {
428: $name = $nPrefix
429: ? "</{$this->htmlNode->name}> for " . Parser::N_PREFIX . implode(' and ' . Parser::N_PREFIX, array_keys($this->htmlNode->macroAttrs))
430: : '{/' . $name . ($args ? ' ' . $args : '') . $modifiers . '}';
431: throw new CompileException("Unexpected $name" . ($node ? ', expecting ' . self::printEndTag($node) : ''));
432: }
433:
434: $this->macroNode = $node->parentNode;
435: if (!$node->args) {
436: $node->setArgs($args);
437: }
438:
439: $isLeftmost = $node->content ? trim(substr($this->output, strrpos("\n$this->output", "\n"))) === '' : FALSE;
440:
441: $node->closing = TRUE;
442: $node->macro->nodeClosed($node);
443:
444: $this->output = & $node->saved[0];
445: $this->writeCode($node->openingCode, $this->output, $node->replaced, $node->saved[1]);
446: $this->writeCode($node->closingCode, $node->content, $node->replaced, $isRightmost, $isLeftmost);
447: $this->output .= $node->content;
448: return $node;
449: }
450:
451:
452: private function writeCode($code, & $output, $replaced, $isRightmost, $isLeftmost = NULL)
453: {
454: if ($isRightmost) {
455: $leftOfs = strrpos("\n$output", "\n");
456: if ($isLeftmost === NULL) {
457: $isLeftmost = trim(substr($output, $leftOfs)) === '';
458: }
459: if ($replaced === NULL) {
460: $replaced = preg_match('#<\?php.*\secho\s#As', $code);
461: }
462: if ($isLeftmost && !$replaced) {
463: $output = substr($output, 0, $leftOfs);
464: } elseif (substr($code, -2) === '?>') {
465: $code .= "\n";
466: }
467: }
468: $output .= $code;
469: }
470:
471:
472: 473: 474: 475: 476: 477:
478: public function writeAttrsMacro($code)
479: {
480: $attrs = $this->htmlNode->macroAttrs;
481: $left = $right = array();
482:
483: foreach ($this->macros as $name => $foo) {
484: $attrName = MacroNode::PREFIX_INNER . "-$name";
485: if (isset($attrs[$attrName])) {
486: if ($this->htmlNode->closing) {
487: $left[] = array('closeMacro', $name, '', MacroNode::PREFIX_INNER);
488: } else {
489: array_unshift($right, array('openMacro', $name, $attrs[$attrName], MacroNode::PREFIX_INNER));
490: }
491: unset($attrs[$attrName]);
492: }
493: }
494:
495: foreach (array_reverse($this->macros) as $name => $foo) {
496: $attrName = MacroNode::PREFIX_TAG . "-$name";
497: if (isset($attrs[$attrName])) {
498: $left[] = array('openMacro', $name, $attrs[$attrName], MacroNode::PREFIX_TAG);
499: array_unshift($right, array('closeMacro', $name, '', MacroNode::PREFIX_TAG));
500: unset($attrs[$attrName]);
501: }
502: }
503:
504: foreach ($this->macros as $name => $foo) {
505: if (isset($attrs[$name])) {
506: if ($this->htmlNode->closing) {
507: $right[] = array('closeMacro', $name, '', MacroNode::PREFIX_NONE);
508: } else {
509: array_unshift($left, array('openMacro', $name, $attrs[$name], MacroNode::PREFIX_NONE));
510: }
511: unset($attrs[$name]);
512: }
513: }
514:
515: if ($attrs) {
516: throw new CompileException('Unknown attribute ' . Parser::N_PREFIX
517: . implode(' and ' . Parser::N_PREFIX, array_keys($attrs)));
518: }
519:
520: if (!$this->htmlNode->closing) {
521: $this->htmlNode->attrCode = & $this->attrCodes[$uniq = ' n:' . substr(lcg_value(), 2, 10)];
522: $code = substr_replace($code, $uniq, strrpos($code, '/>') ?: strrpos($code, '>'), 0);
523: }
524:
525: foreach ($left as $item) {
526: $node = $this->{$item[0]}($item[1], $item[2], NULL, NULL, $item[3]);
527: if ($node->closing || $node->isEmpty) {
528: $this->htmlNode->attrCode .= $node->attrCode;
529: if ($node->isEmpty) {
530: unset($this->htmlNode->macroAttrs[$node->name]);
531: }
532: }
533: }
534:
535: $this->output .= $code;
536:
537: foreach ($right as $item) {
538: $node = $this->{$item[0]}($item[1], $item[2], NULL, NULL, $item[3]);
539: if ($node->closing) {
540: $this->htmlNode->attrCode .= $node->attrCode;
541: }
542: }
543:
544: if ($right && substr($this->output, -2) === '?>') {
545: $this->output .= "\n";
546: }
547: }
548:
549:
550: 551: 552: 553: 554: 555: 556: 557:
558: public function expandMacro($name, $args, $modifiers = NULL, $nPrefix = NULL)
559: {
560: $inScript = in_array($this->context[0], array(self::CONTENT_JS, self::CONTENT_CSS), TRUE);
561:
562: if (empty($this->macros[$name])) {
563: throw new CompileException("Unknown macro {{$name}}" . ($inScript ? ' (in JavaScript or CSS, try to put a space after bracket.)' : ''));
564: }
565:
566: if ($this->context[1] === self::CONTENT_URL) {
567: $modifiers = preg_replace('#\|nosafeurl\s?(?=\||\z)#i', '', $modifiers, -1, $found);
568: if (!$found && !preg_match('#\|datastream(?=\s|\||\z)#i', $modifiers)) {
569: $modifiers .= '|safeurl';
570: }
571: }
572:
573: $modifiers = preg_replace('#\|noescape\s?(?=\||\z)#i', '', $modifiers, -1, $found);
574: if (!$found && strpbrk($name, '=~%^&_')) {
575: $modifiers .= '|escape';
576: }
577:
578: if (!$found && $inScript && $name === '=' && preg_match('#["\'] *\z#', $this->tokens[$this->position - 1]->text)) {
579: throw new CompileException("Do not place {$this->tokens[$this->position]->text} inside quotes.");
580: }
581:
582: foreach (array_reverse($this->macros[$name]) as $macro) {
583: $node = new MacroNode($macro, $name, $args, $modifiers, $this->macroNode, $this->htmlNode, $nPrefix);
584: if ($macro->nodeOpened($node) !== FALSE) {
585: return $node;
586: }
587: }
588:
589: throw new CompileException('Unknown ' . ($nPrefix
590: ? 'attribute ' . Parser::N_PREFIX . ($nPrefix === MacroNode::PREFIX_NONE ? '' : "$nPrefix-") . $name
591: : 'macro {' . $name . ($args ? " $args" : '') . '}'
592: ));
593: }
594:
595:
596: private static function printEndTag(MacroNode $node)
597: {
598: if ($node->prefix) {
599: return "</{$node->htmlNode->name}> for " . Parser::N_PREFIX
600: . implode(' and ' . Parser::N_PREFIX, array_keys($node->htmlNode->macroAttrs));
601: } else {
602: return "{/$node->name}";
603: }
604: }
605:
606: }
607: