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