1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13:
14:
15: 16: 17: 18: 19: 20:
21: class NLatteFilter extends NObject
22: {
23:
24: const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';
25:
26:
27: const RE_IDENTIFIER = '[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF]*';
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: const CONTEXT_CDATA = 'cdata';
56: const CONTEXT_TAG = 'tag';
57: const CONTEXT_ATTRIBUTE = 'attribute';
58: const CONTEXT_NONE = 'none';
59: const CONTEXT_COMMENT = 'comment';
60:
61:
62:
63:
64: 65: 66: 67: 68:
69: public function setHandler($handler)
70: {
71: $this->handler = $handler;
72: return $this;
73: }
74:
75:
76:
77: 78: 79: 80:
81: public function getHandler()
82: {
83: if ($this->handler === NULL) {
84: $this->handler = new NLatteMacros;
85: }
86: return $this->handler;
87: }
88:
89:
90:
91: 92: 93: 94: 95:
96: public function __invoke($s)
97: {
98: if (!$this->macroRe) {
99: $this->setDelimiters('\\{(?![\\s\'"{}])', '\\}');
100: }
101:
102:
103: $this->context = NLatteFilter::CONTEXT_NONE;
104: $this->escape = '$template->escape';
105:
106:
107: $this->getHandler()->initialize($this, $s);
108:
109:
110: $s = str_replace("\r\n", "\n", $s);
111: $s = $this->parse("\n" . $s);
112:
113: $this->getHandler()->finalize($s);
114:
115: return $s;
116: }
117:
118:
119:
120: 121: 122: 123: 124:
125: private function parse($s)
126: {
127: $this->input = & $s;
128: $this->offset = 0;
129: $this->output = '';
130: $this->tags = array();
131: $len = strlen($s);
132:
133: while ($this->offset < $len) {
134: $matches = $this->{"context$this->context"}();
135:
136: if (!$matches) {
137: break;
138:
139: } elseif (!empty($matches['macro'])) {
140: preg_match('#^(/?[a-z]+)?(.*?)(\\|[a-z](?:'.self::RE_STRING.'|[^\'"]+)*)?$()#is', $matches['macro'], $m2);
141: list(, $macro, $value, $modifiers) = $m2;
142: $code = $this->handler->macro($macro, trim($value), isset($modifiers) ? $modifiers : '');
143: if ($code === NULL) {
144: throw new InvalidStateException("Unknown macro {{$matches['macro']}} on line $this->line.");
145: }
146: $nl = isset($matches['newline']) ? "\n" : '';
147: if ($nl && $matches['indent'] && strncmp($code, '<?php echo ', 11)) {
148: $this->output .= "\n" . $code;
149: } else {
150: $this->output .= $matches['indent'] . $code . (substr($code, -2) === '?>' && $this->output !== '' ? $nl : '');
151: }
152:
153: } else {
154: $this->output .= $matches[0];
155: }
156: }
157:
158: foreach ($this->tags as $tag) {
159: if (!$tag->isMacro && !empty($tag->attrs)) {
160: throw new InvalidStateException("Missing end tag </$tag->name> for macro-attribute " . self::HTML_PREFIX . implode(' and ' . self::HTML_PREFIX, array_keys($tag->attrs)) . ".");
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<comment>!--)| ## begin of HTML comment <!--
177: '.$this->macroRe.' ## curly tag
178: ~xsi');
179:
180: if (!$matches || !empty($matches['macro'])) {
181:
182: } elseif (!empty($matches['comment'])) {
183: $this->context = self::CONTEXT_COMMENT;
184: $this->escape = 'NTemplateHelpers::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 = NString::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 = 'NTemplateHelpers::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 = NString::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 = 'NTemplateHelpers::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'])) {
229: $tag->closing = TRUE;
230: $tag->pos = strlen($this->output);
231: $this->context = self::CONTEXT_TAG;
232: $this->escape = 'NTemplateHelpers::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]*(?=\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'])) {
251:
252: } elseif (!empty($matches['end'])) {
253: $tag = end($this->tags);
254: $isEmpty = !$tag->closing && ($matches['end'][0] === '/' || isset(NHtml::$emptyElements[strtolower($tag->name)]));
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') ? 'NTemplateHelpers::escapeJs' : 'NTemplateHelpers::escapeCss';
286: } else {
287: $this->context = self::CONTEXT_TEXT;
288: $this->escape = 'NTemplateHelpers::escapeHtml';
289: if ($tag->closing) array_pop($this->tags);
290: }
291:
292: } else {
293: $name = $matches['attr'];
294: $value = empty($matches['value']) ? TRUE : $matches['value'];
295:
296:
297: if ($isSpecial = NString::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') ? 'NTemplateHelpers::escapeHtml' : 'NTemplateHelpers::escapeHtmlCss')
315: : 'NTemplateHelpers::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 = 'NTemplateHelpers::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 = 'NTemplateHelpers::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 (preg_match($re, $this->input, $matches, 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>(?:' . self::RE_STRING . '|[^\'"]+?)*?)
414: ' . $right . '
415: (?P<newline>[\ \t]*(?=\n))?
416: ';
417: return $this;
418: }
419:
420:
421:
422:
423:
424:
425:
426: 427: 428: 429: 430: 431:
432: public static function formatModifiers($var, $modifiers)
433: {
434: if (!$modifiers) return $var;
435: preg_match_all(
436: '~
437: '.self::RE_STRING.'| ## single or double quoted string
438: [^\'"|:,\s]+| ## symbol
439: [|:,] ## separator
440: ~xs',
441: $modifiers . '|',
442: $tokens
443: );
444: $inside = FALSE;
445: $prev = '';
446: foreach ($tokens[0] as $token) {
447: if ($token === '|' || $token === ':' || $token === ',') {
448: if ($prev === '') {
449:
450: } elseif (!$inside) {
451: if (!preg_match('#^'.self::RE_IDENTIFIER.'$#', $prev)) {
452: throw new InvalidStateException("Modifier name must be alphanumeric string, '$prev' given.");
453: }
454: $var = "\$template->$prev($var";
455: $prev = '';
456: $inside = TRUE;
457:
458: } else {
459: $var .= ', ' . self::formatString($prev);
460: $prev = '';
461: }
462:
463: if ($token === '|' && $inside) {
464: $var .= ')';
465: $inside = FALSE;
466: }
467: } else {
468: $prev .= $token;
469: }
470: }
471: return $var;
472: }
473:
474:
475:
476: 477: 478: 479: 480:
481: public static function fetchToken(& $s)
482: {
483: if (preg_match('#^((?>'.self::RE_STRING.'|[^\'"\s,]+)+)\s*,?\s*(.*)$#', $s, $matches)) {
484: $s = $matches[2];
485: return $matches[1];
486: }
487: return NULL;
488: }
489:
490:
491:
492: 493: 494: 495: 496: 497:
498: public static function formatArray($s, $prefix = '')
499: {
500: $s = preg_replace_callback(
501: '~
502: '.self::RE_STRING.'| ## single or double quoted string
503: (?<=[,=(]|=>|^)\s*([a-z\d_]+)(?=\s*[,=)]|$) ## 1) symbol
504: ~xi',
505: array(__CLASS__, 'cbArgs'),
506: trim($s)
507: );
508: $s = preg_replace('#\$(' . self::RE_IDENTIFIER . ')\s*=>#', '"$1" =>', $s);
509: return $s === '' ? '' : $prefix . "array($s)";
510: }
511:
512:
513:
514: 515: 516:
517: private static function cbArgs($matches)
518: {
519:
520:
521: if (!empty($matches[1])) {
522: list(, $symbol) = $matches;
523: static $keywords = array('true'=>1, 'false'=>1, 'null'=>1, 'and'=>1, 'or'=>1, 'xor'=>1, 'clone'=>1, 'new'=>1);
524: return is_numeric($symbol) || isset($keywords[strtolower($symbol)]) ? $matches[0] : "'$symbol'";
525:
526: } else {
527: return $matches[0];
528: }
529: }
530:
531:
532:
533: 534: 535: 536: 537:
538: public static function formatString($s)
539: {
540: static $keywords = array('true'=>1, 'false'=>1, 'null'=>1);
541: return (is_numeric($s) || strspn($s, '\'"$') || isset($keywords[strtolower($s)])) ? $s : '"' . $s . '"';
542: }
543:
544:
545:
546: 547: 548: 549:
550: public static function invoke($s)
551: {
552: trigger_error(__METHOD__ . '() is deprecated; use non-static __invoke() instead.', E_USER_WARNING);
553: $filter = new self;
554: return $filter->__invoke($s);
555: }
556:
557: }
558:
559:
560:
561:
562: class NCurlyBracketsFilter extends NLatteFilter {}
563: class NCurlyBracketsMacros extends NLatteMacros {}
564: