1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Latte\Macros;
9:
10: use Latte;
11: use Latte\CompileException;
12: use Latte\Helpers;
13: use Latte\MacroNode;
14: use Latte\PhpWriter;
15: use Latte\Runtime\SnippetDriver;
16:
17:
18: 19: 20:
21: class BlockMacros extends MacroSet
22: {
23:
24: private $namedBlocks = [];
25:
26:
27: private $blockTypes = [];
28:
29:
30: private $extends;
31:
32:
33: private $imports;
34:
35:
36: public static function install(Latte\Compiler $compiler)
37: {
38: $me = new static($compiler);
39: $me->addMacro('include', [$me, 'macroInclude']);
40: $me->addMacro('includeblock', [$me, 'macroIncludeBlock']);
41: $me->addMacro('import', [$me, 'macroImport'], null, null, self::ALLOWED_IN_HEAD);
42: $me->addMacro('extends', [$me, 'macroExtends'], null, null, self::ALLOWED_IN_HEAD);
43: $me->addMacro('layout', [$me, 'macroExtends'], null, null, self::ALLOWED_IN_HEAD);
44: $me->addMacro('snippet', [$me, 'macroBlock'], [$me, 'macroBlockEnd']);
45: $me->addMacro('block', [$me, 'macroBlock'], [$me, 'macroBlockEnd'], null, self::AUTO_CLOSE);
46: $me->addMacro('define', [$me, 'macroBlock'], [$me, 'macroBlockEnd']);
47: $me->addMacro('snippetArea', [$me, 'macroBlock'], [$me, 'macroBlockEnd']);
48: $me->addMacro('ifset', [$me, 'macroIfset'], '}');
49: $me->addMacro('elseifset', [$me, 'macroIfset']);
50: }
51:
52:
53: 54: 55: 56:
57: public function initialize()
58: {
59: $this->namedBlocks = [];
60: $this->blockTypes = [];
61: $this->extends = null;
62: $this->imports = [];
63: }
64:
65:
66: 67: 68:
69: public function finalize()
70: {
71: $compiler = $this->getCompiler();
72: $functions = [];
73: foreach ($this->namedBlocks as $name => $code) {
74: $compiler->addMethod(
75: $functions[$name] = $this->generateMethodName($name),
76: '?>' . $compiler->expandTokens($code) . '<?php',
77: '$_args'
78: );
79: }
80:
81: if ($this->namedBlocks) {
82: $compiler->addProperty('blocks', $functions);
83: $compiler->addProperty('blockTypes', $this->blockTypes);
84: }
85:
86: return [
87: ($this->extends === null ? '' : '$this->parentName = ' . $this->extends . ';') . implode($this->imports),
88: ];
89: }
90:
91:
92:
93:
94:
95: 96: 97:
98: public function macroInclude(MacroNode $node, PhpWriter $writer)
99: {
100: $node->replaced = false;
101: $destination = $node->tokenizer->fetchWord();
102: if (!preg_match('~#|[\w-]+\z~A', $destination)) {
103: return false;
104: }
105:
106: $destination = ltrim($destination, '#');
107: $parent = $destination === 'parent';
108: if ($destination === 'parent' || $destination === 'this') {
109: for ($item = $node->parentNode; $item && $item->name !== 'block' && !isset($item->data->name); $item = $item->parentNode);
110: if (!$item) {
111: throw new CompileException("Cannot include $destination block outside of any block.");
112: }
113: $destination = $item->data->name;
114: }
115:
116: $noEscape = Helpers::removeFilter($node->modifiers, 'noescape');
117: if (!$noEscape && Helpers::removeFilter($node->modifiers, 'escape')) {
118: trigger_error('Macro ' . $node->getNotation() . ' provides auto-escaping, remove |escape.');
119: }
120: if ($node->modifiers && !$noEscape) {
121: $node->modifiers .= '|escape';
122: }
123: return $writer->write(
124: '$this->renderBlock' . ($parent ? 'Parent' : '') . '('
125: . (strpos($destination, '$') === false ? var_export($destination, true) : $destination)
126: . ', %node.array? + '
127: . (isset($this->namedBlocks[$destination]) || $parent ? 'get_defined_vars()' : '$this->params')
128: . ($node->modifiers
129: ? ', function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }'
130: : ($noEscape || $parent ? '' : ', ' . var_export(implode($node->context), true)))
131: . ');'
132: );
133: }
134:
135:
136: 137: 138: 139:
140: public function macroIncludeBlock(MacroNode $node, PhpWriter $writer)
141: {
142:
143: $node->replaced = false;
144: if ($node->modifiers) {
145: throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
146: }
147: return $writer->write(
148: 'ob_start(function () {}); $this->createTemplate(%node.word, %node.array? + get_defined_vars(), "includeblock")->renderToContentType(%var); echo rtrim(ob_get_clean());',
149: implode($node->context)
150: );
151: }
152:
153:
154: 155: 156:
157: public function macroImport(MacroNode $node, PhpWriter $writer)
158: {
159: if ($node->modifiers) {
160: throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
161: }
162: $destination = $node->tokenizer->fetchWord();
163: $this->checkExtraArgs($node);
164: $code = $writer->write('$this->createTemplate(%word, $this->params, "import")->render();', $destination);
165: if ($this->getCompiler()->isInHead()) {
166: $this->imports[] = $code;
167: } else {
168: return $code;
169: }
170: }
171:
172:
173: 174: 175:
176: public function macroExtends(MacroNode $node, PhpWriter $writer)
177: {
178: $notation = $node->getNotation();
179: if ($node->modifiers) {
180: throw new CompileException("Modifiers are not allowed in $notation");
181: } elseif (!$node->args) {
182: throw new CompileException("Missing destination in $notation");
183: } elseif ($node->parentNode) {
184: throw new CompileException("$notation must be placed outside any macro.");
185: } elseif ($this->extends !== null) {
186: throw new CompileException("Multiple $notation declarations are not allowed.");
187: } elseif ($node->args === 'none') {
188: $this->extends = 'FALSE';
189: } else {
190: $this->extends = $writer->write('%node.word%node.args');
191: }
192: if (!$this->getCompiler()->isInHead()) {
193: trigger_error("$notation must be placed in template head.", E_USER_WARNING);
194: }
195: }
196:
197:
198: 199: 200: 201: 202: 203:
204: public function macroBlock(MacroNode $node, PhpWriter $writer)
205: {
206: $name = $node->tokenizer->fetchWord();
207:
208: if ($node->name === 'block' && $name === false) {
209: return $node->modifiers === '' ? '' : 'ob_start(function () {})';
210:
211: } elseif ($node->name === 'define' && $node->modifiers) {
212: throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
213: }
214:
215: $node->data->name = $name = ltrim((string) $name, '#');
216: if ($name == null) {
217: if ($node->name === 'define') {
218: throw new CompileException('Missing block name.');
219: }
220:
221: } elseif (strpos($name, '$') !== false) {
222: if ($node->name === 'snippet') {
223: for ($parent = $node->parentNode; $parent && !($parent->name === 'snippet' || $parent->name === 'snippetArea'); $parent = $parent->parentNode);
224: if (!$parent) {
225: throw new CompileException('Dynamic snippets are allowed only inside static snippet/snippetArea.');
226: }
227: $parent->data->dynamic = true;
228: $node->data->leave = true;
229: $node->closingCode = '<?php $this->global->snippetDriver->leave(); ?>';
230: $enterCode = '$this->global->snippetDriver->enter(' . $writer->formatWord($name) . ', "' . SnippetDriver::TYPE_DYNAMIC . '");';
231:
232: if ($node->prefix) {
233: $node->attrCode = $writer->write("<?php echo ' id=\"' . htmlSpecialChars(\$this->global->snippetDriver->getHtmlId({$writer->formatWord($name)})) . '\"' ?>");
234: return $writer->write($enterCode);
235: }
236: $tag = trim((string) $node->tokenizer->fetchWord(), '<>');
237: if ($tag) {
238: trigger_error('HTML tag specified in {snippet} is deprecated, use n:snippet.', E_USER_DEPRECATED);
239: }
240: $tag = $tag ? $tag : 'div';
241: $node->closingCode .= "\n</$tag>";
242: $this->checkExtraArgs($node);
243: return $writer->write("?>\n<$tag id=\"<?php echo htmlSpecialChars(\$this->global->snippetDriver->getHtmlId({$writer->formatWord($name)})) ?>\"><?php " . $enterCode);
244:
245: } else {
246: $node->data->leave = true;
247: $node->data->func = $this->generateMethodName($name);
248: $fname = $writer->formatWord($name);
249: if ($node->name === 'define') {
250: $node->closingCode = '<?php ?>';
251: } else {
252: if (Helpers::startsWith((string) $node->context[1], Latte\Compiler::CONTEXT_HTML_ATTRIBUTE)) {
253: $node->context[1] = '';
254: $node->modifiers .= '|escape';
255: } elseif ($node->modifiers) {
256: $node->modifiers .= '|escape';
257: }
258: $node->closingCode = $writer->write('<?php $this->renderBlock(%raw, get_defined_vars()'
259: . ($node->modifiers ? ', function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }' : '') . '); ?>', $fname);
260: }
261: $blockType = var_export(implode($node->context), true);
262: $this->checkExtraArgs($node);
263: return "\$this->checkBlockContentType($blockType, $fname);"
264: . "\$this->blockQueue[$fname][] = [\$this, '{$node->data->func}'];";
265: }
266: }
267:
268:
269: if ($node->name === 'snippet' || $node->name === 'snippetArea') {
270: if ($node->prefix && isset($node->htmlNode->attrs['id'])) {
271: throw new CompileException('Cannot combine HTML attribute id with n:snippet.');
272: }
273: $node->data->name = $name = '_' . $name;
274: }
275:
276: if (isset($this->namedBlocks[$name])) {
277: throw new CompileException("Cannot redeclare static {$node->name} '$name'");
278: }
279: $extendsCheck = $this->namedBlocks ? '' : 'if ($this->getParentName()) return get_defined_vars();';
280: $this->namedBlocks[$name] = true;
281:
282: if (Helpers::removeFilter($node->modifiers, 'escape')) {
283: trigger_error('Macro ' . $node->getNotation() . ' provides auto-escaping, remove |escape.');
284: }
285: if (Helpers::startsWith((string) $node->context[1], Latte\Compiler::CONTEXT_HTML_ATTRIBUTE)) {
286: $node->context[1] = '';
287: $node->modifiers .= '|escape';
288: } elseif ($node->modifiers) {
289: $node->modifiers .= '|escape';
290: }
291: $this->blockTypes[$name] = implode($node->context);
292:
293: $include = '$this->renderBlock(%var, ' . (($node->name === 'snippet' || $node->name === 'snippetArea') ? '$this->params' : 'get_defined_vars()')
294: . ($node->modifiers ? ', function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }' : '') . ')';
295:
296: if ($node->name === 'snippet') {
297: if ($node->prefix) {
298: if (isset($node->htmlNode->macroAttrs['foreach'])) {
299: trigger_error('Combination of n:snippet with n:foreach is invalid, use n:inner-foreach.', E_USER_WARNING);
300: }
301: $node->attrCode = $writer->write('<?php echo \' id="\' . htmlSpecialChars($this->global->snippetDriver->getHtmlId(%var)) . \'"\' ?>', (string) substr($name, 1));
302: return $writer->write($include, $name);
303: }
304: $tag = trim((string) $node->tokenizer->fetchWord(), '<>');
305: if ($tag) {
306: trigger_error('HTML tag specified in {snippet} is deprecated, use n:snippet.', E_USER_DEPRECATED);
307: }
308: $tag = $tag ? $tag : 'div';
309: $this->checkExtraArgs($node);
310: return $writer->write("?>\n<$tag id=\"<?php echo htmlSpecialChars(\$this->global->snippetDriver->getHtmlId(%var)) ?>\"><?php $include ?>\n</$tag><?php ",
311: (string) substr($name, 1), $name
312: );
313:
314: } elseif ($node->name === 'define') {
315: $tokens = $node->tokenizer;
316: $args = [];
317: while ($tokens->isNext()) {
318: $args[] = $tokens->expectNextValue($tokens::T_VARIABLE);
319: if ($tokens->isNext()) {
320: $tokens->expectNextValue(',');
321: }
322: }
323: if ($args) {
324: $node->data->args = 'list(' . implode(', ', $args) . ') = $_args + [' . str_repeat('NULL, ', count($args)) . '];';
325: }
326: return $extendsCheck;
327:
328: } else {
329: $this->checkExtraArgs($node);
330: return $writer->write($extendsCheck . $include, $name);
331: }
332: }
333:
334:
335: 336: 337: 338: 339: 340:
341: public function macroBlockEnd(MacroNode $node, PhpWriter $writer)
342: {
343: if (isset($node->data->name)) {
344: if ($asInner = $node->name === 'snippet' && $node->prefix === MacroNode::PREFIX_NONE) {
345: $node->content = $node->innerContent;
346: }
347:
348: if (($node->name === 'snippet' || $node->name === 'snippetArea') && strpos($node->data->name, '$') === false) {
349: $type = $node->name === 'snippet' ? SnippetDriver::TYPE_STATIC : SnippetDriver::TYPE_AREA;
350: $node->content = '<?php $this->global->snippetDriver->enter('
351: . $writer->formatWord(substr($node->data->name, 1))
352: . ', "' . $type . '"); ?>'
353: . preg_replace('#(?<=\n)[ \t]+\z#', '', $node->content) . '<?php $this->global->snippetDriver->leave(); ?>';
354: }
355: if (empty($node->data->leave)) {
356: if (preg_match('#\$|n:#', $node->content)) {
357: $node->content = '<?php ' . (isset($node->data->args) ? 'extract($this->params); ' . $node->data->args : 'extract($_args);') . ' ?>'
358: . $node->content;
359: }
360: $this->namedBlocks[$node->data->name] = $tmp = preg_replace('#^\n+|(?<=\n)[ \t]+\z#', '', $node->content);
361: $node->content = substr_replace($node->content, $node->openingCode . "\n", strspn($node->content, "\n"), strlen($tmp));
362: $node->openingCode = '<?php ?>';
363:
364: } elseif (isset($node->data->func)) {
365: $node->content = rtrim($node->content, " \t");
366: $this->getCompiler()->addMethod(
367: $node->data->func,
368: $this->getCompiler()->expandTokens("extract(\$_args);\n?>$node->content<?php"),
369: '$_args'
370: );
371: $node->content = '';
372: }
373:
374: if ($asInner) {
375: $node->innerContent = $node->openingCode . $node->content . $node->closingCode;
376: $node->closingCode = $node->openingCode = '<?php ?>';
377: }
378: return ' ';
379:
380: } elseif ($node->modifiers) {
381: $node->modifiers .= '|escape';
382: return $writer->write('$_fi = new LR\FilterInfo(%var); echo %modifyContent(ob_get_clean());', $node->context[0]);
383: }
384: }
385:
386:
387: 388: 389: 390:
391: public function macroIfset(MacroNode $node, PhpWriter $writer)
392: {
393: if ($node->modifiers) {
394: throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
395: }
396: if (!preg_match('~#|[\w-]+\z~A', $node->args)) {
397: return false;
398: }
399: $list = [];
400: while (($name = $node->tokenizer->fetchWord()) !== false) {
401: $list[] = preg_match('~#|[\w-]+\z~A', $name)
402: ? '$this->blockQueue["' . ltrim($name, '#') . '"]'
403: : $writer->formatArgs(new Latte\MacroTokens($name));
404: }
405: return ($node->name === 'elseifset' ? '} else' : '')
406: . 'if (isset(' . implode(', ', $list) . ')) {';
407: }
408:
409:
410: private function generateMethodName($blockName)
411: {
412: $clean = trim(preg_replace('#\W+#', '_', $blockName), '_');
413: $name = 'block' . ucfirst($clean);
414: $methods = array_keys($this->getCompiler()->getMethods());
415: if (!$clean || in_array(strtolower($name), array_map('strtolower', $methods), true)) {
416: $name .= '_' . substr(md5($blockName), 0, 5);
417: }
418: return $name;
419: }
420: }
421: