Packages

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

Classes

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

Interfaces

  • IMacro

Exceptions

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