Namespaces

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

Classes

  • BaseTemplate
  • CachingHelper
  • CurlyBracketsMacros
  • LatteFilter
  • LatteMacros
  • SnippetHelper
  • Template
  • TemplateCacheStorage
  • TemplateFilters
  • TemplateHelpers

Interfaces

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