Namespaces

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

Classes

  • Engine
  • MacroNode
  • MacroTokenizer
  • Parser
  • PhpWriter

Interfaces

  • IMacro

Exceptions

  • ParseException
  • Overview
  • Namespace
  • Class
  • Tree
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Nette Framework (http://nette.org)
  5:  *
  6:  * Copyright (c) 2004, 2011 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:  */
 11: 
 12: namespace Nette\Latte;
 13: 
 14: use Nette,
 15:     Nette\Utils\Strings;
 16: 
 17: 
 18: 
 19: /**
 20:  * Compile-time filter Latte.
 21:  *
 22:  * @author     David Grudl
 23:  */
 24: class Parser extends Nette\Object
 25: {
 26:     /** @internal regular expression for single & double quoted PHP string */
 27:     const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';
 28: 
 29:     /** @internal special HTML tag or attribute prefix */
 30:     const N_PREFIX = 'n:';
 31: 
 32:     /** @var string */
 33:     private $macroRe;
 34: 
 35:     /** @var string source template */
 36:     private $input;
 37: 
 38:     /** @var string output code */
 39:     private $output;
 40: 
 41:     /** @var int  position on source template */
 42:     private $offset;
 43: 
 44:     /** @var array of [name => array of IMacro] */
 45:     private $macros;
 46: 
 47:     /** @var SplObjectStorage */
 48:     private $macroHandlers;
 49: 
 50:     /** @var array of HtmlNode */
 51:     private $htmlNodes = array();
 52: 
 53:     /** @var array of MacroNode */
 54:     private $macroNodes = array();
 55: 
 56:     /** @var array */
 57:     public $context;
 58: 
 59:     /** @var string */
 60:     public $templateId;
 61: 
 62:     /** @internal Context-aware escaping states */
 63:     const CONTEXT_TEXT = 'text',
 64:         CONTEXT_CDATA = 'cdata',
 65:         CONTEXT_TAG = 'tag',
 66:         CONTEXT_ATTRIBUTE = 'attribute',
 67:         CONTEXT_NONE = 'none',
 68:         CONTEXT_COMMENT = 'comment';
 69: 
 70: 
 71: 
 72:     public function __construct()
 73:     {
 74:         $this->macroHandlers = new \SplObjectStorage;
 75:         $this->setDelimiters('\\{(?![\\s\'"{}])', '\\}');
 76:         $this->context = array(self::CONTEXT_NONE, 'text');
 77:     }
 78: 
 79: 
 80: 
 81:     /**
 82:      * Adds new macro
 83:      * @param
 84:      * @return Parser  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:      * Process all {macros} and <tags/>.
 97:      * @param  string
 98:      * @return string
 99:      */
100:     public function parse($s)
101:     {
102:         if (!Strings::checkEncoding($s)) {
103:             throw new ParseException('Template is not valid UTF-8 stream.');
104:         }
105:         $s = str_replace("\r\n", "\n", $s);
106: 
107:         $this->templateId = Strings::random();
108:         $this->input = & $s;
109:         $this->offset = 0;
110:         $this->output = '';
111:         $this->htmlNodes = $this->macroNodes = array();
112: 
113:         foreach ($this->macroHandlers as $handler) {
114:             $handler->initialize($this);
115:         }
116: 
117:         $len = strlen($s);
118: 
119:         try {
120:             while ($this->offset < $len) {
121:                 $matches = $this->{"context".$this->context[0]}();
122: 
123:                 if (!$matches) { // EOF
124:                     break;
125: 
126:                 } elseif (!empty($matches['comment'])) { // {* *}
127: 
128:                 } elseif (!empty($matches['macro'])) { // {macro}
129:                     list($macroName, $macroArgs, $macroModifiers) = $this->parseMacro($matches['macro']);
130:                     $isRightmost = $this->offset >= $len || $this->input[$this->offset] === "\n";
131:                     $this->writeMacro($macroName, $macroArgs, $macroModifiers, $isRightmost);
132: 
133:                 } else { // common behaviour
134:                     $this->output .= $matches[0];
135:                 }
136:             }
137:         } catch (ParseException $e) {
138:             if (!$e->sourceLine) {
139:                 $e->sourceLine = $this->getLine();
140:             }
141:             throw $e;
142:         }
143: 
144:         $this->output .= substr($this->input, $this->offset);
145: 
146:         foreach ($this->htmlNodes as $node) {
147:             if (!empty($node->attrs)) {
148:                 throw new ParseException("Missing end tag </$node->name> for macro-attribute " . self::N_PREFIX
149:                     . implode(' and ' . self::N_PREFIX, array_keys($node->attrs)) . ".", 0, $this->getLine());
150:             }
151:         }
152: 
153:         $prologs = $epilogs = '';
154:         foreach ($this->macroHandlers as $handler) {
155:             $res = $handler->finalize();
156:             $prologs .= isset($res[0]) ? "<?php $res[0]\n?>" : '';
157:             $epilogs .= isset($res[1]) ? "<?php $res[1]\n?>" : '';
158:         }
159:         $this->output = ($prologs ? $prologs . "<?php\n//\n// main template\n//\n?>\n" : '') . $this->output . $epilogs;
160: 
161:         if ($this->macroNodes) {
162:             throw new ParseException("There are unclosed macros.", 0, $this->getLine());
163:         }
164: 
165:         return $this->output;
166:     }
167: 
168: 
169: 
170:     /**
171:      * Handles CONTEXT_TEXT.
172:      */
173:     private function contextText()
174:     {
175:         $matches = $this->match('~
176:             (?:(?<=\n|^)[ \t]*)?<(?P<closing>/?)(?P<tag>[a-z0-9:]+)|  ##  begin of HTML tag <tag </tag - ignores <!DOCTYPE
177:             <(?P<htmlcomment>!--)|           ##  begin of HTML comment <!--
178:             '.$this->macroRe.'           ##  curly tag
179:         ~xsi');
180: 
181:         if (!$matches || !empty($matches['macro']) || !empty($matches['comment'])) { // EOF or {macro}
182: 
183:         } elseif (!empty($matches['htmlcomment'])) { // <!--
184:             $this->context = array(self::CONTEXT_COMMENT);
185: 
186:         } elseif (empty($matches['closing'])) { // <tag
187:             $this->htmlNodes[] = $node = new HtmlNode($matches['tag']);
188:             $node->offset = strlen($this->output);
189:             $this->context = array(self::CONTEXT_TAG);
190: 
191:         } else { // </tag
192:             do {
193:                 $node = array_pop($this->htmlNodes);
194:                 if (!$node) {
195:                     $node = new HtmlNode($matches['tag']);
196:                 }
197:             } while (strcasecmp($node->name, $matches['tag']));
198:             $this->htmlNodes[] = $node;
199:             $node->closing = TRUE;
200:             $node->offset = strlen($this->output);
201:             $this->context = array(self::CONTEXT_TAG);
202:         }
203:         return $matches;
204:     }
205: 
206: 
207: 
208:     /**
209:      * Handles CONTEXT_CDATA.
210:      */
211:     private function contextCData()
212:     {
213:         $node = end($this->htmlNodes);
214:         $matches = $this->match('~
215:             </'.$node->name.'(?![a-z0-9:])| ##  end HTML tag </tag
216:             '.$this->macroRe.'           ##  curly tag
217:         ~xsi');
218: 
219:         if ($matches && empty($matches['macro']) && empty($matches['comment'])) { // </tag
220:             $node->closing = TRUE;
221:             $node->offset = strlen($this->output);
222:             $this->context = array(self::CONTEXT_TAG);
223:         }
224:         return $matches;
225:     }
226: 
227: 
228: 
229:     /**
230:      * Handles CONTEXT_TAG.
231:      */
232:     private function contextTag()
233:     {
234:         $matches = $this->match('~
235:             (?P<end>\ ?/?>)(?P<tagnewline>[ \t]*\n)?|  ##  end of HTML tag
236:             '.$this->macroRe.'|          ##  curly tag
237:             \s*(?P<attr>[^\s/>={]+)(?:\s*=\s*(?P<value>["\']|[^\s/>{]+))? ## begin of HTML attribute
238:         ~xsi');
239: 
240:         if (!$matches || !empty($matches['macro']) || !empty($matches['comment'])) { // EOF or {macro}
241: 
242:         } elseif (!empty($matches['end'])) { // end of HTML tag />
243:             $node = end($this->htmlNodes);
244:             $isEmpty = !$node->closing && (strpos($matches['end'], '/') !== FALSE || $node->isEmpty);
245: 
246:             if ($isEmpty) {
247:                 $matches[0] = (Nette\Utils\Html::$xhtml ? ' />' : '>')
248:                     . (isset($matches['tagnewline']) ? $matches['tagnewline'] : '');
249:             }
250: 
251:             if (!empty($node->attrs)) {
252:                 $code = substr($this->output, $node->offset) . $matches[0];
253:                 $this->output = substr($this->output, 0, $node->offset);
254:                 $this->writeAttrsMacro($code, $node->attrs, $node->closing);
255:                 if ($isEmpty) {
256:                     $this->writeAttrsMacro('', $node->attrs, TRUE);
257:                 }
258:                 $matches[0] = ''; // remove from output
259:             }
260: 
261:             if ($isEmpty) {
262:                 $node->closing = TRUE;
263:             }
264: 
265:             if (!$node->closing && (strcasecmp($node->name, 'script') === 0 || strcasecmp($node->name, 'style') === 0)) {
266:                 $this->context = array(self::CONTEXT_CDATA, strcasecmp($node->name, 'style') ? 'js' : 'css');
267:             } else {
268:                 $this->context = array(self::CONTEXT_TEXT);
269:                 if ($node->closing) {
270:                     array_pop($this->htmlNodes);
271:                 }
272:             }
273: 
274:         } else { // HTML attribute
275:             $name = $matches['attr'];
276:             $value = isset($matches['value']) ? $matches['value'] : '';
277:             $node = end($this->htmlNodes);
278: 
279:             if (Strings::startsWith($name, self::N_PREFIX)) {
280:                 $name = substr($name, strlen(self::N_PREFIX));
281:                 if ($value === '"' || $value === "'") {
282:                     if ($matches = $this->match('~(.*?)' . $value . '~xsi')) { // overwrites $matches
283:                         $value = $matches[1];
284:                     }
285:                 }
286:                 $node->attrs[$name] = $value;
287:                 $matches[0] = ''; // remove from output
288: 
289:             } elseif ($value === '"' || $value === "'") { // attribute = "'
290:                 $this->context = array(self::CONTEXT_ATTRIBUTE, $name, $value);
291:             }
292:         }
293:         return $matches;
294:     }
295: 
296: 
297: 
298:     /**
299:      * Handles CONTEXT_ATTRIBUTE.
300:      */
301:     private function contextAttribute()
302:     {
303:         $matches = $this->match('~
304:             (' . $this->context[2] . ')|      ##  1) end of HTML attribute
305:             '.$this->macroRe.'                ##  curly tag
306:         ~xsi');
307: 
308:         if ($matches && empty($matches['macro']) && empty($matches['comment'])) { // (attribute end) '"
309:             $this->context = array(self::CONTEXT_TAG);
310:         }
311:         return $matches;
312:     }
313: 
314: 
315: 
316:     /**
317:      * Handles CONTEXT_COMMENT.
318:      */
319:     private function contextComment()
320:     {
321:         $matches = $this->match('~
322:             (--\s*>)|                    ##  1) end of HTML comment
323:             '.$this->macroRe.'           ##  curly tag
324:         ~xsi');
325: 
326:         if ($matches && empty($matches['macro']) && empty($matches['comment'])) { // --\s*>
327:             $this->context = array(self::CONTEXT_TEXT);
328:         }
329:         return $matches;
330:     }
331: 
332: 
333: 
334:     /**
335:      * Handles CONTEXT_NONE.
336:      */
337:     private function contextNone()
338:     {
339:         $matches = $this->match('~
340:             '.$this->macroRe.'           ##  curly tag
341:         ~xsi');
342:         return $matches;
343:     }
344: 
345: 
346: 
347:     /**
348:      * Matches next token.
349:      * @param  string
350:      * @return array
351:      */
352:     private function match($re)
353:     {
354:         if ($matches = Strings::match($this->input, $re, PREG_OFFSET_CAPTURE, $this->offset)) {
355:             $this->output .= substr($this->input, $this->offset, $matches[0][1] - $this->offset);
356:             $this->offset = $matches[0][1] + strlen($matches[0][0]);
357:             foreach ($matches as $k => $v) $matches[$k] = $v[0];
358:         }
359:         return $matches;
360:     }
361: 
362: 
363: 
364:     /**
365:      * Returns current line number.
366:      * @return int
367:      */
368:     public function getLine()
369:     {
370:         return $this->input && $this->offset ? substr_count($this->input, "\n", 0, $this->offset - 1) + 1 : NULL;
371:     }
372: 
373: 
374: 
375:     /**
376:      * Changes macro delimiters.
377:      * @param  string  left regular expression
378:      * @param  string  right regular expression
379:      * @return Engine  provides a fluent interface
380:      */
381:     public function setDelimiters($left, $right)
382:     {
383:         $this->macroRe = '
384:             (?P<comment>' . $left . '\\*.*?\\*' . $right . '\n{0,2})|
385:             ' . $left . '
386:                 (?P<macro>(?:' . self::RE_STRING . '|[^\'"]+?)*?)
387:             ' . $right . '
388:             (?P<rmargin>[ \t]*(?=\n))?
389:         ';
390:         return $this;
391:     }
392: 
393: 
394: 
395:     /********************* macros ****************d*g**/
396: 
397: 
398: 
399:     /**
400:      * Generates code for {macro ...} to the output.
401:      * @param  string
402:      * @param  string
403:      * @param  string
404:      * @param  bool
405:      * @return void
406:      */
407:     public function writeMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE)
408:     {
409:         $isLeftmost = trim(substr($this->output, $leftOfs = strrpos("\n$this->output", "\n"))) === '';
410: 
411:         if ($name[0] === '/') { // closing
412:             $node = end($this->macroNodes);
413: 
414:             if (!$node || ("/$node->name" !== $name && '/' !== $name) || $modifiers
415:                 || ($args && $node->args && !Strings::startsWith("$node->args ", "$args "))
416:             ) {
417:                 $name .= $args ? ' ' : '';
418:                 throw new ParseException("Unexpected macro {{$name}{$args}{$modifiers}}"
419:                     . ($node ? ", expecting {/$node->name}" . ($args && $node->args ? " or eventually {/$node->name $node->args}" : '') : ''),
420:                     0, $this->getLine());
421:             }
422: 
423:             array_pop($this->macroNodes);
424:             if (!$node->args) {
425:                 $node->setArgs($args);
426:             }
427:             if ($isLeftmost && $isRightmost) {
428:                 $this->output = substr($this->output, 0, $leftOfs); // alone macro -> remove indentation
429:             }
430: 
431:             $code = $node->close(substr($this->output, $node->offset));
432: 
433:             if (!$isLeftmost && $isRightmost && substr($code, -2) === '?>') {
434:                 $code .= "\n"; // double newline to avoid newline eating by PHP
435:             }
436:             $this->output = substr($this->output, 0, $node->offset) . $node->content. $code;
437: 
438:         } else { // opening
439:             list($node, $code) = $this->expandMacro($name, $args, $modifiers);
440:             if (!$node->isEmpty) {
441:                 $this->macroNodes[] = $node;
442:             }
443: 
444:             if ($isRightmost) {
445:                 if ($isLeftmost && substr($code, 0, 11) !== '<?php echo ') {
446:                     $this->output = substr($this->output, 0, $leftOfs); // alone macro without output -> remove indentation
447:                 } elseif (substr($code, -2) === '?>') {
448:                     $code .= "\n"; // double newline to avoid newline eating by PHP
449:                 }
450:             }
451: 
452:             $this->output .= $code;
453:             $node->offset = strlen($this->output);
454:         }
455:     }
456: 
457: 
458: 
459:     /**
460:      * Generates code for macro <tag n:attr> to the output.
461:      * @param  string
462:      * @param  array
463:      * @param  bool
464:      * @return void
465:      */
466:     public function writeAttrsMacro($code, $attrs, $closing)
467:     {
468:         $left = $right = array();
469:         foreach ($this->macros as $name => $foo) {
470:             if ($name[0] === '@') {
471:                 $name = substr($name, 1);
472:                 if (isset($attrs[$name])) {
473:                     if (!$closing) {
474:                         $pos = strrpos($code, '>');
475:                         if ($code[$pos-1] === '/') {
476:                             $pos--;
477:                         }
478:                         list(, $macroCode) = $this->expandMacro("@$name", $attrs[$name]);
479:                         $code = substr_replace($code, $macroCode, $pos, 0);
480:                     }
481:                     unset($attrs[$name]);
482:                 }
483:             }
484: 
485:             $macro = $closing ? "/$name" : $name;
486:             if (isset($attrs[$name])) {
487:                 if ($closing) {
488:                     $right[] = array($macro, '');
489:                 } else {
490:                     array_unshift($left, array($macro, $attrs[$name]));
491:                 }
492:             }
493: 
494:             $innerName = "inner-$name";
495:             if (isset($attrs[$innerName])) {
496:                 if ($closing) {
497:                     $left[] = array($macro, '');
498:                 } else {
499:                     array_unshift($right, array($macro, $attrs[$innerName]));
500:                 }
501:             }
502: 
503:             $tagName = "tag-$name";
504:             if (isset($attrs[$tagName])) {
505:                 array_unshift($left, array($name, $attrs[$tagName]));
506:                 $right[] = array("/$name", '');
507:             }
508: 
509:             unset($attrs[$name], $attrs[$innerName], $attrs[$tagName]);
510:         }
511: 
512:         if ($attrs) {
513:             throw new ParseException("Unknown macro-attribute " . self::N_PREFIX
514:                 . implode(' and ' . self::N_PREFIX, array_keys($attrs)), 0, $this->getLine());
515:         }
516: 
517:         foreach ($left as $item) {
518:             $this->writeMacro($item[0], $item[1]);
519:             if (substr($this->output, -2) === '?>') {
520:                 $this->output .= "\n";
521:             }
522:         }
523:         $this->output .= $code;
524: 
525:         foreach ($right as $item) {
526:             $this->writeMacro($item[0], $item[1]);
527:             if (substr($this->output, -2) === '?>') {
528:                 $this->output .= "\n";
529:             }
530:         }
531:     }
532: 
533: 
534: 
535:     /**
536:      * Expands macro and returns node & code.
537:      * @param  string
538:      * @param  string
539:      * @param  string
540:      * @return array(MacroNode, string)
541:      */
542:     public function expandMacro($name, $args, $modifiers = NULL)
543:     {
544:         if (empty($this->macros[$name])) {
545:             throw new ParseException("Unknown macro {{$name}}", 0, $this->getLine());
546:         }
547:         foreach (array_reverse($this->macros[$name]) as $macro) {
548:             $node = new MacroNode($macro, $name, $args, $modifiers, $this->macroNodes ? end($this->macroNodes) : NULL);
549:             $code = $macro->nodeOpened($node);
550:             if ($code !== FALSE) {
551:                 return array($node, $code);
552:             }
553:         }
554:         throw new ParseException("Unhandled macro {{$name}}", 0, $this->getLine());
555:     }
556: 
557: 
558: 
559:     /**
560:      * Parses macro to name, arguments a modifiers parts.
561:      * @param  string {name arguments | modifiers}
562:      * @return array
563:      */
564:     public function parseMacro($macro)
565:     {
566:         $match = Strings::match($macro, '~^
567:             (
568:                 (?P<name>\?|/?[a-z]\w*+(?:[.:]\w+)*+(?!::|\())|   ## ?, name, /name, but not function( or class::
569:                 (?P<noescape>!?)(?P<shortname>/?[=\~#%^&_]?)      ## [!] [=] expression to print
570:             )(?P<args>.*?)
571:             (?P<modifiers>\|[a-z](?:'.Parser::RE_STRING.'|[^\'"]+)*)?
572:         ()$~isx');
573: 
574:         if (!$match) {
575:             return FALSE;
576:         }
577:         if ($match['name'] === '') {
578:             $match['name'] = $match['shortname'] ?: '=';
579:             if (!$match['noescape'] && substr($match['shortname'], 0, 1) !== '/') {
580:                 $match['modifiers'] .= '|escape';
581:             }
582:         }
583:         return array($match['name'], trim($match['args']), $match['modifiers']);
584:     }
585: 
586: }
587: 
Nette Framework 2.0beta1 API API documentation generated by ApiGen 2.3.0