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->htmlNode));
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->htmlNode));
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: $leftOfs = ($tmp = strrpos($this->output, "\n")) === FALSE ? 0 : $tmp + 1;
380: $isLeftmost = trim(substr($this->output, $leftOfs)) === '';
381: $isRightmost = substr($token->text, -1) === "\n";
382: if ($isLeftmost && $isRightmost) {
383: $this->output = substr($this->output, 0, $leftOfs);
384: } else {
385: $this->output .= substr($token->text, strlen(rtrim($token->text, "\n")));
386: }
387: }
388:
389:
390:
391:
392:
393: 394: 395: 396: 397: 398: 399: 400: 401:
402: public function openMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE, $nPrefix = NULL)
403: {
404: $node = $this->expandMacro($name, $args, $modifiers, $nPrefix);
405: if ($node->isEmpty) {
406: $this->writeCode($node->openingCode, $this->output, $node->replaced, $isRightmost);
407: } else {
408: $this->macroNode = $node;
409: $node->saved = array(& $this->output, $isRightmost);
410: $this->output = & $node->content;
411: }
412: return $node;
413: }
414:
415:
416: 417: 418: 419: 420: 421: 422: 423: 424:
425: public function closeMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE, $nPrefix = NULL)
426: {
427: $node = $this->macroNode;
428:
429: if (!$node || ($node->name !== $name && '' !== $name) || $modifiers
430: || ($args && $node->args && strncmp("$node->args ", "$args ", strlen($args) + 1))
431: || $nPrefix !== $node->prefix
432: ) {
433: $name = $nPrefix
434: ? "</{$this->htmlNode->name}> for " . Parser::N_PREFIX . implode(' and ' . Parser::N_PREFIX, array_keys($this->htmlNode->macroAttrs))
435: : '{/' . $name . ($args ? ' ' . $args : '') . $modifiers . '}';
436: throw new CompileException("Unexpected $name" . ($node ? ', expecting ' . self::printEndTag($node->prefix ? $this->htmlNode : $node) : ''));
437: }
438:
439: $this->macroNode = $node->parentNode;
440: if (!$node->args) {
441: $node->setArgs($args);
442: }
443:
444: $isLeftmost = $node->content ? trim(substr($this->output, strrpos("\n$this->output", "\n"))) === '' : FALSE;
445:
446: $node->closing = TRUE;
447: $node->macro->nodeClosed($node);
448:
449: $this->output = & $node->saved[0];
450: $this->writeCode($node->openingCode, $this->output, $node->replaced, $node->saved[1]);
451: $this->writeCode($node->closingCode, $node->content, $node->replaced, $isRightmost, $isLeftmost);
452: $this->output .= $node->content;
453: return $node;
454: }
455:
456:
457: private function writeCode($code, & $output, $replaced, $isRightmost, $isLeftmost = NULL)
458: {
459: if ($isRightmost) {
460: $leftOfs = ($tmp = strrpos($output, "\n")) === FALSE ? 0 : $tmp + 1;
461: if ($isLeftmost === NULL) {
462: $isLeftmost = trim(substr($output, $leftOfs)) === '';
463: }
464: if ($replaced === NULL) {
465: $replaced = preg_match('#<\?php.*\secho\s#As', $code);
466: }
467: if ($isLeftmost && !$replaced) {
468: $output = substr($output, 0, $leftOfs);
469: } elseif (substr($code, -2) === '?>') {
470: $code .= "\n";
471: }
472: }
473: $output .= $code;
474: }
475:
476:
477: 478: 479: 480: 481: 482:
483: public function writeAttrsMacro($code)
484: {
485: $attrs = $this->htmlNode->macroAttrs;
486: $left = $right = array();
487:
488: foreach ($this->macros as $name => $foo) {
489: $attrName = MacroNode::PREFIX_INNER . "-$name";
490: if (isset($attrs[$attrName])) {
491: if ($this->htmlNode->closing) {
492: $left[] = array('closeMacro', $name, '', MacroNode::PREFIX_INNER);
493: } else {
494: array_unshift($right, array('openMacro', $name, $attrs[$attrName], MacroNode::PREFIX_INNER));
495: }
496: unset($attrs[$attrName]);
497: }
498: }
499:
500: foreach (array_reverse($this->macros) as $name => $foo) {
501: $attrName = MacroNode::PREFIX_TAG . "-$name";
502: if (isset($attrs[$attrName])) {
503: $left[] = array('openMacro', $name, $attrs[$attrName], MacroNode::PREFIX_TAG);
504: array_unshift($right, array('closeMacro', $name, '', MacroNode::PREFIX_TAG));
505: unset($attrs[$attrName]);
506: }
507: }
508:
509: foreach ($this->macros as $name => $foo) {
510: if (isset($attrs[$name])) {
511: if ($this->htmlNode->closing) {
512: $right[] = array('closeMacro', $name, '', MacroNode::PREFIX_NONE);
513: } else {
514: array_unshift($left, array('openMacro', $name, $attrs[$name], MacroNode::PREFIX_NONE));
515: }
516: unset($attrs[$name]);
517: }
518: }
519:
520: if ($attrs) {
521: throw new CompileException('Unknown attribute ' . Parser::N_PREFIX
522: . implode(' and ' . Parser::N_PREFIX, array_keys($attrs)));
523: }
524:
525: if (!$this->htmlNode->closing) {
526: $this->htmlNode->attrCode = & $this->attrCodes[$uniq = ' n:' . substr(lcg_value(), 2, 10)];
527: $code = substr_replace($code, $uniq, strrpos($code, '/>') ?: strrpos($code, '>'), 0);
528: }
529:
530: foreach ($left as $item) {
531: $node = $this->{$item[0]}($item[1], $item[2], NULL, NULL, $item[3]);
532: if ($node->closing || $node->isEmpty) {
533: $this->htmlNode->attrCode .= $node->attrCode;
534: if ($node->isEmpty) {
535: unset($this->htmlNode->macroAttrs[$node->name]);
536: }
537: }
538: }
539:
540: $this->output .= $code;
541:
542: foreach ($right as $item) {
543: $node = $this->{$item[0]}($item[1], $item[2], NULL, NULL, $item[3]);
544: if ($node->closing) {
545: $this->htmlNode->attrCode .= $node->attrCode;
546: }
547: }
548:
549: if ($right && substr($this->output, -2) === '?>') {
550: $this->output .= "\n";
551: }
552: }
553:
554:
555: 556: 557: 558: 559: 560: 561: 562:
563: public function expandMacro($name, $args, $modifiers = NULL, $nPrefix = NULL)
564: {
565: $inScript = in_array($this->context[0], array(self::CONTENT_JS, self::CONTENT_CSS), TRUE);
566:
567: if (empty($this->macros[$name])) {
568: $hint = ($t = Helpers::getSuggestion(array_keys($this->macros), $name)) ? ", did you mean {{$t}}?" : '';
569: throw new CompileException("Unknown macro {{$name}}$hint" . ($inScript ? ' (in JavaScript or CSS, try to put a space after bracket or use n:syntax=off)' : ''));
570: }
571:
572: if (strpbrk($name, '=~%^&_')) {
573: if ($this->context[1] === self::CONTENT_URL) {
574: $modifiers = preg_replace('#\|nosafeurl\s?(?=\||\z)#i', '', $modifiers, -1, $found);
575: if (!$found && !preg_match('#\|datastream(?=\s|\||\z)#i', $modifiers)) {
576: $modifiers .= '|safeurl';
577: }
578: }
579:
580: $modifiers = preg_replace('#\|noescape\s?(?=\||\z)#i', '', $modifiers, -1, $found);
581: if (!$found) {
582: $modifiers .= '|escape';
583: }
584:
585: if (!$found && $inScript && $name === '=' && preg_match('#["\'] *\z#', $this->tokens[$this->position - 1]->text)) {
586: throw new CompileException("Do not place {$this->tokens[$this->position]->text} inside quotes.");
587: }
588: }
589:
590: foreach (array_reverse($this->macros[$name]) as $macro) {
591: $node = new MacroNode($macro, $name, $args, $modifiers, $this->macroNode, $this->htmlNode, $nPrefix);
592: if ($macro->nodeOpened($node) !== FALSE) {
593: return $node;
594: }
595: }
596:
597: throw new CompileException('Unknown ' . ($nPrefix
598: ? 'attribute ' . Parser::N_PREFIX . ($nPrefix === MacroNode::PREFIX_NONE ? '' : "$nPrefix-") . $name
599: : 'macro {' . $name . ($args ? " $args" : '') . '}'
600: ));
601: }
602:
603:
604: private static function printEndTag($node)
605: {
606: if ($node instanceof HtmlNode) {
607: return "</{$node->name}> for " . Parser::N_PREFIX
608: . implode(' and ' . Parser::N_PREFIX, array_keys($node->macroAttrs));
609: } else {
610: return "{/$node->name}";
611: }
612: }
613:
614: }
615: