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