Packages

  • 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
  • PHP

Classes

  • HtmlNode
  • LatteCompiler
  • LatteFilter
  • LatteToken
  • MacroNode
  • MacroTokenizer
  • Parser
  • PhpWriter

Interfaces

  • IMacro

Exceptions

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