1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13:
14:
15: 16: 17: 18: 19:
20: class LatteFilter extends Object
21: {
22:
23: const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';
24:
25:
26: const HTML_PREFIX = 'n:';
27:
28:
29: private $handler;
30:
31:
32: private $macroRe;
33:
34:
35: private $input, $output;
36:
37:
38: private $offset;
39:
40:
41: private $quote;
42:
43:
44: private $tags;
45:
46:
47: public $context, $escape;
48:
49:
50: const CONTEXT_TEXT = 'text',
51: CONTEXT_CDATA = 'cdata',
52: CONTEXT_TAG = 'tag',
53: CONTEXT_ATTRIBUTE = 'attribute',
54: CONTEXT_NONE = 'none',
55: CONTEXT_COMMENT = 'comment';
56:
57:
58:
59: 60: 61: 62: 63:
64: public function setHandler($handler)
65: {
66: $this->handler = $handler;
67: return $this;
68: }
69:
70:
71:
72: 73: 74: 75:
76: public function getHandler()
77: {
78: if ($this->handler === NULL) {
79: $this->handler = new LatteMacros;
80: }
81: return $this->handler;
82: }
83:
84:
85:
86: 87: 88: 89: 90:
91: public function __invoke($s)
92: {
93: if (!String::checkEncoding($s)) {
94: throw new LatteException('Template is not valid UTF-8 stream.');
95: }
96:
97: if (!$this->macroRe) {
98: $this->setDelimiters('\\{(?![\\s\'"{}*])', '\\}');
99: }
100:
101: 102: $this->context = LatteFilter::CONTEXT_NONE;
103: $this->escape = '$template->escape';
104:
105: 106: $this->getHandler()->initialize($this, $s);
107:
108: 109: $s = $this->parse("\n" . $s);
110:
111: $this->getHandler()->finalize($s);
112:
113: return $s;
114: }
115:
116:
117:
118: 119: 120: 121: 122:
123: private function parse($s)
124: {
125: $this->input = & $s;
126: $this->offset = 0;
127: $this->output = '';
128: $this->tags = array();
129: $len = strlen($s);
130:
131: while ($this->offset < $len) {
132: $matches = $this->{"context$this->context"}();
133:
134: if (!$matches) { 135: break;
136:
137: } elseif (!empty($matches['comment'])) { 138:
139: } elseif (!empty($matches['macro'])) { 140: $code = $this->handler->macro($matches['macro']);
141: if ($code === FALSE) {
142: throw new LatteException("Unknown macro {{$matches['macro']}}", 0, $this->line);
143: }
144: $nl = isset($matches['newline']) ? "\n" : '';
145: if ($nl && $matches['indent'] && strncmp($code, '<?php echo ', 11)) { 146: $this->output .= "\n" . $code; 147: } else {
148: 149: $this->output .= $matches['indent'] . $code . (substr($code, -2) === '?>' ? $nl : '');
150: }
151:
152: } else { 153: $this->output .= $matches[0];
154: }
155: }
156:
157: foreach ($this->tags as $tag) {
158: if (!$tag->isMacro && !empty($tag->attrs)) {
159: throw new LatteException("Missing end tag </$tag->name> for macro-attribute " . self::HTML_PREFIX
160: . implode(' and ' . self::HTML_PREFIX, array_keys($tag->attrs)) . ".", 0, $this->line);
161: }
162: }
163:
164: return $this->output . substr($this->input, $this->offset);
165: }
166:
167:
168:
169: 170: 171:
172: private function contextText()
173: {
174: $matches = $this->match('~
175: (?:\n[ \t]*)?<(?P<closing>/?)(?P<tag>[a-z0-9:]+)| ## begin of HTML tag <tag </tag - ignores <!DOCTYPE
176: <(?P<htmlcomment>!--)| ## begin of HTML comment <!--
177: '.$this->macroRe.' ## curly tag
178: ~xsi');
179:
180: if (!$matches || !empty($matches['macro']) || !empty($matches['comment'])) { 181:
182: } elseif (!empty($matches['htmlcomment'])) { 183: $this->context = self::CONTEXT_COMMENT;
184: $this->escape = 'TemplateHelpers::escapeHtmlComment';
185:
186: } elseif (empty($matches['closing'])) { 187: $tag = $this->tags[] = (object) NULL;
188: $tag->name = $matches['tag'];
189: $tag->closing = FALSE;
190: $tag->isMacro = String::startsWith($tag->name, self::HTML_PREFIX);
191: $tag->attrs = array();
192: $tag->pos = strlen($this->output);
193: $this->context = self::CONTEXT_TAG;
194: $this->escape = 'TemplateHelpers::escapeHtml';
195:
196: } else { 197: do {
198: $tag = array_pop($this->tags);
199: if (!$tag) {
200: 201: $tag = (object) NULL;
202: $tag->name = $matches['tag'];
203: $tag->isMacro = String::startsWith($tag->name, self::HTML_PREFIX);
204: }
205: } while (strcasecmp($tag->name, $matches['tag']));
206: $this->tags[] = $tag;
207: $tag->closing = TRUE;
208: $tag->pos = strlen($this->output);
209: $this->context = self::CONTEXT_TAG;
210: $this->escape = 'TemplateHelpers::escapeHtml';
211: }
212: return $matches;
213: }
214:
215:
216:
217: 218: 219:
220: private function contextCData()
221: {
222: $tag = end($this->tags);
223: $matches = $this->match('~
224: </'.$tag->name.'(?![a-z0-9:])| ## end HTML tag </tag
225: '.$this->macroRe.' ## curly tag
226: ~xsi');
227:
228: if ($matches && empty($matches['macro']) && empty($matches['comment'])) { 229: $tag->closing = TRUE;
230: $tag->pos = strlen($this->output);
231: $this->context = self::CONTEXT_TAG;
232: $this->escape = 'TemplateHelpers::escapeHtml';
233: }
234: return $matches;
235: }
236:
237:
238:
239: 240: 241:
242: private function contextTag()
243: {
244: $matches = $this->match('~
245: (?P<end>\ ?/?>)(?P<tagnewline>[\ \t]*(?=\r|\n))?| ## end of HTML tag
246: '.$this->macroRe.'| ## curly tag
247: \s*(?P<attr>[^\s/>={]+)(?:\s*=\s*(?P<value>["\']|[^\s/>{]+))? ## begin of HTML attribute
248: ~xsi');
249:
250: if (!$matches || !empty($matches['macro']) || !empty($matches['comment'])) { 251:
252: } elseif (!empty($matches['end'])) { 253: $tag = end($this->tags);
254: $isEmpty = !$tag->closing && (strpos($matches['end'], '/') !== FALSE || isset(Html::$emptyElements[strtolower($tag->name)]));
255:
256: if ($isEmpty) {
257: $matches[0] = (Html::$xhtml ? ' />' : '>')
258: . (isset($matches['tagnewline']) ? $matches['tagnewline'] : '');
259: }
260:
261: if ($tag->isMacro || !empty($tag->attrs)) {
262: if ($tag->isMacro) {
263: $code = $this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, $tag->closing);
264: if ($code === FALSE) {
265: throw new LatteException("Unknown tag-macro <$tag->name>", 0, $this->line);
266: }
267: if ($isEmpty) {
268: $code .= $this->handler->tagMacro(substr($tag->name, strlen(self::HTML_PREFIX)), $tag->attrs, TRUE);
269: }
270: } else {
271: $code = substr($this->output, $tag->pos) . $matches[0] . (isset($matches['tagnewline']) ? "\n" : '');
272: $code = $this->handler->attrsMacro($code, $tag->attrs, $tag->closing);
273: if ($code === FALSE) {
274: throw new LatteException("Unknown macro-attribute " . self::HTML_PREFIX
275: . 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 = 'TemplateHelpers::escape' . (strcasecmp($tag->name, 'style') ? 'Js' : 'Css');
292: } else {
293: $this->context = self::CONTEXT_TEXT;
294: $this->escape = '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: ? ('TemplateHelpers::escape' . (strcasecmp($name, 'style') ? 'Html' : 'Css'))
321: : '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 = '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 = '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: