Skip to content

Commit bbc5da7

Browse files
committed
TASK: Implement TagParser with limited capabilities
Embedded expressions are still missing.
1 parent 07c34ba commit bbc5da7

File tree

8 files changed

+2117
-20
lines changed

8 files changed

+2117
-20
lines changed

src/Language/AST/Node/Tag/TagContentNodes.php renamed to src/Language/AST/Node/Tag/ChildNodes.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,17 @@
2222

2323
namespace PackageFactory\ComponentEngine\Language\AST\Node\Tag;
2424

25-
final class TagContentNodes
25+
use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode;
26+
use PackageFactory\ComponentEngine\Language\AST\Node\Text\TextNode;
27+
28+
final class ChildNodes
2629
{
2730
/**
28-
* @var TagContentNode[]
31+
* @var (TextNode|ExpressionNode|TagNode)[]
2932
*/
3033
public readonly array $items;
3134

32-
public function __construct(TagContentNode ...$items)
35+
public function __construct(TextNode | ExpressionNode | TagNode ...$items)
3336
{
3437
$this->items = $items;
3538
}

src/Language/AST/Node/Tag/TagNode.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function __construct(
3131
public readonly NodeAttributes $attributes,
3232
public readonly TagNameNode $name,
3333
public readonly AttributeNodes $tagAttributes,
34-
public readonly TagContentNodes $children,
34+
public readonly ChildNodes $children,
3535
public readonly bool $isSelfClosing
3636
) {
3737
}

src/Language/Parser/ParserException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
abstract class ParserException extends \Exception
2828
{
29-
protected function __construct(
29+
final protected function __construct(
3030
int $code,
3131
string $message,
3232
public readonly ?Range $affectedRangeInSource = null,

src/Language/AST/Node/Tag/TagContentNode.php renamed to src/Language/Parser/Tag/TagCouldNotBeParsed.php

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,27 @@
2020

2121
declare(strict_types=1);
2222

23-
namespace PackageFactory\ComponentEngine\Language\AST\Node\Tag;
23+
namespace PackageFactory\ComponentEngine\Language\Parser\Tag;
2424

25-
use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode;
26-
use PackageFactory\ComponentEngine\Language\AST\Node\Node;
27-
use PackageFactory\ComponentEngine\Language\AST\Node\Text\TextNode;
28-
use PackageFactory\ComponentEngine\Language\AST\NodeAttributes\NodeAttributes;
25+
use PackageFactory\ComponentEngine\Domain\TagName\TagName;
26+
use PackageFactory\ComponentEngine\Language\Parser\ParserException;
27+
use PackageFactory\ComponentEngine\Parser\Source\Range;
2928

30-
final class TagContentNode extends Node
29+
final class TagCouldNotBeParsed extends ParserException
3130
{
32-
public function __construct(
33-
public readonly NodeAttributes $attributes,
34-
public readonly TextNode | ExpressionNode | TagNode $root
35-
) {
31+
public static function becauseOfClosingTagNameMismatch(
32+
TagName $expectedTagName,
33+
string $actualTagName,
34+
Range $affectedRangeInSource
35+
): self {
36+
return new self(
37+
code: 1690976372,
38+
message: sprintf(
39+
'TagNode could not be parsed, because the closing tag name "%s" did not match the opening tag name "%s".',
40+
$actualTagName,
41+
$expectedTagName->value
42+
),
43+
affectedRangeInSource: $affectedRangeInSource
44+
);
3645
}
3746
}
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
<?php
2+
3+
/**
4+
* PackageFactory.ComponentEngine - Universal View Components for PHP
5+
* Copyright (C) 2023 Contributors of PackageFactory.ComponentEngine
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace PackageFactory\ComponentEngine\Language\Parser\Tag;
24+
25+
use PackageFactory\ComponentEngine\Domain\AttributeName\AttributeName;
26+
use PackageFactory\ComponentEngine\Domain\TagName\TagName;
27+
use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode;
28+
use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNameNode;
29+
use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNode;
30+
use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNodes;
31+
use PackageFactory\ComponentEngine\Language\AST\Node\Tag\ChildNodes;
32+
use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNameNode;
33+
use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNode;
34+
use PackageFactory\ComponentEngine\Language\AST\NodeAttributes\NodeAttributes;
35+
use PackageFactory\ComponentEngine\Language\Parser\StringLiteral\StringLiteralParser;
36+
use PackageFactory\ComponentEngine\Language\Parser\Text\TextParser;
37+
use PackageFactory\ComponentEngine\Parser\Source\Range;
38+
use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner;
39+
use PackageFactory\ComponentEngine\Parser\Tokenizer\Token;
40+
use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType;
41+
42+
final class TagParser
43+
{
44+
private readonly StringLiteralParser $stringLiteralParser;
45+
private readonly TextParser $textParser;
46+
47+
public function __construct()
48+
{
49+
$this->stringLiteralParser = new StringLiteralParser();
50+
$this->textParser = new TextParser();
51+
}
52+
53+
/**
54+
* @param \Iterator<mixed,Token> $tokens
55+
* @return TagNode
56+
*/
57+
public function parse(\Iterator $tokens): TagNode
58+
{
59+
$tagStartOpeningToken = $this->extractTagStartOpeningToken($tokens);
60+
$tagNameNode = $this->parseTagName($tokens);
61+
$attributeNodes = $this->parseAttributes($tokens);
62+
63+
if ($tagSelfCloseToken = $this->extractTagSelfCloseToken($tokens)) {
64+
return new TagNode(
65+
attributes: new NodeAttributes(
66+
rangeInSource: Range::from(
67+
$tagStartOpeningToken->boundaries->start,
68+
$tagSelfCloseToken->boundaries->end
69+
)
70+
),
71+
name: $tagNameNode,
72+
tagAttributes: $attributeNodes,
73+
children: new ChildNodes(),
74+
isSelfClosing: true
75+
);
76+
} else {
77+
$this->skipTagEndToken($tokens);
78+
$children = $this->parseChildren($tokens);
79+
$this->skipTagStartClosingToken($tokens);
80+
$this->assertAndSkipClosingTagName($tokens, $tagNameNode);
81+
$closingTagEndToken = $this->extractTagEndToken($tokens);
82+
83+
return new TagNode(
84+
attributes: new NodeAttributes(
85+
rangeInSource: Range::from(
86+
$tagStartOpeningToken->boundaries->start,
87+
$closingTagEndToken->boundaries->end
88+
)
89+
),
90+
name: $tagNameNode,
91+
tagAttributes: $attributeNodes,
92+
children: $children,
93+
isSelfClosing: false
94+
);
95+
}
96+
}
97+
98+
/**
99+
* @param \Iterator<mixed,Token> $tokens
100+
* @return Token
101+
*/
102+
private function extractTagStartOpeningToken(\Iterator $tokens): Token
103+
{
104+
Scanner::assertType($tokens, TokenType::TAG_START_OPENING);
105+
$tagStartOpeningToken = $tokens->current();
106+
Scanner::skipOne($tokens);
107+
108+
return $tagStartOpeningToken;
109+
}
110+
111+
/**
112+
* @param \Iterator<mixed,Token> $tokens
113+
* @return TagNameNode
114+
*/
115+
private function parseTagName(\Iterator $tokens): TagNameNode
116+
{
117+
Scanner::assertType($tokens, TokenType::STRING);
118+
$tagNameToken = $tokens->current();
119+
Scanner::skipOne($tokens);
120+
121+
return new TagNameNode(
122+
attributes: new NodeAttributes(
123+
rangeInSource: $tagNameToken->boundaries
124+
),
125+
value: TagName::from($tagNameToken->value)
126+
);
127+
}
128+
129+
/**
130+
* @param \Iterator<mixed,Token> $tokens
131+
* @return AttributeNodes
132+
*/
133+
private function parseAttributes(\Iterator $tokens): AttributeNodes
134+
{
135+
$items = [];
136+
while (!$this->isTagEnd($tokens)) {
137+
Scanner::skipSpace($tokens);
138+
139+
$items[] = $this->parseAttribute($tokens);
140+
141+
Scanner::skipSpace($tokens);
142+
}
143+
144+
return new AttributeNodes(...$items);
145+
}
146+
147+
/**
148+
* @param \Iterator<mixed,Token> $tokens
149+
* @return boolean
150+
*/
151+
private function isTagEnd($tokens): bool
152+
{
153+
return (
154+
Scanner::type($tokens) === TokenType::TAG_END ||
155+
Scanner::type($tokens) === TokenType::TAG_SELF_CLOSE
156+
);
157+
}
158+
159+
/**
160+
* @param \Iterator<mixed,Token> $tokens
161+
* @return AttributeNode
162+
*/
163+
private function parseAttribute(\Iterator $tokens): AttributeNode
164+
{
165+
$attributeNameNode = $this->parseAttributeName($tokens);
166+
$attributeValueNode = $this->parseAttributeValue($tokens);
167+
168+
return new AttributeNode(
169+
attributes: new NodeAttributes(
170+
rangeInSource: Range::from(
171+
$attributeNameNode->attributes->rangeInSource->start,
172+
$attributeValueNode?->attributes->rangeInSource->end ??
173+
$attributeNameNode->attributes->rangeInSource->end
174+
)
175+
),
176+
name: $attributeNameNode,
177+
value: $attributeValueNode
178+
);
179+
}
180+
181+
/**
182+
* @param \Iterator<mixed,Token> $tokens
183+
* @return AttributeNameNode
184+
*/
185+
private function parseAttributeName(\Iterator $tokens): AttributeNameNode
186+
{
187+
Scanner::assertType($tokens, TokenType::STRING);
188+
$attributeNameToken = $tokens->current();
189+
Scanner::skipOne($tokens);
190+
191+
return new AttributeNameNode(
192+
attributes: new NodeAttributes(
193+
rangeInSource: $attributeNameToken->boundaries
194+
),
195+
value: AttributeName::from($attributeNameToken->value)
196+
);
197+
}
198+
199+
/**
200+
* @param \Iterator<mixed,Token> $tokens
201+
* @return null|StringLiteralNode
202+
*/
203+
private function parseAttributeValue(\Iterator $tokens): null|StringLiteralNode
204+
{
205+
if (Scanner::type($tokens) === TokenType::EQUALS) {
206+
Scanner::skipOne($tokens);
207+
Scanner::assertType($tokens, TokenType::STRING_QUOTED);
208+
209+
return $this->stringLiteralParser->parse($tokens);
210+
}
211+
212+
return null;
213+
}
214+
215+
/**
216+
* @param \Iterator<mixed,Token> $tokens
217+
* @return null|Token
218+
*/
219+
private function extractTagSelfCloseToken(\Iterator $tokens): ?Token
220+
{
221+
if (Scanner::type($tokens) === TokenType::TAG_SELF_CLOSE) {
222+
$tagSelfCloseToken = $tokens->current();
223+
Scanner::skipOne($tokens);
224+
225+
return $tagSelfCloseToken;
226+
}
227+
228+
return null;
229+
}
230+
231+
/**
232+
* @param \Iterator<mixed,Token> $tokens
233+
* @return void
234+
*/
235+
private function skipTagEndToken(\Iterator $tokens): void
236+
{
237+
Scanner::assertType($tokens, TokenType::TAG_END);
238+
Scanner::skipOne($tokens);
239+
}
240+
241+
/**
242+
* @param \Iterator<mixed,Token> $tokens
243+
* @return ChildNodes
244+
*/
245+
private function parseChildren(\Iterator $tokens): ChildNodes
246+
{
247+
$items = [];
248+
$preserveLeadingSpace = false;
249+
while (Scanner::type($tokens) !== TokenType::TAG_START_CLOSING) {
250+
if ($textNode = $this->textParser->parse($tokens, $preserveLeadingSpace)) {
251+
$items[] = $textNode;
252+
}
253+
254+
if (Scanner::type($tokens) === TokenType::TAG_START_OPENING) {
255+
$items[] = $this->parse($tokens);
256+
$preserveLeadingSpace = Scanner::type($tokens) !== TokenType::END_OF_LINE;
257+
}
258+
}
259+
260+
return new ChildNodes(...$items);
261+
}
262+
263+
/**
264+
* @param \Iterator<mixed,Token> $tokens
265+
* @return void
266+
*/
267+
private function skipTagStartClosingToken(\Iterator $tokens): void
268+
{
269+
Scanner::assertType($tokens, TokenType::TAG_START_CLOSING);
270+
Scanner::skipOne($tokens);
271+
}
272+
273+
/**
274+
* @param \Iterator<mixed,Token> $tokens
275+
* @param TagNameNode $openingTagNameNode
276+
* @return void
277+
*/
278+
private function assertAndSkipClosingTagName(\Iterator $tokens, TagNameNode $openingTagNameNode): void
279+
{
280+
Scanner::assertType($tokens, TokenType::STRING);
281+
$tagNameToken = $tokens->current();
282+
Scanner::skipOne($tokens);
283+
284+
if ($tagNameToken->value !== $openingTagNameNode->value->value) {
285+
throw TagCouldNotBeParsed::becauseOfClosingTagNameMismatch(
286+
expectedTagName: $openingTagNameNode->value,
287+
actualTagName: $tagNameToken->value,
288+
affectedRangeInSource: $tagNameToken->boundaries
289+
);
290+
}
291+
}
292+
293+
/**
294+
* @param \Iterator<mixed,Token> $tokens
295+
* @return Token
296+
*/
297+
private function extractTagEndToken(\Iterator $tokens): Token
298+
{
299+
Scanner::assertType($tokens, TokenType::TAG_END);
300+
$tagEndToken = $tokens->current();
301+
Scanner::skipOne($tokens);
302+
303+
return $tagEndToken;
304+
}
305+
}

0 commit comments

Comments
 (0)