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