Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 73 additions & 21 deletions h2o/datatype.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,38 +29,90 @@ static function le($l, $r) { return $l <= $r; }
static function eq($l, $r) { return $l == $r; }
static function ne($l, $r) { return $l != $r; }

static function plus($l, $r) { return $l + $r; }
static function minus($l,$r) { return $l - $r; }
static function mul($l, $r) { return $l * $r; }
static function div($l, $r) { return $l / $r; }
static function mod($l, $r) { return $l % $r; }

static function not_($bool) { return !$bool; }
static function and_($l, $r) { return ($l && $r); }
static function or_($l, $r) { return ($l && $r); }
static function and_($l, $r) { return ($l and $r); }
static function or_($l, $r) { return ($l or $r); }

static $op_precedence;

static function higher_op_on_stack( $op, $stack ) {
if ( count($stack) < 1 ) return false;
else return Evaluator::$op_precedence[end($stack)] > Evaluator::$op_precedence[$op];
}

static function eval_op_stack( &$op_stack, &$number_stack, &$pos ) {
$op = array_pop($op_stack);
if ( $op == 'not' ) {
$l = array_pop($number_stack);
if ( is_null($l) ) throw new Exception("Not enough arguments to $op at $pos");
$pos = $pos-2;
return ! $l;
} else {
$r = array_pop($number_stack);
$l = array_pop($number_stack);
if ( is_null($r) or is_null($l) ) throw new Exception("Not enough arguments to $op at $pos");
$pos = $pos-3;
return call_user_func(array("Evaluator", $op), $l, $r);
}
}

static function eval_expression( $args, $context ) {
$op_stack = array();
$num_stack = array();
$expression_pos = 0; // Tracks position in the expression for better error reporting.
$back_track_pos = 0; // Tracks position in the expression when backtracking for better error reporting.
foreach( $args as $arg ) {
$expression_pos++;
if ( (is_array($arg) && isset($arg['operator'])) ) {
$back_track_pos = $expression_pos;
while ( Evaluator::higher_op_on_stack( $arg['operator'], $op_stack ) ) {
$val = Evaluator::eval_op_stack( $op_stack, $num_stack, $back_track_pos );
$num_stack[] = $val;
}
$op_stack[] = $arg['operator'];
} else if ( (is_array($arg) && isset($arg['parentheses'])) ) {
if ( $arg['parentheses'] === '(' ) $op_stack[] = '(';
else {
$back_track_pos = $expression_pos;
while( $op = array_pop( $op_stack ) ) {
if ( $op === '(' ) break;
$op_stack[] = $op;
$num_stack[] = Evaluator::eval_op_stack( $op_stack, $num_stack, $back_track_pos );
}
if ( is_null($op) ) throw new Exception("No opening paren for ')' at {$expression_pos}");
}
} else $num_stack[] = $context->resolve($arg);
}
$back_track_pos = $expression_pos;
while ( count( $op_stack ) > 0 ) $num_stack[] = Evaluator::eval_op_stack( $op_stack, $num_stack, $back_track_pos );
return array_pop($num_stack);
}

# Currently only support single expression with no preceddence ,no boolean expression
# [expression] = [optional binary] ? operant [ optional compare operant]
# [operant] = variable|string|numeric|boolean
# [compare] = > | < | == | >= | <=
# [binary] = not | !
static function exec($args, $context) {
$argc = count($args);
$first = array_shift($args);
$first = $context->resolve($first);
switch ($argc) {
case 1 :
return $first;
case 2 :
if (is_array($first) && isset($first['operator']) && $first['operator'] == 'not') {
$operant = array_shift($args);
$operant = $context->resolve($operant);
return !($operant);
}
case 3 :
list($op, $right) = $args;
$right = $context->resolve($right);
return call_user_func(array("Evaluator", $op['operator']), $first, $right);
default:
return false;
}
return Evaluator::eval_expression( $args, $context );
}
}

Evaluator::$op_precedence = array(
'(' => -1,
'not' => 0, 'or_'=>0, 'and_'=>0,
'eq' => 1, 'gt' => 1, 'lt' => 1, 'ge' => 1, 'le' => 1,
'mod' => 2,
'plus' => 3, 'minus' => 3,
'mul' => 4, 'div' => 4
);

/**
* $type of token, Block | Variable
*/
Expand Down
26 changes: 15 additions & 11 deletions h2o/nodes.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,23 @@ function getIterator() {
}

class VariableNode extends H2o_Node {
private $filters = array();
var $variable;

function __construct($variable, $filters, $position = 0) {
if (!empty($filters))
$this->filters = $filters;
$this->variable = $variable;
private $filters = array(), $expression = array();

function __construct($variable, $position = 0) {
$vlen = count($variable);
for($i=0;(! is_array($variable[$i]) || ! isset($variable[$i][0]) || $variable[$i][0] !== 'expression_end') &&
($variable[$i] !== 'expression_end') &&
($i<$vlen);$i++) {
$this->expression[] = $variable[$i];
}
$this->filters = (is_array($variable[$i]) && isset($variable[$i]['filters']) ) ? $variable[$i]['filters'] : array();
}

function render($context, $stream) {
$value = $context->resolve($this->variable);
$value = $context->escape($value, $this->variable);
$stream->write($value);
$exp_value = Evaluator::eval_expression($this->expression,$context);
$value = $context->applyFilters($exp_value, $this->filters);
$value = $context->escape($value, array('filters'=>$this->filters));
$stream->write($value);
}
}

Expand All @@ -81,4 +85,4 @@ function is_blank() {
}


?>
?>
24 changes: 19 additions & 5 deletions h2o/parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,7 @@ function &parse() {
break;
case 'variable' :
$args = H2o_Parser::parseArguments($token->content, $token->position);
$variable = array_shift($args);
$filters = $args;
$node = new VariableNode($variable, $filters, $token->position);
$node = new VariableNode($args, $token->position);
break;
case 'comment' :
$node = new CommentNode($token->content);
Expand Down Expand Up @@ -114,9 +112,14 @@ static function parseArguments($source = null, $fpos = 0){
$current_buffer = &$result;
$filter_buffer = array();
$tokens = $parser->parse();
$in_expression = true;
foreach ($tokens as $token) {
list($token, $data) = $token;
if ($token == 'filter_start') {
if ( $in_expression ) {
$in_expression = false;
$current_buffer[] = 'expression_end';
}
$filter_buffer = array();
$current_buffer = &$filter_buffer;
}
Expand Down Expand Up @@ -153,6 +156,13 @@ static function parseArguments($source = null, $fpos = 0){
elseif( $token == 'operator') {
$current_buffer[] = array('operator'=>$data);
}
elseif( $token == 'parentheses' ) {
$current_buffer[] = array('parentheses'=>$data);
}
}
if ( $in_expression ) {
$in_expression = false;
$current_buffer[] = 'expression_end';
}
return $result;
}
Expand All @@ -170,7 +180,7 @@ static function init() {
self::$boolean = '/true|false/';
self::$seperator = '/,/';
self::$pipe = '/\|/';
self::$operator = '/\s?(>|<|>=|<=|!=|==|!|and |not |or )\s?/i';
self::$operator = '/\s?(>|<|>=|<=|!=|==|!|\+|-|\*|\/|and |not |or |mod )\s?/i';
self::$number = '/\d+(\.\d*)?/';
self::$name = '/[a-zA-Z][a-zA-Z0-9-_]*(?:\.[a-zA-Z_0-9][a-zA-Z0-9_-]*)*/';

Expand All @@ -194,7 +204,8 @@ class ArgumentLexer {
private $match;
private $pos = 0, $fpos, $eos;
private $operator_map = array(
'!' => 'not', '!='=> 'ne', '==' => 'eq', '>' => 'gt', '<' => 'lt', '<=' => 'le', '>=' => 'ge'
'!' => 'not', '!='=> 'ne', '==' => 'eq', '>' => 'gt', '<' => 'lt', '<=' => 'le', '>=' => 'ge',
'*' => 'mul', '/' => 'div', '+' => 'plus', '-' => 'minus', 'or' => 'or_', 'and'=>'and_',
);

function __construct($source, $fpos = 0){
Expand All @@ -215,6 +226,9 @@ function parse(){
$operator = $this->operator_map[$operator];
$result[] = array('operator', $operator);
}
elseif ($this->scan(H2O_RE::$parentheses)) {
$result[] = array('parentheses',$this->match);
}
elseif ($this->scan(H2O_RE::$boolean))
$result[] = array('boolean', $this->match);
elseif ($this->scan(H2O_RE::$named_args))
Expand Down
6 changes: 4 additions & 2 deletions h2o/tags.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ class If_Tag extends H2o_Node {
private $negate;

function __construct($argstring, $parser, $position = 0) {
if (preg_match('/\s(and|or)\s/', $argstring))
throw new TemplateSyntaxError('H2o doesn\'t support multiple expressions');

$this->body = $parser->parse('endif', 'else');

Expand All @@ -74,6 +72,10 @@ function __construct($argstring, $parser, $position = 0) {

$this->args = H2o_Parser::parseArguments($argstring);

// Remove the 'expression_end' token at the end
array_pop($this->args);


$first = current($this->args);
if (isset($first['operator']) && $first['operator'] === 'not') {
array_shift($this->args);
Expand Down
24 changes: 23 additions & 1 deletion spec/nodes_spec.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,27 @@ function should_apply_filter_if_available() {
$result = h2o('{{ name|capitalize }}')->render(compact('name'));
expects($result)->should_be('Taylor Luk');
}

function should_correctly_parse_expressions() {
$pi = 3;
$result = h2o('{{ (pi+1)/2+10 }}')->render(compact('pi'));
expects($result)->should_be('12');

$except = false;
try {
$res = h2o('{{ ()pi+1)/2+10 }}')->render(compact('pi'));
} catch (Exception $e) {
$except = true;
}
expects($except)->should_be(true);

$except = false;
try {
$res = h2o('{{ +1 }}')->render(compact('pi'));
} catch (Exception $e) {
$except = true;
}
expects($except)->should_be(true);
}
}
?>
?>
19 changes: 10 additions & 9 deletions spec/parser_spec.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,21 @@ class Describe_Argument_Lexer extends SimpleSpec {
function should_parse_named_arguments() {
$result = $this->parse("something | filter 11, name: 'something', age: 18, var: variable, active: true");
$expected = array(
array(
':something', 'filters' => array(
array(':filter', 11, array('name' => "'something'", 'age' => 18, 'var' => ':variable', 'active'=>'true'))
':something',
array( 'expression_end',
'filters' => array(
array(':filter', 11, array('name' => "'something'", 'age' => 18, 'var' => ':variable', 'active'=>'true'))
)
)
)
);
expects($result)->should_be($expected);
}

function should_parse_variable_contains_operators() {
expects($this->parse("org"))->should_be(array(':org'));
expects($this->parse("dand"))->should_be(array(':dand'));
expects($this->parse("xor"))->should_be(array(':xor'));
expects($this->parse("notd"))->should_be(array(':notd'));
expects($this->parse("org"))->should_be(array(':org','expression_end'));
expects($this->parse("dand"))->should_be(array(':dand','expression_end'));
expects($this->parse("xor"))->should_be(array(':xor','expression_end'));
expects($this->parse("notd"))->should_be(array(':notd','expression_end'));
}

private function parse($string) {
Expand All @@ -82,4 +83,4 @@ private function parse($string) {
class Describe_Lexer extends SimpleSpec {}

class Describe_Parser extends SimpleSpec {}
?>
?>
7 changes: 6 additions & 1 deletion spec/tags_spec.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ function should_evaluate_boolean_expression() {
$results = h2o('{% if 4 > 3 %}yes{% endif %}')->render();
expects($results)->should_be('yes');
}

function should_evaluate_complex_boolean_expression() {
$results = h2o('{% if (4+7) > 5 and 7 == (2+1)*2+1 %}yes{% endif %}')->render();
expects($results)->should_be('yes');
}
}


Expand Down Expand Up @@ -76,4 +81,4 @@ function should_return_nested_items() {

}

?>
?>