From 63bb246ff48c900b5a22eeb6adba10ba854a00bc Mon Sep 17 00:00:00 2001 From: butschster Date: Tue, 7 Jan 2025 00:17:50 +0400 Subject: [PATCH] fix: Fixes the problem when stempler tries to parse @ char inside a string that is not a directive. --- src/Stempler/src/Lexer/Buffer.php | 7 ++++++- .../Lexer/Grammar/Dynamic/DirectiveGrammar.php | 6 ++++++ .../src/Lexer/Grammar/DynamicGrammar.php | 10 +++++++--- src/Stempler/tests/Directive/DirectiveTest.php | 3 +-- .../tests/Transform/DynamicToPHPTest.php | 16 ++++++++++++++++ 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/Stempler/src/Lexer/Buffer.php b/src/Stempler/src/Lexer/Buffer.php index 6f313d663..05d1a70be 100644 --- a/src/Stempler/src/Lexer/Buffer.php +++ b/src/Stempler/src/Lexer/Buffer.php @@ -20,7 +20,7 @@ final class Buffer implements \IteratorAggregate public function __construct( /** @internal */ private readonly \Generator $generator, - private int $offset = 0 + private int $offset = 0, ) { } @@ -136,4 +136,9 @@ public function replay(int $offset): void } } } + + public function flushReplay(): void + { + $this->replay = []; + } } diff --git a/src/Stempler/src/Lexer/Grammar/Dynamic/DirectiveGrammar.php b/src/Stempler/src/Lexer/Grammar/Dynamic/DirectiveGrammar.php index 2da2d56d1..f49f59c86 100644 --- a/src/Stempler/src/Lexer/Grammar/Dynamic/DirectiveGrammar.php +++ b/src/Stempler/src/Lexer/Grammar/Dynamic/DirectiveGrammar.php @@ -229,6 +229,12 @@ private function finalize(): bool { $tokens = $this->tokens; + // A directive must have at least one keyword + // Without it, it's just a char + if (\count($tokens) === 1 && $tokens[0]->content === self::DIRECTIVE_CHAR) { + return false; + } + foreach (\array_reverse($tokens, true) as $i => $t) { if ($t->type !== DynamicGrammar::TYPE_WHITESPACE) { break; diff --git a/src/Stempler/src/Lexer/Grammar/DynamicGrammar.php b/src/Stempler/src/Lexer/Grammar/DynamicGrammar.php index 46cafddc7..b92fe7c98 100644 --- a/src/Stempler/src/Lexer/Grammar/DynamicGrammar.php +++ b/src/Stempler/src/Lexer/Grammar/DynamicGrammar.php @@ -45,20 +45,20 @@ final class DynamicGrammar implements GrammarInterface private readonly BracesGrammar $raw; public function __construct( - private readonly ?DirectiveRendererInterface $directiveRenderer = null + private readonly ?DirectiveRendererInterface $directiveRenderer = null, ) { $this->echo = new BracesGrammar( '{{', '}}', self::TYPE_OPEN_TAG, - self::TYPE_CLOSE_TAG + self::TYPE_CLOSE_TAG, ); $this->raw = new BracesGrammar( '{!!', '!!}', self::TYPE_OPEN_RAW_TAG, - self::TYPE_CLOSE_RAW_TAG + self::TYPE_CLOSE_RAW_TAG, ); } @@ -107,6 +107,10 @@ public function parse(Buffer $src): \Generator $src->replay($directive->getLastOffset()); continue; + } else { + // When we found directive char but it's not a directive, we need to flush the replay buffer + // because it may contain extra tokens that we don't need to return back to the stream + $src->flushReplay(); } $src->replay($n->offset); diff --git a/src/Stempler/tests/Directive/DirectiveTest.php b/src/Stempler/tests/Directive/DirectiveTest.php index 81f0f15b5..0c7d50b66 100644 --- a/src/Stempler/tests/Directive/DirectiveTest.php +++ b/src/Stempler/tests/Directive/DirectiveTest.php @@ -2,10 +2,9 @@ declare(strict_types=1); -namespace Directive; +namespace Spiral\Tests\Stempler\Directive; use Spiral\Tests\Stempler\fixtures\ImageDirective; -use Spiral\Tests\Stempler\Directive\BaseTestCase; final class DirectiveTest extends BaseTestCase { diff --git a/src/Stempler/tests/Transform/DynamicToPHPTest.php b/src/Stempler/tests/Transform/DynamicToPHPTest.php index 97980962e..c6604dd60 100644 --- a/src/Stempler/tests/Transform/DynamicToPHPTest.php +++ b/src/Stempler/tests/Transform/DynamicToPHPTest.php @@ -4,8 +4,10 @@ namespace Spiral\Tests\Stempler\Transform; +use PHPUnit\Framework\Attributes\DataProvider; use Spiral\Stempler\Directive\LoopDirective; use Spiral\Stempler\Node\PHP; +use Spiral\Stempler\Node\Raw; use Spiral\Stempler\Transform\Finalizer\DynamicToPHP; class DynamicToPHPTest extends BaseTestCase @@ -17,6 +19,20 @@ public function testOutput(): void self::assertInstanceOf(PHP::class, $doc->nodes[0]); } + public static function provideStringWithoutDirective(): iterable + { + yield ['https://unpkg.com/tailwindcss@^1.6/dist/tailwind.min.css']; + } + + #[DataProvider('provideStringWithoutDirective')] + public function testLinkWithReservedSymbol(string $string): void + { + $doc = $this->parse($string); + + self::assertInstanceOf(Raw::class, $doc->nodes[0]); + self::assertSame($string, $doc->nodes[0]->content); + } + public function testDirective(): void { $doc = $this->parse('@foreach($users as $u) @endforeach');