Namespaces

  • Nette
    • Application
      • Diagnostics
      • Responses
      • Routers
      • UI
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Diagnostics
      • Drivers
      • Reflection
      • Table
    • DI
      • Config
        • Adapters
      • Diagnostics
      • Extensions
    • Diagnostics
    • Forms
      • Controls
      • Rendering
    • Http
      • Diagnostics
    • Iterators
    • Latte
      • Macros
    • Loaders
    • Localization
    • Mail
    • PhpGenerator
    • Reflection
    • Security
      • Diagnostics
    • Templating
    • Utils
  • NetteModule
  • none

Classes

  • Compiler
  • Engine
  • HtmlNode
  • MacroNode
  • MacroTokens
  • Parser
  • PhpWriter
  • Token

Interfaces

  • IMacro

Exceptions

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