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 (
110: $item = $node->parentNode;
111: $item && $item->name !== 'block' && !isset($item->data->name);
112: $item = $item->parentNode
113: );
114: if (!$item) {
115: throw new CompileException("Cannot include $destination block outside of any block.");
116: }
117: $destination = $item->data->name;
118: }
119:
120: $noEscape = Helpers::removeFilter($node->modifiers, 'noescape');
121: if (!$noEscape && Helpers::removeFilter($node->modifiers, 'escape')) {
122: trigger_error('Macro ' . $node->getNotation() . ' provides auto-escaping, remove |escape.');
123: }
124: if ($node->modifiers && !$noEscape) {
125: $node->modifiers .= '|escape';
126: }
127: return $writer->write(
128: '$this->renderBlock' . ($parent ? 'Parent' : '') . '('
129: . (strpos($destination, '$') === false ? var_export($destination, true) : $destination)
130: . ', %node.array? + '
131: . (isset($this->namedBlocks[$destination]) || $parent ? 'get_defined_vars()' : '$this->params')
132: . ($node->modifiers
133: ? ', function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }'
134: : ($noEscape || $parent ? '' : ', ' . var_export(implode($node->context), true)))
135: . ');'
136: );
137: }
138:
139:
140: 141: 142: 143:
144: public function macroIncludeBlock(MacroNode $node, PhpWriter $writer)
145: {
146:
147: $node->replaced = false;
148: if ($node->modifiers) {
149: throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
150: }
151: return $writer->write(
152: 'ob_start(function () {}); $this->createTemplate(%node.word, %node.array? + get_defined_vars(), "includeblock")->renderToContentType(%var); echo rtrim(ob_get_clean());',
153: implode($node->context)
154: );
155: }
156:
157:
158: 159: 160:
161: public function macroImport(MacroNode $node, PhpWriter $writer)
162: {
163: if ($node->modifiers) {
164: throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
165: }
166: $destination = $node->tokenizer->fetchWord();
167: $this->checkExtraArgs($node);
168: $code = $writer->write('$this->createTemplate(%word, $this->params, "import")->render();', $destination);
169: if ($this->getCompiler()->isInHead()) {
170: $this->imports[] = $code;
171: } else {
172: return $code;
173: }
174: }
175:
176:
177: 178: 179:
180: public function macroExtends(MacroNode $node, PhpWriter $writer)
181: {
182: $notation = $node->getNotation();
183: if ($node->modifiers) {
184: throw new CompileException("Modifiers are not allowed in $notation");
185: } elseif (!$node->args) {
186: throw new CompileException("Missing destination in $notation");
187: } elseif ($node->parentNode) {
188: throw new CompileException("$notation must be placed outside any macro.");
189: } elseif ($this->extends !== null) {
190: throw new CompileException("Multiple $notation declarations are not allowed.");
191: } elseif ($node->args === 'none') {
192: $this->extends = 'FALSE';
193: } else {
194: $this->extends = $writer->write('%node.word%node.args');
195: }
196: if (!$this->getCompiler()->isInHead()) {
197: trigger_error("$notation must be placed in template head.", E_USER_WARNING);
198: }
199: }
200:
201:
202: 203: 204: 205: 206: 207:
208: public function macroBlock(MacroNode $node, PhpWriter $writer)
209: {
210: $name = $node->tokenizer->fetchWord();
211:
212: if ($node->name === 'block' && $name === false) {
213: return $node->modifiers === '' ? '' : 'ob_start(function () {})';
214:
215: } elseif ($node->name === 'define' && $node->modifiers) {
216: throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
217: }
218:
219: $node->data->name = $name = ltrim((string) $name, '#');
220: if ($name == null) {
221: if ($node->name === 'define') {
222: throw new CompileException('Missing block name.');
223: }
224:
225: } elseif (strpos($name, '$') !== false) {
226: if ($node->name === 'snippet') {
227: for (
228: $parent = $node->parentNode;
229: $parent && !($parent->name === 'snippet' || $parent->name === 'snippetArea');
230: $parent = $parent->parentNode
231: );
232: if (!$parent) {
233: throw new CompileException('Dynamic snippets are allowed only inside static snippet/snippetArea.');
234: }
235: $parent->data->dynamic = true;
236: $node->data->leave = true;
237: $node->closingCode = '<?php $this->global->snippetDriver->leave(); ?>';
238: $enterCode = '$this->global->snippetDriver->enter(' . $writer->formatWord($name) . ', "' . SnippetDriver::TYPE_DYNAMIC . '");';
239:
240: if ($node->prefix) {
241: $node->attrCode = $writer->write("<?php echo ' id=\"' . htmlSpecialChars(\$this->global->snippetDriver->getHtmlId({$writer->formatWord($name)})) . '\"' ?>");
242: return $writer->write($enterCode);
243: }
244: $tag = trim((string) $node->tokenizer->fetchWord(), '<>');
245: if ($tag) {
246: trigger_error('HTML tag specified in {snippet} is deprecated, use n:snippet.', E_USER_DEPRECATED);
247: }
248: $tag = $tag ?: 'div';
249: $node->closingCode .= "\n</$tag>";
250: $this->checkExtraArgs($node);
251: return $writer->write("?>\n<$tag id=\"<?php echo htmlSpecialChars(\$this->global->snippetDriver->getHtmlId({$writer->formatWord($name)})) ?>\"><?php " . $enterCode);
252:
253: } else {
254: $node->data->leave = true;
255: $node->data->func = $this->generateMethodName($name);
256: $fname = $writer->formatWord($name);
257: if ($node->name === 'define') {
258: $node->closingCode = '<?php ?>';
259: } else {
260: if (Helpers::startsWith((string) $node->context[1], Latte\Compiler::CONTEXT_HTML_ATTRIBUTE)) {
261: $node->context[1] = '';
262: $node->modifiers .= '|escape';
263: } elseif ($node->modifiers) {
264: $node->modifiers .= '|escape';
265: }
266: $node->closingCode = $writer->write('<?php $this->renderBlock(%raw, get_defined_vars()'
267: . ($node->modifiers ? ', function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }' : '') . '); ?>', $fname);
268: }
269: $blockType = var_export(implode($node->context), true);
270: $this->checkExtraArgs($node);
271: return "\$this->checkBlockContentType($blockType, $fname);"
272: . "\$this->blockQueue[$fname][] = [\$this, '{$node->data->func}'];";
273: }
274: }
275:
276:
277: if ($node->name === 'snippet' || $node->name === 'snippetArea') {
278: if ($node->prefix && isset($node->htmlNode->attrs['id'])) {
279: throw new CompileException('Cannot combine HTML attribute id with n:snippet.');
280: }
281: $node->data->name = $name = '_' . $name;
282: }
283:
284: if (isset($this->namedBlocks[$name])) {
285: throw new CompileException("Cannot redeclare static {$node->name} '$name'");
286: }
287: $extendsCheck = $this->namedBlocks ? '' : 'if ($this->getParentName()) return get_defined_vars();';
288: $this->namedBlocks[$name] = true;
289:
290: if (Helpers::removeFilter($node->modifiers, 'escape')) {
291: trigger_error('Macro ' . $node->getNotation() . ' provides auto-escaping, remove |escape.');
292: }
293: if (Helpers::startsWith((string) $node->context[1], Latte\Compiler::CONTEXT_HTML_ATTRIBUTE)) {
294: $node->context[1] = '';
295: $node->modifiers .= '|escape';
296: } elseif ($node->modifiers) {
297: $node->modifiers .= '|escape';
298: }
299: $this->blockTypes[$name] = implode($node->context);
300:
301: $include = '$this->renderBlock(%var, ' . (($node->name === 'snippet' || $node->name === 'snippetArea') ? '$this->params' : 'get_defined_vars()')
302: . ($node->modifiers ? ', function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }' : '') . ')';
303:
304: if ($node->name === 'snippet') {
305: if ($node->prefix) {
306: if (isset($node->htmlNode->macroAttrs['foreach'])) {
307: trigger_error('Combination of n:snippet with n:foreach is invalid, use n:inner-foreach.', E_USER_WARNING);
308: }
309: $node->attrCode = $writer->write('<?php echo \' id="\' . htmlSpecialChars($this->global->snippetDriver->getHtmlId(%var)) . \'"\' ?>', (string) substr($name, 1));
310: return $writer->write($include, $name);
311: }
312: $tag = trim((string) $node->tokenizer->fetchWord(), '<>');
313: if ($tag) {
314: trigger_error('HTML tag specified in {snippet} is deprecated, use n:snippet.', E_USER_DEPRECATED);
315: }
316: $tag = $tag ?: 'div';
317: $this->checkExtraArgs($node);
318: return $writer->write("?>\n<$tag id=\"<?php echo htmlSpecialChars(\$this->global->snippetDriver->getHtmlId(%var)) ?>\"><?php $include ?>\n</$tag><?php ",
319: (string) substr($name, 1), $name
320: );
321:
322: } elseif ($node->name === 'define') {
323: $tokens = $node->tokenizer;
324: $args = [];
325: while ($tokens->isNext()) {
326: $args[] = $tokens->expectNextValue($tokens::T_VARIABLE);
327: if ($tokens->isNext()) {
328: $tokens->expectNextValue(',');
329: }
330: }
331: if ($args) {
332: $node->data->args = 'list(' . implode(', ', $args) . ') = $_args + [' . str_repeat('NULL, ', count($args)) . '];';
333: }
334: return $extendsCheck;
335:
336: } else {
337: $this->checkExtraArgs($node);
338: return $writer->write($extendsCheck . $include, $name);
339: }
340: }
341:
342:
343: 344: 345: 346: 347: 348:
349: public function macroBlockEnd(MacroNode $node, PhpWriter $writer)
350: {
351: if (isset($node->data->name)) {
352: if ($asInner = $node->name === 'snippet' && $node->prefix === MacroNode::PREFIX_NONE) {
353: $node->content = $node->innerContent;
354: }
355:
356: if (($node->name === 'snippet' || $node->name === 'snippetArea') && strpos($node->data->name, '$') === false) {
357: $type = $node->name === 'snippet' ? SnippetDriver::TYPE_STATIC : SnippetDriver::TYPE_AREA;
358: $node->content = '<?php $this->global->snippetDriver->enter('
359: . $writer->formatWord(substr($node->data->name, 1))
360: . ', "' . $type . '"); ?>'
361: . preg_replace('#(?<=\n)[ \t]+\z#', '', $node->content) . '<?php $this->global->snippetDriver->leave(); ?>';
362: }
363: if (empty($node->data->leave)) {
364: if (preg_match('#\$|n:#', $node->content)) {
365: $node->content = '<?php ' . (isset($node->data->args) ? 'extract($this->params); ' . $node->data->args : 'extract($_args);') . ' ?>'
366: . $node->content;
367: }
368: $this->namedBlocks[$node->data->name] = $tmp = preg_replace('#^\n+|(?<=\n)[ \t]+\z#', '', $node->content);
369: $node->content = substr_replace($node->content, $node->openingCode . "\n", strspn($node->content, "\n"), strlen($tmp));
370: $node->openingCode = '<?php ?>';
371:
372: } elseif (isset($node->data->func)) {
373: $node->content = rtrim($node->content, " \t");
374: $this->getCompiler()->addMethod(
375: $node->data->func,
376: $this->getCompiler()->expandTokens("extract(\$_args);\n?>$node->content<?php"),
377: '$_args'
378: );
379: $node->content = '';
380: }
381:
382: if ($asInner) {
383: $node->innerContent = $node->openingCode . $node->content . $node->closingCode;
384: $node->closingCode = $node->openingCode = '<?php ?>';
385: }
386: return ' ';
387:
388: } elseif ($node->modifiers) {
389: $node->modifiers .= '|escape';
390: return $writer->write('$_fi = new LR\FilterInfo(%var); echo %modifyContent(ob_get_clean());', $node->context[0]);
391: }
392: }
393:
394:
395: 396: 397: 398:
399: public function macroIfset(MacroNode $node, PhpWriter $writer)
400: {
401: if ($node->modifiers) {
402: throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
403: }
404: if (!preg_match('~#|[\w-]+\z~A', $node->args)) {
405: return false;
406: }
407: $list = [];
408: while (($name = $node->tokenizer->fetchWord()) !== false) {
409: $list[] = preg_match('~#|[\w-]+\z~A', $name)
410: ? '$this->blockQueue["' . ltrim($name, '#') . '"]'
411: : $writer->formatArgs(new Latte\MacroTokens($name));
412: }
413: return ($node->name === 'elseifset' ? '} else' : '')
414: . 'if (isset(' . implode(', ', $list) . ')) {';
415: }
416:
417:
418: private function generateMethodName($blockName)
419: {
420: $clean = trim(preg_replace('#\W+#', '_', $blockName), '_');
421: $name = 'block' . ucfirst($clean);
422: $methods = array_keys($this->getCompiler()->getMethods());
423: if (!$clean || in_array(strtolower($name), array_map('strtolower', $methods), true)) {
424: $name .= '_' . substr(md5($blockName), 0, 5);
425: }
426: return $name;
427: }
428: }
429: