Namespaces

  • Latte
    • Loaders
    • Macros
    • Runtime
  • Nette
    • Application
      • Responses
      • Routers
      • UI
    • Bridges
      • ApplicationDI
      • ApplicationLatte
      • ApplicationTracy
      • CacheDI
      • CacheLatte
      • DatabaseDI
      • DatabaseTracy
      • DITracy
      • FormsDI
      • FormsLatte
      • Framework
      • HttpDI
      • HttpTracy
      • MailDI
      • ReflectionDI
      • SecurityDI
      • SecurityTracy
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Conventions
      • Drivers
      • Reflection
      • Table
    • DI
      • Config
        • Adapters
      • Extensions
    • Forms
      • Controls
      • Rendering
    • Http
    • Iterators
    • Loaders
    • Localization
    • Mail
    • Neon
    • PhpGenerator
    • Reflection
    • Security
    • Utils
  • none
  • Tracy
    • Bridges
      • Nette

Classes

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

Interfaces

  • ILoader
  • IMacro

Exceptions

  • CompileException
  • RegexpException
  • RuntimeException
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Other releases
  • Nette homepage
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Latte (https://latte.nette.org)
  5:  * Copyright (c) 2008 David Grudl (https://davidgrudl.com)
  6:  */
  7: 
  8: namespace Latte;
  9: 
 10: 
 11: /**
 12:  * Latte compiler.
 13:  */
 14: class Compiler extends Object
 15: {
 16:     /** @var Token[] */
 17:     private $tokens;
 18: 
 19:     /** @var string pointer to current node content */
 20:     private $output;
 21: 
 22:     /** @var int  position on source template */
 23:     private $position;
 24: 
 25:     /** @var array of [name => IMacro[]] */
 26:     private $macros;
 27: 
 28:     /** @var \SplObjectStorage */
 29:     private $macroHandlers;
 30: 
 31:     /** @var HtmlNode */
 32:     private $htmlNode;
 33: 
 34:     /** @var MacroNode */
 35:     private $macroNode;
 36: 
 37:     /** @var string[] */
 38:     private $attrCodes = array();
 39: 
 40:     /** @var string */
 41:     private $contentType;
 42: 
 43:     /** @var array [context, subcontext] */
 44:     private $context;
 45: 
 46:     /** @var string */
 47:     private $templateId;
 48: 
 49:     /** @var mixed */
 50:     private $lastAttrValue;
 51: 
 52:     /** Context-aware escaping content types */
 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:     /** @internal Context-aware escaping HTML contexts */
 63:     const CONTEXT_COMMENT = '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:      * Adds new macro.
 77:      * @param  string
 78:      * @return static
 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:      * Compiles tokens to PHP code.
 90:      * @param  Token[]
 91:      * @return string
 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:      * @return static
142:      */
143:     public function setContentType($type)
144:     {
145:         $this->contentType = $type;
146:         $this->context = NULL;
147:         return $this;
148:     }
149: 
150: 
151:     /**
152:      * @return string
153:      */
154:     public function getContentType()
155:     {
156:         return $this->contentType;
157:     }
158: 
159: 
160:     /**
161:      * @return static
162:      */
163:     public function setContext($context, $sub = NULL)
164:     {
165:         $this->context = array($context, $sub);
166:         return $this;
167:     }
168: 
169: 
170:     /**
171:      * @return array [context, subcontext]
172:      */
173:     public function getContext()
174:     {
175:         return $this->context;
176:     }
177: 
178: 
179:     /**
180:      * @return string
181:      */
182:     public function getTemplateId()
183:     {
184:         return $this->templateId;
185:     }
186: 
187: 
188:     /**
189:      * @return MacroNode|NULL
190:      */
191:     public function getMacroNode()
192:     {
193:         return $this->macroNode;
194:     }
195: 
196: 
197:     /**
198:      * Returns current line number.
199:      * @return int
200:      */
201:     public function getLine()
202:     {
203:         return $this->tokens ? $this->tokens[$this->position]->line : NULL;
204:     }
205: 
206: 
207:     /** @internal */
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) { // auto-correct
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 processComment(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:     /********************* macros ****************d*g**/
391: 
392: 
393:     /**
394:      * Generates code for {macro ...} to the output.
395:      * @param  string
396:      * @param  string
397:      * @param  string
398:      * @param  bool
399:      * @return MacroNode
400:      * @internal
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:      * Generates code for {/macro ...} to the output.
418:      * @param  string
419:      * @param  string
420:      * @param  string
421:      * @param  bool
422:      * @return MacroNode
423:      * @internal
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); // alone macro without output -> remove indentation
469:             } elseif (substr($code, -2) === '?>') {
470:                 $code .= "\n"; // double newline to avoid newline eating by PHP
471:             }
472:         }
473:         $output .= $code;
474:     }
475: 
476: 
477:     /**
478:      * Generates code for macro <tag n:attr> to the output.
479:      * @param  string
480:      * @return void
481:      * @internal
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:      * Expands macro and returns node & code.
557:      * @param  string
558:      * @param  string
559:      * @param  string
560:      * @return MacroNode
561:      * @internal
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: 
Nette 2.3-20161221 API API documentation generated by ApiGen 2.8.0