1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13:
14:
15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47:
48: class LatteMacros extends Object
49: {
50:
51: public static $defaultMacros = array(
52: 'syntax' => '%:macroSyntax%',
53: '/syntax' => '%:macroSyntax%',
54:
55: 'block' => '<?php %:macroBlock% ?>',
56: '/block' => '<?php %:macroBlockEnd% ?>',
57:
58: 'capture' => '<?php %:macroCapture% ?>',
59: '/capture' => '<?php %:macroCaptureEnd% ?>',
60:
61: 'snippet' => '<?php %:macroSnippet% ?>',
62: '/snippet' => '<?php %:macroSnippetEnd% ?>',
63:
64: 'cache' => '<?php %:macroCache% ?>',
65: '/cache' => '<?php array_pop($_l->g->caches)->save(); } ?>',
66:
67: 'if' => '<?php if (%%): ?>',
68: 'elseif' => '<?php elseif (%%): ?>',
69: 'else' => '<?php else: ?>',
70: '/if' => '<?php endif ?>',
71: 'ifset' => '<?php if (isset(%:macroIfset%)): ?>',
72: '/ifset' => '<?php endif ?>',
73: 'elseifset' => '<?php elseif (isset(%%)): ?>',
74: 'foreach' => '<?php foreach (%:macroForeach%): ?>',
75: '/foreach' => '<?php endforeach; array_pop($_l->its); $iterator = end($_l->its) ?>',
76: 'for' => '<?php for (%%): ?>',
77: '/for' => '<?php endfor ?>',
78: 'while' => '<?php while (%%): ?>',
79: '/while' => '<?php endwhile ?>',
80: 'continueIf' => '<?php if (%%) continue ?>',
81: 'breakIf' => '<?php if (%%) break ?>',
82: 'first' => '<?php if ($iterator->isFirst(%%)): ?>',
83: '/first' => '<?php endif ?>',
84: 'last' => '<?php if ($iterator->isLast(%%)): ?>',
85: '/last' => '<?php endif ?>',
86: 'sep' => '<?php if (!$iterator->isLast(%%)): ?>',
87: '/sep' => '<?php endif ?>',
88:
89: 'include' => '<?php %:macroInclude% ?>',
90: 'extends' => '<?php %:macroExtends% ?>',
91: 'layout' => '<?php %:macroExtends% ?>',
92:
93: 'plink' => '<?php echo %:escape%(%:macroPlink%) ?>',
94: 'link' => '<?php echo %:escape%(%:macroLink%) ?>',
95: 'ifCurrent' => '<?php %:macroIfCurrent% ?>', 96: '/ifCurrent' => '<?php endif ?>',
97: 'widget' => '<?php %:macroControl% ?>',
98: 'control' => '<?php %:macroControl% ?>',
99:
100: '@href' => ' href="<?php echo %:escape%(%:macroLink%) ?>"',
101: '@class' => '<?php if ($_l->tmp = trim(implode(" ", array_unique(%:formatArray%)))) echo \' class="\' . %:escape%($_l->tmp) . \'"\' ?>',
102: '@attr' => '<?php if (($_l->tmp = (string) (%%)) !== \'\') echo \' @@="\' . %:escape%($_l->tmp) . \'"\' ?>',
103:
104: 'attr' => '<?php echo Html::el(NULL)->%:macroAttr%attributes() ?>',
105: 'contentType' => '<?php %:macroContentType% ?>',
106: 'status' => '<?php Environment::getHttpResponse()->setCode(%%) ?>',
107: 'var' => '<?php %:macroVar% ?>',
108: 'assign' => '<?php %:macroVar% ?>', 109: 'default' => '<?php %:macroDefault% ?>',
110: 'dump' => '<?php %:macroDump% ?>',
111: 'debugbreak' => '<?php %:macroDebugbreak% ?>',
112: 'l' => '{',
113: 'r' => '}',
114:
115: '!_' => '<?php echo %:macroTranslate% ?>',
116: '_' => '<?php echo %:escape%(%:macroTranslate%) ?>',
117: '!=' => '<?php echo %:macroModifiers% ?>',
118: '=' => '<?php echo %:escape%(%:macroModifiers%) ?>',
119: '!$' => '<?php echo %:macroDollar% ?>',
120: '$' => '<?php echo %:escape%(%:macroDollar%) ?>',
121: '?' => '<?php %:macroModifiers% ?>',
122: );
123:
124:
125: const RE_IDENTIFIER = '[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF]*';
126:
127:
128: const T_WHITESPACE = T_WHITESPACE,
129: T_COMMENT = T_COMMENT,
130: T_SYMBOL = -1,
131: T_NUMBER = -2,
132: T_VARIABLE = -3;
133:
134:
135: public $macros;
136:
137:
138: private $tokenizer;
139:
140:
141: private $filter;
142:
143:
144: private $nodes = array();
145:
146:
147: private $blocks = array();
148:
149:
150: private $namedBlocks = array();
151:
152:
153: private $extends;
154:
155:
156: private $uniq;
157:
158:
159: private $cacheCounter;
160:
161:
162: const BLOCK_NAMED = 1,
163: BLOCK_CAPTURE = 2,
164: BLOCK_ANONYMOUS = 3;
165:
166:
167:
168: 169: 170:
171: public function __construct()
172: {
173: $this->macros = self::$defaultMacros;
174:
175: $this->tokenizer = new Tokenizer(array(
176: self::T_WHITESPACE => '\s+',
177: self::T_COMMENT => '(?s)/\*.*?\*/',
178: LatteFilter::RE_STRING,
179: '(?:true|false|null|and|or|xor|clone|new|instanceof|return|continue|break|[A-Z_][A-Z0-9_]{2,})(?!\w)', 180: '\([a-z]+\)', 181: self::T_VARIABLE => '\$\w+',
182: self::T_NUMBER => '[+-]?[0-9]+(?:\.[0-9]+)?(?:e[0-9]+)?',
183: self::T_SYMBOL => '\w+(?:-\w+)*',
184: '::|=>|[^"\']', 185: ), 'u');
186: }
187:
188:
189:
190: 191: 192: 193: 194: 195:
196: public function initialize($filter, & $s)
197: {
198: $this->filter = $filter;
199: $this->nodes = array();
200: $this->blocks = array();
201: $this->namedBlocks = array();
202: $this->extends = NULL;
203: $this->uniq = String::random();
204: $this->cacheCounter = 0;
205:
206: $filter->context = LatteFilter::CONTEXT_TEXT;
207: $filter->escape = 'TemplateHelpers::escapeHtml';
208: }
209:
210:
211:
212: 213: 214: 215: 216:
217: public function finalize(& $s)
218: {
219: 220: if (count($this->blocks) === 1) { 221: $s .= $this->macro('/block');
222:
223: } elseif ($this->blocks) {
224: throw new LatteException("There are unclosed blocks.", 0, $this->filter->line);
225: }
226:
227: 228: if ($this->namedBlocks || $this->extends) {
229: $s = '<?php
230: if ($_l->extends) {
231: ob_start();
232: } elseif (isset($presenter, $control) && $presenter->isAjax() && $control->isControlInvalid()) {
233: return LatteMacros::renderSnippets($control, $_l, get_defined_vars());
234: }
235: ?>' . $s . '<?php
236: if ($_l->extends) {
237: ob_end_clean();
238: LatteMacros::includeTemplate($_l->extends, get_defined_vars(), $template)->render();
239: }
240: ';
241: } else {
242: $s = '<?php
243: if (isset($presenter, $control) && $presenter->isAjax() && $control->isControlInvalid()) {
244: return LatteMacros::renderSnippets($control, $_l, get_defined_vars());
245: }
246: ?>' . $s;
247: }
248:
249: 250: if ($this->namedBlocks) {
251: $uniq = $this->uniq;
252: foreach (array_reverse($this->namedBlocks, TRUE) as $name => $foo) {
253: $code = & $this->namedBlocks[$name];
254: $namere = preg_quote($name, '#');
255: $s = String::replace($s,
256: "#{block $namere} \?>(.*)<\?php {/block $namere}#sU",
257: callback(create_function('$matches', 'extract(NClosureFix::$vars['.NClosureFix::uses(array('name'=>$name, 'code'=>& $code,'uniq'=> $uniq)).'], EXTR_REFS);
258: list(, $content) = $matches;
259: $func = \'_lb\' . substr(md5($uniq . $name), 0, 10) . \'_\' . preg_replace(\'#[^a-z0-9_]#i\', \'_\', $name);
260: $code = "//\\n// block $name\\n//\\n"
261: . "if (!function_exists(\\$_l->blocks[" . var_export($name, TRUE) . "][] = \'$func\')) { "
262: . "function $func(\\$_l, \\$_args) { "
263: . (PHP_VERSION_ID > 50208 ? \'extract($_args)\' : \'foreach ($_args as $__k => $__v) $$__k = $__v\') // PHP bug #46873
264: . ($name[0] === \'_\' ? \'; $control->validateControl(\' . var_export(substr($name, 1), TRUE) . \')\' : \'\') // snippet
265: . "\\n?>$content<?php\\n}}";
266: return \'\';
267: '))
268: );
269: }
270: $s = "<?php\n\n" . implode("\n\n\n", $this->namedBlocks) . "\n\n//\n// end of blocks\n//\n?>" . $s;
271: }
272:
273: 274: $s = "<?php\n"
275: . '$_l = LatteMacros::initRuntime($template, '
276: . var_export($this->extends, TRUE) . ', ' . var_export($this->uniq, TRUE) . '); unset($_extends);'
277: . "\n?>" . $s;
278: }
279:
280:
281:
282: 283: 284: 285: 286: 287: 288:
289: public function macro($macro, $content = '', $modifiers = '')
290: {
291: if (func_num_args() === 1) { 292: list(, $macro, $content, $modifiers) = String::match(
293: $macro,
294: '#^(/?[a-z0-9.:]+)?(.*?)(\\|[a-z](?:'.LatteFilter::RE_STRING.'|[^\'"]+)*)?$()#is'
295: );
296: $content = trim($content);
297: }
298:
299: if ($macro === '') {
300: $macro = substr($content, 0, 2);
301: if (!isset($this->macros[$macro])) {
302: $macro = substr($content, 0, 1);
303: if (!isset($this->macros[$macro])) {
304: return FALSE;
305: }
306: }
307: $content = substr($content, strlen($macro));
308:
309: } elseif (!isset($this->macros[$macro])) {
310: return FALSE;
311: }
312:
313: $closing = $macro[0] === '/';
314: if ($closing) {
315: $node = array_pop($this->nodes);
316: if (!$node || "/$node->name" !== $macro
317: || ($content && !String::startsWith("$node->content ", "$content ")) || $modifiers
318: ) {
319: $macro .= $content ? ' ' : '';
320: throw new LatteException("Unexpected macro {{$macro}{$content}{$modifiers}}"
321: . ($node ? ", expecting {/$node->name}" . ($content && $node->content ? " or eventually {/$node->name $node->content}" : '') : ''),
322: 0, $this->filter->line);
323: }
324: $node->content = $node->modifiers = ''; 325:
326: } else {
327: $node = (object) NULL;
328: $node->name = $macro;
329: $node->content = $content;
330: $node->modifiers = $modifiers;
331: if (isset($this->macros["/$macro"])) {
332: $this->nodes[] = $node;
333: }
334: }
335:
336: $This = $this;
337: return String::replace(
338: $this->macros[$macro],
339: '#%(.*?)%#',
340: callback(create_function('$m', 'extract(NClosureFix::$vars['.NClosureFix::uses(array('This'=>$This,'node'=> $node)).'], EXTR_REFS);
341: if ($m[1]) {
342: return callback($m[1][0] === \':\' ? array($This, substr($m[1], 1)) : $m[1])
343: ->invoke($node->content, $node->modifiers);
344: } else {
345: return $This->formatMacroArgs($node->content);
346: }
347: '))
348: );
349: }
350:
351:
352:
353: 354: 355: 356: 357: 358: 359:
360: public function tagMacro($name, $attrs, $closing)
361: {
362: $knownTags = array(
363: 'include' => 'block',
364: 'for' => 'each',
365: 'block' => 'name',
366: 'if' => 'cond',
367: 'elseif' => 'cond',
368: );
369: return $this->macro(
370: $closing ? "/$name" : $name,
371: isset($knownTags[$name], $attrs[$knownTags[$name]])
372: ? $attrs[$knownTags[$name]]
373: : preg_replace("#'([^\\'$]+)'#", '$1', substr(var_export($attrs, TRUE), 8, -1)),
374: isset($attrs['modifiers']) ? $attrs['modifiers'] : ''
375: );
376: }
377:
378:
379:
380: 381: 382: 383: 384: 385: 386:
387: public function attrsMacro($code, $attrs, $closing)
388: {
389: foreach ($attrs as $name => $content) {
390: if (substr($name, 0, 5) === 'attr-') {
391: if (!$closing) {
392: $pos = strrpos($code, '>');
393: if ($code[$pos-1] === '/') $pos--;
394: $code = substr_replace($code, str_replace('@@', substr($name, 5), $this->macro("@attr", $content)), $pos, 0);
395: }
396: unset($attrs[$name]);
397: }
398: }
399:
400: $left = $right = array();
401: foreach ($this->macros as $name => $foo) {
402: if ($name[0] === '@') {
403: $name = substr($name, 1);
404: if (isset($attrs[$name])) {
405: if (!$closing) {
406: $pos = strrpos($code, '>');
407: if ($code[$pos-1] === '/') $pos--;
408: $code = substr_replace($code, $this->macro("@$name", $attrs[$name]), $pos, 0);
409: }
410: unset($attrs[$name]);
411: }
412: }
413:
414: if (!isset($this->macros["/$name"])) { 415: continue;
416: }
417:
418: $macro = $closing ? "/$name" : $name;
419: if (isset($attrs[$name])) {
420: if ($closing) {
421: $right[] = array($macro, '');
422: } else {
423: array_unshift($left, array($macro, $attrs[$name]));
424: }
425: }
426:
427: $innerName = "inner-$name";
428: if (isset($attrs[$innerName])) {
429: if ($closing) {
430: $left[] = array($macro, '');
431: } else {
432: array_unshift($right, array($macro, $attrs[$innerName]));
433: }
434: }
435:
436: $tagName = "tag-$name";
437: if (isset($attrs[$tagName])) {
438: array_unshift($left, array($name, $attrs[$tagName]));
439: $right[] = array("/$name", '');
440: }
441:
442: unset($attrs[$name], $attrs[$innerName], $attrs[$tagName]);
443: }
444: if ($attrs) {
445: return FALSE;
446: }
447: $s = '';
448: foreach ($left as $item) {
449: $s .= $this->macro($item[0], $item[1]);
450: }
451: $s .= $code;
452: foreach ($right as $item) {
453: $s .= $this->macro($item[0], $item[1]);
454: }
455: return $s;
456: }
457:
458:
459:
460:
461:
462:
463:
464: 465: 466:
467: public function macroDollar($var, $modifiers)
468: {
469: return $this->formatModifiers($this->formatMacroArgs('$' . $var), $modifiers);
470: }
471:
472:
473:
474: 475: 476:
477: public function macroTranslate($var, $modifiers)
478: {
479: return $this->formatModifiers($this->formatMacroArgs($var), '|translate' . $modifiers);
480: }
481:
482:
483:
484: 485: 486:
487: public function macroSyntax($var)
488: {
489: switch ($var) {
490: case '':
491: case 'latte':
492: $this->filter->setDelimiters('\\{(?![\\s\'"{}])', '\\}'); 493: break;
494:
495: case 'double':
496: $this->filter->setDelimiters('\\{\\{(?![\\s\'"{}])', '\\}\\}'); 497: break;
498:
499: case 'asp':
500: $this->filter->setDelimiters('<%\s*', '\s*%>');
501: break;
502:
503: case 'python':
504: $this->filter->setDelimiters('\\{[{%]\s*', '\s*[%}]\\}'); 505: break;
506:
507: case 'off':
508: $this->filter->setDelimiters('[^\x00-\xFF]', '');
509: break;
510:
511: default:
512: throw new LatteException("Unknown syntax '$var'", 0, $this->filter->line);
513: }
514: }
515:
516:
517:
518: 519: 520:
521: public function macroInclude($content, $modifiers, $isDefinition = FALSE)
522: {
523: $destination = $this->fetchToken($content); 524: $params = $this->formatArray($content) . ($content ? ' + ' : '');
525:
526: if ($destination === NULL) {
527: throw new LatteException("Missing destination in {include}", 0, $this->filter->line);
528:
529: } elseif ($destination[0] === '#') { 530: $destination = ltrim($destination, '#');
531: if (!String::match($destination, '#^\$?' . self::RE_IDENTIFIER . '$#')) {
532: throw new LatteException("Included block name must be alphanumeric string, '$destination' given.", 0, $this->filter->line);
533: }
534:
535: $parent = $destination === 'parent';
536: if ($destination === 'parent' || $destination === 'this') {
537: $item = end($this->blocks);
538: while ($item && $item[0] !== self::BLOCK_NAMED) $item = prev($this->blocks);
539: if (!$item) {
540: throw new LatteException("Cannot include $destination block outside of any block.", 0, $this->filter->line);
541: }
542: $destination = $item[1];
543: }
544: $name = $destination[0] === '$' ? $destination : var_export($destination, TRUE);
545: $params .= $isDefinition ? 'get_defined_vars()' : '$template->getParams()';
546: $cmd = isset($this->namedBlocks[$destination]) && !$parent
547: ? "call_user_func(reset(\$_l->blocks[$name]), \$_l, $params)"
548: : 'LatteMacros::callBlock' . ($parent ? 'Parent' : '') . "(\$_l, $name, $params)";
549: return $modifiers
550: ? "ob_start(); $cmd; echo " . $this->formatModifiers('ob_get_clean()', $modifiers)
551: : $cmd;
552:
553: } else { 554: $destination = $this->formatString($destination);
555: $cmd = 'LatteMacros::includeTemplate(' . $destination . ', '
556: . $params . '$template->getParams(), $_l->templates[' . var_export($this->uniq, TRUE) . '])';
557: return $modifiers
558: ? 'echo ' . $this->formatModifiers($cmd . '->__toString(TRUE)', $modifiers)
559: : $cmd . '->render()';
560: }
561: }
562:
563:
564:
565: 566: 567:
568: public function macroExtends($content)
569: {
570: if (!$content) {
571: throw new LatteException("Missing destination in {extends}", 0, $this->filter->line);
572: }
573: if (!empty($this->blocks)) {
574: throw new LatteException("{extends} must be placed outside any block.", 0, $this->filter->line);
575: }
576: if ($this->extends !== NULL) {
577: throw new LatteException("Multiple {extends} declarations are not allowed.", 0, $this->filter->line);
578: }
579: $this->extends = $content !== 'none';
580: return $this->extends ? '$_l->extends = ' . ($content === 'auto' ? '$layout' : $this->formatMacroArgs($content)) : '';
581: }
582:
583:
584:
585: 586: 587:
588: public function macroBlock($content, $modifiers)
589: {
590: $name = $this->fetchToken($content); 591:
592: if ($name === NULL) { 593: $this->blocks[] = array(self::BLOCK_ANONYMOUS, NULL, $modifiers);
594: return $modifiers === '' ? '' : 'ob_start()';
595:
596: } else { 597: $name = ltrim($name, '#');
598: if (!String::match($name, '#^' . self::RE_IDENTIFIER . '$#')) {
599: throw new LatteException("Block name must be alphanumeric string, '$name' given.", 0, $this->filter->line);
600:
601: } elseif (isset($this->namedBlocks[$name])) {
602: throw new LatteException("Cannot redeclare block '$name'", 0, $this->filter->line);
603: }
604:
605: $top = empty($this->blocks);
606: $this->namedBlocks[$name] = $name;
607: $this->blocks[] = array(self::BLOCK_NAMED, $name, '');
608: if ($name[0] === '_') { 609: $tag = $this->fetchToken($content); 610: $tag = trim($tag, '<>');
611: $namePhp = var_export(substr($name, 1), TRUE);
612: if (!$tag) $tag = 'div';
613: return "?><$tag id=\"<?php echo \$control->getSnippetId($namePhp) ?>\"><?php "
614: . $this->macroInclude('#' . $name, $modifiers)
615: . " ?></$tag><?php {block $name}";
616:
617: } elseif (!$top) {
618: return $this->macroInclude('#' . $name, $modifiers, TRUE) . "{block $name}";
619:
620: } elseif ($this->extends) {
621: return "{block $name}";
622:
623: } else {
624: return 'if (!$_l->extends) { ' . $this->macroInclude('#' . $name, $modifiers, TRUE) . "; } {block $name}";
625: }
626: }
627: }
628:
629:
630:
631: 632: 633:
634: public function macroBlockEnd($content)
635: {
636: list($type, $name, $modifiers) = array_pop($this->blocks);
637:
638: if ($type === self::BLOCK_CAPTURE) { 639: $this->blocks[] = array($type, $name, $modifiers);
640: return $this->macroCaptureEnd($content);
641:
642: } elseif ($type === self::BLOCK_NAMED) { 643: return "{/block $name}";
644:
645: } else { 646: return $modifiers === '' ? '' : 'echo ' . $this->formatModifiers('ob_get_clean()', $modifiers);
647: }
648: }
649:
650:
651:
652: 653: 654:
655: public function macroSnippet($content)
656: {
657: return $this->macroBlock('_' . $content, '');
658: }
659:
660:
661:
662: 663: 664:
665: public function macroSnippetEnd($content)
666: {
667: return $this->macroBlockEnd('', '');
668: }
669:
670:
671:
672: 673: 674:
675: public function macroCapture($content, $modifiers)
676: {
677: $name = $this->fetchToken($content); 678:
679: if (substr($name, 0, 1) !== '$') {
680: throw new LatteException("Invalid capture block parameter '$name'", 0, $this->filter->line);
681: }
682:
683: $this->blocks[] = array(self::BLOCK_CAPTURE, $name, $modifiers);
684: return 'ob_start()';
685: }
686:
687:
688:
689: 690: 691:
692: public function macroCaptureEnd($content)
693: {
694: list($type, $name, $modifiers) = array_pop($this->blocks);
695: return $name . '=' . $this->formatModifiers('ob_get_clean()', $modifiers);
696: }
697:
698:
699:
700: 701: 702:
703: public function macroCache($content)
704: {
705: return 'if (CachingHelper::create('
706: . var_export($this->uniq . ':' . $this->cacheCounter++, TRUE)
707: . ', $_l->g->caches' . $this->formatArray($content, ', ') . ')) {';
708: }
709:
710:
711:
712: 713: 714:
715: public function macroForeach($content)
716: {
717: return '$iterator = $_l->its[] = new SmartCachingIterator('
718: . preg_replace('#(.*)\s+as\s+#i', '$1) as ', $this->formatMacroArgs($content), 1);
719: }
720:
721:
722:
723: 724: 725:
726: public function macroIfset($content)
727: {
728: if (strpos($content, '#') === FALSE) return $content;
729: $list = array();
730: while (($name = $this->fetchToken($content)) !== NULL) {
731: $list[] = $name[0] === '#' ? '$_l->blocks["' . substr($name, 1) . '"]' : $name;
732: }
733: return implode(', ', $list);
734: }
735:
736:
737:
738: 739: 740:
741: public function macroAttr($content)
742: {
743: return String::replace($content . ' ', '#\)\s+#', ')->');
744: }
745:
746:
747:
748: 749: 750:
751: public function macroContentType($content)
752: {
753: if (strpos($content, 'html') !== FALSE) {
754: $this->filter->escape = 'TemplateHelpers::escapeHtml';
755: $this->filter->context = LatteFilter::CONTEXT_TEXT;
756:
757: } elseif (strpos($content, 'xml') !== FALSE) {
758: $this->filter->escape = 'TemplateHelpers::escapeXml';
759: $this->filter->context = LatteFilter::CONTEXT_NONE;
760:
761: } elseif (strpos($content, 'javascript') !== FALSE) {
762: $this->filter->escape = 'TemplateHelpers::escapeJs';
763: $this->filter->context = LatteFilter::CONTEXT_NONE;
764:
765: } elseif (strpos($content, 'css') !== FALSE) {
766: $this->filter->escape = 'TemplateHelpers::escapeCss';
767: $this->filter->context = LatteFilter::CONTEXT_NONE;
768:
769: } elseif (strpos($content, 'plain') !== FALSE) {
770: $this->filter->escape = '';
771: $this->filter->context = LatteFilter::CONTEXT_NONE;
772:
773: } else {
774: $this->filter->escape = '$template->escape';
775: $this->filter->context = LatteFilter::CONTEXT_NONE;
776: }
777:
778: 779: if (strpos($content, '/')) {
780: return 'Environment::getHttpResponse()->setHeader("Content-Type", "' . $content . '")';
781: }
782: }
783:
784:
785:
786: 787: 788:
789: public function macroDump($content)
790: {
791: return 'Debug::barDump('
792: . ($content ? 'array(' . var_export($this->formatMacroArgs($content), TRUE) . " => $content)" : 'get_defined_vars()')
793: . ', "Template " . str_replace(dirname(dirname($template->getFile())), "\xE2\x80\xA6", $template->getFile()))';
794: }
795:
796:
797:
798: 799: 800:
801: public function macroDebugbreak()
802: {
803: return 'if (function_exists("debugbreak")) debugbreak(); elseif (function_exists("xdebug_break")) xdebug_break()';
804: }
805:
806:
807:
808: 809: 810:
811: public function macroControl($content)
812: {
813: $pair = $this->fetchToken($content); 814: if ($pair === NULL) {
815: throw new LatteException("Missing control name in {control}", 0, $this->filter->line);
816: }
817: $pair = explode(':', $pair, 2);
818: $name = $this->formatString($pair[0]);
819: $method = isset($pair[1]) ? ucfirst($pair[1]) : '';
820: $method = String::match($method, '#^(' . self::RE_IDENTIFIER . '|)$#') ? "render$method" : "{\"render$method\"}";
821: $param = $this->formatArray($content);
822: if (strpos($content, '=>') === FALSE) $param = substr($param, 6, -1); 823: return ($name[0] === '$' ? "if (is_object($name)) \$_ctrl = $name; else " : '')
824: . '$_ctrl = $control->getWidget(' . $name . '); '
825: . 'if ($_ctrl instanceof IPartiallyRenderable) $_ctrl->validateControl(); '
826: . "\$_ctrl->$method($param)";
827: }
828:
829:
830:
831: 832: 833:
834: public function macroLink($content, $modifiers)
835: {
836: return $this->formatModifiers('$control->link(' . $this->formatLink($content) .')', $modifiers);
837: }
838:
839:
840:
841: 842: 843:
844: public function macroPlink($content, $modifiers)
845: {
846: return $this->formatModifiers('$presenter->link(' . $this->formatLink($content) .')', $modifiers);
847: }
848:
849:
850:
851: 852: 853:
854: public function macroIfCurrent($content)
855: {
856: return ($content ? 'try { $presenter->link(' . $this->formatLink($content) . '); } catch (InvalidLinkException $e) {}' : '')
857: . '; if ($presenter->getLastCreatedRequestFlag("current")):';
858: }
859:
860:
861:
862: 863: 864:
865: private function formatLink($content)
866: {
867: return $this->formatString($this->fetchToken($content)) . $this->formatArray($content, ', '); 868: }
869:
870:
871:
872: 873: 874:
875: public function macroVar($content, $modifiers, $extract = FALSE)
876: {
877: $out = '';
878: $var = TRUE;
879: foreach ($this->parseMacro($content) as $rec) {
880: list($token, $name, $depth) = $rec;
881:
882: if ($var && ($name === self::T_SYMBOL || $name === self::T_VARIABLE)) {
883: if ($extract) {
884: $token = "'" . trim($token, "'$") . "'";
885: } else {
886: $token = '$' . trim($token, "'$");
887: }
888: } elseif (($token === '=' || $token === '=>') && $depth === 0) {
889: $token = $extract ? '=>' : '=';
890: $var = FALSE;
891:
892: } elseif ($token === ',' && $depth === 0) {
893: $token = $extract ? ',' : ';';
894: $var = TRUE;
895: }
896: $out .= $token;
897: }
898: return $out;
899: }
900:
901:
902:
903: 904: 905:
906: public function macroDefault($content)
907: {
908: return 'extract(array(' . $this->macroVar($content, '', TRUE) . '), EXTR_SKIP)';
909: }
910:
911:
912:
913: 914: 915:
916: public function macroModifiers($content, $modifiers)
917: {
918: return $this->formatModifiers($this->formatMacroArgs($content), $modifiers);
919: }
920:
921:
922:
923: 924: 925:
926: public function escape($content)
927: {
928: return $this->filter->escape;
929: }
930:
931:
932:
933:
934:
935:
936:
937: 938: 939: 940: 941: 942:
943: public function formatModifiers($var, $modifiers)
944: {
945: if (!$modifiers) return $var;
946: $inside = FALSE;
947: foreach ($this->parseMacro(ltrim($modifiers, '|')) as $rec) {
948: list($token, $name) = $rec;
949:
950: if ($name === self::T_WHITESPACE) {
951: $var = rtrim($var) . ' ';
952:
953: } elseif (!$inside) {
954: if ($name === self::T_SYMBOL) {
955: $var = "\$template->" . trim($token, "'") . "($var";
956: $inside = TRUE;
957: } else {
958: throw new LatteException("Modifier name must be alphanumeric string, '$token' given.", 0, $this->filter->line);
959: }
960: } else {
961: if ($token === ':' || $token === ',') {
962: $var = $var . ', ';
963:
964: } elseif ($token === '|') {
965: $var = $var . ')';
966: $inside = FALSE;
967:
968: } else {
969: $var .= $token;
970: }
971: }
972: }
973: return $inside ? "$var)" : $var;
974: }
975:
976:
977:
978: 979: 980: 981: 982:
983: public function fetchToken(& $s)
984: {
985: if ($matches = String::match($s, '#^((?>'.LatteFilter::RE_STRING.'|[^\'"\s,]+)+)\s*,?\s*(.*)$#s')) { 986: $s = $matches[2];
987: return $matches[1];
988: }
989: return NULL;
990: }
991:
992:
993:
994: 995: 996: 997: 998: 999:
1000: public function formatMacroArgs($input)
1001: {
1002: $out = '';
1003: foreach ($this->parseMacro($input) as $token) {
1004: $out .= $token[0];
1005: }
1006: return $out;
1007: }
1008:
1009:
1010:
1011: 1012: 1013: 1014: 1015: 1016:
1017: public function formatArray($input, $prefix = '')
1018: {
1019: $tokens = $this->parseMacro($input);
1020: if (!$tokens) {
1021: return '';
1022: }
1023: $out = '';
1024: $expand = NULL;
1025: $tokens[] = NULL; 1026: foreach ($tokens as $rec) {
1027: list($token, $name, $depth) = $rec;
1028: if ($token === '(expand)' && $depth === 0) {
1029: $expand = TRUE;
1030: $token = '),';
1031:
1032: } elseif ($expand && ($token === ',' || $token === NULL) && !$depth) {
1033: $expand = FALSE;
1034: $token = ', array(';
1035: }
1036: $out .= $token;
1037: }
1038: return $prefix . ($expand === NULL ? "array($out)" : "array_merge(array($out))");
1039: }
1040:
1041:
1042:
1043: 1044: 1045: 1046: 1047:
1048: public function formatString($s)
1049: {
1050: static $keywords = array('true'=>1, 'false'=>1, 'null'=>1);
1051: return (is_numeric($s) || strspn($s, '\'"$') || isset($keywords[strtolower($s)])) ? $s : '"' . $s . '"';
1052: }
1053:
1054:
1055:
1056: 1057: 1058: 1059:
1060: private function parseMacro($input)
1061: {
1062: $this->tokenizer->tokenize($input);
1063: $this->tokenizer->tokens[] = NULL; 1064:
1065: $inTernary = $lastSymbol = $prev = NULL;
1066: $tokens = $arrays = array();
1067: $n = -1;
1068: while (++$n < count($this->tokenizer->tokens)) {
1069: list($token, $name) = $current = $this->tokenizer->tokens[$n];
1070: $depth = count($arrays);
1071:
1072: if ($name === self::T_COMMENT) {
1073: continue; 1074:
1075: } elseif ($name === self::T_WHITESPACE) {
1076: $current[2] = $depth;
1077: $tokens[] = $current;
1078: continue;
1079:
1080: } elseif ($name === self::T_SYMBOL && in_array($prev[0], array(',', '(', '[', '=', '=>', ':', '?', NULL), TRUE)) {
1081: $lastSymbol = count($tokens); 1082:
1083: } elseif (is_int($lastSymbol) && in_array($token, array(',', ')', ']', '=', '=>', ':', '|', NULL), TRUE)) {
1084: $tokens[$lastSymbol][0] = "'" . $tokens[$lastSymbol][0] . "'"; 1085: $lastSymbol = NULL;
1086:
1087: } else {
1088: $lastSymbol = NULL;
1089: }
1090:
1091: if ($token === '?') { 1092: $inTernary = $depth;
1093:
1094: } elseif ($token === ':') {
1095: $inTernary = NULL;
1096:
1097: } elseif ($inTernary === $depth && ($token === ',' || $token === ')' || $token === ']' || $token === NULL)) { 1098: $tokens[] = array(':', NULL, $depth);
1099: $tokens[] = array('null', NULL, $depth);
1100: $inTernary = NULL;
1101: }
1102:
1103: if ($token === '[') { 1104: if ($arrays[] = $prev[0] !== ']' && $prev[1] !== self::T_SYMBOL && $prev[1] !== self::T_VARIABLE) {
1105: $tokens[] = array('array', NULL, $depth);
1106: $current = array('(', NULL);
1107: }
1108: } elseif ($token === ']') {
1109: if (array_pop($arrays) === TRUE) {
1110: $current = array(')', NULL);
1111: }
1112: } elseif ($token === '(') { 1113: $arrays[] = '(';
1114:
1115: } elseif ($token === ')') { 1116: array_pop($arrays);
1117: }
1118:
1119: if ($current) {
1120: $current[2] = $depth;
1121: $tokens[] = $prev = $current;
1122: }
1123: }
1124: return $tokens;
1125: }
1126:
1127:
1128:
1129:
1130:
1131:
1132:
1133: 1134: 1135: 1136: 1137: 1138: 1139:
1140: public static function callBlock($context, $name, $params)
1141: {
1142: if (empty($context->blocks[$name])) {
1143: throw new InvalidStateException("Cannot include undefined block '$name'.");
1144: }
1145: $block = reset($context->blocks[$name]);
1146: $block($context, $params);
1147: }
1148:
1149:
1150:
1151: 1152: 1153: 1154: 1155: 1156: 1157:
1158: public static function callBlockParent($context, $name, $params)
1159: {
1160: if (empty($context->blocks[$name]) || ($block = next($context->blocks[$name])) === FALSE) {
1161: throw new InvalidStateException("Cannot include undefined parent block '$name'.");
1162: }
1163: $block($context, $params);
1164: }
1165:
1166:
1167:
1168: 1169: 1170: 1171: 1172: 1173: 1174:
1175: public static function includeTemplate($destination, $params, $template)
1176: {
1177: if ($destination instanceof ITemplate) {
1178: $tpl = $destination;
1179:
1180: } elseif ($destination == NULL) { 1181: throw new InvalidArgumentException("Template file name was not specified.");
1182:
1183: } else {
1184: $tpl = clone $template;
1185: if ($template instanceof IFileTemplate) {
1186: if (substr($destination, 0, 1) !== '/' && substr($destination, 1, 1) !== ':') {
1187: $destination = dirname($template->getFile()) . '/' . $destination;
1188: }
1189: $tpl->setFile($destination);
1190: }
1191: }
1192:
1193: $tpl->setParams($params); 1194: return $tpl;
1195: }
1196:
1197:
1198:
1199: 1200: 1201: 1202: 1203: 1204: 1205:
1206: public static function initRuntime($template, $extends, $realFile)
1207: {
1208: $local = (object) NULL;
1209:
1210: 1211: if (isset($template->_l)) {
1212: $local->blocks = & $template->_l->blocks;
1213: $local->templates = & $template->_l->templates;
1214: }
1215: $local->templates[$realFile] = $template;
1216: $local->extends = is_bool($extends) ? $extends : (empty($template->_extends) ? FALSE : $template->_extends);
1217: unset($template->_l, $template->_extends);
1218:
1219: 1220: if (!isset($template->_g)) {
1221: $template->_g = (object) NULL;
1222: }
1223: $local->g = $template->_g;
1224:
1225: 1226: if (!empty($local->g->caches)) {
1227: end($local->g->caches)->addFile($template->getFile());
1228: }
1229:
1230: return $local;
1231: }
1232:
1233:
1234:
1235: public static function renderSnippets($control, $local, $params)
1236: {
1237: $payload = $control->getPresenter()->getPayload();
1238: if (isset($local->blocks)) {
1239: foreach ($local->blocks as $name => $function) {
1240: if ($name[0] !== '_' || !$control->isControlInvalid(substr($name, 1))) continue;
1241: ob_start();
1242: $function = reset($function);
1243: $function($local, $params);
1244: $payload->snippets[$control->getSnippetId(substr($name, 1))] = ob_get_clean();
1245: }
1246: }
1247: if ($control instanceof Control) {
1248: foreach ($control->getComponents(FALSE, 'Control') as $child) {
1249: if ($child->isControlInvalid()) {
1250: $child->render();
1251: }
1252: }
1253: }
1254: }
1255:
1256: }
1257: