From 86dbf69d05bba266cf49d8dc14d4a63a7c5a331e Mon Sep 17 00:00:00 2001 From: Josh Hanley Date: Mon, 20 Oct 2025 09:07:35 +1000 Subject: [PATCH 1/6] Remove stray newline characters --- src/Memoizer/Memoizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Memoizer/Memoizer.php b/src/Memoizer/Memoizer.php index c3529473..c45e9cf1 100644 --- a/src/Memoizer/Memoizer.php +++ b/src/Memoizer/Memoizer.php @@ -62,7 +62,7 @@ public function memoize(Node $node): Node $name = $node->name; $attributes = $node->getAttributesAsRuntimeArrayString(); - $output = '<' . '?php $blaze_memoized_key = \Livewire\Blaze\Memoizer\Memo::key("' . $name . '", ' . $attributes . '); ?>\n'; + $output = '<' . '?php $blaze_memoized_key = \Livewire\Blaze\Memoizer\Memo::key("' . $name . '", ' . $attributes . '); ?>'; $output .= '<' . '?php if (! \Livewire\Blaze\Memoizer\Memo::has($blaze_memoized_key)) : ?>'; $output .= '<' . '?php ob_start(); ?>'; $output .= $node->render(); From cb91c6a964ac62314cadb417fac8f9bd8e330a14 Mon Sep 17 00:00:00 2001 From: Josh Hanley Date: Mon, 20 Oct 2025 18:18:59 +1000 Subject: [PATCH 2/6] Improve attribute parser --- src/Support/AttributeParser.php | 128 ++++++++------------------------ tests/AttributeParserTest.php | 58 +++++++++++++++ tests/FluxTest.php | 12 +++ tests/FoldTest.php | 2 +- 4 files changed, 104 insertions(+), 96 deletions(-) diff --git a/src/Support/AttributeParser.php b/src/Support/AttributeParser.php index af172a66..3baa1a27 100644 --- a/src/Support/AttributeParser.php +++ b/src/Support/AttributeParser.php @@ -15,23 +15,14 @@ public function parseAndReplaceDynamics( return $attributesString; } - // :name="..." or :name=$var - $attributesString = preg_replace_callback('/(\s*):([a-zA-Z0-9_-]+)\s*=\s*("[^"]*"|\$[a-zA-Z0-9_]+)/', function ($matches) use (&$attributePlaceholders, &$attributeNameToPlaceholder) { + // :name="..." + $attributesString = preg_replace_callback('/(\s*):([a-zA-Z0-9_-]+)\s*=\s*"([^"]*)"/', function ($matches) use (&$attributePlaceholders, &$attributeNameToPlaceholder) { $whitespace = $matches[1]; $attributeName = $matches[2]; $attributeValue = $matches[3]; - // Strip double quotes from attribute value... - $attributeValue = trim($attributeValue, '"'); - $placeholder = 'ATTR_PLACEHOLDER_' . count($attributePlaceholders); - if (preg_match('/^"\$([a-zA-Z0-9_]+)"$/', $attributeValue, $m)) { - $attributePlaceholders[$placeholder] = '{{ $' . $m[1] . ' }}'; - } elseif ($attributeValue !== '' && $attributeValue[0] === '$') { - $attributePlaceholders[$placeholder] = '{{ ' . $attributeValue . ' }}'; - } else { - $attributePlaceholders[$placeholder] = $attributeValue; - } + $attributePlaceholders[$placeholder] = '{{ ' . $attributeValue . ' }}'; $attributeNameToPlaceholder[$attributeName] = $placeholder; return $whitespace . $attributeName . '="' . $placeholder . '"'; @@ -97,111 +88,58 @@ public function parseAndReplaceDynamics( public function parseAttributeStringToArray(string $attributesString): array { $attributes = []; - $processedPositions = []; - // Handle :name="..." or :name=$var syntax - preg_match_all('/(?:^|\s):([a-zA-Z0-9_:-]+)\s*=\s*("[^"]*"|\$[a-zA-Z0-9_]+)/', $attributesString, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); - foreach ($matches as $match) { - $start = $match[0][1]; - $end = $start + strlen($match[0][0]); - $processedPositions[] = [$start, $end]; - - $attributeName = str($match[1][0])->camel()->toString(); - $attributeValue = trim($match[2][0], '"'); - $original = trim($match[0][0]); + // Handle :name="..." syntax + preg_match_all('/(?:^|\s):([A-Za-z0-9_:-]+)\s*=\s*"([^"]*)"/', $attributesString, $matches, PREG_SET_ORDER); + foreach ($matches as $m) { + $attributeName = str($m[1])->camel()->toString(); + if (isset($attributes[$attributeName])) continue; $attributes[$attributeName] = [ 'isDynamic' => true, - 'value' => $attributeValue, - 'original' => $original, + 'value' => $m[2], + 'original' => trim($m[0]), ]; } // Handle short :$var syntax (expands to :var="$var") - preg_match_all('/(?:^|\s):\$([a-zA-Z0-9_:-]+)(?=\s|$)/', $attributesString, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); - foreach ($matches as $match) { - $start = $match[0][1]; - $end = $start + strlen($match[0][0]); - - // Skip if this position was already processed - $skip = false; - foreach ($processedPositions as [$procStart, $procEnd]) { - if ($start >= $procStart && $end <= $procEnd) { - $skip = true; - break; - } - } - if ($skip) continue; - - $processedPositions[] = [$start, $end]; - - $attributeName = str($match[1][0])->camel()->toString(); - $attributeValue = '$' . $match[1][0]; - $original = trim($match[0][0]); + preg_match_all('/(?:^|\s):\$([A-Za-z0-9_:-]+)(?=\s|$)/', $attributesString, $matches, PREG_SET_ORDER); + foreach ($matches as $m) { + $raw = $m[1]; + $attributeName = str($raw)->camel()->toString(); + if (isset($attributes[$attributeName])) continue; $attributes[$attributeName] = [ 'isDynamic' => true, - 'value' => $attributeValue, - 'original' => $original, + 'value' => '$' . $raw, + 'original' => trim($m[0]), ]; } // Handle regular name="value" syntax - preg_match_all('/(\s*)([a-zA-Z0-9_:-]+)\s*=\s*("[^"]*")/', $attributesString, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); - foreach ($matches as $match) { - $start = $match[0][1]; - $end = $start + strlen($match[0][0]); - - // Skip if this position was already processed - $skip = false; - foreach ($processedPositions as [$procStart, $procEnd]) { - if ($start >= $procStart && $end <= $procEnd) { - $skip = true; - break; - } - } - if ($skip) continue; - - $processedPositions[] = [$start, $end]; - - $attributeName = str($match[2][0])->camel()->toString(); - $attributeValue = trim($match[3][0], '"'); - $original = trim($match[0][0]); + preg_match_all('/(?:^|\s)(?!:)([A-Za-z0-9_:-]+)\s*=\s*"([^"]*)"/', $attributesString, $matches, PREG_SET_ORDER); + foreach ($matches as $m) { + $attributeName = str($m[1])->camel()->toString(); + if (isset($attributes[$attributeName])) continue; $attributes[$attributeName] = [ 'isDynamic' => false, - 'value' => $attributeValue, - 'original' => $original, + 'value' => $m[2], + 'original' => trim($m[0]), ]; } // Handle boolean attributes (single words without values) - preg_match_all('/(\s*)([a-zA-Z0-9_:-]+)(?=\s|$)/', $attributesString, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); - foreach ($matches as $match) { - $start = $match[0][1]; - $end = $start + strlen($match[0][0]); - - // Skip if this position was already processed - $skip = false; - foreach ($processedPositions as [$procStart, $procEnd]) { - if ($start >= $procStart && $end <= $procEnd) { - $skip = true; - break; - } - } - if ($skip) continue; - - $attributeName = str($match[2][0])->camel()->toString(); - $original = trim($match[0][0]); - - // Only add if not already processed as a key-value pair - if (!array_key_exists($attributeName, $attributes)) { - $attributes[$attributeName] = [ - 'isDynamic' => false, - 'value' => true, - 'original' => $original, - ]; - } + preg_match_all('/(?:^|\s)([A-Za-z0-9_:-]+)(?=\s|$)/', $attributesString, $matches, PREG_SET_ORDER); + foreach ($matches as $m) { + $attributeName = str($m[1])->camel()->toString(); + if (isset($attributes[$attributeName])) continue; + + $attributes[$attributeName] = [ + 'isDynamic' => false, + 'value' => true, + 'original' => trim($m[0]), + ]; } return $attributes; diff --git a/tests/AttributeParserTest.php b/tests/AttributeParserTest.php index 1942a42b..4625717c 100644 --- a/tests/AttributeParserTest.php +++ b/tests/AttributeParserTest.php @@ -3,6 +3,64 @@ use Livewire\Blaze\Support\AttributeParser; describe('parse attributes', function () { + it('parses and replaces dynamic attributes with name and value syntax', function() { + $input = ':name="$foo"'; + $output = 'name="ATTR_PLACEHOLDER_0"'; + + $attributePlaceholders = []; + $attributeNameToPlaceholder = []; + + $result = (new AttributeParser)->parseAndReplaceDynamics($input, $attributePlaceholders, $attributeNameToPlaceholder); + + expect($result)->toBe($output); + + expect($attributePlaceholders)->toBe([ + 'ATTR_PLACEHOLDER_0' => '{{ $foo }}', + ]); + + expect($attributeNameToPlaceholder)->toBe([ + 'name' => 'ATTR_PLACEHOLDER_0', + ]); + }); + + it('parses and replaces dynamic attributes with short syntax', function() { + $input = ':$name'; + $output = 'name="ATTR_PLACEHOLDER_0"'; + + $attributePlaceholders = []; + $attributeNameToPlaceholder = []; + + $result = (new AttributeParser)->parseAndReplaceDynamics($input, $attributePlaceholders, $attributeNameToPlaceholder); + + expect($result)->toBe($output); + + expect($attributePlaceholders)->toBe([ + 'ATTR_PLACEHOLDER_0' => '{{ $name }}', + ]); + + expect($attributeNameToPlaceholder)->toBe([ + 'name' => 'ATTR_PLACEHOLDER_0', + ]); + }); + + it('parses and replaces dynamic attributes echoed within a value', function() { + $input = 'name="foo {{ $type }}"'; + $output = 'name="foo ATTR_PLACEHOLDER_0"'; + + $attributePlaceholders = []; + $attributeNameToPlaceholder = []; + + $result = (new AttributeParser)->parseAndReplaceDynamics($input, $attributePlaceholders, $attributeNameToPlaceholder); + + expect($result)->toBe($output); + + expect($attributePlaceholders)->toBe([ + 'ATTR_PLACEHOLDER_0' => '{{ $type }}', + ]); + + expect($attributeNameToPlaceholder)->toBe([]); + }); + it('parses static attributes', function () { $input = 'name="Bob" searchable="true"'; $output = [ diff --git a/tests/FluxTest.php b/tests/FluxTest.php index a205bc0c..a01219a5 100644 --- a/tests/FluxTest.php +++ b/tests/FluxTest.php @@ -1,5 +1,7 @@ toContain('data-flux-heading'); expect($output)->not->toContain('flux:heading'); }); + + it('folds link component with dynamic route helper link', function() { + Route::get('/dashboard', fn() => 'dashboard')->name('dashboard'); + + $input = 'Dashboard'; + $output = app('blaze')->compile($input); + + expect($output) + ->toContain(' $date]); ?>\n'; + $output = ' $date]); ?>'; expect(compile($input))->toBe($output); }); From b4f503c176928ac11131054e6aaa74184540c5d8 Mon Sep 17 00:00:00 2001 From: Josh Hanley Date: Mon, 20 Oct 2025 18:30:27 +1000 Subject: [PATCH 3/6] wip --- tests/AttributeParserTest.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/AttributeParserTest.php b/tests/AttributeParserTest.php index 4625717c..b97f6e8e 100644 --- a/tests/AttributeParserTest.php +++ b/tests/AttributeParserTest.php @@ -61,6 +61,22 @@ expect($attributeNameToPlaceholder)->toBe([]); }); + it('does not parse static attributes with colon in the name', function() { + $input = 'icon:trailing="chevrons-up-down"'; + $output = 'icon:trailing="chevrons-up-down"'; + + $attributePlaceholders = []; + $attributeNameToPlaceholder = []; + + $result = (new AttributeParser)->parseAndReplaceDynamics($input, $attributePlaceholders, $attributeNameToPlaceholder); + + expect($result)->toBe($output); + + expect($attributePlaceholders)->toBe([]); + + expect($attributeNameToPlaceholder)->toBe([]); + }); + it('parses static attributes', function () { $input = 'name="Bob" searchable="true"'; $output = [ @@ -202,8 +218,13 @@ }); it('parses static attributes which contain colons', function () { - $input = 'wire:sort:item="{{ $id }}"'; + $input = 'icon:trailing="chevrons-up-down" wire:sort:item="{{ $id }}"'; $output = [ + 'icon:trailing' => [ + 'isDynamic' => false, + 'value' => 'chevrons-up-down', + 'original' => 'icon:trailing="chevrons-up-down"', + ], 'wire:sort:item' => [ 'isDynamic' => false, 'value' => '{{ $id }}', From 50f3cae18133e9471c89c10676fa45759bed4f6f Mon Sep 17 00:00:00 2001 From: Josh Hanley Date: Mon, 20 Oct 2025 18:49:03 +1000 Subject: [PATCH 4/6] wip --- src/Support/AttributeParser.php | 2 +- tests/AttributeParserTest.php | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Support/AttributeParser.php b/src/Support/AttributeParser.php index 3baa1a27..7fbd8a0a 100644 --- a/src/Support/AttributeParser.php +++ b/src/Support/AttributeParser.php @@ -16,7 +16,7 @@ public function parseAndReplaceDynamics( } // :name="..." - $attributesString = preg_replace_callback('/(\s*):([a-zA-Z0-9_-]+)\s*=\s*"([^"]*)"/', function ($matches) use (&$attributePlaceholders, &$attributeNameToPlaceholder) { + $attributesString = preg_replace_callback('/(?toBe([]); }); - it('does not parse static attributes with colon in the name', function() { + it('does not parse static attributes with colon in the name when used alone', function() { $input = 'icon:trailing="chevrons-up-down"'; $output = 'icon:trailing="chevrons-up-down"'; @@ -77,6 +77,26 @@ expect($attributeNameToPlaceholder)->toBe([]); }); + it('does not parse static attributes with colon in the name when used with dynamic attributes', function() { + $input = ':name="$foo" icon:trailing="chevrons-up-down"'; + $output = 'name="ATTR_PLACEHOLDER_0" icon:trailing="chevrons-up-down"'; + + $attributePlaceholders = []; + $attributeNameToPlaceholder = []; + + $result = (new AttributeParser)->parseAndReplaceDynamics($input, $attributePlaceholders, $attributeNameToPlaceholder); + + expect($result)->toBe($output); + + expect($attributePlaceholders)->toBe([ + 'ATTR_PLACEHOLDER_0' => '{{ $foo }}', + ]); + + expect($attributeNameToPlaceholder)->toBe([ + 'name' => 'ATTR_PLACEHOLDER_0', + ]); + }); + it('parses static attributes', function () { $input = 'name="Bob" searchable="true"'; $output = [ From e0c33609d7461452b1f149608018022329a6b1fb Mon Sep 17 00:00:00 2001 From: Josh Hanley Date: Mon, 20 Oct 2025 20:36:50 +1000 Subject: [PATCH 5/6] Fix test --- tests/FluxTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/FluxTest.php b/tests/FluxTest.php index a01219a5..b8e19a2b 100644 --- a/tests/FluxTest.php +++ b/tests/FluxTest.php @@ -27,6 +27,7 @@ $output = app('blaze')->compile($input); expect($output) - ->toContain('toContain('toContain(' href="{{ route(\'dashboard\') }}"'); }); }); From 8d4385edfc6836b6ca67f5796ad72dc8c0fb3a01 Mon Sep 17 00:00:00 2001 From: Josh Hanley Date: Mon, 20 Oct 2025 20:38:52 +1000 Subject: [PATCH 6/6] wip --- src/Memoizer/Memoizer.php | 2 +- tests/FoldTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Memoizer/Memoizer.php b/src/Memoizer/Memoizer.php index c45e9cf1..c3529473 100644 --- a/src/Memoizer/Memoizer.php +++ b/src/Memoizer/Memoizer.php @@ -62,7 +62,7 @@ public function memoize(Node $node): Node $name = $node->name; $attributes = $node->getAttributesAsRuntimeArrayString(); - $output = '<' . '?php $blaze_memoized_key = \Livewire\Blaze\Memoizer\Memo::key("' . $name . '", ' . $attributes . '); ?>'; + $output = '<' . '?php $blaze_memoized_key = \Livewire\Blaze\Memoizer\Memo::key("' . $name . '", ' . $attributes . '); ?>\n'; $output .= '<' . '?php if (! \Livewire\Blaze\Memoizer\Memo::has($blaze_memoized_key)) : ?>'; $output .= '<' . '?php ob_start(); ?>'; $output .= $node->render(); diff --git a/tests/FoldTest.php b/tests/FoldTest.php index bdf82959..251078cc 100644 --- a/tests/FoldTest.php +++ b/tests/FoldTest.php @@ -263,7 +263,7 @@ function compile(string $input): string { it('cant fold dynamic props that get formatted', function () { $input = ' '; - $output = ' $date]); ?>'; + $output = ' $date]); ?>\n'; expect(compile($input))->toBe($output); });