1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette\Latte\Macros;
13:
14: use Nette,
15: Nette\Latte,
16: Nette\Latte\MacroNode,
17: Nette\Latte\ParseException,
18: Nette\Utils\Strings;
19:
20:
21:
22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32:
33: class UIMacros extends MacroSet
34: {
35:
36: const RE_IDENTIFIER = '[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF]*';
37:
38:
39: private $namedBlocks = array();
40:
41:
42: private $extends;
43:
44:
45:
46: public static function install(Latte\Parser $parser)
47: {
48: $me = new static($parser);
49: $me->addMacro('include', array($me, 'macroInclude'));
50: $me->addMacro('includeblock', array($me, 'macroIncludeBlock'));
51: $me->addMacro('extends', array($me, 'macroExtends'));
52: $me->addMacro('layout', array($me, 'macroExtends'));
53: $me->addMacro('block', array($me, 'macroBlock'), array($me, 'macroBlockEnd'));
54: $me->addMacro('define', array($me, 'macroBlock'), array($me, 'macroBlockEnd'));
55: $me->addMacro('snippet', array($me, 'macroBlock'), array($me, 'macroBlockEnd'));
56: $me->addMacro('ifset', array($me, 'macroIfset'), 'endif');
57:
58: $me->addMacro('widget', array($me, 'macroControl'));
59: $me->addMacro('control', array($me, 'macroControl'));
60:
61: $me->addMacro('@href', function(MacroNode $node, $writer) use ($me) {
62: return ' ?> href="<?php ' . $me->macroLink($node, $writer) . ' ?>"<?php ';
63: });
64: $me->addMacro('plink', array($me, 'macroLink'));
65: $me->addMacro('link', array($me, 'macroLink'));
66: $me->addMacro('ifCurrent', array($me, 'macroIfCurrent'), 'endif');
67:
68: $me->addMacro('contentType', array($me, 'macroContentType'));
69: $me->addMacro('status', array($me, 'macroStatus'));
70: }
71:
72:
73:
74: 75: 76: 77:
78: public function initialize()
79: {
80: $this->namedBlocks = array();
81: $this->extends = NULL;
82: }
83:
84:
85:
86: 87: 88: 89:
90: public function finalize()
91: {
92:
93: try {
94: $this->parser->writeMacro('/block');
95: } catch (ParseException $e) {
96: }
97:
98: $epilog = $prolog = array();
99:
100: if ($this->namedBlocks) {
101: foreach ($this->namedBlocks as $name => $code) {
102: $func = '_lb' . substr(md5($this->parser->templateId . $name), 0, 10) . '_' . preg_replace('#[^a-z0-9_]#i', '_', $name);
103: $snippet = $name[0] === '_';
104: $prolog[] = "//\n// block $name\n//\n"
105: . "if (!function_exists(\$_l->blocks[" . var_export($name, TRUE) . "][] = '$func')) { "
106: . "function $func(\$_l, \$_args) { "
107: . (PHP_VERSION_ID > 50208 ? 'extract($_args)' : 'foreach ($_args as $__k => $__v) $$__k = $__v')
108: . ($snippet ? '; $control->validateControl(' . var_export(substr($name, 1), TRUE) . ')' : '')
109: . "\n?>$code<?php\n}}";
110: }
111: $prolog[] = "//\n// end of blocks\n//";
112: }
113:
114: if ($this->namedBlocks || $this->extends) {
115: $prolog[] = "// template extending and snippets support";
116:
117: if (is_bool($this->extends)) {
118: $prolog[] = '$_l->extends = ' . var_export($this->extends, TRUE) . '; unset($_extends, $template->_extends);';
119: } else {
120: $prolog[] = '$_l->extends = empty($template->_extends) ? FALSE : $template->_extends; unset($_extends, $template->_extends);';
121: }
122:
123: $prolog[] = '
124: if ($_l->extends) {
125: ob_start();
126: } elseif (!empty($control->snippetMode)) {
127: return Nette\Latte\Macros\UIMacros::renderSnippets($control, $_l, get_defined_vars());
128: }';
129: $epilog[] = '
130: // template extending support
131: if ($_l->extends) {
132: ob_end_clean();
133: Nette\Latte\Macros\CoreMacros::includeTemplate($_l->extends, get_defined_vars(), $template)->render();
134: }';
135: } else {
136: $prolog[] = '
137: // snippets support
138: if (!empty($control->snippetMode)) {
139: return Nette\Latte\Macros\UIMacros::renderSnippets($control, $_l, get_defined_vars());
140: }';
141: }
142:
143: return array(implode("\n\n", $prolog), implode("\n", $epilog));
144: }
145:
146:
147:
148:
149:
150:
151:
152: 153: 154:
155: public function macroInclude(MacroNode $node, $writer)
156: {
157: $destination = $node->tokenizer->fetchWord();
158: if (substr($destination, 0, 1) !== '#') {
159: return FALSE;
160: }
161:
162: $destination = ltrim($destination, '#');
163: if (!Strings::match($destination, '#^\$?' . self::RE_IDENTIFIER . '$#')) {
164: throw new ParseException("Included block name must be alphanumeric string, '$destination' given.");
165: }
166:
167: $parent = $destination === 'parent';
168: if ($destination === 'parent' || $destination === 'this') {
169: $item = $node->parentNode;
170: while ($item && $item->name !== 'block' && !isset($item->data->name)) $item = $item->parentNode;
171: if (!$item) {
172: throw new ParseException("Cannot include $destination block outside of any block.");
173: }
174: $destination = $item->data->name;
175: }
176:
177: $name = $destination[0] === '$' ? $destination : var_export($destination, TRUE);
178: if (isset($this->namedBlocks[$destination]) && !$parent) {
179: $cmd = "call_user_func(reset(\$_l->blocks[$name]), \$_l, %node.array? + \$template->getParams())";
180: } else {
181: $cmd = 'Nette\Latte\Macros\UIMacros::callBlock' . ($parent ? 'Parent' : '') . "(\$_l, $name, %node.array? + \$template->getParams())";
182: }
183:
184: if ($node->modifiers) {
185: return $writer->write("ob_start(); $cmd; echo %modify(ob_get_clean())");
186: } else {
187: return $writer->write($cmd);
188: }
189: }
190:
191:
192:
193: 194: 195:
196: public function macroIncludeBlock(MacroNode $node, $writer)
197: {
198: return $writer->write('Nette\Latte\Macros\CoreMacros::includeTemplate(%node.word, %node.array? + get_defined_vars(), $_l->templates[%var])->render()',
199: $this->parser->templateId);
200: }
201:
202:
203:
204: 205: 206:
207: public function macroExtends(MacroNode $node, $writer)
208: {
209: if (!$node->args) {
210: throw new ParseException("Missing destination in {extends}");
211: }
212: if (!empty($node->parentNode)) {
213: throw new ParseException("{extends} must be placed outside any macro.");
214: }
215: if ($this->extends !== NULL) {
216: throw new ParseException("Multiple {extends} declarations are not allowed.");
217: }
218: $this->extends = $node->args !== 'none';
219: return $this->extends ? '$_l->extends = ' . ($node->args === 'auto' ? '$layout' : $writer->formatArgs()) : '';
220: }
221:
222:
223:
224: 225: 226: 227: 228:
229: public function macroBlock(MacroNode $node, $writer)
230: {
231: $name = $node->tokenizer->fetchWord();
232:
233: if ($node->name === 'block' && $name === FALSE) {
234: return $node->modifiers === '' ? '' : 'ob_start()';
235: }
236:
237: $node->data->name = $name = ltrim($name, '#');
238: $node->data->end = '';
239: if ($name == NULL) {
240: if ($node->name !== 'snippet') {
241: throw new ParseException("Missing block name.");
242: }
243:
244: } elseif (!Strings::match($name, '#^' . self::RE_IDENTIFIER . '$#')) {
245: if ($node->name === 'snippet') {
246: $parent = $node->parentNode;
247: while ($parent && $parent->name !== 'snippet') $parent = $parent->parentNode;
248: if (!$parent) {
249: throw new ParseException("Dynamic snippets are allowed only inside static snippet.");
250: }
251: $parent->data->dynamic = TRUE;
252:
253: $tag = trim($node->tokenizer->fetchWord(), '<>');
254: $tag = $tag ? $tag : 'div';
255: $node->data->leave = TRUE;
256: $node->data->end = "\$_dynSnippets[\$_dynSnippetId] = ob_get_flush() ?>\n</$tag><?php";
257: return $writer->write("?>\n<$tag id=\"<?php echo \$_dynSnippetId = \$control->getSnippetId({$writer->formatWord($name)}) ?>\"><?php ob_start()");
258:
259: } else {
260: $node->data->leave = TRUE;
261: $fname = $writer->formatWord($name);
262: $node->data->end = "}} call_user_func(reset(\$_l->blocks[$fname]), \$_l, get_defined_vars())";
263: $func = '_lb' . substr(md5($this->parser->templateId . $name), 0, 10) . '_' . preg_replace('#[^a-z0-9_]#i', '_', $name);
264: return "//\n// block $name\n//\n"
265: . "if (!function_exists(\$_l->blocks[$fname][] = '$func')) { "
266: . "function $func(\$_l, \$_args) { "
267: . (PHP_VERSION_ID > 50208 ? 'extract($_args)' : 'foreach ($_args as $__k => $__v) $$__k = $__v');
268: }
269: }
270:
271:
272: if ($node->name === 'snippet') {
273: $node->data->name = $name = '_' . $name;
274: }
275: if (isset($this->namedBlocks[$name])) {
276: throw new ParseException("Cannot redeclare static block '$name'");
277: }
278: $top = empty($node->parentNode);
279: $this->namedBlocks[$name] = TRUE;
280:
281: $include = 'call_user_func(reset($_l->blocks[%var]), $_l, ' . ($node->name === 'snippet' ? '$template->getParams()' : 'get_defined_vars()') . ')';
282: if ($node->modifiers) {
283: $include = "ob_start(); $include; echo %modify(ob_get_clean())";
284: }
285:
286: if ($node->name === 'snippet') {
287: $tag = trim($node->tokenizer->fetchWord(), '<>');
288: $tag = $tag ? $tag : 'div';
289: return $writer->write("?>\n<$tag id=\"<?php echo \$control->getSnippetId(%var) ?>\"><?php $include ?>\n</$tag><?php ",
290: (string) substr($name, 1), $name
291: );
292:
293: } elseif ($node->name === 'define') {
294: return '';
295:
296: } elseif (!$top) {
297: return $writer->write($include, $name);
298:
299: } elseif ($this->extends) {
300: return '';
301:
302: } else {
303: return $writer->write("if (!\$_l->extends) { $include; }", $name);
304: }
305: }
306:
307:
308:
309: 310: 311: 312: 313:
314: public function macroBlockEnd(MacroNode $node, $writer)
315: {
316: if (isset($node->data->name)) {
317: if (empty($node->data->leave)) {
318: if (!empty($node->data->dynamic)) {
319: $node->content .= '<?php if (isset($_dynSnippets)) return $_dynSnippets; ?>';
320: }
321: $this->namedBlocks[$node->data->name] = $node->content;
322: $node->content = '';
323: }
324: return $node->data->end;
325:
326: } elseif ($node->modifiers) {
327: return $writer->write('echo %modify(ob_get_clean())');
328: }
329: }
330:
331:
332:
333: 334: 335:
336: public function macroIfset(MacroNode $node, $writer)
337: {
338: if (strpos($node->args, '#') === FALSE) {
339: return FALSE;
340: }
341: $list = array();
342: while (($name = $node->tokenizer->fetchWord()) !== FALSE) {
343: $list[] = $name[0] === '#' ? '$_l->blocks["' . substr($name, 1) . '"]' : $name;
344: }
345: return 'if (isset(' . implode(', ', $list) . ')):';
346: }
347:
348:
349:
350: 351: 352:
353: public function macroControl(MacroNode $node, $writer)
354: {
355: $pair = $node->tokenizer->fetchWord();
356: if ($pair === FALSE) {
357: throw new ParseException("Missing control name in {control}");
358: }
359: $pair = explode(':', $pair, 2);
360: $name = $writer->formatWord($pair[0]);
361: $method = isset($pair[1]) ? ucfirst($pair[1]) : '';
362: $method = Strings::match($method, '#^(' . self::RE_IDENTIFIER . '|)$#') ? "render$method" : "{\"render$method\"}";
363: $param = $writer->formatArray();
364: if (strpos($node->args, '=>') === FALSE) {
365: $param = substr($param, 6, -1);
366: }
367: return ($name[0] === '$' ? "if (is_object($name)) \$_ctrl = $name; else " : '')
368: . '$_ctrl = $control->getWidget(' . $name . '); '
369: . 'if ($_ctrl instanceof Nette\Application\UI\IPartiallyRenderable) $_ctrl->validateControl(); '
370: . "\$_ctrl->$method($param)";
371: }
372:
373:
374:
375: 376: 377: 378: 379:
380: public function macroLink(MacroNode $node, $writer)
381: {
382: return $writer->write('echo %escape(' . ($node->name === 'plink' ? '$presenter' : '$control') . '->link(%node.word, %node.array?))');
383: }
384:
385:
386:
387: 388: 389:
390: public function macroIfCurrent(MacroNode $node, $writer)
391: {
392: return $writer->write(($node->args ? 'try { $presenter->link(%node.word, %node.array?); } catch (Nette\Application\UI\InvalidLinkException $e) {}' : '')
393: . '; if ($presenter->getLastCreatedRequestFlag("current")):');
394: }
395:
396:
397:
398: 399: 400:
401: public function macroContentType(MacroNode $node, $writer)
402: {
403: if (strpos($node->args, 'html') !== FALSE) {
404: $this->parser->context = array(Latte\Parser::CONTEXT_TEXT);
405:
406: } elseif (strpos($node->args, 'xml') !== FALSE) {
407: $this->parser->context = array(Latte\Parser::CONTEXT_NONE, 'xml');
408:
409: } elseif (strpos($node->args, 'javascript') !== FALSE) {
410: $this->parser->context = array(Latte\Parser::CONTEXT_NONE, 'js');
411:
412: } elseif (strpos($node->args, 'css') !== FALSE) {
413: $this->parser->context = array(Latte\Parser::CONTEXT_NONE, 'css');
414:
415: } elseif (strpos($node->args, 'plain') !== FALSE) {
416: $this->parser->context = array(Latte\Parser::CONTEXT_NONE, 'text');
417:
418: } else {
419: $this->parser->context = array(Latte\Parser::CONTEXT_NONE);
420: }
421:
422:
423: if (strpos($node->args, '/')) {
424: return $writer->write('$netteHttpResponse->setHeader("Content-Type", %var)', $node->args);
425: }
426: }
427:
428:
429:
430: 431: 432:
433: public function macroStatus(MacroNode $node, $writer)
434: {
435: return $writer->write((substr($node->args, -1) === '?' ? 'if (!$netteHttpResponse->isSent()) ' : '') .
436: '$netteHttpResponse->setCode(%var)', (int) $node->args
437: );
438: }
439:
440:
441:
442:
443:
444:
445:
446: 447: 448: 449: 450: 451: 452:
453: public static function callBlock($context, $name, $params)
454: {
455: if (empty($context->blocks[$name])) {
456: throw new Nette\InvalidStateException("Cannot include undefined block '$name'.");
457: }
458: $block = reset($context->blocks[$name]);
459: $block($context, $params);
460: }
461:
462:
463:
464: 465: 466: 467: 468: 469: 470:
471: public static function callBlockParent($context, $name, $params)
472: {
473: if (empty($context->blocks[$name]) || ($block = next($context->blocks[$name])) === FALSE) {
474: throw new Nette\InvalidStateException("Cannot include undefined parent block '$name'.");
475: }
476: $block($context, $params);
477: }
478:
479:
480:
481: public static function renderSnippets($control, $local, $params)
482: {
483: $control->snippetMode = FALSE;
484: $payload = $control->getPresenter()->getPayload();
485: if (isset($local->blocks)) {
486: foreach ($local->blocks as $name => $function) {
487: if ($name[0] !== '_' || !$control->isControlInvalid(substr($name, 1))) {
488: continue;
489: }
490: ob_start();
491: $function = reset($function);
492: $snippets = $function($local, $params);
493: $payload->snippets[$id = $control->getSnippetId(substr($name, 1))] = ob_get_clean();
494: if ($snippets) {
495: $payload->snippets += $snippets;
496: unset($payload->snippets[$id]);
497: }
498: }
499: }
500: if ($control instanceof Nette\Application\UI\Control) {
501: foreach ($control->getComponents(FALSE, 'Nette\Application\UI\Control') as $child) {
502: if ($child->isControlInvalid()) {
503: $child->snippetMode = TRUE;
504: $child->render();
505: $child->snippetMode = FALSE;
506: }
507: }
508: }
509: }
510:
511: }
512: