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: 153: $this->output .= $matches['indent'] . $code . (substr($code, -2) === '?>' ? $nl : '');
154: }
155:
156: } else { 157: $this->output .= $matches[0];
158: }
159: }
160:
161: foreach ($this->tags as $tag) {
162: if (!$tag->isMacro && !empty($tag->attrs)) {
163: throw new LatteException("Missing end tag </$tag->name> for macro-attribute " . self::HTML_PREFIX
164: . implode(' and ' . self::HTML_PREFIX, array_keys($tag->attrs)) . ".", 0, $this->line);
165: }
166: }
167:
168: return $this->output . substr($this->input, $this->offset);
169: }
170:
171:
172:
173: 174: 175:
176: private function contextText()
177: {
178: $matches = $this->match('~
179: (?:\n[ \t]*)?<(?P<closing>/?)(?P<tag>[a-z0-9:]+)| ## begin of HTML tag <tag </tag - ignores <!DOCTYPE
180: <(?P<htmlcomment>!--)| ## begin of HTML comment <!--
181: '.$this->macroRe.' ## curly tag
182: ~xsi');
183:
184: if (!$matches || !empty($matches['macro']) || !empty($matches['comment'])) { 185:
186: } elseif (!empty($matches['htmlcomment'])) { 187: $this->context = self::CONTEXT_COMMENT;
188: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtmlComment';
189:
190: } elseif (empty($matches['closing'])) { 191: $tag = $this->tags[] = (object) NULL;
192: $tag->name = $matches['tag'];
193: $tag->closing = FALSE;
194: $tag->isMacro = String::startsWith($tag->name, self::HTML_PREFIX);
195: $tag->attrs = array();
196: $tag->pos = strlen($this->output);
197: $this->context = self::CONTEXT_TAG;
198: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
199:
200: } else { 201: do {
202: $tag = array_pop($this->tags);
203: if (!$tag) {
204: 205: $tag = (object) NULL;
206: $tag->name = $matches['tag'];
207: $tag->isMacro = String::startsWith($tag->name, self::HTML_PREFIX);
208: }
209: } while (strcasecmp($tag->name, $matches['tag']));
210: $this->tags[] = $tag;
211: $tag->closing = TRUE;
212: $tag->pos = strlen($this->output);
213: $this->context = self::CONTEXT_TAG;
214: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
215: }
216: return $matches;
217: }
218:
219:
220:
221: 222: 223:
224: private function contextCData()
225: {
226: $tag = end($this->tags);
227: $matches = $this->match('~
228: </'.$tag->name.'(?![a-z0-9:])| ## end HTML tag </tag
229: '.$this->macroRe.' ## curly tag
230: ~xsi');
231:
232: if ($matches && empty($matches['macro']) && empty($matches['comment'])) { 233: $tag->closing = TRUE;
234: $tag->pos = strlen($this->output);
235: $this->context = self::CONTEXT_TAG;
236: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
237: }
238: return $matches;
239: }
240:
241:
242:
243: 244: 245:
246: private function contextTag()
247: {
248: $matches = $this->match('~
249: (?P<end>\ ?/?>)(?P<tagnewline>[\ \t]*(?=\r|\n))?| ## end of HTML tag
250: '.$this->macroRe.'| ## curly tag
251: \s*(?P<attr>[^\s/>={]+)(?:\s*=\s*(?P<value>["\']|[^\s/>{]+))? ## begin of HTML attribute
252: ~xsi');
253:
254: if (!$matches || !empty($matches['macro']) || !empty($matches['comment'])) { 255:
256: } elseif (!empty($matches['end'])) { 257: $tag = end($this->tags);
258: $isEmpty = !$tag->closing && (strpos($matches['end'], '/') !== FALSE || isset(Nette\Web\Html::$emptyElements[strtolower($tag->name)]));
259:
260: if ($isEmpty) {
261: $matches[0] = (Nette\Web\Html::$xhtml ? ' />' : '>')
262: . (isset($matches['tagnewline']) ? $matches['tagnewline'] : '');
263: }
264:
265: if ($tag->isMacro || !empty($tag->attrs)) {
266: if ($tag->isMacro) {
267: $code = $this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, $tag->closing);
268: if ($code === FALSE) {
269: throw new LatteException("Unknown tag-macro <$tag->name>", 0, $this->line);
270: }
271: if ($isEmpty) {
272: $code .= $this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, TRUE);
273: }
274: } else {
275: $code = substr($this->output, $tag->pos) . $matches[0] . (isset($matches['tagnewline']) ? "\n" : '');
276: $code = $this->handler->attrsMacro($code, $tag->attrs, $tag->closing);
277: if ($code === FALSE) {
278: throw new LatteException("Unknown macro-attribute " . self::HTML_PREFIX
279: . implode(' or ' . self::HTML_PREFIX, array_keys($tag->attrs)), 0, $this->line);
280: }
281: if ($isEmpty) {
282: $code = $this->handler->attrsMacro($code, $tag->attrs, TRUE);
283: }
284: }
285: $this->output = substr_replace($this->output, $code, $tag->pos);
286: $matches[0] = ''; 287: }
288:
289: if ($isEmpty) {
290: $tag->closing = TRUE;
291: }
292:
293: if (!$tag->closing && (strcasecmp($tag->name, 'script') === 0 || strcasecmp($tag->name, 'style') === 0)) {
294: $this->context = self::CONTEXT_CDATA;
295: $this->escape = 'Nette\Templates\TemplateHelpers::escape' . (strcasecmp($tag->name, 'style') ? 'Js' : 'Css');
296: } else {
297: $this->context = self::CONTEXT_TEXT;
298: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
299: if ($tag->closing) array_pop($this->tags);
300: }
301:
302: } else { 303: $name = $matches['attr'];
304: $value = isset($matches['value']) ? $matches['value'] : '';
305:
306: 307: if ($isSpecial = String::startsWith($name, self::HTML_PREFIX)) {
308: $name = substr($name, strlen(self::HTML_PREFIX));
309: }
310: $tag = end($this->tags);
311: if ($isSpecial || $tag->isMacro) {
312: if ($value === '"' || $value === "'") {
313: if ($matches = $this->match('~(.*?)' . $value . '~xsi')) { 314: $value = $matches[1];
315: }
316: }
317: $tag->attrs[$name] = $value;
318: $matches[0] = ''; 319:
320: } elseif ($value === '"' || $value === "'") { 321: $this->context = self::CONTEXT_ATTRIBUTE;
322: $this->quote = $value;
323: $this->escape = strncasecmp($name, 'on', 2)
324: ? ('Nette\Templates\TemplateHelpers::escape' . (strcasecmp($name, 'style') ? 'Html' : 'Css'))
325: : 'Nette\Templates\TemplateHelpers::escapeHtmlJs';
326: }
327: }
328: return $matches;
329: }
330:
331:
332:
333: 334: 335:
336: private function contextAttribute()
337: {
338: $matches = $this->match('~
339: (' . $this->quote . ')| ## 1) end of HTML attribute
340: '.$this->macroRe.' ## curly tag
341: ~xsi');
342:
343: if ($matches && empty($matches['macro']) && empty($matches['comment'])) { 344: $this->context = self::CONTEXT_TAG;
345: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
346: }
347: return $matches;
348: }
349:
350:
351:
352: 353: 354:
355: private function contextComment()
356: {
357: $matches = $this->match('~
358: (--\s*>)| ## 1) end of HTML comment
359: '.$this->macroRe.' ## curly tag
360: ~xsi');
361:
362: if ($matches && empty($matches['macro']) && empty($matches['comment'])) { 363: $this->context = self::CONTEXT_TEXT;
364: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
365: }
366: return $matches;
367: }
368:
369:
370:
371: 372: 373:
374: private function contextNone()
375: {
376: $matches = $this->match('~
377: '.$this->macroRe.' ## curly tag
378: ~xsi');
379: return $matches;
380: }
381:
382:
383:
384: 385: 386: 387: 388:
389: private function match($re)
390: {
391: if ($matches = String::match($this->input, $re, PREG_OFFSET_CAPTURE, $this->offset)) {
392: $this->output .= substr($this->input, $this->offset, $matches[0][1] - $this->offset);
393: $this->offset = $matches[0][1] + strlen($matches[0][0]);
394: foreach ($matches as $k => $v) $matches[$k] = $v[0];
395: }
396: return $matches;
397: }
398:
399:
400:
401: 402: 403: 404:
405: public function getLine()
406: {
407: return substr_count($this->input, "\n", 0, $this->offset);
408: }
409:
410:
411:
412: 413: 414: 415: 416: 417:
418: public function setDelimiters($left, $right)
419: {
420: $this->macroRe = '
421: (?:\r?\n?)(?P<comment>\\{\\*.*?\\*\\}[\r\n]{0,2})|
422: (?P<indent>\n[\ \t]*)?
423: ' . $left . '
424: (?P<macro>(?:' . self::RE_STRING . '|[^\'"]+?)*?)
425: ' . $right . '
426: (?P<newline>[\ \t]*(?=\r|\n))?
427: ';
428: return $this;
429: }
430:
431:
432:
433:
434: static function formatModifiers($var, $modifiers)
435: {
436: trigger_error(__METHOD__ . '() is deprecated; use LatteMacros::formatModifiers() instead.', E_USER_WARNING);
437: return LatteMacros::formatModifiers($var, $modifiers);
438: }
439:
440:
441: static function fetchToken(& $s)
442: {
443: trigger_error(__METHOD__ . '() is deprecated; use LatteMacros::fetchToken() instead.', E_USER_WARNING);
444: return LatteMacros::fetchToken($s);
445: }
446:
447:
448: static function formatArray($input, $prefix = '')
449: {
450: trigger_error(__METHOD__ . '() is deprecated; use LatteMacros::formatArray() instead.', E_USER_WARNING);
451: return LatteMacros::formatArray($input, $prefix);
452: }
453:
454:
455: static function formatString($s)
456: {
457: trigger_error(__METHOD__ . '() is deprecated; use LatteMacros::formatString() instead.', E_USER_WARNING);
458: return LatteMacros::formatString($s);
459: }
460:
461: }
462: