Skip to content

Commit ec4d44d

Browse files
daundg
authored andcommitted
implicit default block for {embed} (#419)
1 parent 0fc6458 commit ec4d44d

3 files changed

Lines changed: 266 additions & 11 deletions

File tree

src/Latte/Essential/Nodes/EmbedNode.php

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@
88
namespace Latte\Essential\Nodes;
99

1010
use Latte\CompileException;
11+
use Latte\Compiler\Block;
12+
use Latte\Compiler\Nodes\AreaNode;
1113
use Latte\Compiler\Nodes\FragmentNode;
1214
use Latte\Compiler\Nodes\Php\Expression\ArrayNode;
1315
use Latte\Compiler\Nodes\Php\ExpressionNode;
16+
use Latte\Compiler\Nodes\Php\ModifierNode;
1417
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
1518
use Latte\Compiler\Nodes\StatementNode;
1619
use Latte\Compiler\Nodes\TextNode;
1720
use Latte\Compiler\PrintContext;
1821
use Latte\Compiler\Tag;
1922
use Latte\Compiler\TemplateParser;
20-
use function count, preg_match;
23+
use Latte\Compiler\Token;
24+
use function array_pop, array_shift, count, end, preg_match;
2125

2226

2327
/**
@@ -50,21 +54,64 @@ public static function create(Tag $tag, TemplateParser $parser): \Generator
5054
$node->args = $tag->parser->parseArguments();
5155

5256
$prevIndex = $parser->blockLayer;
53-
$parser->blockLayer = $node->layer = count($parser->blocks);
57+
$layer = $parser->blockLayer = $node->layer = count($parser->blocks);
5458
$parser->blocks[$parser->blockLayer] = [];
5559
[$node->blocks] = yield;
5660

61+
// Content not wrapped in a {block} becomes the implicit {block default} block.
62+
$kept = $loose = [];
5763
foreach ($node->blocks->children as $child) {
58-
if (!$child instanceof ImportNode && !$child instanceof BlockNode && !$child instanceof TextNode) {
59-
throw new CompileException('Unexpected content inside {embed} tags.', $child->position);
64+
$child instanceof ImportNode || $child instanceof BlockNode
65+
? $kept[] = $child
66+
: $loose[] = $child;
67+
}
68+
69+
// ignore whitespace-only text surrounding the blocks, but keep it in between
70+
while ($loose && $loose[0] instanceof TextNode && $loose[0]->isWhitespace()) {
71+
array_shift($loose);
72+
}
73+
while ($loose && ($last = end($loose)) instanceof TextNode && $last->isWhitespace()) {
74+
array_pop($loose);
75+
}
76+
77+
if ($loose) {
78+
if (isset($parser->blocks[$layer]['default'])) {
79+
throw new CompileException(
80+
'Cannot combine loose content with an explicit {block default} inside {embed}; both define the default block.',
81+
$loose[0]->position ?? $tag->position,
82+
);
6083
}
84+
85+
$kept[] = $node->wrapInDefaultBlock($loose, $layer, $parser, $tag);
86+
$node->blocks->children = $kept;
6187
}
6288

6389
$parser->blockLayer = $prevIndex;
6490
return $node;
6591
}
6692

6793

94+
/**
95+
* Wraps loose content into the implicit {block default}.
96+
* @param list<AreaNode> $content
97+
*/
98+
private function wrapInDefaultBlock(array $content, int|string $layer, TemplateParser $parser, Tag $tag): BlockNode
99+
{
100+
// the tag name 'block' is load-bearing: TemplateGenerator uses it to give the block
101+
// access to the caller's variables (varStack instead of $this->params)
102+
$blockTag = new Tag('block', [new Token(Token::End, '', $tag->position)], $tag->position, prefix: $tag->prefix);
103+
$block = new Block(new StringNode('default'), $layer, $blockTag);
104+
$parser->blocks[$layer]['default'] = $block; // uniqueness already verified by the caller
105+
106+
$node = new BlockNode;
107+
$node->block = $block;
108+
$node->modifier = new ModifierNode([], position: $tag->position);
109+
$node->content = new FragmentNode($content);
110+
$node->position = $tag->position;
111+
return $node;
112+
}
113+
114+
68115
public function print(PrintContext $context): string
69116
{
70117
$imports = '';

tests/tags/embed.block.phpt

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,83 @@ testTemplate(
424424
);
425425

426426

427+
testTemplate(
428+
'default block: works together with named blocks',
429+
[
430+
'main' => <<<'XX'
431+
432+
{embed embed}custom body{block title}custom title{/block}{/embed}
433+
434+
{define embed}title={block title}fallback title{/block} body={block default}fallback body{/block}{/define}
435+
436+
XX,
437+
],
438+
<<<'XX'
439+
440+
title=custom title body=custom body
441+
442+
XX,
443+
);
444+
445+
446+
testTemplate(
447+
'default block: self-closing caller keeps the fallback',
448+
[
449+
'main' => <<<'XX'
450+
451+
{embed embed/}
452+
453+
{define embed}body={block default}fallback body{/block}{/define}
454+
455+
XX,
456+
],
457+
<<<'XX'
458+
459+
body=fallback body
460+
461+
XX,
462+
);
463+
464+
465+
testTemplate(
466+
'default block: empty caller keeps the fallback',
467+
[
468+
'main' => <<<'XX'
469+
470+
{embed embed}{/embed}
471+
472+
{define embed}body={block default}fallback body{/block}{/define}
473+
474+
XX,
475+
],
476+
<<<'XX'
477+
478+
body=fallback body
479+
480+
XX,
481+
);
482+
483+
484+
testTemplate(
485+
'default block: default content sees caller variables',
486+
[
487+
'main' => <<<'XX'
488+
489+
{var $greeting = 'Hello'}
490+
{embed embed}{var $name = 'world'}{$greeting} {$name}{if true}!{/if}{/embed}
491+
492+
{define embed}body={block default}fallback body{/block}{/define}
493+
494+
XX,
495+
],
496+
<<<'XX'
497+
498+
body=Hello world!
499+
500+
XX,
501+
);
502+
503+
427504
$latte = createLatte();
428505
Assert::exception(
429506
fn() => $latte->renderToString('{embed block (null)/}'),

tests/tags/embed.file.phpt

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,6 @@ function testTemplate(string $title, array $templates, string $exp = '')
1717
}
1818

1919

20-
Assert::exception(function () {
21-
testTemplate('unexpected content', [
22-
'main' => '{embed "embed.latte"} {$a} {/embed}',
23-
]);
24-
}, Latte\CompileException::class, 'Unexpected content inside {embed} tags (on line 1 at column 23)');
25-
26-
2720
testTemplate('keyword file', [
2821
'main' => '{embed file embed}{/embed}',
2922
'embed' => 'embed',
@@ -919,6 +912,144 @@ testTemplate(
919912
);
920913

921914

915+
testTemplate(
916+
'default block: caller content overrides the fallback',
917+
[
918+
'main' => <<<'XX'
919+
{embed "embed.latte"}custom body{/embed}
920+
XX,
921+
'embed.latte' => <<<'XX'
922+
start
923+
{block default}fallback body{/block}
924+
end
925+
XX,
926+
],
927+
<<<'XX'
928+
start
929+
custom body
930+
end
931+
XX,
932+
);
933+
934+
935+
testTemplate(
936+
'default block: empty caller keeps the fallback',
937+
[
938+
'main' => <<<'XX'
939+
{embed "embed.latte"}{/embed}
940+
XX,
941+
'embed.latte' => <<<'XX'
942+
start
943+
{block default}fallback body{/block}
944+
end
945+
XX,
946+
],
947+
<<<'XX'
948+
start
949+
fallback body
950+
end
951+
XX,
952+
);
953+
954+
955+
testTemplate(
956+
'default block: self-closing caller keeps the fallback',
957+
[
958+
'main' => <<<'XX'
959+
{embed "embed.latte"/}
960+
XX,
961+
'embed.latte' => <<<'XX'
962+
start
963+
{block default}fallback body{/block}
964+
end
965+
XX,
966+
],
967+
<<<'XX'
968+
start
969+
fallback body
970+
end
971+
XX,
972+
);
973+
974+
975+
testTemplate(
976+
'default block: whitespace-only caller keeps the fallback',
977+
[
978+
'main' => <<<'XX'
979+
{embed "embed.latte"} {/embed}
980+
XX,
981+
'embed.latte' => <<<'XX'
982+
start
983+
{block default}fallback body{/block}
984+
end
985+
XX,
986+
],
987+
<<<'XX'
988+
start
989+
fallback body
990+
end
991+
XX,
992+
);
993+
994+
995+
testTemplate(
996+
'default block: default content sees caller variables',
997+
[
998+
'main' => <<<'XX'
999+
{var $greeting = 'Hello'}{embed "embed.latte"}{var $name = 'world'}{$greeting} {$name}{if true}!{/if}{/embed}
1000+
XX,
1001+
'embed.latte' => <<<'XX'
1002+
start
1003+
{block default}fallback body{/block}
1004+
end
1005+
XX,
1006+
],
1007+
<<<'XX'
1008+
start
1009+
Hello world!
1010+
end
1011+
XX,
1012+
);
1013+
1014+
1015+
testTemplate(
1016+
'default block: works together with named blocks',
1017+
[
1018+
'main' => <<<'XX'
1019+
{embed "embed.latte"}custom body{block title}custom title{/block}{/embed}
1020+
XX,
1021+
'embed.latte' => <<<'XX'
1022+
title={block title}fallback title{/block}
1023+
body={block default}fallback body{/block}
1024+
XX,
1025+
],
1026+
<<<'XX'
1027+
title=custom title
1028+
body=custom body
1029+
XX,
1030+
);
1031+
1032+
1033+
testTemplate(
1034+
'default block: caller content ignored without a placeholder',
1035+
[
1036+
'main' => <<<'XX'
1037+
{embed "embed.latte"}custom body{/embed}
1038+
XX,
1039+
'embed.latte' => 'no placeholder here',
1040+
],
1041+
'no placeholder here',
1042+
);
1043+
1044+
1045+
Assert::exception(function () {
1046+
testTemplate('default block: loose content mixed with explicit block', [
1047+
'main' => '{embed "embed.latte"}custom body{block default}explicit body{/block}{/embed}',
1048+
'embed.latte' => '[{block default}x{/block}]',
1049+
]);
1050+
}, Latte\CompileException::class, 'Cannot combine loose content with an explicit {block default} inside {embed}; both define the default block (on line 1 at column 22)');
1051+
1052+
9221053
$latte = createLatte();
9231054
Assert::exception(
9241055
fn() => $latte->renderToString('{embed (null)/}'),

0 commit comments

Comments
 (0)