Merge branch 'Mibew-improved_arguments_parsing'

This commit is contained in:
fzerorubigd 2014-09-07 22:01:51 +04:30
commit ec108c0bac
3 changed files with 290 additions and 46 deletions

View File

@ -0,0 +1,189 @@
<?php
/**
* This file is part of Handlebars-php
*
* PHP version 5.3
*
* @category Xamin
* @package Handlebars
* @author Dmitriy Simushev <simushevds@gmail.com>
* @copyright 2014 Authors
* @license MIT <http://opensource.org/licenses/MIT>
* @version GIT: $Id$
* @link http://xamin.ir
*/
namespace Handlebars;
/**
* Encapsulates helpers arguments.
*
* @category Xamin
* @package Handlebars
* @author Dmitriy Simushev <simushevds@gmail.com>
* @copyright 2014 Authors
* @license MIT <http://opensource.org/licenses/MIT>
* @version Release: @package_version@
* @link http://xamin.ir
*/
class Arguments
{
/**
* List of named arguments.
*
* @var array
*/
protected $namedArgs = array();
/**
* List of positional arguments.
*
* @var array
*/
protected $positionalArgs = array();
/**
* The original arguments string that was used to fill in arguments.
*
* @var string
*/
protected $originalString = '';
/**
* Class constructor.
*
* @param string $args_string Arguments string as passed to a helper.
*/
public function __construct($args_string = false)
{
$this->originalString = $args_string;
if ($args_string) {
$this->parse($args_string);
}
}
/**
* Returns string representation of the arguments list.
*
* This method is here mostly for backward compatibility reasons.
*
* @return string
*/
public function __toString()
{
return $this->originalString;
}
/**
* Returns a list of named arguments.
*
* @return array
*/
public function getNamedArguments()
{
return $this->namedArgs;
}
/**
* Returns a list of positional arguments.
*
* @return array
*/
public function getPositionalArguments()
{
return $this->positionalArgs;
}
/**
* Breaks an argument string into arguments and stores them inside the
* object.
*
* @param string $args_string Arguments string as passed to a helper.
*
* @return void
* @throws \InvalidArgumentException
*/
protected function parse($args_string)
{
$bad_chars = preg_quote(Context::NOT_VALID_NAME_CHARS, '#');
$bad_seg_chars = preg_quote(Context::NOT_VALID_SEGMENT_NAME_CHARS, '#');
$name_chunk = '(?:[^' . $bad_chars . '\s]+)|(?:\[[^' . $bad_seg_chars . ']+\])';
$variable_name = '(?:\.\.\/)*(?:(?:' . $name_chunk . ')[\.\/])*(?:' . $name_chunk . ')\.?';
$special_variable_name = '@[a-z]+';
$escaped_value = '(?:(?<!\\\\)".*?(?<!\\\\)"|(?<!\\\\)\'.*?(?<!\\\\)\')';
$argument_name = $name_chunk;
$argument_value = $variable_name . '|' . $escaped_value . '|' . $special_variable_name;
$positional_argument = '#^(' . $argument_value . ')#';
$named_argument = '#^(' . $argument_name . ')\s*=\s*(' . $argument_value . ')#';
$current_str = trim($args_string);
// Split arguments string
while (strlen($current_str) !== 0) {
if (preg_match($named_argument, $current_str, $matches)) {
// Named argument found
$name = $this->prepareArgumentName($matches[1]);
$value = $this->prepareArgumentValue($matches[2]);
$this->namedArgs[$name] = $value;
// Remove found argument from arguments string.
$current_str = ltrim(substr($current_str, strlen($matches[0])));
} elseif (preg_match($positional_argument, $current_str, $matches)) {
// A positional argument found. It cannot follow named arguments
if (count($this->namedArgs) !== 0) {
throw new \InvalidArgumentException('Positional arguments cannot follow named arguments');
}
$this->positionalArgs[] = $this->prepareArgumentValue($matches[1]);
// Remove found argument from arguments string.
$current_str = ltrim(substr($current_str, strlen($matches[0])));
} else {
throw new \InvalidArgumentException('Malformed arguments string');
}
}
}
/**
* Prepares argument's value to add to arguments list.
*
* The method unescapes value and wrap it into \Handlebars\String class if
* needed.
*
* @param string $value Argument's value
*
* @return string|\Handlebars\String
*/
protected function prepareArgumentValue($value)
{
// Check if argument's value is a quoted string literal
if ($value[0] == "'" || $value[0] == '"') {
// Remove enclosing quotes and unescape
$value = new String(stripcslashes(substr($value, 1, -1)));
}
return $value;
}
/**
* Prepares argument's name.
*
* Remove sections braces if needed.
*
* @param strign $name Argument's name
*
* @return string
*/
protected function prepareArgumentName($name)
{
// Check if argument's name is a segment
if ($name[0] == '[') {
$name = substr($name, 1, -1);
}
return $name;
}
}

View File

@ -590,38 +590,14 @@ class Template
*/
public function parseNamedArguments($string)
{
$variableName = '(?:(?:[^\'"\[\]\s]|\[.+?\])+)';
$escapedValue = '(?:(?<!\\\\)".*?(?<!\\\\)"|(?<!\\\\)\'.*?(?<!\\\\)\')';
// Get list of named arguemnts
$matches = array();
preg_match_all(
'#(' . $variableName . ')\s*=\s*(' . $escapedValue . '|' . $variableName . ')#',
$string,
$matches,
PREG_SET_ORDER
);
$args = array();
for ($x = 0, $c = count($matches); $x < $c; $x++) {
$name = $matches[$x][1];
$value = $matches[$x][2];
// Check if argument's name is a segment
if ($name[0] == '[') {
$name = substr($name, 1, -1);
}
// Check if argument's value is a quoted string literal
if ($value[0] == "'" || $value[0] == '"') {
// Remove enclosing quotes and unescape
$value = new \Handlebars\String(stripcslashes(substr($value, 1, -1)));
}
$args[$name] = $value;
if ($string instanceof Arguments) {
// This code is needed only for backward compatibility
$args = $string;
} else {
$args = new Arguments($string);
}
return $args;
return $args->getNamedArguments();
}
/**
@ -634,23 +610,13 @@ class Template
*/
public function parseArguments($string)
{
$args = array();
preg_match_all('#(?:[^\'"\[\]\s]|\[.+?\])+|(?<!\\\\)("|\')(?:[^\\\\]|\\\\.)*?\1|\S+#s', $string, $args);
$args = isset($args[0]) ? $args[0] : array();
for ($x = 0, $argc = count($args); $x < $argc; $x++) {
// check to see if argument is a quoted string literal
if ($args[$x][0] == "'" || $args[$x][0] == '"') {
if ($args[$x][0] === substr($args[$x], -1)) {
// remove enclosing quotes and unescape
$args[$x] = new \Handlebars\String(stripcslashes(substr($args[$x], 1, strlen($args[$x]) - 2)));
} else {
throw new \RuntimeException("Malformed string: " . $args);
}
}
if ($string instanceof Arguments) {
// This code is needed only for backward compatibility
$args = $string;
} else {
$args = new Arguments($string);
}
return $args;
return $args->getPositionalArguments();
}
}

View File

@ -858,6 +858,85 @@ class HandlebarsTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($args, $expected_array);
}
/**
* Test Combined Arguments Parser
*
* @param string $arg_string argument text
* @param $positional_args
* @param $named_args
*
* @dataProvider combinedArgumentsParserProvider
*
* @return void
*/
public function testCombinedArgumentsParser($arg_string, $positional_args, $named_args)
{
$args = new \Handlebars\Arguments($arg_string);
// Get the string version of the arguments array
$stringify = function ($a) {
return (string)$a;
};
if ($positional_args !== false) {
$this->assertEquals(
array_map($stringify, $args->getPositionalArguments()),
$positional_args
);
}
if ($named_args !== false) {
$this->assertEquals(
array_map($stringify, $args->getNamedArguments()),
$named_args
);
}
}
public function combinedArgumentsParserProvider()
{
$result = array();
// Use data provider for positional arguments parser
foreach ($this->argumentParserProvider() as $dataSet) {
$result[] = array(
$dataSet[0],
$dataSet[1],
false,
);
}
// Use data provider for named arguments parser
foreach ($this->namedArgumentParserProvider() as $dataSet) {
$result[] = array(
$dataSet[0],
false,
$dataSet[1],
);
}
// Add test cases with combined arguments
return array_merge(
$result,
array(
array(
'arg1 arg2 arg3=value1 arg4="value2"',
array('arg1', 'arg2'),
array('arg3' => 'value1', 'arg4' => 'value2')
),
array(
'@first arg=@last',
array('@first'),
array('arg' => '@last')
),
array(
'[seg arg1] [seg arg2] = [seg value "1"]',
array('[seg arg1]'),
array('seg arg2' => '[seg value "1"]')
)
)
);
}
public function stringLiteralInCustomHelperProvider()
{
return array(
@ -983,4 +1062,14 @@ class HandlebarsTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('good', $engine->render('{{#with b}}{{#if this}}{{../../a}}{{/if}}{{/with}}', array('a' => 'good', 'b' => 'stump')));
$this->assertEquals('good', $engine->render('{{#with b}}{{#unless false}}{{../../a}}{{/unless}}{{/with}}', array('a' => 'good', 'b' => 'stump')));
}
/**
* Test of Arguments to string conversion.
*/
public function testArgumentsString()
{
$argsString = 'foo bar [foo bar] baz="value"';
$args = new \Handlebars\Arguments($argsString);
$this->assertEquals($argsString, (string)$args);
}
}