Packages

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

Classes

  • NLatteFilter
  • NMacroNode
  • NMacroTokenizer
  • NParser
  • NPhpWriter

Interfaces

  • IMacro

Exceptions

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