From 5d9f4ab72327bcbc3e6945c2af083ea9e8d634f5 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 9 Jan 2026 13:31:57 +0100 Subject: [PATCH 1/7] wip --- packages/debug/src/Debug.php | 3 +- packages/view/src/Elements/ElementFactory.php | 4 ++ packages/view/src/Elements/IsElement.php | 4 ++ .../view/src/Elements/WhitespaceElement.php | 21 ++++++++++ packages/view/src/Parser/TempestViewLexer.php | 22 ++++++++++- packages/view/src/Parser/TokenType.php | 1 + packages/view/tests/TempestViewLexerTest.php | 35 +++++++++++++++-- .../View/TempestViewRendererTest.php | 38 ++++++++++++++++++- 8 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 packages/view/src/Elements/WhitespaceElement.php diff --git a/packages/debug/src/Debug.php b/packages/debug/src/Debug.php index 5a6f957c07..500a34305e 100644 --- a/packages/debug/src/Debug.php +++ b/packages/debug/src/Debug.php @@ -12,6 +12,7 @@ use Tempest\EventBus\EventBus; use Tempest\Highlight\Themes\TerminalStyle; use Tempest\Support\Filesystem; +use Throwable; final readonly class Debug { @@ -27,7 +28,7 @@ public static function resolve(): self config: GenericContainer::instance()->get(DebugConfig::class), eventBus: GenericContainer::instance()->get(EventBus::class), ); - } catch (Exception) { + } catch (Throwable) { return new self(); } } diff --git a/packages/view/src/Elements/ElementFactory.php b/packages/view/src/Elements/ElementFactory.php index bf8e9dcf1e..4d63a88fa7 100644 --- a/packages/view/src/Elements/ElementFactory.php +++ b/packages/view/src/Elements/ElementFactory.php @@ -69,6 +69,10 @@ private function makeElement(Token $token, ?Element $parent): ?Element return new TextElement(text: $text); } + if ($token->type === TokenType::WHITESPACE) { + return new WhitespaceElement($token->content); + } + if (! $token->tag || $token->type === TokenType::COMMENT || $token->type === TokenType::PHP) { return new RawElement(token: $token, tag: null, content: $token->compile()); } diff --git a/packages/view/src/Elements/IsElement.php b/packages/view/src/Elements/IsElement.php index 839b7944a6..7aa66e0307 100644 --- a/packages/view/src/Elements/IsElement.php +++ b/packages/view/src/Elements/IsElement.php @@ -110,6 +110,10 @@ public function setPrevious(?Element $previous): self public function getPrevious(): ?Element { + if ($this->previous instanceof WhitespaceElement) { + return $this->previous->getPrevious(); + } + return $this->previous; } diff --git a/packages/view/src/Elements/WhitespaceElement.php b/packages/view/src/Elements/WhitespaceElement.php new file mode 100644 index 0000000000..148e409e87 --- /dev/null +++ b/packages/view/src/Elements/WhitespaceElement.php @@ -0,0 +1,21 @@ +content; + } +} diff --git a/packages/view/src/Parser/TempestViewLexer.php b/packages/view/src/Parser/TempestViewLexer.php index 658cdb476f..20103f3044 100644 --- a/packages/view/src/Parser/TempestViewLexer.php +++ b/packages/view/src/Parser/TempestViewLexer.php @@ -14,7 +14,8 @@ final class TempestViewLexer public function __construct( private readonly string $html, - ) { + ) + { $this->current = $this->html[$this->position] ?? null; } @@ -35,6 +36,8 @@ public function lex(): TokenCollection $tokens = [...$tokens, ...$this->lexCharacterData()]; } elseif ($this->comesNext('<')) { $tokens = [...$tokens, ...$this->lexTag()]; + } elseif ($this->comesNext(' ') || $this->comesNext(PHP_EOL)) { + $tokens[] = $this->lexWhitespace(); } else { $tokens[] = $this->lexContent(); } @@ -214,6 +217,23 @@ private function lexDoctype(): Token return new Token($buffer, TokenType::DOCTYPE); } + private function lexWhitespace(): Token + { + $buffer = ''; + + while ($this->current !== null) { + $seek = $this->seek(); + + if ($seek !== ' ' && $seek !== PHP_EOL) { + break; + } + + $buffer .= $this->consume(); + } + + return new Token($buffer, TokenType::WHITESPACE); + } + private function lexCharacterData(): array { $tokens = [ diff --git a/packages/view/src/Parser/TokenType.php b/packages/view/src/Parser/TokenType.php index 0a433b5485..2938ea76f2 100644 --- a/packages/view/src/Parser/TokenType.php +++ b/packages/view/src/Parser/TokenType.php @@ -18,4 +18,5 @@ enum TokenType case DOCTYPE; case CHARACTER_DATA_OPEN; case CHARACTER_DATA_CLOSE; + case WHITESPACE; } diff --git a/packages/view/tests/TempestViewLexerTest.php b/packages/view/tests/TempestViewLexerTest.php index 9d533d5351..abc4117b68 100644 --- a/packages/view/tests/TempestViewLexerTest.php +++ b/packages/view/tests/TempestViewLexerTest.php @@ -173,13 +173,40 @@ class=', TokenType::ATTRIBUTE_NAME), new Token("\n>", TokenType::OPEN_TAG_END), new Token(' -', TokenType::CONTENT), +', TokenType::WHITESPACE), new Token('', TokenType::CLOSING_TAG), ], actual: $tokens, ); } + public function test_whitespace(): void + { + $html = <<<'HTML' +

Test Test

+HTML; + + $tokens = new TempestViewLexer($html)->lex(); + + $this->assertTokens( + expected: [ + new Token('', TokenType::OPEN_TAG_END), + new Token('', TokenType::OPEN_TAG_END), + new Token('Test', TokenType::CONTENT), + new Token('', TokenType::CLOSING_TAG), + new Token(' ', TokenType::WHITESPACE), + new Token('', TokenType::OPEN_TAG_END), + new Token('Test', TokenType::CONTENT), + new Token('', TokenType::CLOSING_TAG), + new Token('

', TokenType::CLOSING_TAG) + ], + actual: $tokens, + ); + } + public function test_lexer_with_falsy_values(): void { $html = <<<'HTML' @@ -317,7 +344,8 @@ public function test_cdata(): void { $tokens = new TempestViewLexer(<<<'RSS' <![CDATA[ {{ $post['title'] }} ]]> - RSS)->lex(); + RSS, + )->lex(); $this->assertTokens( expected: [ @@ -336,7 +364,8 @@ public function test_xml(): void { $tokens = new TempestViewLexer(<<<'XML' - XML)->lex(); + XML, + )->lex(); $this->assertTokens( expected: [ diff --git a/tests/Integration/View/TempestViewRendererTest.php b/tests/Integration/View/TempestViewRendererTest.php index 8aad3af5ff..eafd9fb94f 100644 --- a/tests/Integration/View/TempestViewRendererTest.php +++ b/tests/Integration/View/TempestViewRendererTest.php @@ -292,7 +292,7 @@ public function test_default_slot(): void public function test_implicit_default_slot(): void { - $this->assertStringEqualsStringIgnoringLineEndings( + $this->assertSnippetsMatch( <<<'HTML'
@@ -891,4 +891,40 @@ public function test_discovery_locations_are_passed_to_compiler(): void $this->assertSnippetsMatch('
Hi
', $html); } + + public function test_whitespace_between_inline_elements_is_preserved(): void + { + /** @var TempestViewRenderer $renderer */ + $renderer = $this->get(TempestViewRenderer::class); + + $this->assertSame( + '

Test Test

', + $renderer->render('

Test Test

'), + ); + } + + public function test_whitespace_introduced_by_line_breaks_is_preserved(): void + { + /** @var TempestViewRenderer $renderer */ + $renderer = $this->get(TempestViewRenderer::class); + + $this->assertSame( + '

Test Test

', + $renderer->render('

Test +Test

'), + ); + } + + public function test_whitespace_with_blank_lines_between_inline_elements_is_preserved(): void + { + /** @var TempestViewRenderer $renderer */ + $renderer = $this->get(TempestViewRenderer::class); + + $this->assertSame( + '

Test Test

', + $renderer->render('

Test + +Test

'), + ); + } } From a556627c2961bcac64e15f77bd9b5c4a5517f35f Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 9 Jan 2026 13:33:53 +0100 Subject: [PATCH 2/7] wip --- tests/Integration/View/TempestViewRendererTest.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/Integration/View/TempestViewRendererTest.php b/tests/Integration/View/TempestViewRendererTest.php index eafd9fb94f..134326e4c5 100644 --- a/tests/Integration/View/TempestViewRendererTest.php +++ b/tests/Integration/View/TempestViewRendererTest.php @@ -909,7 +909,8 @@ public function test_whitespace_introduced_by_line_breaks_is_preserved(): void $renderer = $this->get(TempestViewRenderer::class); $this->assertSame( - '

Test Test

', + '

Test +Test

', $renderer->render('

Test Test

'), ); @@ -921,7 +922,9 @@ public function test_whitespace_with_blank_lines_between_inline_elements_is_pres $renderer = $this->get(TempestViewRenderer::class); $this->assertSame( - '

Test Test

', + '

Test + +Test

', $renderer->render('

Test Test

'), From b57b361201af4bf005c190173b248e1c0553d154 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 9 Jan 2026 13:36:39 +0100 Subject: [PATCH 3/7] wip --- packages/debug/src/Debug.php | 1 - packages/view/src/Parser/TempestViewLexer.php | 3 +-- packages/view/tests/TempestViewLexerTest.php | 12 ++++----- tests/Integration/View/ViewComponentTest.php | 27 ++++++++++++------- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/debug/src/Debug.php b/packages/debug/src/Debug.php index 500a34305e..9ba9757931 100644 --- a/packages/debug/src/Debug.php +++ b/packages/debug/src/Debug.php @@ -4,7 +4,6 @@ namespace Tempest\Debug; -use Exception; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\VarDumper; diff --git a/packages/view/src/Parser/TempestViewLexer.php b/packages/view/src/Parser/TempestViewLexer.php index 20103f3044..9b38a66c3a 100644 --- a/packages/view/src/Parser/TempestViewLexer.php +++ b/packages/view/src/Parser/TempestViewLexer.php @@ -14,8 +14,7 @@ final class TempestViewLexer public function __construct( private readonly string $html, - ) - { + ) { $this->current = $this->html[$this->position] ?? null; } diff --git a/packages/view/tests/TempestViewLexerTest.php b/packages/view/tests/TempestViewLexerTest.php index abc4117b68..0ba62e2882 100644 --- a/packages/view/tests/TempestViewLexerTest.php +++ b/packages/view/tests/TempestViewLexerTest.php @@ -183,8 +183,8 @@ class=', TokenType::ATTRIBUTE_NAME), public function test_whitespace(): void { $html = <<<'HTML' -

Test Test

-HTML; +

Test Test

+ HTML; $tokens = new TempestViewLexer($html)->lex(); @@ -201,7 +201,7 @@ public function test_whitespace(): void new Token('>', TokenType::OPEN_TAG_END), new Token('Test', TokenType::CONTENT), new Token('', TokenType::CLOSING_TAG), - new Token('

', TokenType::CLOSING_TAG) + new Token('

', TokenType::CLOSING_TAG), ], actual: $tokens, ); @@ -344,8 +344,7 @@ public function test_cdata(): void { $tokens = new TempestViewLexer(<<<'RSS' <![CDATA[ {{ $post['title'] }} ]]> - RSS, - )->lex(); + RSS)->lex(); $this->assertTokens( expected: [ @@ -364,8 +363,7 @@ public function test_xml(): void { $tokens = new TempestViewLexer(<<<'XML' - XML, - )->lex(); + XML)->lex(); $this->assertTokens( expected: [ diff --git a/tests/Integration/View/ViewComponentTest.php b/tests/Integration/View/ViewComponentTest.php index e5d15536b4..38c528d036 100644 --- a/tests/Integration/View/ViewComponentTest.php +++ b/tests/Integration/View/ViewComponentTest.php @@ -471,14 +471,14 @@ public static function view_components(): Generator public function test_full_html_document_as_component(): void { $this->view->registerViewComponent('x-layout', <<<'HTML' - - - Tempest View - - - - - + + + Tempest View + + + + + HTML); $html = $this->view->render(<<<'HTML' @@ -487,9 +487,16 @@ public function test_full_html_document_as_component(): void HTML); - $this->assertStringContainsString('Tempest View', $html); + $this->assertStringContainsString(<<<'HTML' + + + Tempest View + + + HTML, $html); $this->assertStringContainsString('Hello World', $html); - $this->assertStringContainsString('', $html); + $this->assertStringContainsString('', $html); + $this->assertStringContainsString('', $html); } public function test_empty_slots_are_commented_out(): void From d133aafe6c7a10f1797de54b2c8375fe7b596fb9 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 9 Jan 2026 13:41:10 +0100 Subject: [PATCH 4/7] wip --- tests/Integration/View/ElementFactoryTest.php | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/Integration/View/ElementFactoryTest.php b/tests/Integration/View/ElementFactoryTest.php index 5020c5a3f9..ddde0f86d2 100644 --- a/tests/Integration/View/ElementFactoryTest.php +++ b/tests/Integration/View/ElementFactoryTest.php @@ -4,11 +4,14 @@ namespace Tests\Tempest\Integration\View; +use Tempest\View\Element; use Tempest\View\Elements\ElementFactory; use Tempest\View\Elements\GenericElement; use Tempest\View\Elements\TextElement; +use Tempest\View\Elements\WhitespaceElement; use Tempest\View\Parser\TempestViewParser; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; +use function Tempest\Support\arr; /** * @internal @@ -37,33 +40,41 @@ public function test_parental_relations(): void $a = $elementFactory->make(iterator_to_array($ast)[0]); $this->assertInstanceOf(GenericElement::class, $a); - $this->assertCount(1, $a->getChildren()); + $this->assertCount(1, $this->withoutWhitespace($a->getChildren())); $this->assertNull($a->getParent()); - $b = $a->getChildren()[0]; + $b = $this->withoutWhitespace($a->getChildren())[0]; $this->assertInstanceOf(GenericElement::class, $b); - $this->assertCount(3, $b->getChildren()); + $this->assertCount(3, $this->withoutWhitespace($b->getChildren())); $this->assertSame($b->getParent(), $a); - $c = $b->getChildren()[0]; + $c = $this->withoutWhitespace($b->getChildren())[0]; $this->assertInstanceOf(GenericElement::class, $c); - $this->assertCount(1, $c->getChildren()); + $this->assertCount(1, $this->withoutWhitespace($c->getChildren())); $this->assertSame($c->getParent(), $b); - $text = $c->getChildren()[0]; + $text = $this->withoutWhitespace($c->getChildren())[0]; $this->assertInstanceOf(TextElement::class, $text); $this->assertSame($text->getParent(), $c); - $d = $b->getChildren()[1]; + $d = $this->withoutWhitespace($b->getChildren())[1]; $this->assertInstanceOf(GenericElement::class, $d); - $this->assertCount(0, $d->getChildren()); + $this->assertCount(0, $this->withoutWhitespace($d->getChildren())); $this->assertSame($d->getParent(), $b); $this->assertSame($d->getPrevious(), $c); - $e = $b->getChildren()[2]; + $e = $this->withoutWhitespace($b->getChildren())[2]; $this->assertInstanceOf(GenericElement::class, $e); - $this->assertCount(0, $e->getChildren()); + $this->assertCount(0, $this->withoutWhitespace($e->getChildren())); $this->assertSame($e->getParent(), $b); $this->assertSame($e->getPrevious(), $d); } + + private function withoutWhitespace(array $elements): array + { + return arr($elements) + ->filter(fn (Element $element) => ! $element instanceof WhitespaceElement) + ->values() + ->toArray(); + } } From 6565b33c8cfdcf7cb12c8dac781e91378fc92d42 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 9 Jan 2026 13:41:39 +0100 Subject: [PATCH 5/7] wip --- tests/Integration/View/ElementFactoryTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Integration/View/ElementFactoryTest.php b/tests/Integration/View/ElementFactoryTest.php index ddde0f86d2..529a2ca46b 100644 --- a/tests/Integration/View/ElementFactoryTest.php +++ b/tests/Integration/View/ElementFactoryTest.php @@ -11,6 +11,7 @@ use Tempest\View\Elements\WhitespaceElement; use Tempest\View\Parser\TempestViewParser; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; + use function Tempest\Support\arr; /** From 52fe7cc3b88188a953d6605b5f465894df5760e3 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 9 Jan 2026 13:43:44 +0100 Subject: [PATCH 6/7] wip --- .../Testing/Http/HttpRouterTesterIntegrationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Testing/Http/HttpRouterTesterIntegrationTest.php b/tests/Integration/Testing/Http/HttpRouterTesterIntegrationTest.php index 6359e7d433..739ee93daa 100644 --- a/tests/Integration/Testing/Http/HttpRouterTesterIntegrationTest.php +++ b/tests/Integration/Testing/Http/HttpRouterTesterIntegrationTest.php @@ -210,7 +210,7 @@ public function query(): void #[Test] public function raw_body_string(): void { - $this->registerRoute([TestController::class, 'handleRawBody']); + $this->http->registerRoute([TestController::class, 'handleRawBody']); $response = $this->http ->post('/raw-body', body: 'ok') From e73899d3abbaa468857de2af9c2fe860b83eaf79 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 9 Jan 2026 14:10:32 +0100 Subject: [PATCH 7/7] wip --- packages/view/tests/FallthroughAttributesTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/view/tests/FallthroughAttributesTest.php b/packages/view/tests/FallthroughAttributesTest.php index ba301baaf8..89a6f84751 100644 --- a/packages/view/tests/FallthroughAttributesTest.php +++ b/packages/view/tests/FallthroughAttributesTest.php @@ -28,11 +28,11 @@ public function render(): void view(__DIR__ . '/Fixtures/fallthrough.view.php'), ); - $this->assertEquals(<<<'HTML' + $this->assertEquals(str_replace([' ', PHP_EOL], '', <<<'HTML'
- HTML, $html); + HTML), str_replace([' ', PHP_EOL], '', $html)); } }