From 0602d6d3bb26cb3ab292b7452688ebd9d7abef04 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 26 Sep 2021 21:26:30 +0200 Subject: [PATCH] FEATURE: Fusion/AFX preserve dots in escaped paths fixes #3112 - Fusion parser: dont split paths at dots, who are inside quotes. - AFX: allow quoted attribute identifier. Example: AFX: ```html
``` Fusion: ```ts Neos.Fusion:Tag { attributes.'@foo.bar' = 'baz' } ``` Fusion ast (excerpt): ```php 'attributes' => [ '@foo.bar' => 'baz' ] ``` Before this, AFX doesnt allow quoted attribute identifier, and the Fusion Parser split at dots in quotes too, making those paths nested. --- .../Classes/Parser/Expression/Identifier.php | 4 + .../Tests/Functional/ParserTest.php | 112 ++++++++++++++++++ Neos.Fusion/Classes/Core/Parser.php | 16 +-- .../Fixtures/ParserTestFusionFixture25.fusion | 9 ++ .../Tests/Unit/Core/Parser/PatternTest.php | 32 ++++- Neos.Fusion/Tests/Unit/Core/ParserTest.php | 20 ++++ 6 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 Neos.Fusion/Tests/Unit/Core/Fixtures/ParserTestFusionFixture25.fusion diff --git a/Neos.Fusion.Afx/Classes/Parser/Expression/Identifier.php b/Neos.Fusion.Afx/Classes/Parser/Expression/Identifier.php index e924c8394a2..82e2eb74105 100644 --- a/Neos.Fusion.Afx/Classes/Parser/Expression/Identifier.php +++ b/Neos.Fusion.Afx/Classes/Parser/Expression/Identifier.php @@ -39,6 +39,10 @@ public static function parse(Lexer $lexer): string case $lexer->isMinus(): case $lexer->isUnderscore(): case $lexer->isAt(): + case $lexer->isDoubleQuote(): + case $lexer->isSingleQuote(): + case $lexer->isBackSlash() && $lexer->peek(2) === '\"': + case $lexer->isBackSlash() && $lexer->peek(2) === '\\\'': $identifier .= $lexer->consume(); break; case $lexer->isEqualSign(): diff --git a/Neos.Fusion.Afx/Tests/Functional/ParserTest.php b/Neos.Fusion.Afx/Tests/Functional/ParserTest.php index 52e164c8948..991cf9fbf38 100644 --- a/Neos.Fusion.Afx/Tests/Functional/ParserTest.php +++ b/Neos.Fusion.Afx/Tests/Functional/ParserTest.php @@ -219,6 +219,38 @@ public function shouldParseSingleSelfClosingTagWithSingleAttribute(): void ); } + /** + * @test + */ + public function shouldParseSingleSelfClosingTagWithEmptyAttribute(): void + { + $parser = new Parser('
'); + + $this->assertEquals( + [ + [ + 'type' => 'node', + 'payload' => [ + 'identifier' => 'div', + 'attributes' => [ + [ + 'type' => 'prop', + 'payload' => [ + 'type' => 'boolean', + 'payload' => true, + 'identifier' => 'prop' + ] + ] + ], + 'children' => [], + 'selfClosing' => true + ] + ] + ], + $parser->parse() + ); + } + /** * @test */ @@ -299,6 +331,86 @@ public function shouldParseSingleSelfClosingTagWithMultipleAttributesWrappedByMu ); } + /** + * @test + */ + public function shouldParseTagWithSingleOrDoubleQuoteEscapedAttributeIdentifier(): void + { + $parser = new Parser('
'); + + $this->assertEquals( + [ + [ + 'type' => 'node', + 'payload' => [ + 'identifier' => 'div', + 'attributes' => [ + [ + 'type' => 'prop', + 'payload' => [ + 'type' => 'string', + 'payload' => 'value', + 'identifier' => '"@click.blah.blih.blub"' + ] + ], + [ + 'type' => 'prop', + 'payload' => [ + 'type' => 'string', + 'payload' => 'value', + 'identifier' => '\'@click.blah.blih.blub\'' + ] + ] + ], + 'children' => [], + 'selfClosing' => true + ] + ] + ], + $parser->parse() + ); + } + + /** + * @test + */ + public function shouldParseTagWithEscapedAttributeIdentifierWithQuoteEscapesInside(): void + { + $parser = new Parser('
'); + + $this->assertEquals( + [ + [ + 'type' => 'node', + 'payload' => [ + 'identifier' => 'div', + 'attributes' => [ + [ + 'type' => 'prop', + 'payload' => [ + 'type' => 'string', + 'payload' => 'value', + 'identifier' => '"@click.escaped\"escaped"' + ] + ], + [ + 'type' => 'prop', + 'payload' => [ + 'type' => 'boolean', + 'payload' => true, + 'identifier' => '\'escaped\\\'escaped\'' + ] + ] + ], + 'children' => [], + 'selfClosing' => true + ] + ] + ], + $parser->parse() + ); + } + /** * @test */ diff --git a/Neos.Fusion/Classes/Core/Parser.php b/Neos.Fusion/Classes/Core/Parser.php index 7ed300e79d6..c8625308f14 100644 --- a/Neos.Fusion/Classes/Core/Parser.php +++ b/Neos.Fusion/Classes/Core/Parser.php @@ -96,15 +96,17 @@ class Parser implements ParserInterface /x'; /** - * Split an object path like "foo.bar.baz.quux" or "foo.prototype(Neos.Fusion:Something).bar.baz" - * at the dots (but not the dots inside the prototype definition prototype(...)) + * Split an object path like "foo.bar.baz.quux", "foo.'bar.baz.quux'" or "foo.prototype(Neos.Fusion:Something).bar.baz" + * at the dots (but not the dots inside the prototype definition prototype(...) or dots inside quotes) */ const SPLIT_PATTERN_OBJECTPATH = '/ - \. # we split at dot characters... - (?! # which are not inside prototype(...). Thus, the dot does NOT match IF it is followed by: - [^(]* # - any character except ( - \) # - the character ) - ) + ( # Matches area if: + prototype\(.*?\) # inside prototype(...), + |"(?:\\\"|[^"])+" # inside double quotes - respect escape, + |\'(?:\\\\\'|[^\'])+\' # inside single quotes - respect escape + ) + (*SKIP)(*FAIL) # skip, when the preceding matches and fail (dont include them in the match) + |\. # for what was not matched, we split at dot characters... /x'; /** diff --git a/Neos.Fusion/Tests/Unit/Core/Fixtures/ParserTestFusionFixture25.fusion b/Neos.Fusion/Tests/Unit/Core/Fixtures/ParserTestFusionFixture25.fusion new file mode 100644 index 00000000000..bb43e9d2b0c --- /dev/null +++ b/Neos.Fusion/Tests/Unit/Core/Fixtures/ParserTestFusionFixture25.fusion @@ -0,0 +1,9 @@ + +// +// Fusion Fixture 25 +// +// Checks if quoted paths will escape the @ char +// and if dots inside the quotes will not be interpreted as nested. + +attributes."@notAMeta" = "value" +attributes."@notAMeta.notNested.reallyNotNested.string" = "value" diff --git a/Neos.Fusion/Tests/Unit/Core/Parser/PatternTest.php b/Neos.Fusion/Tests/Unit/Core/Parser/PatternTest.php index de62e39ae57..f2cd31472df 100644 --- a/Neos.Fusion/Tests/Unit/Core/Parser/PatternTest.php +++ b/Neos.Fusion/Tests/Unit/Core/Parser/PatternTest.php @@ -134,6 +134,25 @@ public function testSPLIT_PATTERN_OBJECTPATH() ]; self::assertSame($expected, preg_split($pattern, 'foo.bar')); + $expected = [ + 0 => 'attributes', + 1 => '"dots.inside.double.quotes"' + ]; + self::assertSame($expected, preg_split($pattern, 'attributes."dots.inside.double.quotes"')); + + $expected = [ + 0 => 'attributes', + 1 => '\'dots.inside.single.quotes\'' + ]; + self::assertSame($expected, preg_split($pattern, 'attributes.\'dots.inside.single.quotes\'')); + + $expected = [ + 0 => '"quo\"tes.mix\'ed"', + 1 => 'bla', + 2 => '\'he\\\'.llo\'' + ]; + self::assertSame($expected, preg_split($pattern, '"quo\"tes.mix\'ed".bla.\'he\\\'.llo\'')); + $expected = [ 0 => 'prototype(Neos.Foo)', 1 => 'bar' @@ -148,7 +167,7 @@ public function testSPLIT_PATTERN_OBJECTPATH() self::assertSame($expected, preg_split($pattern, 'asdf.prototype(Neos.Foo).bar')); $expected = [ - 0 => 'blah', + 0 => 'blah', 1 => 'asdf', 2 => 'prototype(Neos.Foo)', 3 => 'bar' @@ -156,7 +175,7 @@ public function testSPLIT_PATTERN_OBJECTPATH() self::assertSame($expected, preg_split($pattern, 'blah.asdf.prototype(Neos.Foo).bar')); $expected = [ - 0 => 'b-lah', + 0 => 'b-lah', 1 => 'asdf', 2 => 'prototype(Neos.Foo)', 3 => 'b-ar' @@ -164,12 +183,19 @@ public function testSPLIT_PATTERN_OBJECTPATH() self::assertSame($expected, preg_split($pattern, 'b-lah.asdf.prototype(Neos.Foo).b-ar')); $expected = [ - 0 => 'b:lah', + 0 => 'b:lah', 1 => 'asdf', 2 => 'prototype(Neos.Foo)', 3 => 'b:ar' ]; self::assertSame($expected, preg_split($pattern, 'b:lah.asdf.prototype(Neos.Foo).b:ar')); + + $expected = [ + 0 => 'asdf', + 1 => 'prototype(Neos.Foo)', + 2 => '"@click.blah.blub"' + ]; + self::assertSame($expected, preg_split($pattern, 'asdf.prototype(Neos.Foo)."@click.blah.blub"')); } /** diff --git a/Neos.Fusion/Tests/Unit/Core/ParserTest.php b/Neos.Fusion/Tests/Unit/Core/ParserTest.php index 45ca45bbf7c..563e5880681 100644 --- a/Neos.Fusion/Tests/Unit/Core/ParserTest.php +++ b/Neos.Fusion/Tests/Unit/Core/ParserTest.php @@ -920,6 +920,26 @@ public function parserCorrectlyParsesFixture21() self::assertSame($expectedParseTree, $actualParseTree, 'The parse tree was not as expected after parsing fixture 23.'); } + /** + * Checks if identifiers starting with digits are parsed correctly + * + * @test + */ + public function parserCorrectlyParsesFixture25() + { + $sourceCode = $this->readFusionFixture('ParserTestFusionFixture25'); + + $expectedParseTree = [ + 'attributes' => [ + '@notAMeta' => 'value', + '@notAMeta.notNested.reallyNotNested.string' => 'value' + ] + ]; + + $actualParseTree = $this->parser->parse($sourceCode); + self::assertSame($expectedParseTree, $actualParseTree, 'The parse tree was not as expected after parsing fixture 23.'); + } + /** * Checks if really long strings are parsed correctly *