Packages

  • Nette
    • Application
    • Caching
    • Collections
    • Config
    • Forms
    • IO
    • Loaders
    • Mail
    • Reflection
    • Security
    • Templates
    • Web
  • None
  • PHP

Classes

  • NBaseTemplate
  • NCachingHelper
  • NCurlyBracketsMacros
  • NLatteFilter
  • NLatteMacros
  • NSnippetHelper
  • NTemplate
  • NTemplateCacheStorage
  • NTemplateFilters
  • NTemplateHelpers

Interfaces

  • IFileTemplate
  • ITemplate
  • Overview
  • Package
  • Class
  • Tree
  • Other releases
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Nette Framework (https://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\Templates
 11:  */
 12: 
 13: 
 14: 
 15: /**
 16:  * Compile-time filter Latte.
 17:  *
 18:  * @author     David Grudl
 19:  * @package Nette\Templates
 20:  */
 21: class NLatteFilter extends NObject
 22: {
 23:     /** @internal single & double quoted PHP string */
 24:     const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';
 25: 
 26:     /** @internal PHP identifier */
 27:     const RE_IDENTIFIER = '[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF]*';
 28: 
 29:     /** @internal special HTML tag or attribute prefix */
 30:     const HTML_PREFIX = 'n:';
 31: 
 32:     /** @var ILatteHandler */
 33:     private $handler;
 34: 
 35:     /** @var string */
 36:     private $macroRe;
 37: 
 38:     /** @var string */
 39:     private $input, $output;
 40: 
 41:     /** @var int */
 42:     private $offset;
 43: 
 44:     /** @var strng (for CONTEXT_ATTRIBUTE) */
 45:     private $quote;
 46: 
 47:     /** @var array */
 48:     private $tags;
 49: 
 50:     /** @var string */
 51:     public $context, $escape;
 52: 
 53:     /**#@+ @internal Context-aware escaping states */
 54:     const CONTEXT_TEXT = 'text';
 55:     const CONTEXT_CDATA = 'cdata';
 56:     const CONTEXT_TAG = 'tag';
 57:     const CONTEXT_ATTRIBUTE = 'attribute';
 58:     const CONTEXT_NONE = 'none';
 59:     const CONTEXT_COMMENT = 'comment';
 60:     /**#@-*/
 61: 
 62: 
 63: 
 64:     /**
 65:      * Sets a macro handler.
 66:      * @param  ILatteHandler
 67:      * @return NLatteFilter  provides a fluent interface
 68:      */
 69:     public function setHandler($handler)
 70:     {
 71:         $this->handler = $handler;
 72:         return $this;
 73:     }
 74: 
 75: 
 76: 
 77:     /**
 78:      * Returns macro handler.
 79:      * @return ILatteHandler
 80:      */
 81:     public function getHandler()
 82:     {
 83:         if ($this->handler === NULL) {
 84:             $this->handler = new NLatteMacros;
 85:         }
 86:         return $this->handler;
 87:     }
 88: 
 89: 
 90: 
 91:     /**
 92:      * Invokes filter.
 93:      * @param  string
 94:      * @return string
 95:      */
 96:     public function __invoke($s)
 97:     {
 98:         if (!$this->macroRe) {
 99:             $this->setDelimiters('\\{(?![\\s\'"{}])', '\\}');
100:         }
101: 
102:         // context-aware escaping
103:         $this->context = NLatteFilter::CONTEXT_NONE;
104:         $this->escape = '$template->escape';
105: 
106:         // initialize handlers
107:         $this->getHandler()->initialize($this, $s);
108: 
109:         // process all {tags} and <tags/>
110:         $s = str_replace("\r\n", "\n", $s);
111:         $s = $this->parse("\n" . $s);
112: 
113:         $this->getHandler()->finalize($s);
114: 
115:         return $s;
116:     }
117: 
118: 
119: 
120:     /**
121:      * Searches for curly brackets, HTML tags and attributes.
122:      * @param  string
123:      * @return string
124:      */
125:     private function parse($s)
126:     {
127:         $this->input = & $s;
128:         $this->offset = 0;
129:         $this->output = '';
130:         $this->tags = array();
131:         $len = strlen($s);
132: 
133:         while ($this->offset < $len) {
134:             $matches = $this->{"context$this->context"}();
135: 
136:             if (!$matches) { // EOF
137:                 break;
138: 
139:             } elseif (!empty($matches['macro'])) { // {macro|modifiers}
140:                 preg_match('#^(/?[a-z]+)?(.*?)(\\|[a-z](?:'.self::RE_STRING.'|[^\'"]+)*)?$()#is', $matches['macro'], $m2);
141:                 list(, $macro, $value, $modifiers) = $m2;
142:                 $code = $this->handler->macro($macro, trim($value), isset($modifiers) ? $modifiers : '');
143:                 if ($code === NULL) {
144:                     throw new InvalidStateException("Unknown macro {{$matches['macro']}} on line $this->line.");
145:                 }
146:                 $nl = isset($matches['newline']) ? "\n" : ''; // double newline
147:                 if ($nl && $matches['indent'] && strncmp($code, '<?php echo ', 11)) {
148:                     $this->output .= "\n" . $code; // remove indent, single newline
149:                 } else {
150:                     $this->output .= $matches['indent'] . $code . (substr($code, -2) === '?>' && $this->output !== '' ? $nl : '');
151:                 }
152: 
153:             } else { // common behaviour
154:                 $this->output .= $matches[0];
155:             }
156:         }
157: 
158:         foreach ($this->tags as $tag) {
159:             if (!$tag->isMacro && !empty($tag->attrs)) {
160:                 throw new InvalidStateException("Missing end tag </$tag->name> for macro-attribute " . self::HTML_PREFIX . implode(' and ' . self::HTML_PREFIX, array_keys($tag->attrs)) . ".");
161:             }
162:         }
163: 
164:         return $this->output . substr($this->input, $this->offset);
165:     }
166: 
167: 
168: 
169:     /**
170:      * Handles CONTEXT_TEXT.
171:      */
172:     private function contextText()
173:     {
174:         $matches = $this->match('~
175:             (?:(?<=\n)[ \t]*)?<(?P<closing>/?)(?P<tag>[a-z0-9:]+)|  ##  begin of HTML tag <tag </tag - ignores <!DOCTYPE
176:             <(?P<comment>!--)|           ##  begin of HTML comment <!--
177:             '.$this->macroRe.'           ##  curly tag
178:         ~xsi');
179: 
180:         if (!$matches || !empty($matches['macro'])) { // EOF or {macro}
181: 
182:         } elseif (!empty($matches['comment'])) { // <!--
183:             $this->context = self::CONTEXT_COMMENT;
184:             $this->escape = 'NTemplateHelpers::escapeHtmlComment';
185: 
186:         } elseif (empty($matches['closing'])) { // <tag
187:             $tag = $this->tags[] = (object) NULL;
188:             $tag->name = $matches['tag'];
189:             $tag->closing = FALSE;
190:             $tag->isMacro = NString::startsWith($tag->name, self::HTML_PREFIX);
191:             $tag->attrs = array();
192:             $tag->pos = strlen($this->output);
193:             $this->context = self::CONTEXT_TAG;
194:             $this->escape = 'NTemplateHelpers::escapeHtml';
195: 
196:         } else { // </tag
197:             do {
198:                 $tag = array_pop($this->tags);
199:                 if (!$tag) {
200:                     //throw new InvalidStateException("End tag for element '$matches[tag]' which is not open on line $this->line.");
201:                     $tag = (object) NULL;
202:                     $tag->name = $matches['tag'];
203:                     $tag->isMacro = NString::startsWith($tag->name, self::HTML_PREFIX);
204:                 }
205:             } while (strcasecmp($tag->name, $matches['tag']));
206:             $this->tags[] = $tag;
207:             $tag->closing = TRUE;
208:             $tag->pos = strlen($this->output);
209:             $this->context = self::CONTEXT_TAG;
210:             $this->escape = 'NTemplateHelpers::escapeHtml';
211:         }
212:         return $matches;
213:     }
214: 
215: 
216: 
217:     /**
218:      * Handles CONTEXT_CDATA.
219:      */
220:     private function contextCData()
221:     {
222:         $tag = end($this->tags);
223:         $matches = $this->match('~
224:             </'.$tag->name.'(?![a-z0-9:])| ##  end HTML tag </tag
225:             '.$this->macroRe.'           ##  curly tag
226:         ~xsi');
227: 
228:         if ($matches && empty($matches['macro'])) { // </tag
229:             $tag->closing = TRUE;
230:             $tag->pos = strlen($this->output);
231:             $this->context = self::CONTEXT_TAG;
232:             $this->escape = 'NTemplateHelpers::escapeHtml';
233:         }
234:         return $matches;
235:     }
236: 
237: 
238: 
239:     /**
240:      * Handles CONTEXT_TAG.
241:      */
242:     private function contextTag()
243:     {
244:         $matches = $this->match('~
245:             (?P<end>/?>)(?P<tagnewline>[\ \t]*(?=\n))?|  ##  end of HTML tag
246:             '.$this->macroRe.'|          ##  curly tag
247:             \s*(?P<attr>[^\s/>={]+)(?:\s*=\s*(?P<value>["\']|[^\s/>{]+))? ## begin of HTML attribute
248:         ~xsi');
249: 
250:         if (!$matches || !empty($matches['macro'])) { // EOF or {macro}
251: 
252:         } elseif (!empty($matches['end'])) { // end of HTML tag />
253:             $tag = end($this->tags);
254:             $isEmpty = !$tag->closing && ($matches['end'][0] === '/' || isset(NHtml::$emptyElements[strtolower($tag->name)]));
255: 
256:             if ($tag->isMacro || !empty($tag->attrs)) {
257:                 if ($tag->isMacro) {
258:                     $code = $this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, $tag->closing);
259:                     if ($code === NULL) {
260:                         throw new InvalidStateException("Unknown tag-macro <$tag->name> on line $this->line.");
261:                     }
262:                     if ($isEmpty) {
263:                         $code .= $this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, TRUE);
264:                     }
265:                 } else {
266:                     $code = substr($this->output, $tag->pos) . $matches[0] . (isset($matches['tagnewline']) ? "\n" : '');
267:                     $code = $this->handler->attrsMacro($code, $tag->attrs, $tag->closing);
268:                     if ($code === NULL) {
269:                         throw new InvalidStateException("Unknown macro-attribute " . self::HTML_PREFIX . implode(' or ' . self::HTML_PREFIX, array_keys($tag->attrs)) . " on line $this->line.");
270:                     }
271:                     if ($isEmpty) {
272:                         $code = $this->handler->attrsMacro($code, $tag->attrs, TRUE);
273:                     }
274:                 }
275:                 $this->output = substr_replace($this->output, $code, $tag->pos);
276:                 $matches[0] = ''; // remove from output
277:             }
278: 
279:             if ($isEmpty) {
280:                 $tag->closing = TRUE;
281:             }
282: 
283:             if (!$tag->closing && (strcasecmp($tag->name, 'script') === 0 || strcasecmp($tag->name, 'style') === 0)) {
284:                 $this->context = self::CONTEXT_CDATA;
285:                 $this->escape = strcasecmp($tag->name, 'style') ? 'NTemplateHelpers::escapeJs' : 'NTemplateHelpers::escapeCss';
286:             } else {
287:                 $this->context = self::CONTEXT_TEXT;
288:                 $this->escape = 'NTemplateHelpers::escapeHtml';
289:                 if ($tag->closing) array_pop($this->tags);
290:             }
291: 
292:         } else { // HTML attribute
293:             $name = $matches['attr'];
294:             $value = empty($matches['value']) ? TRUE : $matches['value'];
295: 
296:             // special attribute?
297:             if ($isSpecial = NString::startsWith($name, self::HTML_PREFIX)) {
298:                 $name = substr($name, strlen(self::HTML_PREFIX));
299:             }
300:             $tag = end($this->tags);
301:             if ($isSpecial || $tag->isMacro) {
302:                 if ($value === '"' || $value === "'") {
303:                     if ($matches = $this->match('~(.*?)' . $value . '~xsi')) { // overwrites $matches
304:                         $value = $matches[1];
305:                     }
306:                 }
307:                 $tag->attrs[$name] = $value;
308:                 $matches[0] = ''; // remove from output
309: 
310:             } elseif ($value === '"' || $value === "'") { // attribute = "'
311:                 $this->context = self::CONTEXT_ATTRIBUTE;
312:                 $this->quote = $value;
313:                 $this->escape = strncasecmp($name, 'on', 2)
314:                     ? (strcasecmp($name, 'style') ? 'NTemplateHelpers::escapeHtml' : 'NTemplateHelpers::escapeHtmlCss')
315:                     : 'NTemplateHelpers::escapeHtmlJs';
316:             }
317:         }
318:         return $matches;
319:     }
320: 
321: 
322: 
323:     /**
324:      * Handles CONTEXT_ATTRIBUTE.
325:      */
326:     private function contextAttribute()
327:     {
328:         $matches = $this->match('~
329:             (' . $this->quote . ')|      ##  1) end of HTML attribute
330:             '.$this->macroRe.'           ##  curly tag
331:         ~xsi');
332: 
333:         if ($matches && empty($matches['macro'])) { // (attribute end) '"
334:             $this->context = self::CONTEXT_TAG;
335:             $this->escape = 'NTemplateHelpers::escapeHtml';
336:         }
337:         return $matches;
338:     }
339: 
340: 
341: 
342:     /**
343:      * Handles CONTEXT_COMMENT.
344:      */
345:     private function contextComment()
346:     {
347:         $matches = $this->match('~
348:             (--\s*>)|                    ##  1) end of HTML comment
349:             '.$this->macroRe.'           ##  curly tag
350:         ~xsi');
351: 
352:         if ($matches && empty($matches['macro'])) { // --\s*>
353:             $this->context = self::CONTEXT_TEXT;
354:             $this->escape = 'NTemplateHelpers::escapeHtml';
355:         }
356:         return $matches;
357:     }
358: 
359: 
360: 
361:     /**
362:      * Handles CONTEXT_NONE.
363:      */
364:     private function contextNone()
365:     {
366:         $matches = $this->match('~
367:             '.$this->macroRe.'           ##  curly tag
368:         ~xsi');
369:         return $matches;
370:     }
371: 
372: 
373: 
374:     /**
375:      * Matches next token.
376:      * @param  string
377:      * @return array
378:      */
379:     private function match($re)
380:     {
381:         if (preg_match($re, $this->input, $matches, PREG_OFFSET_CAPTURE, $this->offset)) {
382:             $this->output .= substr($this->input, $this->offset, $matches[0][1] - $this->offset);
383:             $this->offset = $matches[0][1] + strlen($matches[0][0]);
384:             foreach ($matches as $k => $v) $matches[$k] = $v[0];
385:         }
386:         return $matches;
387:     }
388: 
389: 
390: 
391:     /**
392:      * Returns current line number.
393:      * @return int
394:      */
395:     public function getLine()
396:     {
397:         return substr_count($this->input, "\n", 0, $this->offset);
398:     }
399: 
400: 
401: 
402:     /**
403:      * Changes macro delimiters.
404:      * @param  string  left regular expression
405:      * @param  string  right regular expression
406:      * @return NLatteFilter  provides a fluent interface
407:      */
408:     public function setDelimiters($left, $right)
409:     {
410:         $this->macroRe = '
411:             (?P<indent>\n[\ \t]*)?
412:             ' . $left . '
413:                 (?P<macro>(?:' . self::RE_STRING . '|[^\'"]+?)*?)
414:             ' . $right . '
415:             (?P<newline>[\ \t]*(?=\n))?
416:         ';
417:         return $this;
418:     }
419: 
420: 
421: 
422:     /********************* compile-time helpers ****************d*g**/
423: 
424: 
425: 
426:     /**
427:      * Applies modifiers.
428:      * @param  string
429:      * @param  string
430:      * @return string
431:      */
432:     public static function formatModifiers($var, $modifiers)
433:     {
434:         if (!$modifiers) return $var;
435:         preg_match_all(
436:             '~
437:                 '.self::RE_STRING.'|  ## single or double quoted string
438:                 [^\'"|:,\s]+|         ## symbol
439:                 [|:,]                 ## separator
440:             ~xs',
441:             $modifiers . '|',
442:             $tokens
443:         );
444:         $inside = FALSE;
445:         $prev = '';
446:         foreach ($tokens[0] as $token) {
447:             if ($token === '|' || $token === ':' || $token === ',') {
448:                 if ($prev === '') {
449: 
450:                 } elseif (!$inside) {
451:                     if (!preg_match('#^'.self::RE_IDENTIFIER.'$#', $prev)) {
452:                         throw new InvalidStateException("Modifier name must be alphanumeric string, '$prev' given.");
453:                     }
454:                     $var = "\$template->$prev($var";
455:                     $prev = '';
456:                     $inside = TRUE;
457: 
458:                 } else {
459:                     $var .= ', ' . self::formatString($prev);
460:                     $prev = '';
461:                 }
462: 
463:                 if ($token === '|' && $inside) {
464:                     $var .= ')';
465:                     $inside = FALSE;
466:                 }
467:             } else {
468:                 $prev .= $token;
469:             }
470:         }
471:         return $var;
472:     }
473: 
474: 
475: 
476:     /**
477:      * Reads single token (optionally delimited by comma) from string.
478:      * @param  string
479:      * @return string
480:      */
481:     public static function fetchToken(& $s)
482:     {
483:         if (preg_match('#^((?>'.self::RE_STRING.'|[^\'"\s,]+)+)\s*,?\s*(.*)$#', $s, $matches)) { // token [,] tail
484:             $s = $matches[2];
485:             return $matches[1];
486:         }
487:         return NULL;
488:     }
489: 
490: 
491: 
492:     /**
493:      * Formats parameters to PHP array.
494:      * @param  string
495:      * @param  string
496:      * @return string
497:      */
498:     public static function formatArray($s, $prefix = '')
499:     {
500:         $s = preg_replace_callback(
501:             '~
502:                 '.self::RE_STRING.'|                          ## single or double quoted string
503:                 (?<=[,=(]|=>|^)\s*([a-z\d_]+)(?=\s*[,=)]|$)   ## 1) symbol
504:             ~xi',
505:             array(__CLASS__, 'cbArgs'),
506:             trim($s)
507:         );
508:         $s = preg_replace('#\$(' . self::RE_IDENTIFIER . ')\s*=>#', '"$1" =>', $s);
509:         return $s === '' ? '' : $prefix . "array($s)";
510:     }
511: 
512: 
513: 
514:     /**
515:      * Callback for formatArgs().
516:      */
517:     private static function cbArgs($matches)
518:     {
519:         //    [1] => symbol
520: 
521:         if (!empty($matches[1])) { // symbol
522:             list(, $symbol) = $matches;
523:             static $keywords = array('true'=>1, 'false'=>1, 'null'=>1, 'and'=>1, 'or'=>1, 'xor'=>1, 'clone'=>1, 'new'=>1);
524:             return is_numeric($symbol) || isset($keywords[strtolower($symbol)]) ? $matches[0] : "'$symbol'";
525: 
526:         } else {
527:             return $matches[0];
528:         }
529:     }
530: 
531: 
532: 
533:     /**
534:      * Formats parameter to PHP string.
535:      * @param  string
536:      * @return string
537:      */
538:     public static function formatString($s)
539:     {
540:         static $keywords = array('true'=>1, 'false'=>1, 'null'=>1);
541:         return (is_numeric($s) || strspn($s, '\'"$') || isset($keywords[strtolower($s)])) ? $s : '"' . $s . '"';
542:     }
543: 
544: 
545: 
546:     /**
547:      * Invokes filter.
548:      * @deprecated
549:      */
550:     public static function invoke($s)
551:     {
552:         trigger_error(__METHOD__ . '() is deprecated; use non-static __invoke() instead.', E_USER_WARNING);
553:         $filter = new self;
554:         return $filter->__invoke($s);
555:     }
556: 
557: }
558: 
559: 
560: 
561: /** @deprecated */
562: class NCurlyBracketsFilter extends NLatteFilter {}
563: class NCurlyBracketsMacros extends NLatteMacros {}
564: 
Nette Framework 0.9.7 (for PHP 5.2) API documentation generated by ApiGen 2.3.0