diff --git a/h2o/datatype.php b/h2o/datatype.php index da174af..d1f7563 100644 --- a/h2o/datatype.php +++ b/h2o/datatype.php @@ -29,9 +29,70 @@ 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] @@ -39,28 +100,19 @@ static function or_($l, $r) { return ($l && $r); } # [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 */ diff --git a/h2o/nodes.php b/h2o/nodes.php index 9368ea6..b5cec93 100644 --- a/h2o/nodes.php +++ b/h2o/nodes.php @@ -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); } } @@ -81,4 +85,4 @@ function is_blank() { } -?> \ No newline at end of file +?> diff --git a/h2o/parser.php b/h2o/parser.php index 530d00f..067574d 100644 --- a/h2o/parser.php +++ b/h2o/parser.php @@ -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); @@ -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; } @@ -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; } @@ -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_-]*)*/'; @@ -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){ @@ -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)) diff --git a/h2o/tags.php b/h2o/tags.php index ea2fdbf..0af5c87 100644 --- a/h2o/tags.php +++ b/h2o/tags.php @@ -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'); @@ -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); diff --git a/spec/nodes_spec.php b/spec/nodes_spec.php index 5709fd0..870c273 100644 --- a/spec/nodes_spec.php +++ b/spec/nodes_spec.php @@ -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); + } } -?> \ No newline at end of file +?> diff --git a/spec/parser_spec.php b/spec/parser_spec.php index 9862655..f572530 100644 --- a/spec/parser_spec.php +++ b/spec/parser_spec.php @@ -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) { @@ -82,4 +83,4 @@ private function parse($string) { class Describe_Lexer extends SimpleSpec {} class Describe_Parser extends SimpleSpec {} -?> \ No newline at end of file +?> diff --git a/spec/tags_spec.php b/spec/tags_spec.php index bfa485d..124bf96 100644 --- a/spec/tags_spec.php +++ b/spec/tags_spec.php @@ -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'); + } } @@ -76,4 +81,4 @@ function should_return_nested_items() { } -?> \ No newline at end of file +?>