Namespaces

  • Latte
    • Loaders
    • Macros
    • Runtime
  • Nette
    • Application
      • Responses
      • Routers
      • UI
    • Bridges
      • ApplicationLatte
      • ApplicationTracy
      • CacheLatte
      • DatabaseDI
      • DatabaseTracy
      • DITracy
      • FormsLatte
      • Framework
      • HttpTracy
      • SecurityTracy
    • Caching
      • Storages
    • ComponentModel
    • Database
      • 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

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