Skip to content

Commit

Permalink
New AttributeCompiler and some new helper functions
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnathonKoster committed Oct 16, 2024
1 parent 3b155a3 commit 9fd6390
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 0 deletions.
125 changes: 125 additions & 0 deletions src/Compiler/CompilerServices/AttributeCompiler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

namespace Stillat\BladeParser\Compiler\CompilerServices;

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Str;
use Stillat\BladeParser\Nodes\Components\ComponentNode;
use Stillat\BladeParser\Nodes\Components\ParameterNode;
use Stillat\BladeParser\Nodes\Components\ParameterType;

class AttributeCompiler
{
protected array $attributeWrapCallbacks = [];

protected string $escapedParameterPrefix = '';

public function prefixEscapedParametersWith(string $prefix): static
{
$this->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('<?php echo ', '\'.', $value);

return 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;
}
}
20 changes: 20 additions & 0 deletions src/Parser/DocumentParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*
Expand Down
57 changes: 57 additions & 0 deletions tests/CompilerServices/AttributeCompilerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

uses(\Stillat\BladeParser\Tests\ParserTestCase::class);
use Stillat\BladeParser\Compiler\CompilerServices\AttributeCompiler;
use Stillat\BladeParser\Parser\DocumentParser;

beforeEach(function () {
$this->attributeCompiler = new AttributeCompiler;
$template = <<<'TEMAPLTE'
<t:component
parameter="content"
:binding="$theVariable"
:$shortHand
::escaped="true"
just-an-attribute
interpolated="{{ $value }}"
/>
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);
});

0 comments on commit 9fd6390

Please sign in to comment.