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: