diff --git a/src/Support/AttributeParser.php b/src/Support/AttributeParser.php index af172a66..7fbd8a0a 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('/(?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..c4d430d7 100644 --- a/tests/AttributeParserTest.php +++ b/tests/AttributeParserTest.php @@ -3,6 +3,100 @@ 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('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"'; + + $attributePlaceholders = []; + $attributeNameToPlaceholder = []; + + $result = (new AttributeParser)->parseAndReplaceDynamics($input, $attributePlaceholders, $attributeNameToPlaceholder); + + expect($result)->toBe($output); + + expect($attributePlaceholders)->toBe([]); + + 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 = [ @@ -144,8 +238,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 }}', diff --git a/tests/FluxTest.php b/tests/FluxTest.php index a205bc0c..b8e19a2b 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('toContain(' href="{{ route(\'dashboard\') }}"'); + }); });