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