Skip to content

Commit 1865863

Browse files
committed
StatementNode: added tagPositions
1 parent 6f842f7 commit 1865863

7 files changed

Lines changed: 182 additions & 22 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
@@ -194,6 +194,7 @@ public function parseLatteStatement(?\Closure $resolver = null): ?Node
194194

195195
$token = $this->stream->peek();
196196
$startTag = $this->pushTag($this->parseLatteTag());
197+
$tagPositions = [$startTag->position];
197198

198199
$parser = $this->getTagParser($startTag->name, $token->position);
199200
$res = $parser($startTag, $this);
@@ -231,10 +232,12 @@ public function parseLatteStatement(?\Closure $resolver = null): ?Node
231232

232233
if ($tag->closing) {
233234
$this->checkEndTag($startTag, $tag);
235+
$tagPositions[] = $tag->position;
234236
$res->send([$content, $tag]);
235237
$this->ensureIsConsumed($tag);
236238
break;
237239
} elseif (in_array($tag->name, $this->lookFor[$startTag] ?? [], strict: true)) {
240+
$tagPositions[] = $tag->position;
238241
$this->pushTag($tag);
239242
$res->send([$content, $tag]);
240243
$this->ensureIsConsumed($tag);
@@ -273,6 +276,11 @@ public function parseLatteStatement(?\Closure $resolver = null): ?Node
273276
$node->position = isset($tag) && $tag->closing
274277
? $this->endPosition($startTag->position, $tag->position->offset + $tag->position->length)
275278
: $startTag->position;
279+
280+
if ($node instanceof Nodes\StatementNode) {
281+
$node->tagPositions = $tagPositions;
282+
}
283+
276284
return $node;
277285
}
278286

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 & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use Latte\Compiler\Nodes\Php\ExpressionNode;
1414
use Latte\Compiler\Nodes\StatementNode;
1515
use Latte\Compiler\Nodes\TextNode;
16-
use Latte\Compiler\Position;
1716
use Latte\Compiler\PrintContext;
1817
use Latte\Compiler\Tag;
1918

@@ -26,7 +25,7 @@ class SwitchNode extends StatementNode
2625
{
2726
public ?ExpressionNode $expression;
2827

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

3231

@@ -53,17 +52,16 @@ public static function create(Tag $tag): \Generator
5352
while (true) {
5453
if ($nextTag->name === 'case') {
5554
$nextTag->expectArguments();
56-
[$case, $line] = [$nextTag->parser->parseArguments(), $nextTag->position];
55+
$case = $nextTag->parser->parseArguments();
5756
[$content, $nextTag] = yield ['case', 'default'];
58-
$node->cases[] = [$case, $line, $content];
57+
$node->cases[] = [$case, $content];
5958

6059
} elseif ($nextTag->name === 'default') {
6160
if ($default++) {
6261
throw new CompileException('Tag {switch} may only contain one {default} clause.', $nextTag->position);
6362
}
64-
$line = $nextTag->position;
6563
[$content, $nextTag] = yield ['case', 'default'];
66-
$node->cases[] = [null, $line, $content];
64+
$node->cases[] = [null, $content];
6765

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

0 commit comments

Comments
 (0)