Namespaces

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

Classes

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