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