1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette\Templates;
13:
14: use Nette,
15: Nette\String;
16:
17:
18:
19: 20: 21: 22: 23:
24: class LatteFilter extends Nette\Object
25: {
26:
27: const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';
28:
29:
30: const HTML_PREFIX = 'n:';
31:
32:
33: private $handler;
34:
35:
36: private $macroRe;
37:
38:
39: private $input, $output;
40:
41:
42: private $offset;
43:
44:
45: private $quote;
46:
47:
48: private $tags;
49:
50:
51: public $context, $escape;
52:
53:
54: const CONTEXT_TEXT = 'text',
55: CONTEXT_CDATA = 'cdata',
56: CONTEXT_TAG = 'tag',
57: CONTEXT_ATTRIBUTE = 'attribute',
58: CONTEXT_NONE = 'none',
59: CONTEXT_COMMENT = 'comment';
60:
61:
62:
63: 64: 65: 66: 67:
68: public function setHandler($handler)
69: {
70: $this->handler = $handler;
71: return $this;
72: }
73:
74:
75:
76: 77: 78: 79:
80: public function getHandler()
81: {
82: if ($this->handler === NULL) {
83: $this->handler = new LatteMacros;
84: }
85: return $this->handler;
86: }
87:
88:
89:
90: 91: 92: 93: 94:
95: public function __invoke($s)
96: {
97: if (!String::checkEncoding($s)) {
98: throw new LatteException('Template is not valid UTF-8 stream.');
99: }
100:
101: if (!$this->macroRe) {
102: $this->setDelimiters('\\{(?![\\s\'"{}*])', '\\}');
103: }
104:
105: 106: $this->context = LatteFilter::CONTEXT_NONE;
107: $this->escape = '$template->escape';
108:
109: 110: $this->getHandler()->initialize($this, $s);
111:
112: 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['comment'])) { 142:
143: } elseif (!empty($matches['macro'])) { 144: $code = $this->handler->macro($matches['macro']);
145: if ($code === FALSE) {
146: throw new LatteException("Unknown macro {{$matches['macro']}}", 0, $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) === '?>' ? $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 LatteException("Missing end tag </$tag->name> for macro-attribute " . self::HTML_PREFIX . implode(' and ' . self::HTML_PREFIX, array_keys($tag->attrs)) . ".", 0, $this->line);
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<htmlcomment>!--)| ## begin of HTML comment <!--
179: '.$this->macroRe.' ## curly tag
180: ~xsi');
181:
182: if (!$matches || !empty($matches['macro']) || !empty($matches['comment'])) { 183:
184: } elseif (!empty($matches['htmlcomment'])) { 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 = 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 = 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']) && empty($matches['comment'])) { 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]*(?=\r|\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']) || !empty($matches['comment'])) { 253:
254: } elseif (!empty($matches['end'])) { 255: $tag = end($this->tags);
256: $isEmpty = !$tag->closing && (strpos($matches['end'], '/') !== FALSE || isset(Nette\Web\Html::$emptyElements[strtolower($tag->name)]));
257:
258: if ($isEmpty) {
259: $matches[0] = (Nette\Web\Html::$xhtml ? ' />' : '>') . (isset($matches['tagnewline']) ? $matches['tagnewline'] : '');
260: }
261:
262: if ($tag->isMacro || !empty($tag->attrs)) {
263: if ($tag->isMacro) {
264: $code = $this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, $tag->closing);
265: if ($code === FALSE) {
266: throw new LatteException("Unknown tag-macro <$tag->name>", 0, $this->line);
267: }
268: if ($isEmpty) {
269: $code .= $this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, TRUE);
270: }
271: } else {
272: $code = substr($this->output, $tag->pos) . $matches[0] . (isset($matches['tagnewline']) ? "\n" : '');
273: $code = $this->handler->attrsMacro($code, $tag->attrs, $tag->closing);
274: if ($code === FALSE) {
275: throw new LatteException("Unknown macro-attribute " . self::HTML_PREFIX . implode(' or ' . self::HTML_PREFIX, array_keys($tag->attrs)), 0, $this->line);
276: }
277: if ($isEmpty) {
278: $code = $this->handler->attrsMacro($code, $tag->attrs, TRUE);
279: }
280: }
281: $this->output = substr_replace($this->output, $code, $tag->pos);
282: $matches[0] = ''; 283: }
284:
285: if ($isEmpty) {
286: $tag->closing = TRUE;
287: }
288:
289: if (!$tag->closing && (strcasecmp($tag->name, 'script') === 0 || strcasecmp($tag->name, 'style') === 0)) {
290: $this->context = self::CONTEXT_CDATA;
291: $this->escape = strcasecmp($tag->name, 'style') ? 'Nette\Templates\TemplateHelpers::escapeJs' : 'Nette\Templates\TemplateHelpers::escapeCss';
292: } else {
293: $this->context = self::CONTEXT_TEXT;
294: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
295: if ($tag->closing) array_pop($this->tags);
296: }
297:
298: } else { 299: $name = $matches['attr'];
300: $value = isset($matches['value']) ? $matches['value'] : '';
301:
302: 303: if ($isSpecial = String::startsWith($name, self::HTML_PREFIX)) {
304: $name = substr($name, strlen(self::HTML_PREFIX));
305: }
306: $tag = end($this->tags);
307: if ($isSpecial || $tag->isMacro) {
308: if ($value === '"' || $value === "'") {
309: if ($matches = $this->match('~(.*?)' . $value . '~xsi')) { 310: $value = $matches[1];
311: }
312: }
313: $tag->attrs[$name] = $value;
314: $matches[0] = ''; 315:
316: } elseif ($value === '"' || $value === "'") { 317: $this->context = self::CONTEXT_ATTRIBUTE;
318: $this->quote = $value;
319: $this->escape = strncasecmp($name, 'on', 2)
320: ? (strcasecmp($name, 'style') ? 'Nette\Templates\TemplateHelpers::escapeHtml' : 'Nette\Templates\TemplateHelpers::escapeHtmlCss')
321: : 'Nette\Templates\TemplateHelpers::escapeHtmlJs';
322: }
323: }
324: return $matches;
325: }
326:
327:
328:
329: 330: 331:
332: private function contextAttribute()
333: {
334: $matches = $this->match('~
335: (' . $this->quote . ')| ## 1) end of HTML attribute
336: '.$this->macroRe.' ## curly tag
337: ~xsi');
338:
339: if ($matches && empty($matches['macro']) && empty($matches['comment'])) { 340: $this->context = self::CONTEXT_TAG;
341: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
342: }
343: return $matches;
344: }
345:
346:
347:
348: 349: 350:
351: private function contextComment()
352: {
353: $matches = $this->match('~
354: (--\s*>)| ## 1) end of HTML comment
355: '.$this->macroRe.' ## curly tag
356: ~xsi');
357:
358: if ($matches && empty($matches['macro']) && empty($matches['comment'])) { 359: $this->context = self::CONTEXT_TEXT;
360: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
361: }
362: return $matches;
363: }
364:
365:
366:
367: 368: 369:
370: private function contextNone()
371: {
372: $matches = $this->match('~
373: '.$this->macroRe.' ## curly tag
374: ~xsi');
375: return $matches;
376: }
377:
378:
379:
380: 381: 382: 383: 384:
385: private function match($re)
386: {
387: if ($matches = String::match($this->input, $re, PREG_OFFSET_CAPTURE, $this->offset)) {
388: $this->output .= substr($this->input, $this->offset, $matches[0][1] - $this->offset);
389: $this->offset = $matches[0][1] + strlen($matches[0][0]);
390: foreach ($matches as $k => $v) $matches[$k] = $v[0];
391: }
392: return $matches;
393: }
394:
395:
396:
397: 398: 399: 400:
401: public function getLine()
402: {
403: return substr_count($this->input, "\n", 0, $this->offset);
404: }
405:
406:
407:
408: 409: 410: 411: 412: 413:
414: public function setDelimiters($left, $right)
415: {
416: $this->macroRe = '
417: (?:\r?\n?)(?P<comment>\\{\\*.*?\\*\\}[\r\n]{0,2})|
418: (?P<indent>\n[\ \t]*)?
419: ' . $left . '
420: (?P<macro>(?:' . self::RE_STRING . '|[^\'"]+?)*?)
421: ' . $right . '
422: (?P<newline>[\ \t]*(?=\r|\n))?
423: ';
424: return $this;
425: }
426:
427:
428:
429:
430: static function formatModifiers($var, $modifiers)
431: {
432: trigger_error(__METHOD__ . '() is deprecated; use LatteMacros::formatModifiers() instead.', E_USER_WARNING);
433: return LatteMacros::formatModifiers($var, $modifiers);
434: }
435:
436:
437: static function fetchToken(& $s)
438: {
439: trigger_error(__METHOD__ . '() is deprecated; use LatteMacros::fetchToken() instead.', E_USER_WARNING);
440: return LatteMacros::fetchToken($s);
441: }
442:
443:
444: static function formatArray($input, $prefix = '')
445: {
446: trigger_error(__METHOD__ . '() is deprecated; use LatteMacros::formatArray() instead.', E_USER_WARNING);
447: return LatteMacros::formatArray($input, $prefix);
448: }
449:
450:
451: static function formatString($s)
452: {
453: trigger_error(__METHOD__ . '() is deprecated; use LatteMacros::formatString() instead.', E_USER_WARNING);
454: return LatteMacros::formatString($s);
455: }
456:
457: }
458: