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