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