From 23be2cd63cad9a7c6a3af79c19157a905b8b42d2 Mon Sep 17 00:00:00 2001 From: Caleb Porzio Date: Tue, 15 Sep 2026 10:30:35 -0400 Subject: [PATCH] Fix Volt template extraction with PHP tags in strings --- src/Precompilers/ExtractTemplate.php | 17 ++++- tests/Feature/FunctionalComponentTest.php | 8 +++ .../php-tags-in-strings.blade.php | 17 +++++ .../Unit/Precompilers/ExtractTemplateTest.php | 62 +++++++++++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/resources/views/functional-api/php-tags-in-strings.blade.php diff --git a/src/Precompilers/ExtractTemplate.php b/src/Precompilers/ExtractTemplate.php index c7b0438..8528a02 100644 --- a/src/Precompilers/ExtractTemplate.php +++ b/src/Precompilers/ExtractTemplate.php @@ -6,6 +6,7 @@ use Livewire\Volt\Exceptions\ReturnNewClassExecutionEndingException; use Livewire\Volt\MountedDirectories; use Livewire\Volt\Volt; +use PhpToken; class ExtractTemplate { @@ -48,9 +49,21 @@ protected function shouldExtractTemplate(string $template): bool */ protected function html(string $template): string { - $template = trim(preg_replace('/<\?php\s*(.*?)\s*\?>/s', '', $template)); + $html = ''; + $inPhp = false; - return str($template)->beforeLast('trim()->value(); + foreach (PhpToken::tokenize($template) as $token) { + if ($token->is(T_OPEN_TAG)) { + $inPhp = true; + } elseif ($inPhp && $token->is(T_CLOSE_TAG)) { + $inPhp = false; + $html .= substr($token->text, 2); + } elseif (! $inPhp) { + $html .= $token->text; + } + } + + return trim($html); } /** diff --git a/tests/Feature/FunctionalComponentTest.php b/tests/Feature/FunctionalComponentTest.php index c9c1b47..c147a3a 100644 --- a/tests/Feature/FunctionalComponentTest.php +++ b/tests/Feature/FunctionalComponentTest.php @@ -939,3 +939,11 @@ public function render() ->call('increment') ->assertSet('counter', 3); }); + +it('renders PHP tags in state strings before and after an update', function () { + Volt::test('php-tags-in-strings') + ->assertSee('Example of ?> causing problems') + ->assertSee('PHP starts with ') + ->call('update') + ->assertSee('Updated text containing ?>'); +}); diff --git a/tests/Feature/resources/views/functional-api/php-tags-in-strings.blade.php b/tests/Feature/resources/views/functional-api/php-tags-in-strings.blade.php new file mode 100644 index 0000000..787d1b9 --- /dev/null +++ b/tests/Feature/resources/views/functional-api/php-tags-in-strings.blade.php @@ -0,0 +1,17 @@ + 'Example of ?> causing problems', + 'message' => 'PHP starts with ', +]); + +$update = fn () => $this->title = 'Updated text containing ?>'; +?> + +
+

{{ $title }}

+

{{ $message }}

+ +
diff --git a/tests/Unit/Precompilers/ExtractTemplateTest.php b/tests/Unit/Precompilers/ExtractTemplateTest.php index 47ba907..19c2051 100644 --- a/tests/Unit/Precompilers/ExtractTemplateTest.php +++ b/tests/Unit/Precompilers/ExtractTemplateTest.php @@ -418,3 +418,65 @@ function something() use () { expect(trim($result))->toBe($expected); })->with($conflictsDataset); + +it('ignores PHP tags inside strings when extracting the template', function () { + $template = <<<'HTML' + 'Example of ?> causing problems', + 'message' => 'PHP starts with ', + ]); + + ?> + +
+

{{ $title }}

+

{{ $message }}

+
+ HTML; + + $expected = <<<'HTML' +
+

{{ $title }}

+

{{ $message }}

+
+ HTML; + + expect($this->precompiler->__invoke($template))->toBe($expected); +}); + +it('ignores PHP tags inside comments and multiline strings', function () { + $template = <<<'HTML' + and + TEXT; + + ?> + +
{{ $message }}
+ HTML; + + expect($this->precompiler->__invoke($template))->toBe('
{{ $message }}
'); +}); + +it('preserves short echo tags in the template', function () { + $template = <<<'HTML' + + +
+ HTML; + + expect($this->precompiler->__invoke($template))->toBe('
'); +});