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\CompileException,
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\Compiler $compiler)
47: {
48: $me = new static($compiler);
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', NULL, NULL, 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->getCompiler()->writeMacro('/block');
95: } catch (CompileException $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->getCompiler()->getTemplateId() . $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: $prolog[] = '$_l->extends = '
118: . ($this->extends ? $this->extends : 'empty($template->_extended) && isset($_control) && $_control instanceof Nette\Application\UI\Presenter ? $_control->findLayoutTemplateFile() : NULL')
119: . '; $template->_extended = $_extended = TRUE;';
120:
121: $prolog[] = '
122: if ($_l->extends) {
123: ' . ($this->namedBlocks ? 'ob_start();' : 'return Nette\Latte\Macros\CoreMacros::includeTemplate($_l->extends, get_defined_vars(), $template)->render();') . '
124:
125: } elseif (!empty($_control->snippetMode)) {
126: return Nette\Latte\Macros\UIMacros::renderSnippets($_control, $_l, get_defined_vars());
127: }';
128: } else {
129: $prolog[] = '
130: // snippets support
131: if (!empty($_control->snippetMode)) {
132: return Nette\Latte\Macros\UIMacros::renderSnippets($_control, $_l, get_defined_vars());
133: }';
134: }
135:
136: return array(implode("\n\n", $prolog), implode("\n", $epilog));
137: }
138:
139:
140:
141:
142:
143:
144:
145: 146: 147:
148: public function macroInclude(MacroNode $node, $writer)
149: {
150: $destination = $node->tokenizer->fetchWord();
151: if (substr($destination, 0, 1) !== '#') {
152: return FALSE;
153: }
154:
155: $destination = ltrim($destination, '#');
156: if (!Strings::match($destination, '#^\$?' . self::RE_IDENTIFIER . '$#')) {
157: throw new CompileException("Included block name must be alphanumeric string, '$destination' given.");
158: }
159:
160: $parent = $destination === 'parent';
161: if ($destination === 'parent' || $destination === 'this') {
162: $item = $node->parentNode;
163: for ($item = $node->parentNode; $item && $item->name !== 'block' && !isset($item->data->name); $item = $item->parentNode);
164: if (!$item) {
165: throw new CompileException("Cannot include $destination block outside of any block.");
166: }
167: $destination = $item->data->name;
168: }
169:
170: $name = $destination[0] === '$' ? $destination : var_export($destination, TRUE);
171: if (isset($this->namedBlocks[$destination]) && !$parent) {
172: $cmd = "call_user_func(reset(\$_l->blocks[$name]), \$_l, %node.array? + get_defined_vars())";
173: } else {
174: $cmd = 'Nette\Latte\Macros\UIMacros::callBlock' . ($parent ? 'Parent' : '') . "(\$_l, $name, %node.array? + " . ($parent ? 'get_defined_vars' : '$template->getParameters') . '())';
175: }
176:
177: if ($node->modifiers) {
178: return $writer->write("ob_start(); $cmd; echo %modify(ob_get_clean())");
179: } else {
180: return $writer->write($cmd);
181: }
182: }
183:
184:
185:
186: 187: 188:
189: public function macroIncludeBlock(MacroNode $node, $writer)
190: {
191: return $writer->write('Nette\Latte\Macros\CoreMacros::includeTemplate(%node.word, %node.array? + get_defined_vars(), $_l->templates[%var])->render()',
192: $this->getCompiler()->getTemplateId());
193: }
194:
195:
196:
197: 198: 199:
200: public function macroExtends(MacroNode $node, $writer)
201: {
202: if (!$node->args) {
203: throw new CompileException("Missing destination in {extends}");
204: }
205: if (!empty($node->parentNode)) {
206: throw new CompileException("{extends} must be placed outside any macro.");
207: }
208: if ($this->extends !== NULL) {
209: throw new CompileException("Multiple {extends} declarations are not allowed.");
210: }
211: if ($node->args === 'none') {
212: $this->extends = 'FALSE';
213: } elseif ($node->args === 'auto') {
214: $this->extends = '$_presenter->findLayoutTemplateFile()';
215: } else {
216: $this->extends = $writer->write('%node.word%node.args');
217: }
218: return;
219: }
220:
221:
222:
223: 224: 225: 226: 227:
228: public function macroBlock(MacroNode $node, $writer)
229: {
230: $name = $node->tokenizer->fetchWord();
231:
232: if ($node->name === 'block' && $name === FALSE) {
233: return $node->modifiers === '' ? '' : 'ob_start()';
234: }
235:
236: $node->data->name = $name = ltrim($name, '#');
237: if ($name == NULL) {
238: if ($node->name !== 'snippet') {
239: throw new CompileException("Missing block name.");
240: }
241:
242: } elseif (!Strings::match($name, '#^' . self::RE_IDENTIFIER . '$#')) {
243: if ($node->name === 'snippet') {
244: for ($parent = $node->parentNode; $parent && $parent->name !== 'snippet'; $parent = $parent->parentNode);
245: if (!$parent) {
246: throw new CompileException("Dynamic snippets are allowed only inside static snippet.");
247: }
248: $parent->data->dynamic = TRUE;
249: $node->data->leave = TRUE;
250: $node->closingCode = "<?php \$_dynSnippets[\$_dynSnippetId] = ob_get_flush() ?>";
251:
252: if ($node->htmlNode) {
253: $node->attrCode = $writer->write("<?php echo ' id=\"' . (\$_dynSnippetId = \$_control->getSnippetId({$writer->formatWord($name)})) . '\"' ?>");
254: return $writer->write('ob_start()');
255: }
256: $tag = trim($node->tokenizer->fetchWord(), '<>');
257: $tag = $tag ? $tag : 'div';
258: $node->closingCode .= "\n</$tag>";
259: return $writer->write("?>\n<$tag id=\"<?php echo \$_dynSnippetId = \$_control->getSnippetId({$writer->formatWord($name)}) ?>\"><?php ob_start()");
260:
261: } else {
262: $node->data->leave = TRUE;
263: $fname = $writer->formatWord($name);
264: $node->closingCode = "<?php }} call_user_func(reset(\$_l->blocks[$fname]), \$_l, get_defined_vars()) ?>";
265: $func = '_lb' . substr(md5($this->getCompiler()->getTemplateId() . $name), 0, 10) . '_' . preg_replace('#[^a-z0-9_]#i', '_', $name);
266: return "//\n// block $name\n//\n"
267: . "if (!function_exists(\$_l->blocks[$fname][] = '$func')) { "
268: . "function $func(\$_l, \$_args) { "
269: . (PHP_VERSION_ID > 50208 ? 'extract($_args)' : 'foreach ($_args as $__k => $__v) $$__k = $__v');
270: }
271: }
272:
273:
274: if ($node->name === 'snippet') {
275: $node->data->name = $name = '_' . $name;
276: }
277: if (isset($this->namedBlocks[$name])) {
278: throw new CompileException("Cannot redeclare static block '$name'");
279: }
280: $prolog = $this->namedBlocks ? '' : "if (\$_l->extends) { ob_end_clean(); return Nette\\Latte\\Macros\\CoreMacros::includeTemplate(\$_l->extends, get_defined_vars(), \$template)->render(); }\n";
281: $top = empty($node->parentNode);
282: $this->namedBlocks[$name] = TRUE;
283:
284: $include = 'call_user_func(reset($_l->blocks[%var]), $_l, ' . ($node->name === 'snippet' ? '$template->getParameters()' : 'get_defined_vars()') . ')';
285: if ($node->modifiers) {
286: $include = "ob_start(); $include; echo %modify(ob_get_clean())";
287: }
288:
289: if ($node->name === 'snippet') {
290: if ($node->htmlNode) {
291: $node->attrCode = $writer->write('<?php echo \' id="\' . $_control->getSnippetId(%var) . \'"\' ?>', (string) substr($name, 1));
292: return $writer->write($prolog . $include, $name);
293: }
294: $tag = trim($node->tokenizer->fetchWord(), '<>');
295: $tag = $tag ? $tag : 'div';
296: return $writer->write("$prolog ?>\n<$tag id=\"<?php echo \$_control->getSnippetId(%var) ?>\"><?php $include ?>\n</$tag><?php ",
297: (string) substr($name, 1), $name
298: );
299:
300: } elseif ($node->name === 'define') {
301: return $prolog;
302:
303: } else {
304: return $writer->write($prolog . $include, $name);
305: }
306: }
307:
308:
309:
310: 311: 312: 313: 314:
315: public function macroBlockEnd(MacroNode $node, $writer)
316: {
317: if (isset($node->data->name)) {
318: if ($node->name === 'snippet' && isset($node->htmlNode->macroAttrs['snippet'])
319: && preg_match("#^(.*? n:\w+>\n?)(.*?)([ \t]*<[^<]+)$#sD", $node->content, $m))
320: {
321: $node->openingCode = $m[1] . $node->openingCode;
322: $node->content = $m[2];
323: $node->closingCode .= $m[3];
324: }
325:
326: if (empty($node->data->leave)) {
327: if (!empty($node->data->dynamic)) {
328: $node->content .= '<?php if (isset($_dynSnippets)) return $_dynSnippets; ?>';
329: }
330: preg_match("#^(\n)?(.*?)([ \t]*)$#sD", $node->content, $m);
331: $this->namedBlocks[$node->data->name] = $m[2];
332: $node->content = $m[1] . $node->openingCode . "\n" . $m[3];
333: $node->openingCode = "<?php ?>";
334: }
335:
336: } elseif ($node->modifiers) {
337: return $writer->write('echo %modify(ob_get_clean())');
338: }
339: }
340:
341:
342:
343: 344: 345:
346: public function macroIfset(MacroNode $node, $writer)
347: {
348: if (!Strings::contains($node->args, '#')) {
349: return FALSE;
350: }
351: $list = array();
352: while (($name = $node->tokenizer->fetchWord()) !== FALSE) {
353: $list[] = $name[0] === '#' ? '$_l->blocks["' . substr($name, 1) . '"]' : $name;
354: }
355: return 'if (isset(' . implode(', ', $list) . ')):';
356: }
357:
358:
359:
360: 361: 362:
363: public function macroControl(MacroNode $node, $writer)
364: {
365: $pair = $node->tokenizer->fetchWord();
366: if ($pair === FALSE) {
367: throw new CompileException("Missing control name in {control}");
368: }
369: $pair = explode(':', $pair, 2);
370: $name = $writer->formatWord($pair[0]);
371: $method = isset($pair[1]) ? ucfirst($pair[1]) : '';
372: $method = Strings::match($method, '#^(' . self::RE_IDENTIFIER . '|)$#') ? "render$method" : "{\"render$method\"}";
373: $param = $writer->formatArray();
374: if (!Strings::contains($node->args, '=>')) {
375: $param = substr($param, 6, -1);
376: }
377: return ($name[0] === '$' ? "if (is_object($name)) \$_ctrl = $name; else " : '')
378: . '$_ctrl = $_control->getComponent(' . $name . '); '
379: . 'if ($_ctrl instanceof Nette\Application\UI\IRenderable) $_ctrl->validateControl(); '
380: . "\$_ctrl->$method($param)";
381: }
382:
383:
384:
385: 386: 387: 388: 389:
390: public function macroLink(MacroNode $node, $writer)
391: {
392: return $writer->write('echo %escape(' . ($node->name === 'plink' ? '$_presenter' : '$_control') . '->link(%node.word, %node.array?))');
393: }
394:
395:
396:
397: 398: 399:
400: public function macroIfCurrent(MacroNode $node, $writer)
401: {
402: return $writer->write(($node->args ? 'try { $_presenter->link(%node.word, %node.array?); } catch (Nette\Application\UI\InvalidLinkException $e) {}' : '')
403: . '; if ($_presenter->getLastCreatedRequestFlag("current")):');
404: }
405:
406:
407:
408: 409: 410:
411: public function macroContentType(MacroNode $node, $writer)
412: {
413: if (Strings::contains($node->args, 'xhtml')) {
414: $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_XHTML);
415:
416: } elseif (Strings::contains($node->args, 'html')) {
417: $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_HTML);
418:
419: } elseif (Strings::contains($node->args, 'xml')) {
420: $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_XML);
421:
422: } elseif (Strings::contains($node->args, 'javascript')) {
423: $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_JS);
424:
425: } elseif (Strings::contains($node->args, 'css')) {
426: $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_CSS);
427:
428: } elseif (Strings::contains($node->args, 'calendar')) {
429: $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_ICAL);
430:
431: } else {
432: $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_TEXT);
433: }
434:
435:
436: if (Strings::contains($node->args, '/')) {
437: return $writer->write('$netteHttpResponse->setHeader("Content-Type", %var)', $node->args);
438: }
439: }
440:
441:
442:
443: 444: 445:
446: public function macroStatus(MacroNode $node, $writer)
447: {
448: return $writer->write((substr($node->args, -1) === '?' ? 'if (!$netteHttpResponse->isSent()) ' : '') .
449: '$netteHttpResponse->setCode(%var)', (int) $node->args
450: );
451: }
452:
453:
454:
455:
456:
457:
458:
459: 460: 461: 462: 463: 464: 465:
466: public static function callBlock($context, $name, $params)
467: {
468: if (empty($context->blocks[$name])) {
469: throw new Nette\InvalidStateException("Cannot include undefined block '$name'.");
470: }
471: $block = reset($context->blocks[$name]);
472: $block($context, $params);
473: }
474:
475:
476:
477: 478: 479: 480: 481: 482: 483:
484: public static function callBlockParent($context, $name, $params)
485: {
486: if (empty($context->blocks[$name]) || ($block = next($context->blocks[$name])) === FALSE) {
487: throw new Nette\InvalidStateException("Cannot include undefined parent block '$name'.");
488: }
489: $block($context, $params);
490: }
491:
492:
493:
494: public static function renderSnippets($control, $local, $params)
495: {
496: $control->snippetMode = FALSE;
497: $payload = $control->getPresenter()->getPayload();
498: if (isset($local->blocks)) {
499: foreach ($local->blocks as $name => $function) {
500: if ($name[0] !== '_' || !$control->isControlInvalid(substr($name, 1))) {
501: continue;
502: }
503: ob_start();
504: $function = reset($function);
505: $snippets = $function($local, $params);
506: $payload->snippets[$id = $control->getSnippetId(substr($name, 1))] = ob_get_clean();
507: if ($snippets) {
508: $payload->snippets += $snippets;
509: unset($payload->snippets[$id]);
510: }
511: }
512: }
513: if ($control instanceof Nette\Application\UI\Control) {
514: foreach ($control->getComponents(FALSE, 'Nette\Application\UI\Control') as $child) {
515: if ($child->isControlInvalid()) {
516: $child->snippetMode = TRUE;
517: $child->render();
518: $child->snippetMode = FALSE;
519: }
520: }
521: }
522: }
523:
524: }
525: