From e6e59fd32cd6480e25285d418a10274c9c2c1fce Mon Sep 17 00:00:00 2001 From: Josh Hanley Date: Thu, 23 Oct 2025 17:50:58 +1000 Subject: [PATCH 1/4] Add imprinter --- src/Blaze.php | 1 + src/BlazeManager.php | 17 ++- src/BlazeServiceProvider.php | 4 + src/Imprinter/Imprinter.php | 136 ++++++++++++++++++++++ tests/ImprintTest.php | 26 +++++ tests/fixtures/components/error.blade.php | 13 +++ tests/fixtures/components/field.blade.php | 13 +++ 7 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 src/Imprinter/Imprinter.php create mode 100644 tests/ImprintTest.php create mode 100644 tests/fixtures/components/error.blade.php create mode 100644 tests/fixtures/components/field.blade.php diff --git a/src/Blaze.php b/src/Blaze.php index bbf9c9f..f622539 100644 --- a/src/Blaze.php +++ b/src/Blaze.php @@ -16,6 +16,7 @@ * @method static \Livewire\Blaze\Tokenizer\Tokenizer tokenizer() * @method static \Livewire\Blaze\Parser\Parser parser() * @method static \Livewire\Blaze\Folder\Folder folder() + * @method static \Livewire\Blaze\Imprinter\Imprinter imprinter() * @method static array flushFoldedEvents() * @see \Livewire\Blaze\BlazeManager */ diff --git a/src/BlazeManager.php b/src/BlazeManager.php index 75a663e..1f19eb9 100644 --- a/src/BlazeManager.php +++ b/src/BlazeManager.php @@ -4,6 +4,7 @@ use Livewire\Blaze\Events\ComponentFolded; use Livewire\Blaze\Nodes\ComponentNode; +use Livewire\Blaze\Imprinter\Imprinter; use Livewire\Blaze\Tokenizer\Tokenizer; use Illuminate\Support\Facades\Event; use Livewire\Blaze\Memoizer\Memoizer; @@ -27,6 +28,7 @@ public function __construct( protected Walker $walker, protected Folder $folder, protected Memoizer $memoizer, + protected Imprinter $imprinter, ) { Event::listen(ComponentFolded::class, function (ComponentFolded $event) { $this->foldedEvents[] = $event; @@ -109,7 +111,15 @@ public function compile(string $template): string array_pop($dataStack); } - return $this->memoizer->memoize($this->folder->fold($node)); + return $this->memoizer->memoize( + $this->imprinter->restore( + $this->folder->fold( + $this->imprinter->imprint( + $node + ), + ), + ), + ); }, ); @@ -167,4 +177,9 @@ public function folder(): Folder { return $this->folder; } + + public function imprinter(): Imprinter + { + return $this->imprinter; + } } diff --git a/src/BlazeServiceProvider.php b/src/BlazeServiceProvider.php index 98b9776..7e26a0d 100644 --- a/src/BlazeServiceProvider.php +++ b/src/BlazeServiceProvider.php @@ -3,6 +3,7 @@ namespace Livewire\Blaze; use Livewire\Blaze\Directive\BlazeDirective; +use Livewire\Blaze\Imprinter\Imprinter; use Livewire\Blaze\Tokenizer\Tokenizer; use Illuminate\Support\ServiceProvider; use Livewire\Blaze\Memoizer\Memoizer; @@ -37,6 +38,9 @@ protected function registerBlazeManager(): void new Memoizer( componentNameToPath: fn ($name) => $bladeService->componentNameToPath($name), ), + new Imprinter( + componentNameToPath: fn ($name) => $bladeService->componentNameToPath($name), + ), )); $this->app->alias(BlazeManager::class, Blaze::class); diff --git a/src/Imprinter/Imprinter.php b/src/Imprinter/Imprinter.php new file mode 100644 index 0000000..cea4747 --- /dev/null +++ b/src/Imprinter/Imprinter.php @@ -0,0 +1,136 @@ +componentNameToPath = $componentNameToPath; + $this->cacheDirectory = storage_path('framework/views/livewire/blaze/components'); + Blade::anonymousComponentPath($this->cacheDirectory, $this->cacheNamespace); + } + + public function imprint(Node $node): Node + { + if (! $node instanceof ComponentNode) { + return $node; + } + + $this->capture($node); + + return $node; + } + + public function restore(Node $node): Node + { + if (! $node instanceof TextNode) { + return $node; + } + + // Look for IMPRINT_PLACEHOLDER throughout $node->content and capture the full string + $node->content = preg_replace_callback('/IMPRINT_PLACEHOLDER_\d+/i', function (array $matches) { + $imprintPlaceholder = $matches[0]; + + $key = str()->random(5); + + $output = ''; + $output .= '<'.'?php '; + $output .= '$blazeImprintData = \Livewire\Blaze\Blaze::imprinter()->getAttributes(\'' . $imprintPlaceholder . '\');'; + $output .= ' extract($blazeImprintData, EXTR_PREFIX_ALL, \'' . $key . '\');'; + $output .= ' unset($blazeImprintData);'; + $output .= ' ?' . '>'; + + $imprint = $this->imprintPlaceholders[$imprintPlaceholder]; + $attributes = $imprint['attributes']; + $content = $imprint['content']; + + foreach ($attributes as $name => $value) { + $content = preg_replace('/\$'.$name.'(?![a-zA-Z0-9_])/', '\''.$value.'\'', $content); + } + + $output .= $content; + + return $output; + }, $node->content); + + return $node; + } + + public function getAttributes(string $imprintPlaceholder): array + { + return $this->imprintPlaceholders[$imprintPlaceholder]['attributes'] ?? []; + } + + public function getContent(string $imprintPlaceholder): string + { + return $this->imprintPlaceholders[$imprintPlaceholder]['content']; + } + + public function storeAttributes(string $imprintPlaceholder, array $attributes): void + { + $this->imprintPlaceholders[$imprintPlaceholder]['attributes'] = $attributes; + } + + protected function capture(ComponentNode $node): void + { + $componentPath = ($this->componentNameToPath)($node->name); + + if (empty($componentPath) || ! file_exists($componentPath)) { + return; + } + + $source = file_get_contents($componentPath); + + preg_match_all('/(\s*)@imprint\((.*?)\)(.*?)@endimprint/s', $source, $matches); + + if (empty($matches[0])) { + return; + } + + $modifiedSource = $source; + + foreach ($matches[0] as $index => $match) { + $imprintBlock = $matches[0][$index]; + $whitespace = $matches[1][$index]; + $attributes = $matches[2][$index]; + $content = $matches[3][$index]; + + $placeholder = 'IMPRINT_PLACEHOLDER_' . $index; + + $output = $whitespace; + $output .= '<'.'?php \Livewire\Blaze\Blaze::imprinter()->storeAttributes(\'' . $placeholder . '\', ' . $attributes . '); ?'.'>'; + $output .= $placeholder; + + $modifiedSource = str_replace($imprintBlock, $output, $modifiedSource); + + $this->imprintPlaceholders[$placeholder] = [ + 'attributes' => [], + 'content' => $content, + ]; + } + + $name = $node->name; + $path = str_replace('.', '/', $name); + + $directory = $this->cacheDirectory . '/' . str($path)->beforeLast('/'); + $filename = str($path)->afterLast('/')->value() . '.blade.php'; + + File::ensureDirectoryExists($directory); + + File::put($directory . '/' . $filename, $modifiedSource); + + $node->name = $this->cacheNamespace . '::' . $name; + } +} diff --git a/tests/ImprintTest.php b/tests/ImprintTest.php new file mode 100644 index 0000000..eef2c39 --- /dev/null +++ b/tests/ImprintTest.php @@ -0,0 +1,26 @@ +anonymousComponentPath(__DIR__ . '/fixtures/components'); + + \Illuminate\Support\Facades\Artisan::call('view:clear'); + }); + + function compile(string $input): string { + return app('blaze')->compile($input); + } + + it('can imprint components with nested components that use the error bag', function () { + $input = 'Search'; + $output = <<<'HTML' +
+
Search
+ + +
+ HTML; + + expect(compile($input))->toContain(''); + }); +}); diff --git a/tests/fixtures/components/error.blade.php b/tests/fixtures/components/error.blade.php new file mode 100644 index 0000000..b1dceb8 --- /dev/null +++ b/tests/fixtures/components/error.blade.php @@ -0,0 +1,13 @@ +@props([ + 'name' => null, +]) + +@php +$message = isset($errors) ? $errors->first($name) : null; +@endphp + +
class($message ? 'mt-3 text-sm font-medium text-red-500 dark:text-red-400' : 'hidden') }}> + + {{ $message }} + +
diff --git a/tests/fixtures/components/field.blade.php b/tests/fixtures/components/field.blade.php new file mode 100644 index 0000000..61e68c5 --- /dev/null +++ b/tests/fixtures/components/field.blade.php @@ -0,0 +1,13 @@ +@blaze + +@props([ + 'name' => $attributes->whereStartsWith('wire:model')->first(), +]) + +
+
{{ $slot }}
+ + @imprint(['name' => $name]) + + @endimprint +
From 2d7cd059c5d3b0532ea03d2491464ff1a14672fb Mon Sep 17 00:00:00 2001 From: Josh Hanley Date: Thu, 23 Oct 2025 18:14:08 +1000 Subject: [PATCH 2/4] Make placeholder more robust --- src/Imprinter/Imprinter.php | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/Imprinter/Imprinter.php b/src/Imprinter/Imprinter.php index cea4747..78daed3 100644 --- a/src/Imprinter/Imprinter.php +++ b/src/Imprinter/Imprinter.php @@ -40,18 +40,9 @@ public function restore(Node $node): Node } // Look for IMPRINT_PLACEHOLDER throughout $node->content and capture the full string - $node->content = preg_replace_callback('/IMPRINT_PLACEHOLDER_\d+/i', function (array $matches) { + $node->content = preg_replace_callback('/IMPRINT_PLACEHOLDER_[a-zA-Z0-9]{10}/i', function (array $matches) { $imprintPlaceholder = $matches[0]; - $key = str()->random(5); - - $output = ''; - $output .= '<'.'?php '; - $output .= '$blazeImprintData = \Livewire\Blaze\Blaze::imprinter()->getAttributes(\'' . $imprintPlaceholder . '\');'; - $output .= ' extract($blazeImprintData, EXTR_PREFIX_ALL, \'' . $key . '\');'; - $output .= ' unset($blazeImprintData);'; - $output .= ' ?' . '>'; - $imprint = $this->imprintPlaceholders[$imprintPlaceholder]; $attributes = $imprint['attributes']; $content = $imprint['content']; @@ -60,9 +51,7 @@ public function restore(Node $node): Node $content = preg_replace('/\$'.$name.'(?![a-zA-Z0-9_])/', '\''.$value.'\'', $content); } - $output .= $content; - - return $output; + return $content; }, $node->content); return $node; @@ -107,7 +96,7 @@ protected function capture(ComponentNode $node): void $attributes = $matches[2][$index]; $content = $matches[3][$index]; - $placeholder = 'IMPRINT_PLACEHOLDER_' . $index; + $placeholder = 'IMPRINT_PLACEHOLDER_' . str()->random(10); $output = $whitespace; $output .= '<'.'?php \Livewire\Blaze\Blaze::imprinter()->storeAttributes(\'' . $placeholder . '\', ' . $attributes . '); ?'.'>'; From be645ec6d89f8fd603b933694baf441cf235d0ab Mon Sep 17 00:00:00 2001 From: Josh Hanley Date: Thu, 23 Oct 2025 18:14:39 +1000 Subject: [PATCH 3/4] Move compile calls to a pipeline sort of thing --- src/BlazeManager.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/BlazeManager.php b/src/BlazeManager.php index 1f19eb9..d094742 100644 --- a/src/BlazeManager.php +++ b/src/BlazeManager.php @@ -2,11 +2,13 @@ namespace Livewire\Blaze; +use Illuminate\Process\Pipe; use Livewire\Blaze\Events\ComponentFolded; use Livewire\Blaze\Nodes\ComponentNode; use Livewire\Blaze\Imprinter\Imprinter; use Livewire\Blaze\Tokenizer\Tokenizer; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Pipeline; use Livewire\Blaze\Memoizer\Memoizer; use Livewire\Blaze\Walker\Walker; use Livewire\Blaze\Parser\Parser; @@ -111,15 +113,16 @@ public function compile(string $template): string array_pop($dataStack); } - return $this->memoizer->memoize( - $this->imprinter->restore( - $this->folder->fold( - $this->imprinter->imprint( - $node - ), - ), - ), - ); + foreach ([ + $this->imprinter->imprint(...), + $this->folder->fold(...), + $this->imprinter->restore(...), + $this->memoizer->memoize(...), + ] as $process) { + $node = $process($node); + } + + return $node; }, ); From 1cc4d470e0f468d419d65adba5e5db7bb3f463b9 Mon Sep 17 00:00:00 2001 From: Josh Hanley Date: Thu, 23 Oct 2025 18:17:14 +1000 Subject: [PATCH 4/4] wip --- src/BlazeManager.php | 2 -- tests/ImprintTest.php | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/BlazeManager.php b/src/BlazeManager.php index d094742..d63e017 100644 --- a/src/BlazeManager.php +++ b/src/BlazeManager.php @@ -2,13 +2,11 @@ namespace Livewire\Blaze; -use Illuminate\Process\Pipe; use Livewire\Blaze\Events\ComponentFolded; use Livewire\Blaze\Nodes\ComponentNode; use Livewire\Blaze\Imprinter\Imprinter; use Livewire\Blaze\Tokenizer\Tokenizer; use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Pipeline; use Livewire\Blaze\Memoizer\Memoizer; use Livewire\Blaze\Walker\Walker; use Livewire\Blaze\Parser\Parser; diff --git a/tests/ImprintTest.php b/tests/ImprintTest.php index eef2c39..6b62a85 100644 --- a/tests/ImprintTest.php +++ b/tests/ImprintTest.php @@ -7,10 +7,6 @@ \Illuminate\Support\Facades\Artisan::call('view:clear'); }); - function compile(string $input): string { - return app('blaze')->compile($input); - } - it('can imprint components with nested components that use the error bag', function () { $input = 'Search'; $output = <<<'HTML' @@ -21,6 +17,6 @@ function compile(string $input): string { HTML; - expect(compile($input))->toContain(''); + expect(app('blaze')->compile($input))->toContain(''); }); });