1: <?php
  2: 
  3:   4:   5:   6:   7:   8:   9:  10: 
 11: 
 12: namespace Nette\Templates;
 13: 
 14: use Nette;
 15: 
 16: 
 17: 
 18:  19:  20:  21:  22: 
 23: class LatteFilter extends Nette\Object
 24: {
 25:     
 26:     const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';
 27: 
 28:     
 29:     const RE_IDENTIFIER = '[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF]*';
 30: 
 31:     
 32:     const HTML_PREFIX = 'n:';
 33: 
 34:     
 35:     private $handler;
 36: 
 37:     
 38:     private $macroRe;
 39: 
 40:     
 41:     private $input, $output;
 42: 
 43:     
 44:     private $offset;
 45: 
 46:     
 47:     private $quote;
 48: 
 49:     
 50:     private $tags;
 51: 
 52:     
 53:     public $context, $escape;
 54: 
 55:     
 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:  68:  69:  70: 
 71:     public function setHandler($handler)
 72:     {
 73:         $this->handler = $handler;
 74:         return $this;
 75:     }
 76: 
 77: 
 78: 
 79:      80:  81:  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:  95:  96:  97: 
 98:     public function __invoke($s)
 99:     {
100:         if (!$this->macroRe) {
101:             $this->setDelimiters('\\{(?![\\s\'"{}])', '\\}');
102:         }
103: 
104:         
105:         $this->context = LatteFilter::CONTEXT_NONE;
106:         $this->escape = '$template->escape';
107: 
108:         
109:         $this->getHandler()->initialize($this, $s);
110: 
111:         
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: 124: 125: 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) { 
139:                 break;
140: 
141:             } elseif (!empty($matches['macro'])) { 
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" : ''; 
149:                 if ($nl && $matches['indent'] && strncmp($code, '<?php echo ', 11)) {
150:                     $this->output .= "\n" . $code; 
151:                 } else {
152:                     $this->output .= $matches['indent'] . $code . (substr($code, -2) === '?>' && $this->output !== '' ? $nl : '');
153:                 }
154: 
155:             } else { 
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: 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'])) { 
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'])) { 
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 { 
199:             do {
200:                 $tag = array_pop($this->tags);
201:                 if (!$tag) {
202:                     
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: 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'])) { 
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: 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'])) { 
253: 
254:         } elseif (!empty($matches['end'])) { 
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] = ''; 
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 { 
295:             $name = $matches['attr'];
296:             $value = empty($matches['value']) ? TRUE : $matches['value'];
297: 
298:             
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')) { 
306:                         $value = $matches[1];
307:                     }
308:                 }
309:                 $tag->attrs[$name] = $value;
310:                 $matches[0] = ''; 
311: 
312:             } elseif ($value === '"' || $value === "'") { 
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: 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'])) { 
336:             $this->context = self::CONTEXT_TAG;
337:             $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
338:         }
339:         return $matches;
340:     }
341: 
342: 
343: 
344:     345: 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'])) { 
355:             $this->context = self::CONTEXT_TEXT;
356:             $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
357:         }
358:         return $matches;
359:     }
360: 
361: 
362: 
363:     364: 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: 378: 379: 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: 395: 396: 
397:     public function getLine()
398:     {
399:         return substr_count($this->input, "\n", 0, $this->offset);
400:     }
401: 
402: 
403: 
404:     405: 406: 407: 408: 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:     
425: 
426: 
427: 
428:     429: 430: 431: 432: 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: 480: 481: 482: 
483:     public static function fetchToken(& $s)
484:     {
485:         if (preg_match('#^((?>'.self::RE_STRING.'|[^\'"\s,]+)+)\s*,?\s*(.*)$#', $s, $matches)) { 
486:             $s = $matches[2];
487:             return $matches[1];
488:         }
489:         return NULL;
490:     }
491: 
492: 
493: 
494:     495: 496: 497: 498: 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: 518: 
519:     private static function cbArgs($matches)
520:     {
521:         
522: 
523:         if (!empty($matches[1])) { 
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: 537: 538: 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: 550: 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: 
564: class CurlyBracketsFilter extends LatteFilter {}
565: class CurlyBracketsMacros extends LatteMacros {}
566: