Skip to content

Commit e276a4b

Browse files
committed
StatementNode: added tagPositions
1 parent c7cb351 commit e276a4b

7 files changed

Lines changed: 183 additions & 21 deletions

File tree

src/Latte/Compiler/Nodes/StatementNode.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
namespace Latte\Compiler\Nodes;
99

10+
use Latte\Compiler\Position;
11+
1012

1113
/**
1214
* Base for Latte tags like {if}, {foreach}, {block}.
@@ -16,4 +18,6 @@
1618
*/
1719
abstract class StatementNode extends AreaNode
1820
{
21+
/** @var list<Position> positions of all tags (opening, intermediate like {else}, closing) */
22+
public array $tagPositions = [];
1923
}

src/Latte/Compiler/TemplateParser.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ public function parseLatteStatement(?\Closure $resolver = null): ?Node
200200

201201
$token = $this->stream->peek();
202202
$startTag = $this->pushTag($this->parseLatteTag());
203+
$tagPositions = [$startTag->position];
203204

204205
$parser = $this->getTagParser($startTag->name, $token->position);
205206
$res = $parser($startTag, $this);
@@ -237,10 +238,12 @@ public function parseLatteStatement(?\Closure $resolver = null): ?Node
237238

238239
if ($tag->closing) {
239240
$this->checkEndTag($startTag, $tag);
241+
$tagPositions[] = $tag->position;
240242
$res->send([$content, $tag]);
241243
$this->ensureIsConsumed($tag);
242244
break;
243245
} elseif (in_array($tag->name, $this->lookFor[$startTag] ?? [], strict: true)) {
246+
$tagPositions[] = $tag->position;
244247
$this->pushTag($tag);
245248
$res->send([$content, $tag]);
246249
$this->ensureIsConsumed($tag);
@@ -279,6 +282,11 @@ public function parseLatteStatement(?\Closure $resolver = null): ?Node
279282
$node->position = isset($tag) && $tag->closing
280283
? $this->endPosition($startTag->position, $tag->position->offset + $tag->position->length)
281284
: $startTag->position;
285+
286+
if ($node instanceof Nodes\StatementNode) {
287+
$node->tagPositions = $tagPositions;
288+
}
289+
282290
return $node;
283291
}
284292

src/Latte/Essential/Nodes/FirstLastSepNode.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use Latte\Compiler\Nodes\AreaNode;
1111
use Latte\Compiler\Nodes\Php\ExpressionNode;
1212
use Latte\Compiler\Nodes\StatementNode;
13-
use Latte\Compiler\Position;
1413
use Latte\Compiler\PrintContext;
1514
use Latte\Compiler\Tag;
1615

@@ -25,7 +24,6 @@ class FirstLastSepNode extends StatementNode
2524
public ?ExpressionNode $width;
2625
public AreaNode $then;
2726
public ?AreaNode $else = null;
28-
public ?Position $elseLine = null;
2927

3028

3129
/** @return \Generator<int, ?list<string>, array{AreaNode, ?Tag}, static> */
@@ -37,7 +35,6 @@ public static function create(Tag $tag): \Generator
3735

3836
[$node->then, $nextTag] = yield ['else'];
3937
if ($nextTag?->name === 'else') {
40-
$node->elseLine = $nextTag->position;
4138
[$node->else] = yield;
4239
}
4340

@@ -59,7 +56,7 @@ public function print(PrintContext $context): string
5956
$this->width,
6057
$this->position,
6158
$this->then,
62-
$this->elseLine,
59+
$this->tagPositions[1] ?? null,
6360
$this->else,
6461
);
6562
}

src/Latte/Essential/Nodes/ForeachNode.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use Latte\Compiler\Nodes\StatementNode;
1919
use Latte\Compiler\Nodes\TemplateNode;
2020
use Latte\Compiler\NodeTraverser;
21-
use Latte\Compiler\Position;
2221
use Latte\Compiler\PrintContext;
2322
use Latte\Compiler\Tag;
2423
use Latte\Compiler\TagParser;
@@ -38,7 +37,6 @@ class ForeachNode extends StatementNode
3837
public ExpressionNode|ListNode $value;
3938
public AreaNode $content;
4039
public ?AreaNode $else = null;
41-
public ?Position $elseLine = null;
4240
public ?bool $iterator = null;
4341
public bool $checkArgs = true;
4442

@@ -66,7 +64,6 @@ public static function create(Tag $tag): \Generator
6664

6765
[$node->content, $nextTag] = yield ['else'];
6866
if ($nextTag?->name === 'else') {
69-
$node->elseLine = $nextTag->position;
7067
[$node->else] = yield;
7168
}
7269

@@ -102,7 +99,7 @@ public function print(PrintContext $context): string
10299
$code .= $context->format(
103100
"if (%raw) %line { %node\n}\n",
104101
$useIterator ? '$iterator->isEmpty()' : $context->format('empty(%node)', $this->expression),
105-
$this->elseLine,
102+
$this->tagPositions[1] ?? null,
106103
$this->else,
107104
);
108105
}

src/Latte/Essential/Nodes/IfChangedNode.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use Latte\Compiler\Nodes\AreaNode;
1111
use Latte\Compiler\Nodes\Php\Expression\ArrayNode;
1212
use Latte\Compiler\Nodes\StatementNode;
13-
use Latte\Compiler\Position;
1413
use Latte\Compiler\PrintContext;
1514
use Latte\Compiler\Tag;
1615

@@ -24,7 +23,6 @@ class IfChangedNode extends StatementNode
2423
public ArrayNode $conditions;
2524
public AreaNode $then;
2625
public ?AreaNode $else = null;
27-
public ?Position $elseLine = null;
2826

2927

3028
/** @return \Generator<int, ?list<string>, array{AreaNode, ?Tag}, static> */
@@ -35,7 +33,6 @@ public static function create(Tag $tag): \Generator
3533

3634
[$node->then, $nextTag] = yield ['else'];
3735
if ($nextTag?->name === 'else') {
38-
$node->elseLine = $nextTag->position;
3936
[$node->else] = yield;
4037
}
4138

@@ -68,7 +65,7 @@ private function printExpression(PrintContext $context): string
6865
$context->generateId(),
6966
$this->conditions,
7067
$this->then,
71-
$this->elseLine,
68+
$this->tagPositions[1] ?? null,
7269
$this->else,
7370
)
7471
: $context->format(
@@ -107,7 +104,7 @@ private function printCapturing(PrintContext $context): string
107104
$this->position,
108105
$this->then,
109106
$context->generateId(),
110-
$this->elseLine,
107+
$this->tagPositions[1] ?? null,
111108
$this->else,
112109
)
113110
: $context->format(

src/Latte/Essential/Nodes/SwitchNode.php

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class SwitchNode extends StatementNode
2626
{
2727
public ?ExpressionNode $expression;
2828

29-
/** @var array<array{?ArrayNode, Position, FragmentNode}> */
29+
/** @var array<array{?ArrayNode, FragmentNode}> */
3030
public array $cases = [];
3131

3232

@@ -53,17 +53,16 @@ public static function create(Tag $tag): \Generator
5353
while (true) {
5454
if ($nextTag->name === 'case') {
5555
$nextTag->expectArguments();
56-
[$case, $line] = [$nextTag->parser->parseArguments(), $nextTag->position];
56+
$case = $nextTag->parser->parseArguments();
5757
[$content, $nextTag] = yield ['case', 'default'];
58-
$node->cases[] = [$case, $line, $content];
58+
$node->cases[] = [$case, $content];
5959

6060
} elseif ($nextTag->name === 'default') {
6161
if ($default++) {
6262
throw new CompileException('Tag {switch} may only contain one {default} clause.', $nextTag->position);
6363
}
64-
$line = $nextTag->position;
6564
[$content, $nextTag] = yield ['case', 'default'];
66-
$node->cases[] = [null, $line, $content];
65+
$node->cases[] = [null, $content];
6766

6867
} else {
6968
return $node;
@@ -81,7 +80,7 @@ public function print(PrintContext $context): string
8180
);
8281
$first = true;
8382
$default = null;
84-
foreach ($this->cases as [$case, $line, $content]) {
83+
foreach ($this->cases as $i => [$case, $content]) {
8584
if (!$case) {
8685
$default = $content->print($context);
8786
continue;
@@ -93,7 +92,7 @@ public function print(PrintContext $context): string
9392
$res .= $context->format(
9493
'if (in_array($ʟ_switch, %node, true)) %line { %node } ',
9594
$case,
96-
$line,
95+
$this->tagPositions[$i + 1] ?? null,
9796
$content,
9897
);
9998
}
@@ -110,7 +109,7 @@ public function &getIterator(): \Generator
110109
if ($this->expression) {
111110
yield $this->expression;
112111
}
113-
foreach ($this->cases as [&$case, , &$stmt]) {
112+
foreach ($this->cases as [&$case, &$stmt]) {
114113
if ($case) {
115114
yield $case;
116115
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Latte\Compiler\Nodes\StatementNode;
6+
use Latte\Essential\Nodes\IfNode;
7+
use Latte\Essential\Nodes\ForeachNode;
8+
use Tester\Assert;
9+
10+
require __DIR__ . '/../bootstrap.php';
11+
12+
13+
test('simple tag has one tagPosition', function () {
14+
$engine = new Latte\Engine;
15+
$ast = $engine->parse('{=$var}');
16+
17+
$node = $ast->main->children[0];
18+
Assert::type(Latte\Compiler\Nodes\PrintNode::class, $node);
19+
Assert::count(1, $node->tagPositions);
20+
Assert::same(0, $node->tagPositions[0]->offset);
21+
Assert::same(7, $node->tagPositions[0]->length);
22+
});
23+
24+
25+
test('paired tag has two tagPositions', function () {
26+
$engine = new Latte\Engine;
27+
$ast = $engine->parse('{if $cond}text{/if}');
28+
29+
$node = $ast->main->children[0];
30+
Assert::type(IfNode::class, $node);
31+
Assert::count(2, $node->tagPositions);
32+
33+
// opening tag {if $cond}
34+
Assert::same(0, $node->tagPositions[0]->offset);
35+
Assert::same(10, $node->tagPositions[0]->length);
36+
37+
// closing tag {/if}
38+
Assert::same(14, $node->tagPositions[1]->offset);
39+
Assert::same(5, $node->tagPositions[1]->length);
40+
});
41+
42+
43+
test('if/else has three tagPositions', function () {
44+
$engine = new Latte\Engine;
45+
$ast = $engine->parse('{if $a}text{else}more{/if}');
46+
47+
$node = $ast->main->children[0];
48+
Assert::type(IfNode::class, $node);
49+
Assert::count(3, $node->tagPositions);
50+
51+
// {if $a}
52+
Assert::same(0, $node->tagPositions[0]->offset);
53+
Assert::same(7, $node->tagPositions[0]->length);
54+
55+
// {else}
56+
Assert::same(11, $node->tagPositions[1]->offset);
57+
Assert::same(6, $node->tagPositions[1]->length);
58+
59+
// {/if}
60+
Assert::same(21, $node->tagPositions[2]->offset);
61+
Assert::same(5, $node->tagPositions[2]->length);
62+
});
63+
64+
65+
test('if/elseif/else has four tagPositions', function () {
66+
$engine = new Latte\Engine;
67+
$ast = $engine->parse('{if $a}A{elseif $b}B{else}C{/if}');
68+
69+
$node = $ast->main->children[0];
70+
Assert::type(IfNode::class, $node);
71+
Assert::count(4, $node->tagPositions);
72+
73+
// {if $a}
74+
Assert::same(0, $node->tagPositions[0]->offset);
75+
Assert::same(7, $node->tagPositions[0]->length);
76+
77+
// {elseif $b}
78+
Assert::same(8, $node->tagPositions[1]->offset);
79+
Assert::same(11, $node->tagPositions[1]->length);
80+
81+
// {else}
82+
Assert::same(20, $node->tagPositions[2]->offset);
83+
Assert::same(6, $node->tagPositions[2]->length);
84+
85+
// {/if}
86+
Assert::same(27, $node->tagPositions[3]->offset);
87+
Assert::same(5, $node->tagPositions[3]->length);
88+
});
89+
90+
91+
test('foreach has two tagPositions', function () {
92+
$engine = new Latte\Engine;
93+
$ast = $engine->parse('{foreach $items as $item}x{/foreach}');
94+
95+
$node = $ast->main->children[0];
96+
Assert::type(ForeachNode::class, $node);
97+
Assert::count(2, $node->tagPositions);
98+
99+
// {foreach $items as $item}
100+
Assert::same(0, $node->tagPositions[0]->offset);
101+
Assert::same(25, $node->tagPositions[0]->length);
102+
103+
// {/foreach}
104+
Assert::same(26, $node->tagPositions[1]->offset);
105+
Assert::same(10, $node->tagPositions[1]->length);
106+
});
107+
108+
109+
test('foreach/else has three tagPositions', function () {
110+
$engine = new Latte\Engine;
111+
$ast = $engine->parse('{foreach $items as $item}x{else}empty{/foreach}');
112+
113+
$node = $ast->main->children[0];
114+
Assert::type(ForeachNode::class, $node);
115+
Assert::count(3, $node->tagPositions);
116+
117+
// {foreach $items as $item}
118+
Assert::same(0, $node->tagPositions[0]->offset);
119+
Assert::same(25, $node->tagPositions[0]->length);
120+
121+
// {else}
122+
Assert::same(26, $node->tagPositions[1]->offset);
123+
Assert::same(6, $node->tagPositions[1]->length);
124+
125+
// {/foreach}
126+
Assert::same(37, $node->tagPositions[2]->offset);
127+
Assert::same(10, $node->tagPositions[2]->length);
128+
});
129+
130+
131+
test('nested if tags have correct tagPositions', function () {
132+
$engine = new Latte\Engine;
133+
$ast = $engine->parse('{if $a}{if $b}x{/if}{/if}');
134+
135+
// Outer if
136+
$outer = $ast->main->children[0];
137+
Assert::type(IfNode::class, $outer);
138+
Assert::count(2, $outer->tagPositions);
139+
Assert::same(0, $outer->tagPositions[0]->offset); // {if $a}
140+
Assert::same(20, $outer->tagPositions[1]->offset); // {/if}
141+
142+
// Inner if
143+
$inner = $outer->then->children[0];
144+
Assert::type(IfNode::class, $inner);
145+
Assert::count(2, $inner->tagPositions);
146+
Assert::same(7, $inner->tagPositions[0]->offset); // {if $b}
147+
Assert::same(15, $inner->tagPositions[1]->offset); // {/if}
148+
});
149+
150+
151+
test('unpaired tag has one tagPosition', function () {
152+
$engine = new Latte\Engine;
153+
$ast = $engine->parse('{var $x = 1}');
154+
155+
// {var} goes to head, not main
156+
$node = $ast->head->children[0];
157+
Assert::count(1, $node->tagPositions);
158+
Assert::same(0, $node->tagPositions[0]->offset);
159+
Assert::same(12, $node->tagPositions[0]->length);
160+
});

0 commit comments

Comments
 (0)