From e6ac408427095b0636a132d44a4692974e88052e Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 2 Aug 2026 00:46:21 -0400 Subject: [PATCH 1/2] Revert --- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .gitignore | 2 +- LICENSE | 2 +- UPGRADING.md | 2 +- src/BladeService.php | 28 ---- src/BlazeManager.php | 2 +- src/Compiler/SlotCompiler.php | 54 +++++-- src/Events/ComponentFolded.php | 2 +- .../LeftoverPlaceholdersException.php | 2 +- src/Folder/Folder.php | 26 --- src/Parser/Nodes/DirectiveNode.php | 18 --- src/Parser/Nodes/Node.php | 2 +- src/Parser/Nodes/TextNode.php | 2 +- src/Parser/ParseStack.php | 4 +- src/Parser/Parser.php | 14 -- src/Parser/Tokenizer.php | 109 +------------ src/Parser/TokenizerState.php | 17 +- src/Parser/Tokens/DirectiveToken.php | 12 -- src/Parser/Tokens/SlotCloseToken.php | 2 +- src/Parser/Tokens/SlotOpenToken.php | 2 +- src/Parser/Tokens/TagCloseToken.php | 2 +- src/Parser/Tokens/TagOpenToken.php | 2 +- src/Parser/Tokens/TagSelfCloseToken.php | 2 +- src/Parser/Tokens/TextToken.php | 2 +- src/Support/DirectiveStack.php | 107 ------------ src/Support/DirectiveStructure.php | 38 ----- src/Support/LaravelRegex.php | 7 - src/Unblaze.php | 2 +- tests/BladeServiceTest.php | 6 - tests/ComparisonTest.php | 22 --- tests/Compiler/CompilerTest.php | 4 +- tests/Compiler/DirectiveCompilerTest.php | 2 +- tests/Compiler/UseExtractorTest.php | 2 +- tests/FluxProTest.php | 2 +- tests/FluxTest.php | 2 +- tests/Folder/DirectiveStackTest.php | 95 ----------- tests/Folder/FolderTest.php | 37 +---- tests/IntegrationTest.php | 2 +- tests/Memoizer/MemoTest.php | 2 +- tests/Memoizer/MemoizerTest.php | 2 +- tests/Parser/ParserTest.php | 11 +- tests/Parser/TokenizerTest.php | 153 +----------------- tests/Pest.php | 2 +- tests/Runtime/BlazeRuntimeTest.php | 2 +- tests/Support/AttributeParserTest.php | 2 +- tests/Support/DirectivesTest.php | 2 +- tests/TestCase.php | 2 +- tests/WhitespaceTest.php | 2 +- tests/fixtures/views/blaze.blade.php | 2 +- .../views/components/compile-false.blade.php | 2 +- .../foldable/card-unsafe-slot.blade.php | 2 +- .../components/foldable/card-unsafe.blade.php | 2 +- .../views/components/foldable/card.blade.php | 2 +- .../components/foldable/fold-false.blade.php | 2 +- .../foldable/input-aware-unsafe.blade.php | 2 +- .../components/foldable/input-aware.blade.php | 2 +- .../foldable/input-no-blaze.blade.php | 2 +- .../components/foldable/input-safe.blade.php | 2 +- .../input-unsafe-attributes.blade.php | 2 +- .../foldable/input-unsafe.blade.php | 2 +- .../views/components/foldable/input.blade.php | 2 +- .../foldable/nested-input-unblaze.blade.php | 2 +- .../foldable/unblaze-only-wrapper.blade.php | 2 +- .../foldable/unblaze-only.blade.php | 2 +- .../foldable/unblaze-slot-wrapper.blade.php | 2 +- .../components/foldable/wrapper-nl.blade.php | 2 +- .../components/input-aware-no-blaze.blade.php | 2 +- .../views/components/input-no-blaze.blade.php | 2 +- .../memoizable/avatar-no-blaze.blade.php | 2 +- .../components/memoizable/avatar.blade.php | 2 +- .../memoizable/memo-false.blade.php | 2 +- tests/fixtures/views/mix.blade.php | 2 +- tests/fixtures/views/php-view.php | 2 +- 74 files changed, 108 insertions(+), 764 deletions(-) delete mode 100644 src/Parser/Nodes/DirectiveNode.php delete mode 100644 src/Parser/Tokens/DirectiveToken.php delete mode 100644 src/Support/DirectiveStack.php delete mode 100644 src/Support/DirectiveStructure.php delete mode 100644 tests/Folder/DirectiveStackTest.php diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4d570c0e..d049aad1 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -5,4 +5,4 @@ contact_links: about: Ask questions and discuss with other community members - name: Documentation url: https://github.com/livewire/blaze#readme - about: Check the README for usage instructions + about: Check the README for usage instructions \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 3ab88584..15c4c8a4 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -17,4 +17,4 @@ What you want to happen. Any alternative solutions or features you've considered. **Additional context** -Add any code examples or use cases here. +Add any code examples or use cases here. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8470d5dd..483a37a3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ composer.lock phpunit.xml.dist .env -.env.testing +.env.testing \ No newline at end of file diff --git a/LICENSE b/LICENSE index a7304f0f..4d0299eb 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/UPGRADING.md b/UPGRADING.md index 50e1da4c..83e1abd1 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -29,4 +29,4 @@ Blaze::optimize() ->in(resource_path('views/components/icons'), memo: true); ``` -Only use these strategies if you understand their limitations. To learn more, read the [Optimization strategies](README.md#optimization-strategies) section in the README. +Only use these strategies if you understand their limitations. To learn more, read the [Optimization strategies](README.md#optimization-strategies) section in the README. \ No newline at end of file diff --git a/src/BladeService.php b/src/BladeService.php index 31af937a..78898c0a 100644 --- a/src/BladeService.php +++ b/src/BladeService.php @@ -16,8 +16,6 @@ class BladeService { protected ComponentTagCompiler $tagCompiler; - protected ?array $customConditions = null; - public function __construct( public BladeCompiler $compiler, protected Factory $view, @@ -124,17 +122,6 @@ public function compileComments(string $input): string return $compileComments->invoke($this->compiler, $input); } - /** - * Invoke the Blade compiler's hasEvenNumberOfParentheses via reflection. - */ - public function hasEvenNumberOfParentheses(string $expression): bool - { - $reflection = new ReflectionClass($this->compiler); - $method = $reflection->getMethod('hasEvenNumberOfParentheses'); - - return $method->invoke($this->compiler, $expression); - } - /** * Preprocess a component attribute string using Laravel's ComponentTagCompiler. * @@ -172,21 +159,6 @@ public function compileUseStatements(string $input): string })->compile($input); } - /** - * Get the custom conditional directives registered with the Blade compiler. - */ - public function customConditions(): array - { - if ($this->customConditions !== null) { - return $this->customConditions; - } - - $reflection = new ReflectionClass($this->compiler); - $conditions = $reflection->getProperty('conditions')->getValue($this->compiler); - - return $this->customConditions = collect($conditions)->keys()->all(); - } - /** * Compile an attribute to a PHP array entry string (e.g. "'propName' => value"). */ diff --git a/src/BlazeManager.php b/src/BlazeManager.php index 5b001b35..eef12d80 100644 --- a/src/BlazeManager.php +++ b/src/BlazeManager.php @@ -50,7 +50,7 @@ public function __construct( protected BladeService $blade, ) { $this->renderer = new BladeRenderer($bladeCompiler, app('view'), $this->runtime, $this); - $this->parser = new Parser(new Tokenizer($this->blade), new AttributeParser($this->blade)); + $this->parser = new Parser(new Tokenizer, new AttributeParser($this->blade)); $this->walker = new Walker; $this->compiler = new Compiler($config, $this->blade, $this); $this->folder = new Folder($config, $this->blade, $this->renderer, $this); diff --git a/src/Compiler/SlotCompiler.php b/src/Compiler/SlotCompiler.php index 9f90e5cd..92e11ec4 100644 --- a/src/Compiler/SlotCompiler.php +++ b/src/Compiler/SlotCompiler.php @@ -26,33 +26,24 @@ public function __construct( public function compile(string $slotsVariableName, array $children): string { $output = ''; - $hasExplicitDefault = $this->hasExplicitDefaultSlot($children); - if (! $hasExplicitDefault) { - $output .= '<' . '?php ob_start(); ?>'; + // Compile implicit default slot from loose content (non-SlotNode children) + if (! $this->hasExplicitDefaultSlot($children)) { + $output .= $this->compileSlot('slot', $this->renderLooseContent($children), '[]', $slotsVariableName) . "\n"; } + // Compile each named slot foreach ($children as $child) { if ($child instanceof SlotNode) { - $output .= ' ' . $this->compileSlot( + $output .= $this->compileSlot( $this->resolveSlotName($child), $this->renderChildren($child->children), $this->compileSlotAttributes($child), $slotsVariableName, - ); - } else { - $output .= $child->render(); + ) . "\n"; } } - if (! $hasExplicitDefault) { - $contentHandler = $this->manager->isFolding() - ? '$__blaze->processPassthroughContent(\'trim\', trim(ob_get_clean()))' - : 'trim(ob_get_clean())'; - - $output .= '<' . '?php ' . $slotsVariableName . '[\'slot\'] = new \Illuminate\View\ComponentSlot(' . $contentHandler . ', []); ?>' . "\n"; - } - return $output; } @@ -72,13 +63,44 @@ protected function hasExplicitDefaultSlot(array $children): bool return false; } + /** + * Render non-SlotNode children as the default slot content. + * + * @param array $children + */ + protected function renderLooseContent(array $children): string + { + $content = ''; + $previousWasSlot = false; + + foreach ($children as $child) { + if ($child instanceof SlotNode) { + $previousWasSlot = true; + continue; + } + + $rendered = $child->render(); + + // Laravel's slot compilation consumes the newline after and adds a leading space. + // We match this by prepending a space and stripping any leading newline. + if ($previousWasSlot) { + $rendered = ' ' . preg_replace('/^\n/', '', $rendered); + } + + $content .= $rendered; + $previousWasSlot = false; + } + + return $content; + } + /** * Compile a slot into ob_start/ob_get_clean code. */ protected function compileSlot(string $name, string $content, string $attributes, string $slotsVariableName): string { $contentHandler = $this->manager->isFolding() ? '$__blaze->processPassthroughContent(\'trim\', trim(ob_get_clean()))' : 'trim(ob_get_clean())'; - + return '<' . '?php ob_start(); ?>' . $content . '<' . '?php ' . $slotsVariableName . '[\'' . $name . '\'] = new \Illuminate\View\ComponentSlot(' . $contentHandler . ', ' . $attributes . '); ?>'; diff --git a/src/Events/ComponentFolded.php b/src/Events/ComponentFolded.php index 4ef68438..49a2eba9 100644 --- a/src/Events/ComponentFolded.php +++ b/src/Events/ComponentFolded.php @@ -12,4 +12,4 @@ public function __construct( public string $path, public int $filemtime ) {} -} +} \ No newline at end of file diff --git a/src/Exceptions/LeftoverPlaceholdersException.php b/src/Exceptions/LeftoverPlaceholdersException.php index 47fc6821..448e3bb4 100644 --- a/src/Exceptions/LeftoverPlaceholdersException.php +++ b/src/Exceptions/LeftoverPlaceholdersException.php @@ -40,4 +40,4 @@ public function getRenderedSnippet(): ?string { return $this->renderedSnippet; } -} +} \ No newline at end of file diff --git a/src/Folder/Folder.php b/src/Folder/Folder.php index 26707e7d..eda0cdd5 100644 --- a/src/Folder/Folder.php +++ b/src/Folder/Folder.php @@ -15,8 +15,6 @@ use Livewire\Blaze\BlazeManager; use Illuminate\Support\Arr; use Livewire\Blaze\Config; -use Livewire\Blaze\Parser\Nodes\DirectiveNode; -use Livewire\Blaze\Support\DirectiveStack; use Throwable; /** @@ -99,10 +97,6 @@ protected function shouldFold(ComponentSource $source): bool */ protected function isSafeToFold(ComponentSource $source, ComponentNode $node): bool { - if ($this->slotsAreWrappedInDirective($node)) { - return false; - } - $dynamicAttributes = array_filter($node->attributes, fn ($attribute) => ! $attribute->isStaticValue()); foreach ($source->directives->aware() as $prop) { @@ -177,26 +171,6 @@ protected function isSafeToFold(ComponentSource $source, ComponentNode $node): b return true; } - /** - * Check if a slot is wrapped in a directive. - */ - protected function slotsAreWrappedInDirective(ComponentNode $node): bool - { - $stack = DirectiveStack::make($this->blade->customConditions()); - - foreach ($node->children as $child) { - if ($child instanceof DirectiveNode) { - $stack->add($child->name); - } - - if ($child instanceof SlotNode && $stack->open()) { - return true; - } - } - - return false; - } - /** * Check if a slot has any dynamically-bound attributes. */ diff --git a/src/Parser/Nodes/DirectiveNode.php b/src/Parser/Nodes/DirectiveNode.php deleted file mode 100644 index 5f325dc6..00000000 --- a/src/Parser/Nodes/DirectiveNode.php +++ /dev/null @@ -1,18 +0,0 @@ -original; - } -} diff --git a/src/Parser/Nodes/Node.php b/src/Parser/Nodes/Node.php index bc5f3462..75a4397a 100644 --- a/src/Parser/Nodes/Node.php +++ b/src/Parser/Nodes/Node.php @@ -11,4 +11,4 @@ abstract class Node * Render this node to its string output. */ abstract public function render(): string; -} +} \ No newline at end of file diff --git a/src/Parser/Nodes/TextNode.php b/src/Parser/Nodes/TextNode.php index 626bbf66..796197c1 100644 --- a/src/Parser/Nodes/TextNode.php +++ b/src/Parser/Nodes/TextNode.php @@ -16,4 +16,4 @@ public function render(): string { return $this->content; } -} +} \ No newline at end of file diff --git a/src/Parser/ParseStack.php b/src/Parser/ParseStack.php index fb805c2a..f35b1415 100644 --- a/src/Parser/ParseStack.php +++ b/src/Parser/ParseStack.php @@ -28,7 +28,7 @@ public function addToRoot(Node $node): void if ($current instanceof ComponentNode || $current instanceof SlotNode) { $current->children[] = $node; } else { - $this->ast[] = $node; + $this->ast[] = $node; } } } @@ -86,4 +86,4 @@ public function depth(): int { return count($this->stack); } -} +} \ No newline at end of file diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index 88b343f3..b3f1dee9 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -4,11 +4,9 @@ use Livewire\Blaze\BladeService; use Livewire\Blaze\Parser\Nodes\ComponentNode; -use Livewire\Blaze\Parser\Nodes\DirectiveNode; use Livewire\Blaze\Parser\Nodes\SlotNode; use Livewire\Blaze\Parser\Nodes\TextNode; use Livewire\Blaze\Parser\Tokenizer; -use Livewire\Blaze\Parser\Tokens\DirectiveToken; use Livewire\Blaze\Parser\Tokens\SlotCloseToken; use Livewire\Blaze\Parser\Tokens\SlotOpenToken; use Livewire\Blaze\Parser\Tokens\TagCloseToken; @@ -44,7 +42,6 @@ public function parse(string $content): array TagCloseToken::class => $this->handleTagClose($token, $stack), SlotOpenToken::class => $this->handleSlotOpen($token, $stack), SlotCloseToken::class => $this->handleSlotClose($token, $stack), - DirectiveToken::class => $this->handleDirective($token, $stack), TextToken::class => $this->handleText($token, $stack), default => throw new \RuntimeException('Unknown token type: ' . get_class($token)) }; @@ -133,17 +130,6 @@ protected function handleSlotClose(SlotCloseToken $token, ParseStack $stack): vo } } - protected function handleDirective(DirectiveToken $token, ParseStack $stack): void - { - $node = new DirectiveNode( - name: $token->name, - original: $token->original, - arguments: $token->arguments, - ); - - $stack->addToRoot($node); - } - /** * Handle a text content token. */ diff --git a/src/Parser/Tokenizer.php b/src/Parser/Tokenizer.php index d9dfbc87..35978d9a 100644 --- a/src/Parser/Tokenizer.php +++ b/src/Parser/Tokenizer.php @@ -2,9 +2,6 @@ namespace Livewire\Blaze\Parser; -use Illuminate\Support\Str; -use Livewire\Blaze\BladeService; -use Livewire\Blaze\Parser\Tokens\DirectiveToken; use Livewire\Blaze\Parser\Tokens\TagSelfCloseToken; use Livewire\Blaze\Parser\Tokens\SlotCloseToken; use Livewire\Blaze\Parser\Tokens\SlotOpenToken; @@ -19,11 +16,6 @@ */ class Tokenizer { - public function __construct( - protected BladeService $blade, - ) { - } - protected array $prefixes = [ 'flux:' => [ 'namespace' => 'flux::', @@ -85,7 +77,6 @@ public function tokenize(string $template): array TokenizerState::SLOT_OPEN => $this->handleSlotOpenState(), TokenizerState::SLOT_CLOSE => $this->handleSlotCloseState(), TokenizerState::SHORT_SLOT => $this->handleShortSlotState(), - TokenizerState::DIRECTIVE => $this->handleDirectiveState(), default => throw new \RuntimeException("Unknown state: $state"), }; } @@ -166,28 +157,6 @@ protected function handleTextState(): TokenizerState } } - if ($char === '@') { - // Skip escaped directives like `@@if` - if ($this->peek(1) === '@') { - $this->advance(2); - - return TokenizerState::TEXT; - } - - // Skip @ preceded by a word char like `info@example` - if ($this->position > 0 && preg_match('/\w/', $this->content[$this->position - 1])) { - $this->advance(); - - return TokenizerState::TEXT; - } - - $this->flushBuffer(); - - $this->currentToken = new DirectiveToken(name: '', original: ''); - - return TokenizerState::DIRECTIVE; - } - $this->advance(); return TokenizerState::TEXT; @@ -347,82 +316,6 @@ protected function handleShortSlotState(): TokenizerState return TokenizerState::SHORT_SLOT; } - /** - * Process directive state, extracting the directive name and arguments. - */ - protected function handleDirectiveState(): TokenizerState - { - if (! $match = $this->matchDirective()) { - $this->advance(); - - return TokenizerState::TEXT; - } - - $this->advance(strlen($match['original'])); - - $this->currentToken->name = $match['name']; - $this->currentToken->original = $match['original']; - $this->currentToken->arguments = $match['arguments']; - - $this->emitToken(); - - return TokenizerState::TEXT; - } - - /** - * Match a Blade directive at the current position. - */ - protected function matchDirective(): ?array - { - /** - * The following code matches the parenthesis handling in Blade as closely as possible. - * - * @see \Illuminate\View\Compilers\BladeCompiler::compileStatements() - */ - - $template = $this->remaining(); - - if (! preg_match(LaravelRegex::BLADE_STATEMENT, $template, $matches, PREG_UNMATCHED_AS_NULL)) { - return null; - } - - $match = [ - $matches[0], - $matches[1], - $matches[2], - $matches[3] ?: null, - $matches[4] ?: null, - ]; - - // Here we check to see if we have properly found the closing parenthesis by - // regex pattern or not, and will recursively continue on to the next ")" - // then check again until the tokenizer confirms we find the right one. - while (isset($match[4]) && - Str::endsWith($match[0], ')') && - ! $this->blade->hasEvenNumberOfParentheses($match[0])) { - if (($after = Str::after($template, $match[0])) === $template) { - break; - } - - $rest = Str::before($after, ')'); - - $match[0] = $match[0].$rest.')'; - $match[3] = $match[3].$rest.')'; - $match[4] = $match[4].$rest; - } - - // No closing parenthesis found - if (! Str::startsWith($template, $match[0])) { - return null; - } - - return [ - 'name' => $match[1], - 'original' => $match[0], - 'arguments' => isset($match[3]) ? (Str::substr($match[3], 1, -1) ?: null) : null, - ]; - } - /** * Collect all attributes on the current token, splitting on unquoted/unbracketed whitespace. * Stops at > or /> without consuming them. @@ -663,4 +556,4 @@ protected function flushBuffer(): void $this->buffer = ''; } } -} +} \ No newline at end of file diff --git a/src/Parser/TokenizerState.php b/src/Parser/TokenizerState.php index e0f41502..d02fd47e 100644 --- a/src/Parser/TokenizerState.php +++ b/src/Parser/TokenizerState.php @@ -5,13 +5,12 @@ /** * Tokenizer FSM states for Blade template lexing. */ -enum TokenizerState +enum TokenizerState: string { - case TEXT; - case TAG_OPEN; - case SLOT_OPEN; - case TAG_CLOSE; - case SLOT_CLOSE; - case SHORT_SLOT; - case DIRECTIVE; -} + case TEXT = 'TEXT'; + case TAG_OPEN = 'TAG_OPEN'; + case SLOT_OPEN = 'SLOT'; + case TAG_CLOSE = 'TAG_CLOSE'; + case SLOT_CLOSE = 'SLOT_CLOSE'; + case SHORT_SLOT = 'SHORT_SLOT'; +} \ No newline at end of file diff --git a/src/Parser/Tokens/DirectiveToken.php b/src/Parser/Tokens/DirectiveToken.php deleted file mode 100644 index 604e61c2..00000000 --- a/src/Parser/Tokens/DirectiveToken.php +++ /dev/null @@ -1,12 +0,0 @@ -structures = [ - // Blade - new DirectiveStructure(['if']), - new DirectiveStructure(['unless'], ['endif']), - new DirectiveStructure(['isset'], ['endif']), - new DirectiveStructure(['empty'], ['endif']), - new DirectiveStructure(['auth'], ['endif']), - new DirectiveStructure(['guest'], ['endif']), - new DirectiveStructure(['env'], ['endif']), - new DirectiveStructure(['production'], ['endif']), - new DirectiveStructure(['once'], ['endif']), - new DirectiveStructure(['can'], ['endif']), - new DirectiveStructure(['cannot'], ['endif']), - new DirectiveStructure(['canany'], ['endif']), - new DirectiveStructure(['hassection', 'hasstack', 'sectionmissing'], ['endif']), - new DirectiveStructure(['switch']), - new DirectiveStructure(['pushif']), - new DirectiveStructure(['for']), - new DirectiveStructure(['foreach']), - new DirectiveStructure(['forelse']), - new DirectiveStructure(['while']), - new DirectiveStructure(['error']), - new DirectiveStructure(['session']), - new DirectiveStructure(['context']), - new DirectiveStructure(['fragment']), - new DirectiveStructure(['section'], ['show', 'append', 'overwrite', 'stop']), - new DirectiveStructure(['push', 'pushonce']), - new DirectiveStructure(['prepend', 'prependonce']), - new DirectiveStructure(['lang']), - new DirectiveStructure(['component'], ['endcomponentclass']), - new DirectiveStructure(['componentfirst']), - new DirectiveStructure(['slot']), - - // Livewire - new DirectiveStructure(['script']), - new DirectiveStructure(['assets']), - new DirectiveStructure(['island']), - new DirectiveStructure(['teleport']), - new DirectiveStructure(['persist']), - new DirectiveStructure(['placeholder']), - ]; - - foreach ($conditions as $condition) { - $condition = Str::lower($condition); - - $this->structures[] = new DirectiveStructure([$condition], ['endif']); - $this->structures[] = new DirectiveStructure(['unless'.$condition], ['end'.$condition, 'endif']); - } - } - - /** - * Create a new directive stack. - */ - public static function make(array $conditions = []): static - { - return new static($conditions); - } - - /** - * Add a directive to the stack, opening or closing its matching structure. - */ - public function add(string $name): void - { - $name = Str::lower($name); - - for ($index = count($this->stack) - 1; $index >= 0; $index--) { - if ($this->stack[$index]->closesWith($name)) { - $this->stack = Arr::take($this->stack, $index); - - return; - } - } - - foreach ($this->structures as $structure) { - if ($structure->opensWith($name)) { - $this->stack[] = $structure; - - return; - } - } - } - - /** - * Determine whether any directive structure is currently open. - */ - public function open(): bool - { - return $this->stack !== []; - } -} diff --git a/src/Support/DirectiveStructure.php b/src/Support/DirectiveStructure.php deleted file mode 100644 index 10e2ee18..00000000 --- a/src/Support/DirectiveStructure.php +++ /dev/null @@ -1,38 +0,0 @@ -openers, true); - } - - /** - * Determine whether the directive closes this structure. - */ - public function closesWith(string $name): bool - { - if (in_array($name, $this->closers, true)) { - return true; - } - - foreach ($this->openers as $opener) { - if ($name === 'end'.$opener) { - return true; - } - } - - return false; - } -} diff --git a/src/Support/LaravelRegex.php b/src/Support/LaravelRegex.php index 3d0b76f4..e035ee63 100644 --- a/src/Support/LaravelRegex.php +++ b/src/Support/LaravelRegex.php @@ -73,11 +73,4 @@ class LaravelRegex * @see BladeCompiler::storePhpBlocks() — /(?componentNameToPath('my-input')) ->toBe(fixture_path('views/components/input.blade.php')); }); - -test('gets custom Blade conditions', function () { - Blade::if('disk', fn (string $disk) => $disk === 'local'); - - expect(app(BladeService::class)->customConditions())->toContain('disk'); -}); diff --git a/tests/ComparisonTest.php b/tests/ComparisonTest.php index dedb80cf..155bd2a1 100644 --- a/tests/ComparisonTest.php +++ b/tests/ComparisonTest.php @@ -123,25 +123,3 @@ BLADE )); - -test('conditional slots', fn () => compare(<<<'BLADE' - - @if(false) - - Header - - @endif - - BLADE -)); - -test('foldable conditional slots', fn () => compare(<<<'BLADE' - - @if(false) - - Header - - @endif - - BLADE -)); diff --git a/tests/Compiler/CompilerTest.php b/tests/Compiler/CompilerTest.php index 7860abf4..06f1413b 100644 --- a/tests/Compiler/CompilerTest.php +++ b/tests/Compiler/CompilerTest.php @@ -50,11 +50,9 @@ ' \'mt-8\']; ?> ', ' ', 'pushData($__attrs'. $hash .'); ?> ', - ' ', + ' Body ', ' Header \'p-2\']); ?> ', - 'Body ', ' Footer \'mt-4\']); ?> ', - ' ', 'pushSlots($__slots'. $hash .'); ?> ', ' ', ' ', diff --git a/tests/Compiler/DirectiveCompilerTest.php b/tests/Compiler/DirectiveCompilerTest.php index 63fa1759..df265ab8 100644 --- a/tests/Compiler/DirectiveCompilerTest.php +++ b/tests/Compiler/DirectiveCompilerTest.php @@ -31,4 +31,4 @@ ->compile($input); expect($result)->toBe($input); -}); +}); \ No newline at end of file diff --git a/tests/Compiler/UseExtractorTest.php b/tests/Compiler/UseExtractorTest.php index fbbbea3c..dd4d95e5 100644 --- a/tests/Compiler/UseExtractorTest.php +++ b/tests/Compiler/UseExtractorTest.php @@ -86,4 +86,4 @@ $result = (new UseExtractor)->extract($input, function () {}); expect($result)->toBe($input); -}); +}); \ No newline at end of file diff --git a/tests/FluxProTest.php b/tests/FluxProTest.php index 24c16661..79118ee9 100644 --- a/tests/FluxProTest.php +++ b/tests/FluxProTest.php @@ -129,4 +129,4 @@ Web development BLADE -)); +)); \ No newline at end of file diff --git a/tests/FluxTest.php b/tests/FluxTest.php index 03e9a32f..b6dcaf98 100644 --- a/tests/FluxTest.php +++ b/tests/FluxTest.php @@ -75,4 +75,4 @@ test('select with bound attribute with ampersand', fn () => compare(<<<'BLADE' BLADE -)); +)); \ No newline at end of file diff --git a/tests/Folder/DirectiveStackTest.php b/tests/Folder/DirectiveStackTest.php deleted file mode 100644 index 3b410305..00000000 --- a/tests/Folder/DirectiveStackTest.php +++ /dev/null @@ -1,95 +0,0 @@ -open())->toBeFalse(); - - $stack->add('if'); - $stack->add('foreach'); - - expect($stack->open())->toBeTrue(); - - $stack->add('endforeach'); - - expect($stack->open())->toBeTrue(); - - $stack->add('endif'); - - expect($stack->open())->toBeFalse(); -}); - -test('supports alternative closing directives', function (string $closer) { - $stack = DirectiveStack::make(); - - $stack->add('section'); - $stack->add($closer); - - expect($stack->open())->toBeFalse(); -})->with(['show', 'append', 'overwrite', 'stop', 'endsection']); - -test('supports endif for condition-like directives', function (string $opener) { - $stack = DirectiveStack::make(); - - $stack->add($opener); - $stack->add('endif'); - - expect($stack->open())->toBeFalse(); -})->with(['unless', 'isset', 'empty', 'auth', 'guest', 'env', 'production', 'once', 'can', 'cannot', 'canany', 'hassection', 'hasstack', 'sectionmissing']); - -test('only closes compatible structures', function () { - $stack = DirectiveStack::make(); - - $stack->add('if'); - $stack->add('endforeach'); - - expect($stack->open())->toBeTrue(); -}); - -test('closes intervening unmatched structures with a matching outer structure', function () { - $stack = DirectiveStack::make(); - - $stack->add('if'); - $stack->add('foreach'); - $stack->add('endif'); - - expect($stack->open())->toBeFalse(); -}); - -test('tracks livewire directive structures', function (string $opener, string $closer) { - $stack = DirectiveStack::make(); - - $stack->add($opener); - - expect($stack->open())->toBeTrue(); - - $stack->add($closer); - - expect($stack->open())->toBeFalse(); -})->with([ - ['script', 'endscript'], - ['assets', 'endassets'], - ['island', 'endisland'], - ['teleport', 'endteleport'], - ['persist', 'endpersist'], - ['placeholder', 'endplaceholder'], -]); - -test('tracks custom Blade conditions', function (string $opener, string $closer) { - $stack = DirectiveStack::make(['custom']); - - $stack->add($opener); - - expect($stack->open())->toBeTrue(); - - $stack->add($closer); - - expect($stack->open())->toBeFalse(); -})->with([ - ['custom', 'endcustom'], - ['custom', 'endif'], - ['unlesscustom', 'endcustom'], - ['unlesscustom', 'endif'], -]); diff --git a/tests/Folder/FolderTest.php b/tests/Folder/FolderTest.php index 45f70d24..d2f7079f 100644 --- a/tests/Folder/FolderTest.php +++ b/tests/Folder/FolderTest.php @@ -258,6 +258,7 @@ expect($compiled)->toBeInstanceOf(ComponentNode::class); }); + test('folds components with no blaze directive if enabled in config', function () { $input = ''; @@ -288,39 +289,3 @@ expect(fn () => app(Folder::class)->fold($node)) ->toThrow(InvalidBlazeFoldUsageException::class); })->with(['errors', 'session', 'error', 'csrf', 'auth', 'request', 'old', 'once']); - -test('does not fold components with slots wrapped in directives', function () { - $input = '@if(false)Header@endif'; - - $node = app(Parser::class)->parse($input)[0]; - $result = app(Folder::class)->fold($node); - - expect($result)->toBeInstanceOf(ComponentNode::class); -}); - -test('folds components with nonclosing directives', function () { - $input = '@csrfHeader'; - - $node = app(Parser::class)->parse($input)[0]; - $result = app(Folder::class)->fold($node); - - expect($result)->toBeInstanceOf(TextNode::class); -}); - -test('folds components with closing directives outside slot', function () { - $input = ' @if(false) before @endif Header @if(false) after @endif '; - - $node = app(Parser::class)->parse($input)[0]; - $result = app(Folder::class)->fold($node); - - expect($result)->toBeInstanceOf(TextNode::class); -}); - -test('folds components with non-closing directive before slot followed by closing directive', function () { - $input = '@csrfHeader@if(false)after@endif'; - - $node = app(Parser::class)->parse($input)[0]; - $result = app(Folder::class)->fold($node); - - expect($result)->toBeInstanceOf(TextNode::class); -}); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 25c902a5..c63a08c5 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -92,4 +92,4 @@ public function render() BLADE, ['required' => true] ); -})->throwsNoExceptions(); +})->throwsNoExceptions(); \ No newline at end of file diff --git a/tests/Memoizer/MemoTest.php b/tests/Memoizer/MemoTest.php index 5e6bf128..be22e5aa 100644 --- a/tests/Memoizer/MemoTest.php +++ b/tests/Memoizer/MemoTest.php @@ -11,4 +11,4 @@ test('key returns null for non-serializable params', function () { expect(Memo::key('avatar', ['callback' => fn () => null]))->toBeNull(); -}); +}); \ No newline at end of file diff --git a/tests/Memoizer/MemoizerTest.php b/tests/Memoizer/MemoizerTest.php index c4e50db5..67a13964 100644 --- a/tests/Memoizer/MemoizerTest.php +++ b/tests/Memoizer/MemoizerTest.php @@ -75,4 +75,4 @@ $compiled = app(Memoizer::class)->memoize($node); expect($compiled)->toBeInstanceOf(ComponentNode::class); -}); +}); \ No newline at end of file diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index 4c214ee3..58638b1a 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -2,7 +2,6 @@ use Livewire\Blaze\Parser\Parser; use Livewire\Blaze\Parser\Nodes\ComponentNode; -use Livewire\Blaze\Parser\Nodes\DirectiveNode; use Livewire\Blaze\Parser\Nodes\SlotNode; use Livewire\Blaze\Parser\Nodes\TextNode; use Livewire\Blaze\Support\AttributeParser; @@ -141,12 +140,4 @@ ' "red"]) />', '', ], -]); - -test('parses directives', function () { - $input = '@csrf'; - - expect(app(Parser::class)->parse($input))->toEqual([ - new DirectiveNode('csrf', $input), - ]); -}); +]); \ No newline at end of file diff --git a/tests/Parser/TokenizerTest.php b/tests/Parser/TokenizerTest.php index b824c184..89d2c347 100644 --- a/tests/Parser/TokenizerTest.php +++ b/tests/Parser/TokenizerTest.php @@ -7,7 +7,6 @@ use Livewire\Blaze\Parser\Tokens\TagOpenToken; use Livewire\Blaze\Parser\Tokens\TagSelfCloseToken; use Livewire\Blaze\Parser\Tokens\TextToken; -use Livewire\Blaze\Parser\Tokens\DirectiveToken; test('tokenizes tags', function () { $input = ''; @@ -63,26 +62,6 @@ ]); }); -test('tokenizes directives without parameters', function () { - $input = '@csrf'; - - $result = app(Tokenizer::class)->tokenize($input); - - expect($result)->toEqual([ - new DirectiveToken(name: 'csrf', original: $input) - ]); -}); - -test('tokenizes directives with parameters', function () { - $input = '@dd($foo)'; - - $result = app(Tokenizer::class)->tokenize($input); - - expect($result)->toEqual([ - new DirectiveToken(name: 'dd', original: $input, arguments: '$foo') - ]); -}); - test('handles whitespace in tags', function () { $input = '< x-button >'; // This is valid Blade syntax... @@ -163,134 +142,4 @@ expect($result)->toEqual([ new TextToken(content: '>'), ]); -}); - -test('handles escaped directives', function () { - $input = '@@csrf'; - - $result = app(Tokenizer::class)->tokenize($input); - - expect($result)->toEqual([ - new TextToken(content: '@@csrf') - ]); -}); - -test('does not tokenize directives preceded by a word character', function () { - $input = 'foo@if($bar)'; - - $result = app(Tokenizer::class)->tokenize($input); - - expect($result)->toEqual([ - new TextToken(content: $input), - ]); -}); - -test('does not skip invalid directive prefixes', function () { - $input = '@- @if($foo)'; - - $result = app(Tokenizer::class)->tokenize($input); - - expect($result)->toEqual([ - new TextToken(content: '@- '), - new DirectiveToken(name: 'if', original: '@if($foo)', arguments: '$foo'), - ]); -}); - -test('handles directives with whitespace', function () { - $input = '@if ($foo)'; - - $result = app(Tokenizer::class)->tokenize($input); - - expect($result)->toEqual([ - new DirectiveToken(name: 'if', original: $input, arguments: '$foo') - ]); -}); - -test('handles namespaced directives', function () { - $input = '@Foo::bar($foo)'; - - $result = app(Tokenizer::class)->tokenize($input); - - expect($result)->toEqual([ - new DirectiveToken(name: 'Foo::bar', original: $input, arguments: '$foo') - ]); -}); - -test('handles directives with nested parentheses', function () { - $input = "@include('foo', ['((a)' => '((a)'])"; - - $result = app(Tokenizer::class)->tokenize($input); - - expect($result)->toEqual([ - new DirectiveToken(name: 'include', original: $input, arguments: "'foo', ['((a)' => '((a)']"), - ]); -}); - -test('handles unclosed directives', function () { - $input = "@include('foo'"; - - $result = app(Tokenizer::class)->tokenize($input); - - expect($result)->toEqual([ - new DirectiveToken(name: 'include', original: '@include'), - new TextToken("('foo'") - ]); -}); - -test('preserves directives whose parentheses cannot be repaired', function () { - $input = '@if(foo(bar) trailing text'; - - $result = app(Tokenizer::class)->tokenize($input); - - expect($result)->toEqual([ - new TextToken(content: $input), - ]); -}); - -test('handles Laravel directive parenthesis cases', function (string $input, array $expected) { - $result = app(Tokenizer::class)->tokenize($input); - - expect($result)->toEqual([ - new DirectiveToken(name: $expected[0], original: $input, arguments: $expected[1] ?? null) - ]); -})->with([ - 'nested function calls' => [ - '@if (name(foo(bar)))', - ['if', 'name(foo(bar))'], - ], - 'closing parentheses in an each argument' => [ - "@each('foo', '(bar))')", - ['each', "'foo', '(bar))'"], - ], - 'opening parentheses in include data' => [ - "@include('foo', ['(('])", - ['include', "'foo', ['((']"], - ], - 'mixed parentheses in include data' => [ - "@include('foo', ['((a)' => '((a)'])", - ['include', "'foo', ['((a)' => '((a)']"], - ], - 'multiple closing parentheses in include data' => [ - '@includeUnless(true, \'foo\', ["foo" => "bar_))-))>"])', - ['includeUnless', 'true, \'foo\', ["foo" => "bar_))-))>"]'], - ], - 'mixed parentheses and a cast' => [ - '@includeFirst(["issue", "#45424)"], [(string) "foo()" => "bar(-(("])', - ['includeFirst', '["issue", "#45424)"], [(string) "foo()" => "bar(-(("]'], - ], - 'parentheses in a section name' => [ - "@section('issue#18317 :))')", - ['section', "'issue#18317 :))'"], - ], -]); - -test('handles parantheses after a directive', function () { - $input = '@unset ($unset)))'; - - $result = app(Tokenizer::class)->tokenize($input); - - expect($result)->toEqual([ - new DirectiveToken(name: 'unset', original: '@unset ($unset)', arguments: '$unset'), - new TextToken(content: '))'), - ]); -}); +}); \ No newline at end of file diff --git a/tests/Pest.php b/tests/Pest.php index f0d14132..595de675 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -35,4 +35,4 @@ function compare(string $input, array $data = []): void $blade = Blade::render($input, $data); expect($blade)->toBe($blaze); -} +} \ No newline at end of file diff --git a/tests/Runtime/BlazeRuntimeTest.php b/tests/Runtime/BlazeRuntimeTest.php index 0fb56efd..0cb45ab4 100644 --- a/tests/Runtime/BlazeRuntimeTest.php +++ b/tests/Runtime/BlazeRuntimeTest.php @@ -20,4 +20,4 @@ ['[UNBLAZE]', ['ltrim', null, 'ltrim']], ['
[UNBLAZE]', [null, 'rtrim', 'rtrim']], ['
[UNBLAZE]
', [null, null, null]], -]); +]); \ No newline at end of file diff --git a/tests/Support/AttributeParserTest.php b/tests/Support/AttributeParserTest.php index 7409d0b1..a2fdcebd 100644 --- a/tests/Support/AttributeParserTest.php +++ b/tests/Support/AttributeParserTest.php @@ -77,4 +77,4 @@ expect($attrs)->toHaveCount(1)->toHaveKey('fooBar'); expect($attrs['fooBar']->value)->toBe('first'); -}); +}); \ No newline at end of file diff --git a/tests/Support/DirectivesTest.php b/tests/Support/DirectivesTest.php index 0ab83e3a..dc216233 100644 --- a/tests/Support/DirectivesTest.php +++ b/tests/Support/DirectivesTest.php @@ -36,4 +36,4 @@ ['@php // @aware @endphp'], [''], ['{{-- @aware --}}'], -]); +]); \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index f6496fba..ce7a360a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -40,4 +40,4 @@ protected function getEnvironmentSetUp($app) $app['config']->set('view.compiled', $basePath . '/test_' . $token); } } -} +} \ No newline at end of file diff --git a/tests/WhitespaceTest.php b/tests/WhitespaceTest.php index 7df1297f..2e9495fa 100644 --- a/tests/WhitespaceTest.php +++ b/tests/WhitespaceTest.php @@ -61,4 +61,4 @@ test('nested foldable components with unblaze only', fn () => compare(<<<'BLADE' BLADE, -)); +)); \ No newline at end of file diff --git a/tests/fixtures/views/blaze.blade.php b/tests/fixtures/views/blaze.blade.php index da71035b..e1c3b946 100644 --- a/tests/fixtures/views/blaze.blade.php +++ b/tests/fixtures/views/blaze.blade.php @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/tests/fixtures/views/components/compile-false.blade.php b/tests/fixtures/views/components/compile-false.blade.php index 18320ff4..7cded334 100644 --- a/tests/fixtures/views/components/compile-false.blade.php +++ b/tests/fixtures/views/components/compile-false.blade.php @@ -1,3 +1,3 @@ @blaze(compile: false) -
+
\ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/card-unsafe-slot.blade.php b/tests/fixtures/views/components/foldable/card-unsafe-slot.blade.php index 19e138f9..0969ed81 100644 --- a/tests/fixtures/views/components/foldable/card-unsafe-slot.blade.php +++ b/tests/fixtures/views/components/foldable/card-unsafe-slot.blade.php @@ -1,3 +1,3 @@ @blaze(fold: true, unsafe: ['slot']) -
{{ $header ?? 'Default' }} | {{ $slot ?? 'Default' }} | {{ $footer ?? 'Default' }}
+
{{ $header ?? 'Default' }} | {{ $slot ?? 'Default' }} | {{ $footer ?? 'Default' }}
\ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/card-unsafe.blade.php b/tests/fixtures/views/components/foldable/card-unsafe.blade.php index 71282248..53c58b26 100644 --- a/tests/fixtures/views/components/foldable/card-unsafe.blade.php +++ b/tests/fixtures/views/components/foldable/card-unsafe.blade.php @@ -1,3 +1,3 @@ @blaze(fold: true, unsafe: ['*']) -
{{ $header ?? 'Default' }} | {{ $slot ?? 'Default' }} | {{ $footer ?? 'Default' }}
+
{{ $header ?? 'Default' }} | {{ $slot ?? 'Default' }} | {{ $footer ?? 'Default' }}
\ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/card.blade.php b/tests/fixtures/views/components/foldable/card.blade.php index cb074881..e6b0833b 100644 --- a/tests/fixtures/views/components/foldable/card.blade.php +++ b/tests/fixtures/views/components/foldable/card.blade.php @@ -6,4 +6,4 @@ {{ $slot ?? 'Default' }}
{{ $footer ?? 'Default' }} -
+ \ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/fold-false.blade.php b/tests/fixtures/views/components/foldable/fold-false.blade.php index 6bc353eb..dcd2fbbb 100644 --- a/tests/fixtures/views/components/foldable/fold-false.blade.php +++ b/tests/fixtures/views/components/foldable/fold-false.blade.php @@ -1,3 +1,3 @@ @blaze(fold: false) -
+
\ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/input-aware-unsafe.blade.php b/tests/fixtures/views/components/foldable/input-aware-unsafe.blade.php index 1a5bd3ad..457ddb95 100644 --- a/tests/fixtures/views/components/foldable/input-aware-unsafe.blade.php +++ b/tests/fixtures/views/components/foldable/input-aware-unsafe.blade.php @@ -6,4 +6,4 @@ @else -@endif +@endif \ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/input-aware.blade.php b/tests/fixtures/views/components/foldable/input-aware.blade.php index ddc6e6c0..6048afbf 100644 --- a/tests/fixtures/views/components/foldable/input-aware.blade.php +++ b/tests/fixtures/views/components/foldable/input-aware.blade.php @@ -2,4 +2,4 @@ @aware(['type' => 'text']) - + \ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/input-no-blaze.blade.php b/tests/fixtures/views/components/foldable/input-no-blaze.blade.php index 435a4bb4..50eefea1 100644 --- a/tests/fixtures/views/components/foldable/input-no-blaze.blade.php +++ b/tests/fixtures/views/components/foldable/input-no-blaze.blade.php @@ -4,4 +4,4 @@ type="{{ $type }}" @if ($disabled) disabled @endif @if ($attributes->has('required')) required @endif -> +> \ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/input-safe.blade.php b/tests/fixtures/views/components/foldable/input-safe.blade.php index 00bc27f0..256ca673 100644 --- a/tests/fixtures/views/components/foldable/input-safe.blade.php +++ b/tests/fixtures/views/components/foldable/input-safe.blade.php @@ -6,4 +6,4 @@ type="{{ $type }}" @if ($disabled) disabled @endif @if ($attributes->has('required')) required @endif -> +> \ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/input-unsafe-attributes.blade.php b/tests/fixtures/views/components/foldable/input-unsafe-attributes.blade.php index c2846866..3bbebef5 100644 --- a/tests/fixtures/views/components/foldable/input-unsafe-attributes.blade.php +++ b/tests/fixtures/views/components/foldable/input-unsafe-attributes.blade.php @@ -6,4 +6,4 @@ type="{{ $type }}" @if ($disabled) disabled @endif @if ($attributes->has('required')) required @endif -> +> \ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/input-unsafe.blade.php b/tests/fixtures/views/components/foldable/input-unsafe.blade.php index c09161d7..c11e3617 100644 --- a/tests/fixtures/views/components/foldable/input-unsafe.blade.php +++ b/tests/fixtures/views/components/foldable/input-unsafe.blade.php @@ -6,4 +6,4 @@ type="{{ $type }}" @if ($disabled) disabled @endif @if ($attributes->has('required')) required @endif -> +> \ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/input.blade.php b/tests/fixtures/views/components/foldable/input.blade.php index 40e6496d..a2b3f2fd 100644 --- a/tests/fixtures/views/components/foldable/input.blade.php +++ b/tests/fixtures/views/components/foldable/input.blade.php @@ -7,4 +7,4 @@ type="{{ $type }}" @if ($disabled) aria-disabled="true" @endif @if ($attributes->has('required')) aria-required="true" @endif -> +> \ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/nested-input-unblaze.blade.php b/tests/fixtures/views/components/foldable/nested-input-unblaze.blade.php index 0fbe3abd..78a62404 100644 --- a/tests/fixtures/views/components/foldable/nested-input-unblaze.blade.php +++ b/tests/fixtures/views/components/foldable/nested-input-unblaze.blade.php @@ -2,4 +2,4 @@
-
+ \ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/unblaze-only-wrapper.blade.php b/tests/fixtures/views/components/foldable/unblaze-only-wrapper.blade.php index f1a337ae..b12805c0 100644 --- a/tests/fixtures/views/components/foldable/unblaze-only-wrapper.blade.php +++ b/tests/fixtures/views/components/foldable/unblaze-only-wrapper.blade.php @@ -2,4 +2,4 @@
-
+ \ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/unblaze-only.blade.php b/tests/fixtures/views/components/foldable/unblaze-only.blade.php index 03fd212f..d7f5646a 100644 --- a/tests/fixtures/views/components/foldable/unblaze-only.blade.php +++ b/tests/fixtures/views/components/foldable/unblaze-only.blade.php @@ -2,4 +2,4 @@ @unblaze
-@endunblaze +@endunblaze \ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/unblaze-slot-wrapper.blade.php b/tests/fixtures/views/components/foldable/unblaze-slot-wrapper.blade.php index 78d6f69d..95d60b4a 100644 --- a/tests/fixtures/views/components/foldable/unblaze-slot-wrapper.blade.php +++ b/tests/fixtures/views/components/foldable/unblaze-slot-wrapper.blade.php @@ -4,4 +4,4 @@ @unblaze
@endunblaze - + \ No newline at end of file diff --git a/tests/fixtures/views/components/foldable/wrapper-nl.blade.php b/tests/fixtures/views/components/foldable/wrapper-nl.blade.php index e87f9eb8..556019ec 100644 --- a/tests/fixtures/views/components/foldable/wrapper-nl.blade.php +++ b/tests/fixtures/views/components/foldable/wrapper-nl.blade.php @@ -3,4 +3,4 @@
{{ $slot }}
-{{-- This file must end with new line --}} +{{-- This file must end with new line --}} \ No newline at end of file diff --git a/tests/fixtures/views/components/input-aware-no-blaze.blade.php b/tests/fixtures/views/components/input-aware-no-blaze.blade.php index e607e32d..72498e1b 100644 --- a/tests/fixtures/views/components/input-aware-no-blaze.blade.php +++ b/tests/fixtures/views/components/input-aware-no-blaze.blade.php @@ -6,4 +6,4 @@ {{ $attributes }} type="{{ $type }}" @if ($disabled) disabled @endif -> +> \ No newline at end of file diff --git a/tests/fixtures/views/components/input-no-blaze.blade.php b/tests/fixtures/views/components/input-no-blaze.blade.php index 2cf0fa3d..7e0d2c5b 100644 --- a/tests/fixtures/views/components/input-no-blaze.blade.php +++ b/tests/fixtures/views/components/input-no-blaze.blade.php @@ -6,4 +6,4 @@ {{ $attributes }} type="{{ $type }}" @if ($disabled) disabled @endif -> +> \ No newline at end of file diff --git a/tests/fixtures/views/components/memoizable/avatar-no-blaze.blade.php b/tests/fixtures/views/components/memoizable/avatar-no-blaze.blade.php index 3e497270..79479468 100644 --- a/tests/fixtures/views/components/memoizable/avatar-no-blaze.blade.php +++ b/tests/fixtures/views/components/memoizable/avatar-no-blaze.blade.php @@ -1,3 +1,3 @@ @props(['src']) -Avatar +Avatar \ No newline at end of file diff --git a/tests/fixtures/views/components/memoizable/avatar.blade.php b/tests/fixtures/views/components/memoizable/avatar.blade.php index 07425b0c..3ee3d6b2 100644 --- a/tests/fixtures/views/components/memoizable/avatar.blade.php +++ b/tests/fixtures/views/components/memoizable/avatar.blade.php @@ -2,4 +2,4 @@ @props(['src']) -Avatar +Avatar \ No newline at end of file diff --git a/tests/fixtures/views/components/memoizable/memo-false.blade.php b/tests/fixtures/views/components/memoizable/memo-false.blade.php index a3b12130..2660be18 100644 --- a/tests/fixtures/views/components/memoizable/memo-false.blade.php +++ b/tests/fixtures/views/components/memoizable/memo-false.blade.php @@ -1,3 +1,3 @@ @blaze(memo: false) -
+
\ No newline at end of file diff --git a/tests/fixtures/views/mix.blade.php b/tests/fixtures/views/mix.blade.php index 1146c816..35084dae 100644 --- a/tests/fixtures/views/mix.blade.php +++ b/tests/fixtures/views/mix.blade.php @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/tests/fixtures/views/php-view.php b/tests/fixtures/views/php-view.php index f6bde4d3..c63d161f 100644 --- a/tests/fixtures/views/php-view.php +++ b/tests/fixtures/views/php-view.php @@ -1 +1 @@ - + \ No newline at end of file From 41c1b47ccea5ad3f95c30d62706ca9279351a202 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 2 Aug 2026 00:44:29 -0400 Subject: [PATCH 2/2] Add compilation benchmark (#195) * Add compilation benchmark * Update ci.yml * Update ci.yml * Update benchmark * Update ci.yml * Update benchmark-comment.yml * Refactor * Update ci.yml --- .github/workflows/benchmark-comment.yml | 2 +- .github/workflows/benchmark-on-demand.yml | 2 +- .../app/Console/Commands/BenchmarkCommand.php | 91 +++++++++++++++---- .../Commands/BenchmarkVarianceCommand.php | 34 +++---- 4 files changed, 94 insertions(+), 35 deletions(-) diff --git a/.github/workflows/benchmark-comment.yml b/.github/workflows/benchmark-comment.yml index 02766190..9fde47f9 100644 --- a/.github/workflows/benchmark-comment.yml +++ b/.github/workflows/benchmark-comment.yml @@ -30,7 +30,7 @@ jobs: echo "heading=$(sed -n '2p' benchmark-result.md)" >> "$GITHUB_OUTPUT" tail -n +2 benchmark-result.md > benchmark-comment.md echo "" >> benchmark-comment.md - echo "To run a specific benchmark, comment /benchmark <name> where name is one of: attributes, aware, class, default, forwarding, merge, named-slots, no-attributes, slot" >> benchmark-comment.md + echo "To run a specific benchmark, comment /benchmark <name>
attributes, aware, class, default, forwarding, merge, named-slots, no-attributes, slot, compilation
" >> benchmark-comment.md - name: Find existing comment uses: peter-evans/find-comment@v3 diff --git a/.github/workflows/benchmark-on-demand.yml b/.github/workflows/benchmark-on-demand.yml index 83b62b59..fa776c79 100644 --- a/.github/workflows/benchmark-on-demand.yml +++ b/.github/workflows/benchmark-on-demand.yml @@ -18,7 +18,7 @@ jobs: COMMENT: ${{ github.event.comment.body }} run: | BENCHMARK=$(echo "$COMMENT" | awk '{print $2}') - VALID="attributes aware class default forwarding merge named-slots no-attributes slot" + VALID="attributes aware class compilation default forwarding merge named-slots no-attributes slot" if ! echo "$VALID" | grep -qw "$BENCHMARK"; then echo "::error::Unknown benchmark '$BENCHMARK'. Valid options: $VALID" exit 1 diff --git a/workbench/app/Console/Commands/BenchmarkCommand.php b/workbench/app/Console/Commands/BenchmarkCommand.php index 930637a3..74d51ace 100644 --- a/workbench/app/Console/Commands/BenchmarkCommand.php +++ b/workbench/app/Console/Commands/BenchmarkCommand.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\View; use Illuminate\Support\Str; +use Livewire\Blaze\Blaze; class BenchmarkCommand extends Command { @@ -77,6 +78,11 @@ public function handle(): int protected function runBenchmark(): array { $benchmarkName = $this->argument('benchmark'); + + if ($benchmarkName === 'compilation') { + return $this->runCompilationBenchmark(); + } + $bladeView = "bench.blade.{$benchmarkName}"; $blazeView = "bench.blaze.{$benchmarkName}"; $showProgress = ! $this->option('ci') && ! $this->option('json'); @@ -144,6 +150,37 @@ protected function runBenchmark(): array ]; } + protected function runCompilationBenchmark(): array + { + for ($w = 0; $w < $this->warmupRounds; $w++) { + $this->measureCompilation(false); + $this->measureCompilation(true); + } + + $bladeTimes = []; + $blazeTimes = []; + + for ($r = 0; $r < $this->rounds; $r++) { + if ($r % 2 === 0) { + $bladeTimes[] = $this->measureCompilation(false); + $blazeTimes[] = $this->measureCompilation(true); + } else { + $blazeTimes[] = $this->measureCompilation(true); + $bladeTimes[] = $this->measureCompilation(false); + } + } + + $keptRounds = $this->nonOutlierIndices(collect($bladeTimes)) + ->intersect($this->nonOutlierIndices(collect($blazeTimes))) + ->values(); + $this->filteredRounds = $this->rounds - $keptRounds->count(); + + return [ + 'blade_ms' => round($keptRounds->map(fn ($r) => $bladeTimes[$r])->median(), 2), + 'blaze_ms' => round($keptRounds->map(fn ($r) => $blazeTimes[$r])->median(), 2), + ]; + } + protected function runMultipleAttempts(int $attempts): int { $benchmarkName = $this->argument('benchmark'); @@ -246,20 +283,20 @@ protected function buildTable(array $results): array { $snapshot = $this->option('snapshot') ? null : $this->loadSnapshot(); - $headers = ['Blade', 'Blaze', 'Improvement']; + $headers = ['Blade', 'Blaze', 'Change']; $rows = collect($results)->map(function ($result, $name) use ($snapshot) { $blade = $this->formatTime($result['blade_ms']); $blaze = $this->formatTime($result['blaze_ms']); - $improvement = $this->improvement($result).'%'; + $change = $this->change($result).'%'; if ($prev = $snapshot['benchmarks'][$name] ?? null) { $blade .= ' '.$this->formatChange($prev['blade_ms'], $result['blade_ms']); $blaze .= ' '.$this->formatChange($prev['blaze_ms'], $result['blaze_ms']); - $improvement .= ' '.$this->formatImprovementChange($prev['improvement'], $this->improvement($result)); + $change .= ' '.$this->formatChangeDelta($prev['change'], $this->change($result)); } - return [$blade, $blaze, $improvement]; + return [$blade, $blaze, $change]; })->values()->all(); return [$headers, $rows, $snapshot]; @@ -333,13 +370,13 @@ protected function displayAttemptsResults(array $allAttempts, array $results, Co foreach ($allAttempts as $i => $attempt) { $isOutlier = ! $keptIndices->contains($i); - $improvement = $this->improvement($attempt); + $change = $this->change($attempt); $line = sprintf( ' Attempt %d: Blade %s Blaze %s (%s%%)', $i + 1, $this->formatTime($attempt['blade_ms']), $this->formatTime($attempt['blaze_ms']), - $improvement + $change ); $isOutlier @@ -374,7 +411,7 @@ protected function outputMarkdownAttempts(array $allAttempts, array $results, Co $snapshot = $this->option('snapshot') ? null : $this->loadSnapshot(); $snapshotData = $snapshot['benchmarks'][$benchmarkName] ?? null; - $headers = ['Attempt', 'Blade', 'Blaze', 'Improvement']; + $headers = ['Attempt', 'Blade', 'Blaze', 'Change']; $rows = []; @@ -386,7 +423,7 @@ protected function outputMarkdownAttempts(array $allAttempts, array $results, Co '`#'.($i + 1).'`'.($isOutlier ? ' \*' : ''), $this->formatTime($attempt['blade_ms']), $this->formatTime($attempt['blaze_ms']), - $this->improvement($attempt).'%', + $this->change($attempt).'%', ]; } @@ -396,22 +433,22 @@ protected function outputMarkdownAttempts(array $allAttempts, array $results, Co 'Snapshot', $this->formatTime($snapshotData['blade_ms']), $this->formatTime($snapshotData['blaze_ms']), - $snapshotData['improvement'].'%', + $snapshotData['change'].'%', ]; } // Result row (median with comparison deltas when snapshot exists). $blade = $this->formatTime($medianResult['blade_ms']); $blaze = $this->formatTime($medianResult['blaze_ms']); - $improvement = $this->improvement($medianResult).'%'; + $change = $this->change($medianResult).'%'; if ($snapshotData) { $blade .= ' '.$this->formatChange($snapshotData['blade_ms'], $medianResult['blade_ms']); $blaze .= ' '.$this->formatChange($snapshotData['blaze_ms'], $medianResult['blaze_ms']); - $improvement .= ' '.$this->formatImprovementChange($snapshotData['improvement'], $this->improvement($medianResult)); + $change .= ' '.$this->formatChangeDelta($snapshotData['change'], $this->change($medianResult)); } - $rows[] = ['**Result**', "**{$blade}**", "**{$blaze}**", "**{$improvement}**"]; + $rows[] = ['**Result**', "**{$blade}**", "**{$blaze}**", "**{$change}**"]; $allRows = collect([$headers, ...$rows]); $widths = collect($headers)->keys()->map( @@ -456,7 +493,7 @@ protected function outputJsonAttemptsResults(string $benchmarkName, array $allAt 'attempts_detail' => collect($allAttempts)->map(fn ($attempt, $i) => [ 'blade_ms' => $attempt['blade_ms'], 'blaze_ms' => $attempt['blaze_ms'], - 'improvement' => $this->improvement($attempt), + 'change' => $this->change($attempt), 'outlier' => ! $keptIndices->contains($i), ])->values()->all(), 'benchmarks' => $results, @@ -471,7 +508,7 @@ protected function saveSnapshot(array $results): void 'benchmarks' => collect($results)->map(fn ($result) => [ 'blade_ms' => $result['blade_ms'], 'blaze_ms' => $result['blaze_ms'], - 'improvement' => $this->improvement($result), + 'change' => $this->change($result), ])->all(), ]; @@ -501,7 +538,7 @@ protected function snapshotPath(): string return dirname(__DIR__, 4).'/benchmark-snapshot.json'; } - protected function improvement(array $result): float + protected function change(array $result): float { return $result['blade_ms'] > 0 ? round((1 - $result['blaze_ms'] / $result['blade_ms']) * 100, 1) @@ -525,7 +562,7 @@ protected function formatChange(float $old, float $new, float $threshold = 2): s return "({$sign}".round($change, 1).'%)'; } - protected function formatImprovementChange(float $old, float $new, float $threshold = 0.2): string + protected function formatChangeDelta(float $old, float $new, float $threshold = 0.2): string { $delta = round($new - $old, 1); @@ -577,6 +614,28 @@ protected function measureView(string $view): float return (hrtime(true) - $start) / 1_000_000; } + protected function measureCompilation(bool $blaze): float + { + $blaze ? Blaze::enable() : Blaze::disable(); + + Artisan::call('view:clear'); + + $compiler = app('blade.compiler'); + $views = File::allFiles(resource_path('views')); + + gc_collect_cycles(); + + $start = hrtime(true); + + foreach ($views as $view) { + if ($view->getExtension() === 'php' && str_ends_with($view->getFilename(), '.blade.php')) { + $compiler->compile($view->getPathname()); + } + } + + return (hrtime(true) - $start) / 1_000_000; + } + protected function formatTime(float $ms): string { return number_format($ms, 2).'ms'; diff --git a/workbench/app/Console/Commands/BenchmarkVarianceCommand.php b/workbench/app/Console/Commands/BenchmarkVarianceCommand.php index 9bbcb458..b59aa5e0 100644 --- a/workbench/app/Console/Commands/BenchmarkVarianceCommand.php +++ b/workbench/app/Console/Commands/BenchmarkVarianceCommand.php @@ -128,31 +128,31 @@ protected function runBenchmarkInProcess(string $name): array protected function displayVarianceResults(array $snapshot, array $allRuns, float $avgRunDuration, float $totalDuration): void { - $snapshotImprovement = $this->improvement($snapshot); + $snapshotChange = $this->change($snapshot); $bladeChanges = collect($allRuns)->map(fn ($run) => $this->percentChange($snapshot['blade_ms'], $run['blade_ms'])); $blazeChanges = collect($allRuns)->map(fn ($run) => $this->percentChange($snapshot['blaze_ms'], $run['blaze_ms'])); - $improvementChanges = collect($allRuns)->map(fn ($run) => round($this->improvement($run) - $snapshotImprovement, 1)); + $changes = collect($allRuns)->map(fn ($run) => round($this->change($run) - $snapshotChange, 1)); - $headers = ['', 'Blade', 'Blaze', 'Improvement']; + $headers = ['', 'Blade', 'Blaze', 'Change']; $rows = [ [ 'Snapshot', $this->formatTime($snapshot['blade_ms']), $this->formatTime($snapshot['blaze_ms']), - $snapshotImprovement.'%', + $snapshotChange.'%', ], [ 'Variance', $this->formatVarianceRange($bladeChanges->min(), $bladeChanges->max()), $this->formatVarianceRange($blazeChanges->min(), $blazeChanges->max()), - $this->formatVarianceRange($improvementChanges->min(), $improvementChanges->max()), + $this->formatVarianceRange($changes->min(), $changes->max()), ], [ 'Std Dev', '±'.$this->stddev($bladeChanges).'%', '±'.$this->stddev($blazeChanges).'%', - '±'.$this->stddev($improvementChanges).'%', + '±'.$this->stddev($changes).'%', ], ]; @@ -168,31 +168,31 @@ protected function displayVarianceResults(array $snapshot, array $allRuns, float protected function outputVarianceMarkdown(array $snapshot, array $allRuns, float $avgRunDuration, float $totalDuration): void { - $snapshotImprovement = $this->improvement($snapshot); + $snapshotChange = $this->change($snapshot); $bladeChanges = collect($allRuns)->map(fn ($run) => $this->percentChange($snapshot['blade_ms'], $run['blade_ms'])); $blazeChanges = collect($allRuns)->map(fn ($run) => $this->percentChange($snapshot['blaze_ms'], $run['blaze_ms'])); - $improvementChanges = collect($allRuns)->map(fn ($run) => round($this->improvement($run) - $snapshotImprovement, 1)); + $changes = collect($allRuns)->map(fn ($run) => round($this->change($run) - $snapshotChange, 1)); - $headers = ['', 'Blade', 'Blaze', 'Improvement']; + $headers = ['', 'Blade', 'Blaze', 'Change']; $rows = [ [ 'Snapshot', $this->formatTime($snapshot['blade_ms']), $this->formatTime($snapshot['blaze_ms']), - $snapshotImprovement.'%', + $snapshotChange.'%', ], [ 'Variance', $this->formatVarianceRange($bladeChanges->min(), $bladeChanges->max()), $this->formatVarianceRange($blazeChanges->min(), $blazeChanges->max()), - $this->formatVarianceRange($improvementChanges->min(), $improvementChanges->max()), + $this->formatVarianceRange($changes->min(), $changes->max()), ], [ 'Std Dev', '±'.$this->stddev($bladeChanges).'%', '±'.$this->stddev($blazeChanges).'%', - '±'.$this->stddev($improvementChanges).'%', + '±'.$this->stddev($changes).'%', ], ]; @@ -230,7 +230,7 @@ protected function saveSnapshot(array $results): void 'benchmarks' => collect($results)->map(fn ($result) => [ 'blade_ms' => $result['blade_ms'], 'blaze_ms' => $result['blaze_ms'], - 'improvement' => $this->improvement($result), + 'change' => $this->change($result), ])->all(), ]; @@ -242,11 +242,11 @@ protected function saveSnapshot(array $results): void protected function outputJson(array $snapshot, array $allRuns, float $avgRunDuration, float $totalDuration): void { - $snapshotImprovement = $this->improvement($snapshot); + $snapshotChange = $this->change($snapshot); $bladeChanges = collect($allRuns)->map(fn ($run) => $this->percentChange($snapshot['blade_ms'], $run['blade_ms'])); $blazeChanges = collect($allRuns)->map(fn ($run) => $this->percentChange($snapshot['blaze_ms'], $run['blaze_ms'])); - $improvementChanges = collect($allRuns)->map(fn ($run) => round($this->improvement($run) - $snapshotImprovement, 1)); + $changes = collect($allRuns)->map(fn ($run) => round($this->change($run) - $snapshotChange, 1)); $this->output->writeln(json_encode([ 'iterations' => $this->iterations, @@ -258,12 +258,12 @@ protected function outputJson(array $snapshot, array $allRuns, float $avgRunDura 'snapshot' => [ 'blade_ms' => $snapshot['blade_ms'], 'blaze_ms' => $snapshot['blaze_ms'], - 'improvement' => $snapshotImprovement, + 'change' => $snapshotChange, ], 'variance' => [ 'blade' => ['min' => $bladeChanges->min(), 'max' => $bladeChanges->max(), 'stddev' => $this->stddev($bladeChanges)], 'blaze' => ['min' => $blazeChanges->min(), 'max' => $blazeChanges->max(), 'stddev' => $this->stddev($blazeChanges)], - 'improvement' => ['min' => $improvementChanges->min(), 'max' => $improvementChanges->max(), 'stddev' => $this->stddev($improvementChanges)], + 'change' => ['min' => $changes->min(), 'max' => $changes->max(), 'stddev' => $this->stddev($changes)], ], ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); }