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: trigger_error('Macro {includeblock} is deprecated, use similar macro {import}.', E_USER_DEPRECATED);
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:
212: $node->data->name = $name = ltrim($name, '#');
213: if ($name == NULL) {
214: if ($node->name === 'define') {
215: throw new CompileException('Missing block name.');
216: }
217:
218: } elseif (strpos($name, '$') !== FALSE) {
219: if ($node->name === 'snippet') {
220: for ($parent = $node->parentNode; $parent && !($parent->name === 'snippet' || $parent->name === 'snippetArea'); $parent = $parent->parentNode);
221: if (!$parent) {
222: throw new CompileException('Dynamic snippets are allowed only inside static snippet/snippetArea.');
223: }
224: $parent->data->dynamic = TRUE;
225: $node->data->leave = TRUE;
226: $node->closingCode = "<?php \$this->global->snippetDriver->leave(); ?>";
227: $enterCode = '$this->global->snippetDriver->enter(' . $writer->formatWord($name) . ', "' . SnippetDriver::TYPE_DYNAMIC . '");';
228:
229: if ($node->prefix) {
230: $node->attrCode = $writer->write("<?php echo ' id=\"' . htmlSpecialChars(\$this->global->snippetDriver->getHtmlId({$writer->formatWord($name)})) . '\"' ?>");
231: return $writer->write($enterCode);
232: }
233: $tag = trim($node->tokenizer->fetchWord(), '<>');
234: if ($tag) {
235: trigger_error('HTML tag specified in {snippet} is deprecated, use n:snippet.', E_USER_DEPRECATED);
236: }
237: $tag = $tag ? $tag : 'div';
238: $node->closingCode .= "\n</$tag>";
239: $this->checkExtraArgs($node);
240: return $writer->write("?>\n<$tag id=\"<?php echo htmlSpecialChars(\$this->global->snippetDriver->getHtmlId({$writer->formatWord($name)})) ?>\"><?php " . $enterCode);
241:
242: } else {
243: $node->data->leave = TRUE;
244: $node->data->func = $this->generateMethodName($name);
245: $fname = $writer->formatWord($name);
246: $node->closingCode = '<?php ' . ($node->name === 'define' ? '' : "\$this->renderBlock($fname, get_defined_vars());") . ' ?>';
247: $blockType = var_export(implode($node->context), TRUE);
248: $this->checkExtraArgs($node);
249: return "\$this->checkBlockContentType($blockType, $fname);"
250: . "\$this->blockQueue[$fname][] = [\$this, '{$node->data->func}'];";
251: }
252: }
253:
254:
255: if ($node->name === 'snippet' || $node->name === 'snippetArea') {
256: if ($node->prefix && isset($node->htmlNode->attrs['id'])) {
257: throw new CompileException('Cannot combine HTML attribute id with n:snippet.');
258: }
259: $node->data->name = $name = '_' . $name;
260: }
261:
262: if (isset($this->namedBlocks[$name])) {
263: throw new CompileException("Cannot redeclare static {$node->name} '$name'");
264: }
265: $extendsCheck = $this->namedBlocks ? '' : 'if ($this->getParentName()) return get_defined_vars();';
266: $this->namedBlocks[$name] = TRUE;
267:
268: if (Helpers::removeFilter($node->modifiers, 'escape')) {
269: trigger_error('Macro ' . $node->getNotation() . ' provides auto-escaping, remove |escape.');
270: }
271: if (Helpers::startsWith($node->context[1], Latte\Compiler::CONTEXT_HTML_ATTRIBUTE)) {
272: $node->context[1] = '';
273: $node->modifiers .= '|escape';
274: } elseif ($node->modifiers) {
275: $node->modifiers .= '|escape';
276: }
277: $this->blockTypes[$name] = implode($node->context);
278:
279: $include = '$this->renderBlock(%var, ' . (($node->name === 'snippet' || $node->name === 'snippetArea') ? '$this->params' : 'get_defined_vars()')
280: . ($node->modifiers ? ', function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }' : '') . ')';
281:
282: if ($node->name === 'snippet') {
283: if ($node->prefix) {
284: if (isset($node->htmlNode->macroAttrs['foreach'])) {
285: trigger_error('Combination of n:snippet with n:foreach is invalid, use n:inner-foreach.', E_USER_WARNING);
286: }
287: $node->attrCode = $writer->write('<?php echo \' id="\' . htmlSpecialChars($this->global->snippetDriver->getHtmlId(%var)) . \'"\' ?>', (string) substr($name, 1));
288: return $writer->write($include, $name);
289: }
290: $tag = trim($node->tokenizer->fetchWord(), '<>');
291: if ($tag) {
292: trigger_error('HTML tag specified in {snippet} is deprecated, use n:snippet.', E_USER_DEPRECATED);
293: }
294: $tag = $tag ? $tag : 'div';
295: $this->checkExtraArgs($node);
296: return $writer->write("?>\n<$tag id=\"<?php echo htmlSpecialChars(\$this->global->snippetDriver->getHtmlId(%var)) ?>\"><?php $include ?>\n</$tag><?php ",
297: (string) substr($name, 1), $name
298: );
299:
300: } elseif ($node->name === 'define') {
301: $tokens = $node->tokenizer;
302: $args = [];
303: while ($tokens->isNext()) {
304: $args[] = $tokens->expectNextValue($tokens::T_VARIABLE);
305: if ($tokens->isNext()) {
306: $tokens->expectNextValue(',');
307: }
308: }
309: if ($args) {
310: $node->data->args = 'list(' . implode(', ', $args) . ') = $_args + [' . str_repeat('NULL, ', count($args)) . '];';
311: }
312: return $extendsCheck;
313:
314: } else {
315: $this->checkExtraArgs($node);
316: return $writer->write($extendsCheck . $include, $name);
317: }
318: }
319:
320:
321: 322: 323: 324: 325: 326:
327: public function macroBlockEnd(MacroNode $node, PhpWriter $writer)
328: {
329: if (isset($node->data->name)) {
330: if ($asInner = $node->name === 'snippet' && $node->prefix === MacroNode::PREFIX_NONE) {
331: $node->content = $node->innerContent;
332: }
333:
334: if (($node->name === 'snippet' || $node->name === 'snippetArea') && strpos($node->data->name, '$') === FALSE) {
335: $type = $node->name === 'snippet' ? SnippetDriver::TYPE_STATIC : SnippetDriver::TYPE_AREA;
336: $node->content = '<?php $this->global->snippetDriver->enter('
337: . $writer->formatWord(substr($node->data->name, 1))
338: . ', "' . $type . '"); ?>'
339: . preg_replace('#(?<=\n)[ \t]+\z#', '', $node->content) . '<?php $this->global->snippetDriver->leave(); ?>';
340: }
341: if (empty($node->data->leave)) {
342: if (preg_match('#\$|n:#', $node->content)) {
343: $node->content = '<?php ' . (isset($node->data->args) ? 'extract($this->params); ' . $node->data->args : 'extract($_args);') . ' ?>'
344: . $node->content;
345: }
346: $this->namedBlocks[$node->data->name] = $tmp = preg_replace('#^\n+|(?<=\n)[ \t]+\z#', '', $node->content);
347: $node->content = substr_replace($node->content, $node->openingCode . "\n", strspn($node->content, "\n"), strlen($tmp));
348: $node->openingCode = '<?php ?>';
349:
350: } elseif (isset($node->data->func)) {
351: $node->content = rtrim($node->content, " \t");
352: $this->getCompiler()->addMethod(
353: $node->data->func,
354: $this->getCompiler()->expandTokens("extract(\$_args);\n?>$node->content<?php"),
355: '$_args'
356: );
357: $node->content = '';
358: }
359:
360: if ($asInner) {
361: $node->innerContent = $node->openingCode . $node->content . $node->closingCode;
362: $node->closingCode = $node->openingCode = '<?php ?>';
363: }
364: return ' ';
365:
366: } elseif ($node->modifiers) {
367: $node->modifiers .= '|escape';
368: return $writer->write('$_fi = new LR\FilterInfo(%var); echo %modifyContent(ob_get_clean());', $node->context[0]);
369: }
370: }
371:
372:
373: 374: 375: 376:
377: public function macroIfset(MacroNode $node, PhpWriter $writer)
378: {
379: if ($node->modifiers) {
380: throw new CompileException('Modifiers are not allowed in ' . $node->getNotation());
381: }
382: if (!preg_match('~#|[\w-]+\z~A', $node->args)) {
383: return FALSE;
384: }
385: $list = [];
386: while (($name = $node->tokenizer->fetchWord()) !== FALSE) {
387: $list[] = preg_match('~#|[\w-]+\z~A', $name)
388: ? '$this->blockQueue["' . ltrim($name, '#') . '"]'
389: : $writer->formatArgs(new Latte\MacroTokens($name));
390: }
391: return ($node->name === 'elseifset' ? '} else' : '')
392: . 'if (isset(' . implode(', ', $list) . ')) {';
393: }
394:
395:
396: private function generateMethodName($blockName)
397: {
398: $clean = trim(preg_replace('#\W+#', '_', $blockName), '_');
399: $name = 'block' . ucfirst($clean);
400: $methods = array_keys($this->getCompiler()->getMethods());
401: if (!$clean || in_array(strtolower($name), array_map('strtolower', $methods))) {
402: $name .= '_' . substr(md5($blockName), 0, 5);
403: }
404: return $name;
405: }
406:
407: }
408: