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..d63e017 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,16 @@ public function compile(string $template): string array_pop($dataStack); } - return $this->memoizer->memoize($this->folder->fold($node)); + foreach ([ + $this->imprinter->imprint(...), + $this->folder->fold(...), + $this->imprinter->restore(...), + $this->memoizer->memoize(...), + ] as $process) { + $node = $process($node); + } + + return $node; }, ); @@ -167,4 +178,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..78daed3 --- /dev/null +++ b/src/Imprinter/Imprinter.php @@ -0,0 +1,125 @@ +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_[a-zA-Z0-9]{10}/i', function (array $matches) { + $imprintPlaceholder = $matches[0]; + + $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); + } + + return $content; + }, $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_' . str()->random(10); + + $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..6b62a85 --- /dev/null +++ b/tests/ImprintTest.php @@ -0,0 +1,22 @@ +anonymousComponentPath(__DIR__ . '/fixtures/components'); + + \Illuminate\Support\Facades\Artisan::call('view:clear'); + }); + + it('can imprint components with nested components that use the error bag', function () { + $input = 'Search'; + $output = <<<'HTML' +
+
Search
+ + +
+ HTML; + + expect(app('blaze')->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 +