1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette\Templates;
13:
14: use Nette,
15: Nette\String,
16: Nette\Tokenizer;
17:
18:
19:
20: 21: 22: 23: 24:
25: class LatteFilter extends Nette\Object
26: {
27:
28: const HTML_PREFIX = 'n:';
29:
30:
31: private $handler;
32:
33:
34: private $macroRe;
35:
36:
37: private $input, $output;
38:
39:
40: private $offset;
41:
42:
43: private $quote;
44:
45:
46: private $tags;
47:
48:
49: public $context, $escape;
50:
51:
52: const CONTEXT_TEXT = 'text';
53: const CONTEXT_CDATA = 'cdata';
54: const CONTEXT_TAG = 'tag';
55: const CONTEXT_ATTRIBUTE = 'attribute';
56: const CONTEXT_NONE = 'none';
57: const CONTEXT_COMMENT = 'comment';
58:
59:
60:
61:
62: 63: 64: 65: 66:
67: public function setHandler($handler)
68: {
69: $this->handler = $handler;
70: return $this;
71: }
72:
73:
74:
75: 76: 77: 78:
79: public function getHandler()
80: {
81: if ($this->handler === NULL) {
82: $this->handler = new LatteMacros;
83: }
84: return $this->handler;
85: }
86:
87:
88:
89: 90: 91: 92: 93:
94: public function __invoke($s)
95: {
96: if (!$this->macroRe) {
97: $this->setDelimiters('\\{(?![\\s\'"{}])', '\\}');
98: }
99:
100: 101: $this->context = LatteFilter::CONTEXT_NONE;
102: $this->escape = '$template->escape';
103:
104: 105: $this->getHandler()->initialize($this, $s);
106:
107: 108: $s = $this->parse("\n" . $s);
109:
110: $this->getHandler()->finalize($s);
111:
112: return $s;
113: }
114:
115:
116:
117: 118: 119: 120: 121:
122: private function parse($s)
123: {
124: $this->input = & $s;
125: $this->offset = 0;
126: $this->output = '';
127: $this->tags = array();
128: $len = strlen($s);
129:
130: while ($this->offset < $len) {
131: $matches = $this->{"context$this->context"}();
132:
133: if (!$matches) { 134: break;
135:
136: } elseif (!empty($matches['macro'])) { 137: list(, $macro, $value, $modifiers) = String::match($matches['macro'], '#^(/?[a-z0-9.:]+)?(.*?)(\\|[a-z](?:'.Tokenizer::RE_STRING.'|[^\'"]+)*)?$()#is');
138: $code = $this->handler->macro($macro, trim($value), isset($modifiers) ? $modifiers : '');
139: if ($code === NULL) {
140: throw new \InvalidStateException("Unknown macro {{$matches['macro']}} on line $this->line.");
141: }
142: $nl = isset($matches['newline']) ? "\n" : ''; 143: if ($nl && $matches['indent'] && strncmp($code, '<?php echo ', 11)) {
144: $this->output .= "\n" . $code; 145: } else {
146: $this->output .= $matches['indent'] . $code . (substr($code, -2) === '?>' ? $nl : '');
147: }
148:
149: } else { 150: $this->output .= $matches[0];
151: }
152: }
153:
154: foreach ($this->tags as $tag) {
155: if (!$tag->isMacro && !empty($tag->attrs)) {
156: throw new \InvalidStateException("Missing end tag </$tag->name> for macro-attribute " . self::HTML_PREFIX . implode(' and ' . self::HTML_PREFIX, array_keys($tag->attrs)) . ".");
157: }
158: }
159:
160: return $this->output . substr($this->input, $this->offset);
161: }
162:
163:
164:
165: 166: 167:
168: private function contextText()
169: {
170: $matches = $this->match('~
171: (?:\n[ \t]*)?<(?P<closing>/?)(?P<tag>[a-z0-9:]+)| ## begin of HTML tag <tag </tag - ignores <!DOCTYPE
172: <(?P<comment>!--)| ## begin of HTML comment <!--
173: '.$this->macroRe.' ## curly tag
174: ~xsi');
175:
176: if (!$matches || !empty($matches['macro'])) { 177:
178: } elseif (!empty($matches['comment'])) { 179: $this->context = self::CONTEXT_COMMENT;
180: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtmlComment';
181:
182: } elseif (empty($matches['closing'])) { 183: $tag = $this->tags[] = (object) NULL;
184: $tag->name = $matches['tag'];
185: $tag->closing = FALSE;
186: $tag->isMacro = String::startsWith($tag->name, self::HTML_PREFIX);
187: $tag->attrs = array();
188: $tag->pos = strlen($this->output);
189: $this->context = self::CONTEXT_TAG;
190: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
191:
192: } else { 193: do {
194: $tag = array_pop($this->tags);
195: if (!$tag) {
196: 197: $tag = (object) NULL;
198: $tag->name = $matches['tag'];
199: $tag->isMacro = String::startsWith($tag->name, self::HTML_PREFIX);
200: }
201: } while (strcasecmp($tag->name, $matches['tag']));
202: $this->tags[] = $tag;
203: $tag->closing = TRUE;
204: $tag->pos = strlen($this->output);
205: $this->context = self::CONTEXT_TAG;
206: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
207: }
208: return $matches;
209: }
210:
211:
212:
213: 214: 215:
216: private function contextCData()
217: {
218: $tag = end($this->tags);
219: $matches = $this->match('~
220: </'.$tag->name.'(?![a-z0-9:])| ## end HTML tag </tag
221: '.$this->macroRe.' ## curly tag
222: ~xsi');
223:
224: if ($matches && empty($matches['macro'])) { 225: $tag->closing = TRUE;
226: $tag->pos = strlen($this->output);
227: $this->context = self::CONTEXT_TAG;
228: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
229: }
230: return $matches;
231: }
232:
233:
234:
235: 236: 237:
238: private function contextTag()
239: {
240: $matches = $this->match('~
241: (?P<end>\ ?/?>)(?P<tagnewline>[\ \t]*(?=\r|\n))?| ## end of HTML tag
242: '.$this->macroRe.'| ## curly tag
243: \s*(?P<attr>[^\s/>={]+)(?:\s*=\s*(?P<value>["\']|[^\s/>{]+))? ## begin of HTML attribute
244: ~xsi');
245:
246: if (!$matches || !empty($matches['macro'])) { 247:
248: } elseif (!empty($matches['end'])) { 249: $tag = end($this->tags);
250: $isEmpty = !$tag->closing && (strpos($matches['end'], '/') !== FALSE || isset(Nette\Web\Html::$emptyElements[strtolower($tag->name)]));
251:
252: if ($isEmpty) {
253: $matches[0] = (Nette\Web\Html::$xhtml ? ' />' : '>') . (isset($matches['tagnewline']) ? $matches['tagnewline'] : '');
254: }
255:
256: if ($tag->isMacro || !empty($tag->attrs)) {
257: if ($tag->isMacro) {
258: $code = $this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, $tag->closing);
259: if ($code === NULL) {
260: throw new \InvalidStateException("Unknown tag-macro <$tag->name> on line $this->line.");
261: }
262: if ($isEmpty) {
263: $code .= $this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, TRUE);
264: }
265: } else {
266: $code = substr($this->output, $tag->pos) . $matches[0] . (isset($matches['tagnewline']) ? "\n" : '');
267: $code = $this->handler->attrsMacro($code, $tag->attrs, $tag->closing);
268: if ($code === NULL) {
269: throw new \InvalidStateException("Unknown macro-attribute " . self::HTML_PREFIX . implode(' or ' . self::HTML_PREFIX, array_keys($tag->attrs)) . " on line $this->line.");
270: }
271: if ($isEmpty) {
272: $code = $this->handler->attrsMacro($code, $tag->attrs, TRUE);
273: }
274: }
275: $this->output = substr_replace($this->output, $code, $tag->pos);
276: $matches[0] = ''; 277: }
278:
279: if ($isEmpty) {
280: $tag->closing = TRUE;
281: }
282:
283: if (!$tag->closing && (strcasecmp($tag->name, 'script') === 0 || strcasecmp($tag->name, 'style') === 0)) {
284: $this->context = self::CONTEXT_CDATA;
285: $this->escape = strcasecmp($tag->name, 'style') ? 'Nette\Templates\TemplateHelpers::escapeJs' : 'Nette\Templates\TemplateHelpers::escapeCss';
286: } else {
287: $this->context = self::CONTEXT_TEXT;
288: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
289: if ($tag->closing) array_pop($this->tags);
290: }
291:
292: } else { 293: $name = $matches['attr'];
294: $value = isset($matches['value']) ? $matches['value'] : '';
295:
296: 297: if ($isSpecial = String::startsWith($name, self::HTML_PREFIX)) {
298: $name = substr($name, strlen(self::HTML_PREFIX));
299: }
300: $tag = end($this->tags);
301: if ($isSpecial || $tag->isMacro) {
302: if ($value === '"' || $value === "'") {
303: if ($matches = $this->match('~(.*?)' . $value . '~xsi')) { 304: $value = $matches[1];
305: }
306: }
307: $tag->attrs[$name] = $value;
308: $matches[0] = ''; 309:
310: } elseif ($value === '"' || $value === "'") { 311: $this->context = self::CONTEXT_ATTRIBUTE;
312: $this->quote = $value;
313: $this->escape = strncasecmp($name, 'on', 2)
314: ? (strcasecmp($name, 'style') ? 'Nette\Templates\TemplateHelpers::escapeHtml' : 'Nette\Templates\TemplateHelpers::escapeHtmlCss')
315: : 'Nette\Templates\TemplateHelpers::escapeHtmlJs';
316: }
317: }
318: return $matches;
319: }
320:
321:
322:
323: 324: 325:
326: private function contextAttribute()
327: {
328: $matches = $this->match('~
329: (' . $this->quote . ')| ## 1) end of HTML attribute
330: '.$this->macroRe.' ## curly tag
331: ~xsi');
332:
333: if ($matches && empty($matches['macro'])) { 334: $this->context = self::CONTEXT_TAG;
335: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
336: }
337: return $matches;
338: }
339:
340:
341:
342: 343: 344:
345: private function contextComment()
346: {
347: $matches = $this->match('~
348: (--\s*>)| ## 1) end of HTML comment
349: '.$this->macroRe.' ## curly tag
350: ~xsi');
351:
352: if ($matches && empty($matches['macro'])) { 353: $this->context = self::CONTEXT_TEXT;
354: $this->escape = 'Nette\Templates\TemplateHelpers::escapeHtml';
355: }
356: return $matches;
357: }
358:
359:
360:
361: 362: 363:
364: private function contextNone()
365: {
366: $matches = $this->match('~
367: '.$this->macroRe.' ## curly tag
368: ~xsi');
369: return $matches;
370: }
371:
372:
373:
374: 375: 376: 377: 378:
379: private function match($re)
380: {
381: if ($matches = String::match($this->input, $re, PREG_OFFSET_CAPTURE, $this->offset)) {
382: $this->output .= substr($this->input, $this->offset, $matches[0][1] - $this->offset);
383: $this->offset = $matches[0][1] + strlen($matches[0][0]);
384: foreach ($matches as $k => $v) $matches[$k] = $v[0];
385: }
386: return $matches;
387: }
388:
389:
390:
391: 392: 393: 394:
395: public function getLine()
396: {
397: return substr_count($this->input, "\n", 0, $this->offset);
398: }
399:
400:
401:
402: 403: 404: 405: 406: 407:
408: public function setDelimiters($left, $right)
409: {
410: $this->macroRe = '
411: (?P<indent>\n[\ \t]*)?
412: ' . $left . '
413: (?P<macro>(?:' . Tokenizer::RE_STRING . '|[^\'"]+?)*?)
414: ' . $right . '
415: (?P<newline>[\ \t]*(?=\r|\n))?
416: ';
417: return $this;
418: }
419:
420:
421:
422:
423: static function formatModifiers($var, $modifiers)
424: {
425: trigger_error(__METHOD__ . '() is deprecated; use LatteMacros::formatModifiers() instead.', E_USER_WARNING);
426: return LatteMacros::formatModifiers($var, $modifiers);
427: }
428:
429: static function fetchToken(& $s)
430: {
431: trigger_error(__METHOD__ . '() is deprecated; use LatteMacros::fetchToken() instead.', E_USER_WARNING);
432: return LatteMacros::fetchToken($s);
433: }
434:
435: static function formatArray($input, $prefix = '')
436: {
437: trigger_error(__METHOD__ . '() is deprecated; use LatteMacros::formatArray() instead.', E_USER_WARNING);
438: return LatteMacros::formatArray($input, $prefix);
439: }
440:
441: static function formatString($s)
442: {
443: trigger_error(__METHOD__ . '() is deprecated; use LatteMacros::formatString() instead.', E_USER_WARNING);
444: return LatteMacros::formatString($s);
445: }
446:
447:
448: }
449: