Skip to content

Commit

Permalink
[FEATURE] Support for quoted keys in arrays
Browse files Browse the repository at this point in the history
The syntax for Fluid arrays is based on the JSON syntax, but up to now
it didn't support quoted arrays, so::

 {x:someViewHelper(arrayArgument: '{"foo": "bar"}')}

wouldn't parse.

With this change the parser is a little less strict for arrays allowing
the keys to be quoted with single or double quotes.
It also allows to use special characters within quoted keys::

 <x:someViewHelper arrayArgument="{'@this[will]': 'work'}" />
 {x:someViewhelper(arrayArgument: '{"and.so": "will.this"}'}

See https://jira.neos.io/browse/FLOW-248 for more information.
  • Loading branch information
Bastian Waidelich authored and NamelessCoder committed Sep 15, 2015
1 parent 35b7b88 commit 2f1e815
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 40 deletions.
40 changes: 24 additions & 16 deletions src/Core/Parser/Patterns.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,22 +194,26 @@ abstract class Patterns {
*
*/
static public $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS = '/^
(?P<Recursion> # Start the recursive part of the regular expression - describing the array syntax
{ # Each array needs to start with {
(?P<Array> # Start sub-match
(?P<Recursion> # Start the recursive part of the regular expression - describing the array syntax
{ # Each array needs to start with {
(?P<Array> # Start sub-match
(?:
\s*[a-zA-Z0-9\-_]+ # The keys of the array
\s*:\s* # Key|Value delimiter :
(?: # Possible value options:
"(?:\\\"|[^"])*" # Double quoted string
|\'(?:\\\\\'|[^\'])*\' # Single quoted string
|[a-zA-Z0-9\-_.]+ # variable identifiers
|(?P>Recursion) # Another sub-array
) # END possible value options
\s*,? # There might be a , to separate different parts of the array
)* # The above cycle is repeated for all array elements
) # End array sub-match
} # Each array ends with }
\s*(
[a-zA-Z0-9\\-_]+ # Unquoted key
|"(?:\\\"|[^"])+" # Double quoted key, supporting more characters like dots and square brackets
|\'(?:\\\\\'|[^\'])+\' # Single quoted key, supporting more characters like dots and square brackets
)
\s*:\s* # Key|Value delimiter :
(?: # Possible value options:
"(?:\\\"|[^"])*" # Double quoted string
|\'(?:\\\\\'|[^\'])*\' # Single quoted string
|[a-zA-Z0-9\-_.]+ # variable identifiers
|(?P>Recursion) # Another sub-array
) # END possible value options
\s*,? # There might be a , to separate different parts of the array
)* # The above cycle is repeated for all array elements
) # End array sub-match
} # Each array ends with }
)$/x';

/**
Expand All @@ -219,7 +223,11 @@ abstract class Patterns {
*/
static public $SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS = '/
(?P<ArrayPart> # Start sub-match
(?P<Key>[a-zA-Z0-9\-_]+) # The keys of the array
(?P<Key> # The keys of the array
[a-zA-Z0-9\\-_]+ # Unquoted
|"(?:\\\"|[^"])+" # Double quoted
|\'(?:\\\\\'|[^\'])+\' # Single quoted
)
\s*:\s* # Key|Value delimiter :
(?: # Possible value options:
(?P<QuotedString> # Quoted string
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Parser/TemplateParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ protected function recursiveArrayHandler($arrayText) {
preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS, $arrayText, $matches, PREG_SET_ORDER);
$arrayToBuild = array();
foreach ($matches as $singleMatch) {
$arrayKey = $singleMatch['Key'];
$arrayKey = $this->unquoteString($singleMatch['Key']);
if (!empty($singleMatch['VariableIdentifier'])) {
$arrayToBuild[$arrayKey] = new ObjectAccessorNode($singleMatch['VariableIdentifier']);
} elseif (array_key_exists('Number', $singleMatch) && (!empty($singleMatch['Number']) || $singleMatch['Number'] === '0' )) {
Expand Down
159 changes: 136 additions & 23 deletions tests/Unit/Core/Parser/TemplateParserPatternTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -282,23 +282,76 @@ public function testSCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS() {
}

/**
* @return array
*/
public function dataProviderSCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS() {
return array(
array('string' => '{a:b}'),
array('string' => '{a:b, c : d}'),
array('string' => '{ a:b, c : d }'),

// multiple lines
array('string' => '{' . chr(10) . ' foo: 1,' . chr(10) . ' bar: 2' . chr(10) . '}'),

// trailing comma
array('string' => '{a:b, c : d,}'),
#array('string' => '{' . chr(10) . ' foo: 1,' . chr(10) . ' bar: 2,' . chr(10) . '}'),

array('string' => '{a : 123}'),
array('string' => '{a:"String"}'),
array('string' => '{a:\'String\'}'),

// empty string value
array('string' => '{a:""}'),
array('string' => '{a:\'\'}'),

// nested arrays
array('string' => '{a:{bla:{x:z}, b: a}}'),
array('string' => '{a:"{bla{{}"}'),

// quoted keys
array('string' => '{"foo": "bar"}'),
array('string' => '{"foo[bar]": "baz"}'),
array('string' => '{"foo.bar": "baz"}'),
array('string' => '{\'foo\': "bar"}'),
array('string' => '{ \'foo\' : "bar" }'),
array('string' => '{"a":{bla:{x:z}, b: a}}'),
array('string' => '{a:{bla:{"x":z}, b: a}}'),
array('string' => '{"@a": "bar"}'),
array('string' => '{\'_b\': "bar"}')
);
}

/**
* @param string $string
* @dataProvider dataProviderSCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS()
* @test
*/
public function testSCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS($string) {
$success = preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS, $string, $matches) === 1;
$this->assertTrue($success);
$this->assertSame($string, $matches[0]);
}

/**
* @return array
*/
public function dataProviderInvalidSCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS() {
return array(
array('string' => '{"foo\': "bar"}'),
array('string' => '{"": "bar"}'),
array('string' => '{\'\': "bar"}'),
);
}

/**
* @param string $string
* @dataProvider dataProviderInvalidSCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS()
* @test
*/
public function testSCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS() {
$pattern = Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS;
$this->assertEquals(preg_match($pattern, '{a:b}'), 1, 'Array syntax not identified!');
$this->assertEquals(preg_match($pattern, '{a:b, c : d}'), 1, 'Array syntax not identified in case there are multiple properties!');
$this->assertEquals(preg_match($pattern, '{a : 123}'), 1, 'Array syntax not identified when a number is passed as argument!');
$this->assertEquals(preg_match($pattern, '{a:"String"}'), 1, 'Array syntax not identified in case of a double quoted string!');
$this->assertEquals(preg_match($pattern, '{a:\'String\'}'), 1, 'Array syntax not identified in case of a single quoted string!');

$expected = '{a:{bla:{x:z}, b: a}}';
preg_match($pattern, $expected, $match);
$this->assertEquals($match[0], $expected, 'If nested arrays appear, the string is not parsed correctly.');

$expected = '{a:"{bla{{}"}';
preg_match($pattern, $expected, $match);
$this->assertEquals($match[0], $expected, 'If nested strings with {} inside appear, the string is not parsed correctly.');
public function SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS_doesNotMatchInvalidSyntax($string) {
$success = preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS, $string, $matches) === 1;
$this->assertFalse($success);
}

/**
Expand All @@ -307,8 +360,8 @@ public function testSCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS() {
public function testSPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS() {
$pattern = Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS;

$source = '{a: b, e: {c:d, e:f}}';
preg_match_all($pattern, $source, $matches, PREG_SET_ORDER);
$source = '{a: b, e: {c:d, "e#":f, \'g\': "h"}}';
$success = preg_match_all($pattern, $source, $matches, PREG_SET_ORDER) > 0;

$expected = array(
0 => array(
Expand All @@ -323,9 +376,9 @@ public function testSPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS() {
4 => 'b'
),
1 => array(
0 => 'e: {c:d, e:f}',
'ArrayPart' => 'e: {c:d, e:f}',
1 => 'e: {c:d, e:f}',
0 => 'e: {c:d, "e#":f, \'g\': "h"}',
'ArrayPart' => 'e: {c:d, "e#":f, \'g\': "h"}',
1 => 'e: {c:d, "e#":f, \'g\': "h"}',
'Key' => 'e',
2 => 'e',
'QuotedString' => '',
Expand All @@ -334,11 +387,71 @@ public function testSPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS() {
4 => '',
'Number' => '',
5 => '',
'Subarray' => 'c:d, e:f',
6 => 'c:d, e:f'
'Subarray' => 'c:d, "e#":f, \'g\': "h"',
6 => 'c:d, "e#":f, \'g\': "h"'
)
);
$this->assertEquals($matches, $expected, 'The regular expression splitting the array apart does not work!');
$this->assertTrue($success);
$this->assertEquals($expected, $matches, 'The regular expression splitting the array apart does not work!');
}

/**
* @test
*/
public function SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS_matchesQuotedKeys() {
$pattern = Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS;

$source = '{"a": b, \'c\': d}';
$success = preg_match_all($pattern, $source, $matches, PREG_SET_ORDER) > 0;

$expected = array(
0 => array(
0 => '"a": b',
'ArrayPart' => '"a": b',
1 => '"a": b',
'Key' => '"a"',
2 => '"a"',
'QuotedString' => '',
3 => '',
'VariableIdentifier' => 'b',
4 => 'b'
),
1 => array(
0 => '\'c\': d',
'ArrayPart' => '\'c\': d',
1 => '\'c\': d',
'Key' => '\'c\'',
2 => '\'c\'',
'QuotedString' => '',
3 => '',
'VariableIdentifier' => 'd',
4 => 'd'
)
);
$this->assertTrue($success);
$this->assertEquals($expected, $matches, 'The regular expression splitting the array apart does not work!');
}

/**
* @return array
*/
public function dataProviderInvalidSPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS() {
return array(
array('string' => '{"a\': b}'),
array('string' => '{"": "bar"}'),
array('string' => '{\'\': "bar"}'),
);
}

/**
* @param string $string
* @dataProvider dataProviderInvalidSPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS()
* @test
*/
public function SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS_doesNotMatchInvalidSyntax($string) {
$pattern = Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS;
$success = preg_match_all($pattern, $string, $matches, PREG_SET_ORDER) > 0;
$this->assertFalse($success);
}

/**
Expand Down

0 comments on commit 2f1e815

Please sign in to comment.