From 9fd6390c397c6d61232dfb92d74248c2581f205f Mon Sep 17 00:00:00 2001 From: John Koster Date: Wed, 16 Oct 2024 18:00:04 -0500 Subject: [PATCH] New AttributeCompiler and some new helper functions --- .../CompilerServices/AttributeCompiler.php | 125 ++++++++++++++++++ src/Parser/DocumentParser.php | 20 +++ .../AttributeCompilerTest.php | 57 ++++++++ 3 files changed, 202 insertions(+) create mode 100644 src/Compiler/CompilerServices/AttributeCompiler.php create mode 100644 tests/CompilerServices/AttributeCompilerTest.php diff --git a/src/Compiler/CompilerServices/AttributeCompiler.php b/src/Compiler/CompilerServices/AttributeCompiler.php new file mode 100644 index 0000000..e240542 --- /dev/null +++ b/src/Compiler/CompilerServices/AttributeCompiler.php @@ -0,0 +1,125 @@ +escapedParameterPrefix = $prefix; + + return $this; + } + + public function wrapResultIn(string|array $attribute, callable $callback): static + { + if (! is_array($attribute)) { + $attribute = [$attribute]; + } + + foreach ($attribute as $attributeName) { + $this->attributeWrapCallbacks[$attributeName] = $callback; + } + + return $this; + } + + protected function applyWraps(string $attribute, string $value): string + { + if (! array_key_exists($attribute, $this->attributeWrapCallbacks)) { + return $value; + } + + return call_user_func($this->attributeWrapCallbacks[$attribute], $value); + } + + protected function getParamValue(string $value): string + { + return Str::replace("'", "\\'", $value); + } + + protected function toArraySyntax(string $name, string $value, bool $isString = true): string + { + if ($isString) { + $value = "'{$value}'"; + } + + $value = $this->applyWraps($name, $value); + + return "'{$name}'=>{$value}"; + } + + protected function compileAttributeEchos(string $attributeString): string + { + $value = Blade::compileEchos($attributeString); + + $value = $this->escapeSingleQuotesOutsideOfPhpBlocks($value); + + $value = str_replace('', '.\'', $value); + } + + protected function escapeSingleQuotesOutsideOfPhpBlocks(string $value): string + { + return collect(token_get_all($value))->map(function ($token) { + if (! is_array($token)) { + return $token; + } + + return $token[0] === T_INLINE_HTML + ? str_replace("'", "\\'", $token[1]) + : $token[1]; + })->implode(''); + } + + public function compileComponent(ComponentNode $component): string + { + return $this->compile($component->parameters); + } + + public function compile(array $parameters): string + { + return '['.implode(',', $this->toCompiledArray($parameters)).']'; + } + + /** + * @param ParameterNode[] $parameters + */ + public function toCompiledArray(array $parameters): array + { + if (count($parameters) === 0) { + return []; + } + + $compiledParameters = []; + + foreach ($parameters as $parameter) { + if ($parameter->type == ParameterType::Parameter) { + $compiledParameters[] = $this->toArraySyntax($parameter->name, $this->getParamValue($parameter->value)); + } elseif ($parameter->type == ParameterType::DynamicVariable) { + $compiledParameters[] = $this->toArraySyntax($parameter->materializedName, $parameter->value, false); + } elseif ($parameter->type == ParameterType::ShorthandDynamicVariable) { + $compiledParameters[] = $this->toArraySyntax($parameter->materializedName, $parameter->value, false); + } elseif ($parameter->type == ParameterType::EscapedParameter) { + $compiledParameters[] = $this->toArraySyntax($this->escapedParameterPrefix.$parameter->materializedName, $parameter->value); + } elseif ($parameter->type == ParameterType::Attribute) { + $compiledParameters[] = $this->toArraySyntax($parameter->materializedName, 'true', false); + } elseif ($parameter->type == ParameterType::InterpolatedValue) { + $compiledParameters[] = $this->toArraySyntax($parameter->materializedName, "'".$this->compileAttributeEchos($parameter->value)."'", false); + } + } + + return $compiledParameters; + } +} diff --git a/src/Parser/DocumentParser.php b/src/Parser/DocumentParser.php index 28f913f..7495c48 100644 --- a/src/Parser/DocumentParser.php +++ b/src/Parser/DocumentParser.php @@ -7,6 +7,7 @@ use Stillat\BladeParser\Compiler\CompilerServices\CoreDirectiveRetriever; use Stillat\BladeParser\Compiler\CompilerServices\LiteralContentHelpers; use Stillat\BladeParser\Compiler\CompilerServices\StringUtilities; +use Stillat\BladeParser\Document\Document; use Stillat\BladeParser\Document\Structures\DirectiveClosingAnalyzer; use Stillat\BladeParser\Errors\BladeError; use Stillat\BladeParser\Errors\ConstructContext; @@ -181,6 +182,18 @@ public function ignoreDirectives(array $directives): DocumentParser return $this; } + public function toDocument(bool $resolveStructures = true): Document + { + $document = new Document; + $document->syncFromParser($this); + + if ($resolveStructures) { + $document->resolveStructures(); + } + + return $document; + } + /** * Retrieves a list of directive names supported by the parser instance. */ @@ -605,6 +618,13 @@ private function makeComponentNode(int $startLocation, string $content): Compone return $componentNode; } + public function parseTemplate(string $document): static + { + $this->parse($document); + + return $this; + } + /** * Parses the input document and returns an array of nodes. * diff --git a/tests/CompilerServices/AttributeCompilerTest.php b/tests/CompilerServices/AttributeCompilerTest.php new file mode 100644 index 0000000..0bebd58 --- /dev/null +++ b/tests/CompilerServices/AttributeCompilerTest.php @@ -0,0 +1,57 @@ +attributeCompiler = new AttributeCompiler; + $template = <<<'TEMAPLTE' + +TEMAPLTE; + + $this->parameters = (new DocumentParser) + ->onlyParseComponents() + ->registerCustomComponentTags(['t']) + ->parseTemplate($template) + ->toDocument() + ->getComponents() + ->first() + ->parameters; +}); + +test('it compiles attributes', function () { + $expected = <<<'COMPILED' +['parameter'=>'content','binding'=>$theVariable,'short-hand'=>$shortHand,':escaped'=>'true','just-an-attribute'=>true,'interpolated'=>''.e($value).''] +COMPILED; + + expect($this->attributeCompiler->compile($this->parameters))->toBe($expected); +}); + +test('it can prefix escaped parameters', function () { + $this->attributeCompiler->prefixEscapedParametersWith('attr:'); + $expected = <<<'COMPILED' +['parameter'=>'content','binding'=>$theVariable,'short-hand'=>$shortHand,'attr::escaped'=>'true','just-an-attribute'=>true,'interpolated'=>''.e($value).''] +COMPILED; + + expect($this->attributeCompiler->compile($this->parameters))->toBe($expected); +}); + +test('it can wrap param content in a callback', function () { + $this->attributeCompiler->wrapResultIn(['binding', 'interpolated'], function ($value) { + return "customFunctionToUse({$value})"; + }); + + $expected = <<<'COMPILED' +['parameter'=>'content','binding'=>customFunctionToUse($theVariable),'short-hand'=>$shortHand,':escaped'=>'true','just-an-attribute'=>true,'interpolated'=>customFunctionToUse(''.e($value).'')] +COMPILED; + + expect($this->attributeCompiler->compile($this->parameters))->toBe($expected); +});