Skip to content

Commit

Permalink
Merge pull request neos#3442 from mhsdesign/Feature-FusionAfx-DotAttr
Browse files Browse the repository at this point in the history
FEATURE: Fusion/AFX escaped attribute names (paths) with dots inside
  • Loading branch information
jonnitto authored Oct 8, 2021
2 parents 1077e89 + 0602d6d commit 78d9dff
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 10 deletions.
4 changes: 4 additions & 0 deletions Neos.Fusion.Afx/Classes/Parser/Expression/Identifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
112 changes: 112 additions & 0 deletions Neos.Fusion.Afx/Tests/Functional/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,38 @@ public function shouldParseSingleSelfClosingTagWithSingleAttribute(): void
);
}

/**
* @test
*/
public function shouldParseSingleSelfClosingTagWithEmptyAttribute(): void
{
$parser = new Parser('<div prop/>');

$this->assertEquals(
[
[
'type' => 'node',
'payload' => [
'identifier' => 'div',
'attributes' => [
[
'type' => 'prop',
'payload' => [
'type' => 'boolean',
'payload' => true,
'identifier' => 'prop'
]
]
],
'children' => [],
'selfClosing' => true
]
]
],
$parser->parse()
);
}

/**
* @test
*/
Expand Down Expand Up @@ -299,6 +331,86 @@ public function shouldParseSingleSelfClosingTagWithMultipleAttributesWrappedByMu
);
}

/**
* @test
*/
public function shouldParseTagWithSingleOrDoubleQuoteEscapedAttributeIdentifier(): void
{
$parser = new Parser('<div "@click.blah.blih.blub"="value" \'@click.blah.blih.blub\'="value"/>');

$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('<div "@click.escaped\"escaped"="value" \'escaped\\\'escaped\' />');

$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
*/
Expand Down
16 changes: 9 additions & 7 deletions Neos.Fusion/Classes/Core/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
32 changes: 29 additions & 3 deletions Neos.Fusion/Tests/Unit/Core/Parser/PatternTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -148,28 +167,35 @@ 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'
];
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'
];
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"'));
}

/**
Expand Down
20 changes: 20 additions & 0 deletions Neos.Fusion/Tests/Unit/Core/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down

0 comments on commit 78d9dff

Please sign in to comment.