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