Skip to content

Commit 237e501

Browse files
authored
refactor(view): implement custom html parser (#1115)
1 parent 1694ab3 commit 237e501

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+6868
-440
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ tests/Unit/Log
1616
log/
1717
node_modules
1818
dist
19+
profile/

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"carthage-software/mago": "0.22.2",
4343
"guzzlehttp/psr7": "^2.6.1",
4444
"illuminate/view": "~11.7.0",
45+
"masterminds/html5": "^2.9",
4546
"mikey179/vfsstream": "^2.0@dev",
4647
"nesbot/carbon": "^3.8",
4748
"nyholm/psr7": "^1.8",

phpstan.neon.dist

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ parameters:
2929
message: '#.*uninitialized readonly property \$composer*#'
3030
-
3131
message: '#.*uninitialized readonly property \$stubFileGenerator*#'
32-
-
33-
message: '#.*undefined property Dom*#'
3432
-
3533
message: '#.*undefined method Dom*#'
3634

src/Tempest/View/src/Attributes/AttributeFactory.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,17 @@
55
namespace Tempest\View\Attributes;
66

77
use Tempest\View\Attribute;
8-
use Tempest\View\Element;
98

109
final readonly class AttributeFactory
1110
{
12-
public function make(Element $element, string $attributeName): Attribute
11+
public function make(string $attributeName): Attribute
1312
{
1413
return match (true) {
1514
$attributeName === ':if' => new IfAttribute(),
1615
$attributeName === ':elseif' => new ElseIfAttribute(),
1716
$attributeName === ':else' => new ElseAttribute(),
1817
$attributeName === ':foreach' => new ForeachAttribute(),
1918
$attributeName === ':forelse' => new ForelseAttribute(),
20-
BooleanAttribute::matches($element, $attributeName) => new BooleanAttribute($attributeName),
2119
str_starts_with($attributeName, ':') => new ExpressionAttribute($attributeName),
2220
default => new DataAttribute($attributeName),
2321
};

src/Tempest/View/src/Attributes/BooleanAttribute.php

Lines changed: 0 additions & 75 deletions
This file was deleted.

src/Tempest/View/src/Attributes/DataAttribute.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use Tempest\View\Elements\TextElement;
1111
use Tempest\View\Elements\ViewComponentElement;
1212
use Tempest\View\Exceptions\InvalidDataAttribute;
13-
use Tempest\View\Renderers\TempestViewCompiler;
13+
use Tempest\View\Parser\TempestViewCompiler;
1414

1515
use function Tempest\Support\str;
1616

@@ -35,7 +35,7 @@ public function apply(Element $element): Element
3535

3636
$value = $element->getAttribute($this->name);
3737

38-
if (str($value)->startsWith(TempestViewCompiler::TOKEN_MAPPING)) {
38+
if (str($value)->startsWith(TempestViewCompiler::PHP_TOKENS)) {
3939
throw new InvalidDataAttribute($this->name, $value);
4040
}
4141

src/Tempest/View/src/Attributes/ExpressionAttribute.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use Tempest\View\Element;
1111
use Tempest\View\Elements\PhpDataElement;
1212
use Tempest\View\Exceptions\InvalidExpressionAttribute;
13-
use Tempest\View\Renderers\TempestViewCompiler;
13+
use Tempest\View\Parser\TempestViewCompiler;
1414

1515
use function Tempest\Support\str;
1616

@@ -24,7 +24,7 @@ public function apply(Element $element): Element
2424
{
2525
$value = str($element->getAttribute($this->name));
2626

27-
if ($value->startsWith(['{{', '{!!', ...TempestViewCompiler::TOKEN_MAPPING])) {
27+
if ($value->startsWith(['{{', '{!!', ...TempestViewCompiler::PHP_TOKENS])) {
2828
throw new InvalidExpressionAttribute($value);
2929
}
3030

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Tempest\View\Attributes;
4+
5+
use Tempest\View\Attribute;
6+
use Tempest\View\Element;
7+
8+
/*
9+
* This class is used whenever PHP code occurs within element tags, it will make sure this code is left untouched
10+
* <div <?= 'hi' ?> class="foo">
11+
* The most common use case for this happening is when conditional attributes are rendered within a view component
12+
*/
13+
final readonly class PhpAttribute implements Attribute
14+
{
15+
public function __construct(
16+
private string $index,
17+
private string $content,
18+
) {}
19+
20+
public function apply(Element $element): Element
21+
{
22+
$element
23+
->addRawAttribute($this->content)
24+
->unsetAttribute($this->index);
25+
26+
return $element;
27+
}
28+
}

src/Tempest/View/src/Element.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public function getAttribute(string $name): ?string;
1616

1717
public function setAttribute(string $name, string $value): self;
1818

19+
public function addRawAttribute(string $attribute): self;
20+
1921
public function consumeAttribute(string $name): ?string;
2022

2123
public function unsetAttribute(string $name): self;

src/Tempest/View/src/Elements/ElementFactory.php

Lines changed: 46 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@
44

55
namespace Tempest\View\Elements;
66

7-
use Dom\Comment;
8-
use Dom\DocumentType;
9-
use Dom\Element as DomElement;
10-
use Dom\Node;
11-
use Dom\Text;
127
use Tempest\Container\Container;
138
use Tempest\Core\AppConfig;
9+
use Tempest\View\Attributes\PhpAttribute;
1410
use Tempest\View\Element;
15-
use Tempest\View\Renderers\TempestViewCompiler;
11+
use Tempest\View\Parser\TempestViewCompiler;
12+
use Tempest\View\Parser\Token;
13+
use Tempest\View\Parser\TokenType;
1614
use Tempest\View\ViewComponent;
1715
use Tempest\View\ViewConfig;
1816

@@ -35,64 +33,69 @@ public function setViewCompiler(TempestViewCompiler $compiler): self
3533
return $this;
3634
}
3735

38-
public function make(Node $node): ?Element
36+
public function make(Token $token): ?Element
3937
{
4038
return $this->makeElement(
41-
node: $node,
39+
token: $token,
4240
parent: null,
4341
);
4442
}
4543

46-
private function makeElement(Node $node, ?Element $parent): ?Element
44+
private function makeElement(Token $token, ?Element $parent): ?Element
4745
{
48-
if ($node instanceof DocumentType) {
49-
$content = $node->ownerDocument->saveHTML($node);
50-
51-
return new RawElement(tag: null, content: $content);
46+
if (
47+
$token->type === TokenType::OPEN_TAG_END ||
48+
$token->type === TokenType::ATTRIBUTE_NAME ||
49+
$token->type === TokenType::ATTRIBUTE_VALUE ||
50+
$token->type === TokenType::SELF_CLOSING_TAG_END
51+
) {
52+
return null;
5253
}
5354

54-
if ($node instanceof Text) {
55-
if (trim($node->textContent) === '') {
55+
if ($token->type === TokenType::CONTENT) {
56+
$text = $token->compile();
57+
58+
if (trim($text) === '') {
5659
return null;
5760
}
5861

59-
return new TextElement(
60-
text: $node->textContent,
61-
);
62+
return new TextElement(text: $text);
6263
}
6364

64-
if ($node instanceof Comment) {
65-
return new CommentElement(
66-
content: $node->textContent,
67-
);
65+
if (! $token->tag || $token->type === TokenType::COMMENT || $token->type === TokenType::PHP) {
66+
return new RawElement(tag: null, content: $token->compile());
6867
}
6968

70-
$tagName = strtolower($node->tagName);
71-
7269
$attributes = [];
7370

74-
/** @var \Dom\Attr $attribute */
75-
foreach ($node->attributes ?? [] as $attribute) {
76-
$name = str($attribute->name)->camel()->toString();
71+
foreach ($token->htmlAttributes as $name => $value) {
72+
$name = str($name)
73+
->trim()
74+
->before('=')
75+
->camel()
76+
->toString();
7777

78-
$attributes[$name] = $attribute->value;
79-
}
78+
$value = str($value)
79+
->afterFirst('"')
80+
->beforeLast('"')
81+
->toString();
8082

81-
if (! ($node instanceof DomElement) || $tagName === 'pre' || $tagName === 'code') {
82-
$content = '';
83+
$attributes[$name] = $value;
84+
}
8385

84-
foreach ($node->childNodes as $child) {
85-
$content .= $node->ownerDocument->saveHTML($child);
86-
}
86+
foreach ($token->phpAttributes as $index => $content) {
87+
$attributes[] = new PhpAttribute((string) $index, $content);
88+
}
8789

90+
if ($token->tag === 'code' || $token->tag === 'pre') {
8891
return new RawElement(
89-
tag: $tagName,
90-
content: $content,
92+
tag: $token->tag,
93+
content: $token->compileChildren(),
9194
attributes: $attributes,
9295
);
9396
}
9497

95-
if ($viewComponentClass = $this->viewConfig->viewComponents[$tagName] ?? null) {
98+
if ($viewComponentClass = $this->viewConfig->viewComponents[$token->tag] ?? null) {
9699
if (! ($viewComponentClass instanceof ViewComponent)) {
97100
$viewComponentClass = $this->container->get($viewComponentClass);
98101
}
@@ -103,27 +106,27 @@ private function makeElement(Node $node, ?Element $parent): ?Element
103106
viewComponent: $viewComponentClass,
104107
attributes: $attributes,
105108
);
106-
} elseif ($tagName === 'x-template') {
109+
} elseif ($token->tag === 'x-template') {
107110
$element = new TemplateElement(
108111
attributes: $attributes,
109112
);
110-
} elseif ($tagName === 'x-slot') {
113+
} elseif ($token->tag === 'x-slot') {
111114
$element = new SlotElement(
112-
name: $node->getAttribute('name') ?: 'slot',
115+
name: $token->getAttribute('name') ?? 'slot',
113116
attributes: $attributes,
114117
);
115118
} else {
116119
$element = new GenericElement(
117-
tag: $tagName,
120+
tag: $token->tag,
118121
attributes: $attributes,
119122
);
120123
}
121124

122125
$children = [];
123126

124-
foreach ($node->childNodes as $child) {
127+
foreach ($token->children as $child) {
125128
$childElement = $this->clone()->makeElement(
126-
node: $child,
129+
token: $child,
127130
parent: $parent,
128131
);
129132

0 commit comments

Comments
 (0)