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