From c4610a7778de325a42035e48cc0d710eb8298ab5 Mon Sep 17 00:00:00 2001 From: bzick Date: Mon, 22 Jul 2013 18:03:43 +0400 Subject: [PATCH] Update to 1.1 Read CHANGELOG.md#1.1.0 --- CHANGELOG.md | 32 ++ benchmark/run.php | 46 ++- benchmark/sandbox/fenom.php | 3 + benchmark/scripts/bootstrap.php | 99 +++--- benchmark/scripts/run.php | 21 +- docs/operators.md | 124 ++++++-- docs/tags/autoescape.md | 4 +- src/Fenom.php | 39 +-- src/Fenom/Compiler.php | 5 +- src/Fenom/Modifier.php | 12 +- src/Fenom/Scope.php | 11 +- src/Fenom/Template.php | 438 ++++++++++++++++++--------- src/Fenom/Tokenizer.php | 23 ++ tests/TestCase.php | 27 +- tests/cases/Fenom/AutoEscapeTest.php | 26 +- tests/cases/Fenom/TagsTest.php | 2 +- tests/cases/Fenom/TemplateTest.php | 125 +++++++- 17 files changed, 760 insertions(+), 277 deletions(-) create mode 100644 benchmark/sandbox/fenom.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b3b120..45e4b3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,38 @@ CHANGELOG ========= +## 1.1.0 + +- Bug #19: Bug with "if" expressions starting with "(" +- Bug #16: Allow modifiers for function calls +- Bug #25: Invalid option flag for `auto_reload` +- Bug: Invalid options for cached templates +- Bug: Removed memory leak after render +- Fix nested bracket pull #10 +- Fix bugs with provider +- Improve providers' performance +- Improve #1: Add `is` and `in` operator +- Remove Fenom::addTemplate(). Use providers for adding custom templates. +- Big refractory: parsers, providers, storage +- Improve tokenizer +- Internal optimization +- Add options for benchmark +- Add stress test (thanks to @klkvsk) +- Comments++ +- Docs++ +- Test++ + +## 1.0.8 + +- Perform auto_escape options +- Fix bugs +- Update documentation + +## 1.0.7 + +- Perform auto_escape options +- Fix bugs + ## 1.0.6 (2013-07-04) - Fix modifiers insertions diff --git a/benchmark/run.php b/benchmark/run.php index 6f5d6e0..ceff4ae 100644 --- a/benchmark/run.php +++ b/benchmark/run.php @@ -1,5 +1,35 @@ 0 +); + +extract($opt); + +if(isset($h) || isset($help)) { + echo " +Start: ".basename(__FILE__)." [--stress COUNT] [--auto_reload] [--cleanup] +Usage: ".basename(__FILE__)." [--help | -h] +"; + exit; +} + +Benchmark::$stress = intval($stress); +Benchmark::$auto_reload = isset($auto_reload); + exec("rm -rf ".__DIR__."/compile/*"); echo "Smarty3 vs Twig vs Fenom\n\n"; @@ -17,7 +47,6 @@ Benchmark::runs("fenom", 'echo/smarty.tpl', __DIR__.'/templates/echo/data.js //if(extension_loaded("phalcon")) { // Benchmark::runs("volt", 'echo/twig.tpl', __DIR__.'/templates/echo/data.json'); //} - echo "\nTesting 'foreach' of big array...\n"; Benchmark::runs("smarty3", 'foreach/smarty.tpl', __DIR__.'/templates/foreach/data.json'); @@ -36,11 +65,10 @@ Benchmark::runs("fenom", 'inheritance/smarty/b100.tpl', __DIR__.'/templates/for // Benchmark::runs("volt", 'inheritance/twig/b100.tpl', __DIR__.'/templates/foreach/data.json'); //} -echo "\nDone. Cleanup.\n"; -//passthru("rm -rf ".__DIR__."/compile/*"); -passthru("rm -f ".__DIR__."/templates/inheritance/smarty/*"); -passthru("rm -f ".__DIR__."/templates/inheritance/twig/*"); - -echo "\nSmarty3 vs Fenom (more details)\n\n"; - -echo "Coming soon\n"; \ No newline at end of file +echo "\nDone\n"; +if(isset($cleanup)) { + echo "Cleanup.\n"; + passthru("rm -rf ".__DIR__."/compile/*"); + passthru("rm -f ".__DIR__."/templates/inheritance/smarty/*"); + passthru("rm -f ".__DIR__."/templates/inheritance/twig/*"); +} diff --git a/benchmark/sandbox/fenom.php b/benchmark/sandbox/fenom.php new file mode 100644 index 0000000..f162d74 --- /dev/null +++ b/benchmark/sandbox/fenom.php @@ -0,0 +1,3 @@ +compile_check = false; + $smarty->compile_check = $auto_reload; $smarty->setTemplateDir(__DIR__.'/../templates'); $smarty->setCompileDir(__DIR__."/../compile/"); @@ -18,20 +21,28 @@ class Benchmark { } $start = microtime(true); - $smarty->assign($data); - $smarty->fetch($tpl); + if($stress) { + for($i=0; $i<$stress; $i++) { + $smarty->assign($data); + $smarty->fetch($tpl); + } + } else { + $smarty->assign($data); + $smarty->fetch($tpl); + } + return microtime(true) - $start; - printf(self::$t, __FUNCTION__, $message, round(microtime(true)-$start, 4), round(memory_get_peak_usage()/1024/1024, 2)); +// printf(self::$t, __FUNCTION__, $message, round(microtime(true)-$start, 4), round(memory_get_peak_usage()/1024/1024, 2)); } - public static function twig($tpl, $data, $double, $message) { + public static function twig($tpl, $data, $double, $stress = false, $auto_reload = false) { Twig_Autoloader::register(); $loader = new Twig_Loader_Filesystem(__DIR__.'/../templates'); $twig = new Twig_Environment($loader, array( 'cache' => __DIR__."/../compile/", 'autoescape' => false, - 'auto_reload' => false, + 'auto_reload' => $auto_reload, )); if($double) { @@ -39,45 +50,57 @@ class Benchmark { } $start = microtime(true); - $twig->loadTemplate($tpl)->render($data); - printf(self::$t, __FUNCTION__, $message, round(microtime(true)-$start, 4), round(memory_get_peak_usage()/1024/1024, 2)); + if($stress) { + for($i=0; $i<$stress; $i++) { + $twig->loadTemplate($tpl)->render($data); + } + } else { + $twig->loadTemplate($tpl)->render($data); + } + return microtime(true) - $start; } - public static function fenom($tpl, $data, $double, $message) { + public static function fenom($tpl, $data, $double, $stress = false, $auto_reload = false) { $fenom = Fenom::factory(__DIR__.'/../templates', __DIR__."/../compile"); - + if($auto_reload) { + $fenom->setOptions(Fenom::AUTO_RELOAD); + } if($double) { $fenom->fetch($tpl, $data); } - $_SERVER["t"] = $start = microtime(true); - $fenom->fetch($tpl, $data); - printf(self::$t, __FUNCTION__, $message, round(microtime(true)-$start, 4), round(memory_get_peak_usage()/1024/1024, 2)); - } - - public static function volt($tpl, $data, $double, $message) { - $view = new \Phalcon\Mvc\View(); - //$view->setViewsDir(__DIR__.'/../templates'); - $volt = new \Phalcon\Mvc\View\Engine\Volt($view); - - - $volt->setOptions(array( - "compiledPath" => __DIR__.'/../compile', - "compiledExtension" => __DIR__."/../.compile" - )); - - if($double) { - $volt->render($tpl, $data); - } - $start = microtime(true); - var_dump($tpl); - $volt->render(__DIR__.'/../templates/'.$tpl, $data); - printf(self::$t, __FUNCTION__, $message, round(microtime(true)-$start, 4), round(memory_get_peak_usage()/1024/1024, 2)); + if($stress) { + for($i=0; $i<$stress; $i++) { + $fenom->fetch($tpl, $data); + } + } else { + $fenom->fetch($tpl, $data); + } + return microtime(true) - $start; } +// public static function volt($tpl, $data, $double, $message) { +// $view = new \Phalcon\Mvc\View(); +// //$view->setViewsDir(__DIR__.'/../templates'); +// $volt = new \Phalcon\Mvc\View\Engine\Volt($view); +// +// +// $volt->setOptions(array( +// "compiledPath" => __DIR__.'/../compile' +// )); +// $tpl = __DIR__.'/../templates/'.$tpl; +// if($double) { +// $volt->render($tpl, $data); +// } +// +// $start = microtime(true); +// $volt->render($tpl, $data); +// printf(self::$t, __FUNCTION__, $message, round(microtime(true)-$start, 4), round(memory_get_peak_usage()/1024/1024, 2)); +// } + public static function run($engine, $template, $data, $double, $message) { - passthru(sprintf(PHP_BINARY." -dmemory_limit=512M -dxdebug.max_nesting_level=1024 %s/run.php --engine '%s' --template '%s' --data '%s' --message '%s' %s", __DIR__, $engine, $template, $data, $message, $double ? '--double' : '')); + passthru(sprintf(PHP_BINARY." -n -dextension=phalcon.so -ddate.timezone=Europe/Moscow -dmemory_limit=512M %s/run.php --engine '%s' --template '%s' --data '%s' --message '%s' %s --stress %d %s", __DIR__, $engine, $template, $data, $message, $double ? '--double' : '', self::$stress, self::$auto_reload ? '--auto_reload' : '')); } /** @@ -92,7 +115,3 @@ class Benchmark { echo "\n"; } } - -function t() { - if(isset($_SERVER["t"])) var_dump(round(microtime(1) - $_SERVER["t"], 4)); -} \ No newline at end of file diff --git a/benchmark/scripts/run.php b/benchmark/scripts/run.php index eb064a2..fdbdc1f 100644 --- a/benchmark/scripts/run.php +++ b/benchmark/scripts/run.php @@ -1,15 +1,32 @@ "plain", + "stress" => 0, +); + extract($opt); -Benchmark::$engine($template, json_decode(file_get_contents($data), true), isset($double), $message); \ No newline at end of file + +$time = Benchmark::$engine($template, json_decode(file_get_contents($data), true), isset($double), $stress, isset($auto_reload)); + +printf(Benchmark::OUTPUT, $engine, $message, round($time, 4), round(memory_get_peak_usage()/1024/1024, 2)); \ No newline at end of file diff --git a/docs/operators.md b/docs/operators.md index 58cb463..ad264a4 100644 --- a/docs/operators.md +++ b/docs/operators.md @@ -1,41 +1,92 @@ Operators ========= -### Math +### Arithmetic operators -Operators: `+ - / *` +* `$a + $b` - addition +* `$a - $b` - subtraction +* `$a * $b` - multiplication +* `$a / $b` - division +* `$a % $b` - modulus ```smarty {$a + $b * $c/$d - $e*5 + 1e3} ``` -### Boolean +### Logical operators -Operators: `|| && and or < > <= >= == === !== !=` +* `$a || $b` - or +* `$a && $b` - and +* `!$a` - not, unary operator +* `$a and $b` - and +* `$a or $b` - or +* `$a xor $b` - xor ```smarty -{if $a && $b >= 5 && $c != 3} {/if} +{if $b && $c} ... {/if} ``` -### Bitwize +### Comparison operators -Operators: `| & << >> |= &= <<= >>=` +* `$a < $b` - less than +* `$a > $b` - greater than +* `$a <= $b` - less than or equal to +* `$a >= $b` - greater than or equal to +* `$a == $b` - equal +* `$a === $b` - identical +* `$a !== $b` - not identical +* `$a != $b` - not equal +* `$a <> $b` - not equal ```smarty -{if $a & 1} {var $b |= $flags} {/if} +{if $b >= 5} ... {/if} ``` -### Unary +### Bitwise operators -Operators: `^ ~ - !` +* `$a | $b` - or +* `$a & $b` - and +* `$a ^ $b` - xor +* `~$a` - not, unary operator +* `$a << $b` - shift left +* `$a >> $b` - shift right ```smarty -{var $b |= $flags & ^$c} +{if $a & 1} {var $b = 4 | $flags} {/if} ``` -### Ternar +### Assignment Operators -Operators: `? :` +* `$a = $b` - assignment +* `$a += $b` - assignment with addition +* `$a -= $b` - assignment with subtraction +* `$a *= $b` - assignment with multiplication +* `$a /= $b` - assignment with division +* `$a %= $b` - assignment with modulus +* `$a &= $b` - assignment with bitwise And +* `$a |= $b` - assignment with bitwise or +* `$a ^= $b` - assignment with bitwise xor +* `$a <<= $b` - assignment with left shift +* `$a >>= $b` - assignment with right shift + + +```smarty +{var $b |= $flags} +``` + +### Incrementing/Decrementing operators + +* `++$a` - increment the variable and use it +* `$a++` - use the variable and increment it +* `--$a` - decrement the variable and use it +* `$a--` - use the variable and decrement it + +### Ternary operator + +* `$a ? $b : $c` - returns `$b` if `$a` is not empty, and `$c` otherwise +* `$a ! $b : $c` - returns `$b` if `$a` is set, and `$c` otherwise +* `$a ?: $c` - returns `$a` if `$a` is not empty, and `$c` otherwise +* `$a !: $c` - returns `$a` if `$a` is set, and `$c` otherwise ```smarty {var $a = true} @@ -44,24 +95,43 @@ Operators: `? :` {$a ? 5 : 10} {* outputs 10 *} ``` -### Variable operator +### Check operator + +* `$a?` - returns `TRUE` if `$a` is not empty +* `$a!` - returns `TRUE` if `$a` is set -Checking variable value ```smarty {if $a?} {* instead of {if !empty($a)} *} -``` - -Checking variable existence -```smarty {if $a!} {* instead of {if isset($a)} *} -``` - -Get default if variable is empty -```smarty {$a?:"some text"} {* instead of {if empty($a) ? "some text" : $a} *} -``` - -Get default if variable doesn't exist -```smarty {$a!:"some text"} {* instead of {if isset($a) ? $a : "some text"} *} ``` + +### Test operator + +Tests can be negated by using the `is not` operator. + +* `$a is $b` - $a identical $b +* `$a is integer` - test variable type. Type may be int/integer, bool/boolean, float/double/decimal, array, object, scalar, string, callback/callable, number/numeric. +* `$a is iterable` - test variable for iteration. +* `$a is template` - variable `$a` contain existing template name. +* `$a is empty` - checks if a variable is empty. +* `$a is set` - checks if a variable is set. +* `$a is even` - variable `$a` is even. +* `$a is odd` - variable `$a` is odd. +* `$a is MyClass` or `$a is \MyClass` - variable `$a` instance of `MyClass` class + +### Containment operator + +Tests can be negated by using the `not in` operator. + +* `$a in $b` - variable `$a` contains in `$b`, $b may be string, plain or assoc array. +* `$a in list $b` - variable `$a` contains in array `$b` as value +* `$a in keys $b` - variable `$a` contains in array `$b` as key +* `$a in string $b` - variable `$a` contains in string `$b` as substring + +```smarty +{'df' in 'abcdefg'} +{5 in [1, 5, 25, 125]} +{2 in keys [1, 5, 25, 125]} +``` \ No newline at end of file diff --git a/docs/tags/autoescape.md b/docs/tags/autoescape.md index d9b2dc9..1e0dde4 100644 --- a/docs/tags/autoescape.md +++ b/docs/tags/autoescape.md @@ -1,4 +1,4 @@ -Tag {autoescape} [RU] +Tag {autoescape} ===================== Force enable or disable `auto_escape` option for block area: @@ -11,4 +11,4 @@ Force enable or disable `auto_escape` option for block area: {/autoescape} ``` -Also see {raw} tag for +Also see {raw} tag and :raw tag option diff --git a/src/Fenom.php b/src/Fenom.php index cfcfc36..2d6413d 100644 --- a/src/Fenom.php +++ b/src/Fenom.php @@ -16,7 +16,7 @@ use Fenom\Template, * @author Ivan Shalganov */ class Fenom { - const VERSION = '1.0'; + const VERSION = '1.1'; /* Actions */ const INLINE_COMPILER = 1; @@ -26,16 +26,16 @@ class Fenom { const MODIFIER = 5; /* Options */ - const DENY_METHODS = 0x10; - const DENY_INLINE_FUNCS = 0x20; - const FORCE_INCLUDE = 0x40; - const AUTO_RELOAD = 0x80; - const FORCE_COMPILE = 0xF0; - const DISABLE_CACHE = 0x1F0; - const AUTO_ESCAPE = 0x200; - const FORCE_VERIFY = 0x400; - const AUTO_TRIM = 0x800; - const DENY_STATIC_METHODS = 0xF00; + const DENY_METHODS = 0x10; + const DENY_INLINE_FUNCS = 0x20; + const FORCE_INCLUDE = 0x40; + const AUTO_RELOAD = 0x80; + const FORCE_COMPILE = 0x100; + const AUTO_ESCAPE = 0x200; + const DISABLE_CACHE = 0x400; + const FORCE_VERIFY = 0x800; // reserved + const AUTO_TRIM = 0x1000; // reserved + const DENY_STATIC_METHODS = 0x2000; // reserved /* Default parsers */ const DEFAULT_CLOSE_COMPILER = 'Fenom\Compiler::stdClose'; @@ -49,14 +49,14 @@ class Fenom { * @see setOptions */ private static $_option_list = array( - "disable_methods" => self::DENY_METHODS, + "disable_methods" => self::DENY_METHODS, "disable_native_funcs" => self::DENY_INLINE_FUNCS, - "disable_cache" => self::DISABLE_CACHE, - "force_compile" => self::FORCE_COMPILE, - "auto_reload" => self::AUTO_RELOAD, - "force_include" => self::FORCE_INCLUDE, - "auto_escape" => self::AUTO_ESCAPE, - "force_verify" => self::FORCE_VERIFY + "disable_cache" => self::DISABLE_CACHE, + "force_compile" => self::FORCE_COMPILE, + "auto_reload" => self::AUTO_RELOAD, + "force_include" => self::FORCE_INCLUDE, + "auto_escape" => self::AUTO_ESCAPE, + "force_verify" => self::FORCE_VERIFY ); /** @@ -615,7 +615,8 @@ class Fenom { * @return Fenom\Template */ public function getTemplate($template, $options = 0) { - $key = dechex($this->_options | $options)."@".$template; + $options |= $this->_options; + $key = dechex($options)."@".$template; if(isset($this->_storage[ $key ])) { /** @var Fenom\Template $tpl */ $tpl = $this->_storage[ $key ]; diff --git a/src/Fenom/Compiler.php b/src/Fenom/Compiler.php index d27361b..f6d0b08 100644 --- a/src/Fenom/Compiler.php +++ b/src/Fenom/Compiler.php @@ -875,7 +875,7 @@ class Compiler { * @return string */ public static function tagRaw(Tokenizer $tokens, Template $tpl) { - $escape = $tpl->escape; + $escape = (bool)$tpl->escape; $tpl->escape = false; if($tokens->is(':')) { $func = $tokens->getNext(Tokenizer::MACRO_STRING); @@ -885,11 +885,12 @@ class Compiler { } elseif ($tag["type"] == \Fenom::BLOCK_FUNCTION) { $code = $tpl->parseAct($tokens); $tpl->getLastScope()->escape = false; + return $code; } else { throw new InvalidUsageException("Raw mode allow for expressions or functions"); } } else { - $code = $tpl->out($tpl->parseExp($tokens, true), false); + $code = $tpl->out($tpl->parseExp($tokens, true)); } $tpl->escape = $escape; return $code; diff --git a/src/Fenom/Modifier.php b/src/Fenom/Modifier.php index 9cccfa9..95b50b7 100644 --- a/src/Fenom/Modifier.php +++ b/src/Fenom/Modifier.php @@ -147,13 +147,15 @@ class Modifier { /** * - * @param $value - * @param $list + * @param mixed $value + * @param mixed $haystack * @return bool */ - public static function in($value, $list) { - if(is_array($list)) { - return in_array($value, $list); + public static function in($value, $haystack) { + if(is_array($haystack)) { + return in_array($value, $haystack) || array_key_exists($value, $haystack); + } elseif(is_string($haystack)) { + return strpos($haystack, $value) !== false; } return false; } diff --git a/src/Fenom/Scope.php b/src/Fenom/Scope.php index 889bdb5..3dda98a 100644 --- a/src/Fenom/Scope.php +++ b/src/Fenom/Scope.php @@ -29,7 +29,6 @@ class Scope extends \ArrayObject { private $_action; private $_body; private $_offset; - public $_global_escape = false; /** * Creating cope @@ -58,8 +57,7 @@ class Scope extends \ArrayObject { public function setFuncName($function) { $this["function"] = $function; $this->is_compiler = false; - $this->_global_escape = $this->tpl->escape; - $this->tpl->escape = false; + $this->escape = $this->tpl->escape; } /** @@ -108,9 +106,6 @@ class Scope extends \ArrayObject { * @return string */ public function close($tokenizer) { - if(!$this->is_compiler) { - $this->tpl->escape = $this->_global_escape; - } return call_user_func($this->_action["close"], $tokenizer, $this); } @@ -145,4 +140,8 @@ class Scope extends \ArrayObject { $this->cutContent(); $this->_body .= $new_content; } + + public function unEscapeContent() { + + } } \ No newline at end of file diff --git a/src/Fenom/Template.php b/src/Fenom/Template.php index b27b822..46f51e8 100644 --- a/src/Fenom/Template.php +++ b/src/Fenom/Template.php @@ -59,9 +59,10 @@ class Template extends Render { public $parents = array(); /** - * Escape output value + * Escape outputs value * @var bool */ +// public $escape = false; public $escape = false; public $_extends; public $_extended = false; @@ -90,6 +91,34 @@ class Template extends Render { private $_filter = array(); + private static $_checkers = array( + 'integer' => 'is_int(%s)', + 'int' => 'is_int(%s)', + 'float' => 'is_float(%s)', + 'double' => 'is_float(%s)', + 'decimal' => 'is_float(%s)', + 'string' => 'is_string(%s)', + 'bool' => 'is_bool(%s)', + 'boolean' => 'is_bool(%s)', + 'number' => 'is_numeric(%s)', + 'numeric' => 'is_numeric(%s)', + 'scalar' => 'is_scalar(%s)', + 'object' => 'is_object(%s)', + 'callable' => 'is_callable(%s)', + 'callback' => 'is_callable(%s)', + 'array' => 'is_array(%s)', + 'iterable' => '\Fenom\Modifier::isIterable(%s)', + 'const' => 'defined(%s)', + 'template' => '$this->getStorage()->templateExists(%s)', + 'empty' => 'empty(%s)', + 'set' => 'isset(%s)', + '_empty' => '!%s', // for none variable + '_set' => '(%s !== null)', // for none variable + 'odd' => '(%s & 1)', + 'even' => '!(%s %% 2)', + 'third' => '!(%s %% 3)' + ); + /** * Just factory * @@ -172,7 +201,7 @@ class Template extends Render { $this->_appendText(substr($this->_src, $pos, $start - $pos + 2)); $end = $start + 1; break; - case "*": // if comments + case "*": // comment block $end = strpos($this->_src, '*}', $start); // find end of the comment block if($end === false) { throw new CompileException("Unclosed comment block in line {$this->_line}", 0, 1, $this->_name, $this->_line); @@ -182,7 +211,6 @@ class Template extends Render { $comment = substr($this->_src, $start, $end - $start); // read the comment block for processing $this->_line += substr_count($comment, "\n"); // count lines in comments unset($comment); // cleanup -// $pos = $end + 1; break; default: $this->_appendText(substr($this->_src, $pos, $start - $pos)); @@ -208,22 +236,21 @@ class Template extends Render { if($tokens->isIncomplete()) { // all strings finished? $need_next_close_symbol = true; } else { - $this->_appendCode( $this->_tag($tokens) , $tag); // start the tag lexer + $this->_appendCode( $this->parseTag($tokens) , $tag); // start the tag lexer if($tokens->key()) { // if tokenizer have tokens - throws exceptions throw new CompileException("Unexpected token '".$tokens->current()."' in {$this} line {$this->_line}, near '{".$tokens->getSnippetAsString(0,0)."' <- there", 0, E_ERROR, $this->_name, $this->_line); } } } } while ($need_next_close_symbol); -// $pos = $end + 1; // move search-pointer to end of the tag unset($_tag, $tag); // cleanup break; } - $pos = $end + 1; // move search-pointer to end of the tag + $pos = $end + 1; // move search-pointer to end of the tag } gc_collect_cycles(); - $this->_appendText(substr($this->_src, $end ? $end + 1 : 0)); + $this->_appendText(substr($this->_src, $end ? $end + 1 : 0)); // append tail of the template if($this->_stack) { $_names = array(); $_line = 0; @@ -241,6 +268,7 @@ class Template extends Render { call_user_func_array($cb, array(&$this->_body, $this)); } } + $this->addDepend($this); // for 'verify' performance } /** @@ -303,7 +331,6 @@ class Template extends Render { * @param $source */ private function _appendCode($code, $source) { - if(!$code) { return; } else { @@ -384,22 +411,6 @@ class Template extends Render { $this->_depends[$tpl->getScm()][$tpl->getName()] = $tpl->getTime(); } - /** - * Execute template and return result as string - * @param array $values for template - * @throws CompileException - * @return string - */ - public function fetch(array $values) { - if(!$this->_code) { - eval("\$this->_code = ".$this->_getClosureSource().";"); - if(!$this->_code) { - throw new CompileException("Fatal error while creating the template"); - } - } - return parent::fetch($values); - } - /** * Output the value * @@ -414,14 +425,14 @@ class Template extends Render { } } /** - * Internal tags router + * Tag router * @param Tokenizer $tokens * * @throws SecurityException * @throws CompileException * @return string executable PHP code */ - private function _tag(Tokenizer $tokens) { + public function parseTag(Tokenizer $tokens) { try { if($tokens->is(Tokenizer::MACRO_STRING)) { if($tokens->current() === "ignore") { @@ -432,11 +443,11 @@ class Template extends Render { return $this->parseAct($tokens); } } elseif ($tokens->is('/')) { - return $this->_end($tokens); + return $this->parseEndTag($tokens); } elseif ($tokens->is('#')) { - return $this->out($this->parseConst($tokens), $tokens).';'; + return $this->out($this->parseConst($tokens), $tokens); } else { - return $code = $this->out($this->parseExp($tokens), $tokens).";"; + return $this->out($this->parseExp($tokens), $tokens); } } catch (InvalidUsageException $e) { throw new CompileException($e->getMessage()." in {$this} line {$this->_line}", 0, E_ERROR, $this->_name, $this->_line, $e); @@ -454,7 +465,7 @@ class Template extends Render { * @return string * @throws TokenizeException */ - private function _end(Tokenizer $tokens) { + public function parseEndTag(Tokenizer $tokens) { $name = $tokens->getNext(Tokenizer::MACRO_STRING); $tokens->next(); if(!$this->_stack) { @@ -469,7 +480,7 @@ class Template extends Render { return $scope->close($tokens); } else { $code = $this->out($scope->close($tokens)); - $scope->tpl->escape = $scope->escape; + $scope->tpl->escape = $scope->escape; // restore escape option return $code; } } @@ -495,13 +506,14 @@ class Template extends Render { if($tokens->is(Tokenizer::MACRO_STRING)) { $action = $tokens->getAndNext(); } else { - return $this->out($this->parseExp($tokens), $tokens).';'; // may be math and/or boolean expression + return $this->out($this->parseExp($tokens)); // may be math and/or boolean expression + } + if($tokens->is("(", T_NAMESPACE, T_DOUBLE_COLON) && !$tokens->isWhiteSpaced()) { // just invoke function or static method + $tokens->back(); + return $this->out($this->parseExp($tokens)); } - if($tokens->is("(", T_NAMESPACE, T_DOUBLE_COLON)) { // just invoke function or static method - $tokens->back(); - return $this->out($this->parseExp($tokens), $tokens).";"; - } elseif($tokens->is('.')) { + if($tokens->is('.')) { $name = $tokens->skip()->get(Tokenizer::MACRO_STRING); if($action !== "macro") { $name = $action.".".$name; @@ -526,7 +538,8 @@ class Template extends Render { $scope = new Scope($action, $this, $this->_line, $act, count($this->_stack), $this->_body); $scope->setFuncName($act["function"]); array_push($this->_stack, $scope); - $scope->escape = $this->_options & Fenom::AUTO_ESCAPE; + $scope->escape = $this->escape; + $this->escape = false; return $scope->open($tokens); default: throw new \LogicException("Unknown function type"); @@ -557,63 +570,91 @@ class Template extends Render { * @return string */ public function parseExp(Tokenizer $tokens, $required = false) { - $_exp = ""; - $brackets = 0; - $term = false; - $cond = false; + $_exp = array(); // expression as PHP code + $term = false; // last item was variable or value. + // 0 - was operator, but trem required + // false - was operator or no one term + // true - was trem + // 1 - term is strict varaible + $cond = false; // last item was operator while($tokens->valid()) { - if(!$term && $tokens->is(Tokenizer::MACRO_SCALAR, '"', '`', T_ENCAPSED_AND_WHITESPACE)) { - $_exp .= $this->parseScalar($tokens, true); - $term = 1; - } elseif(!$term && $tokens->is(T_VARIABLE)) { - - $pp = $tokens->isPrev(Tokenizer::MACRO_INCDEC); - $_exp .= $this->parseVariable($tokens, 0, $only_var); - if($only_var && !$pp) { - $term = 2; + if(!$term && $tokens->is(Tokenizer::MACRO_SCALAR, '"', '`', T_ENCAPSED_AND_WHITESPACE)) { // like quoted string + $_exp[] = $this->parseScalar($tokens, true); + $term = true; + } elseif(!$term && $tokens->is(Tokenizer::MACRO_INCDEC)) { // like variable + $_exp[] = $this->parseVariable($tokens); + $term = true; + } elseif(!$term && $tokens->is(T_VARIABLE)) { // like variable too + $var = $this->parseVar($tokens); + if($tokens->is(Tokenizer::MACRO_EQUALS)) { + $_exp[] = $var; + if($tokens->isLast()) { + break; + } + $_exp[] = $tokens->getAndNext(); + $term = 0; + } elseif($tokens->is(Tokenizer::MACRO_INCDEC, "|", "!", "?", '(')) { + $_exp[] = $this->parseVariable($tokens, 0, $var); + $term = true; } else { + $_exp[] = $var; $term = 1; } - } elseif(!$term && $tokens->is('#')) { + } elseif(!$term && $tokens->is("(")) { // open bracket + $tokens->next(); + $_exp[] = "(".$this->parseExp($tokens, true).")"; + $tokens->get(")"); + $tokens->next(); $term = 1; - $_exp .= $this->parseConst($tokens); - } elseif(!$term && $tokens->is("(")) { - $_exp .= $tokens->getAndNext(); - $brackets++; - $term = false; - } elseif($term && $tokens->is(")")) { - if(!$brackets) { - break; + } elseif($tokens->is(T_STRING)) { + if($term) { // parse 'in' or 'is' operators + if(!$_exp) { + break; + } + $operator = $tokens->current(); + if($operator == "is") { + $item = array_pop($_exp); + $_exp[] = $this->parseIs($tokens, $item, $term === 1); + } elseif($operator == "in" || ($operator == "not" && $tokens->isNextToken("in"))) { + $item = array_pop($_exp); + $_exp[] = $this->parseIn($tokens, $item, $term === 1); + } else { + break; + } + } else { // function or special value + if($tokens->isSpecialVal()) { + $_exp[] = $tokens->getAndNext(); + } elseif($tokens->isNext("(") && !$tokens->getWhitespace()) { + $func = $this->_fenom->getModifier($tokens->current()); + $tokens->next(); + $func = $func.$this->parseArgs($tokens); + if($tokens->is('|')) { + $_exp[] = $this->parseModifier($tokens, $func); + } else { + $_exp[] = $func; + } + } else { + break; + } + $term = true; } - $brackets--; - $_exp .= $tokens->getAndNext(); - $term = 1; - } elseif(!$term && $tokens->is(T_STRING)) { - if($tokens->isSpecialVal()) { - $_exp .= $tokens->getAndNext(); - } elseif($tokens->isNext("(")) { - $func = $this->_fenom->getModifier($tokens->current()); - $tokens->next(); - $_exp .= $func.$this->parseArgs($tokens); - } else { - break; - } - $term = 1; - } elseif(!$term && $tokens->is(T_ISSET, T_EMPTY)) { - $_exp .= $tokens->getAndNext(); + } elseif(!$term && $tokens->is(T_ISSET, T_EMPTY)) { // empty and isset operators + $func = $tokens->getAndNext(); if($tokens->is("(") && $tokens->isNext(T_VARIABLE)) { - $_exp .= $this->parseArgs($tokens); + $tokens->next(); + $_exp[] = $func."(".$this->parseVar($tokens).")"; + $tokens->need(')')->next(); } else { throw new TokenizeException("Unexpected token ".$tokens->getNext().", isset() and empty() accept only variables"); } - $term = 1; - } elseif(!$term && $tokens->is(Tokenizer::MACRO_UNARY)) { + $term = true; + } elseif(!$term && $tokens->is(Tokenizer::MACRO_UNARY)) { // like unary operator, see Tokenizer::MACRO_UNARY if(!$tokens->isNext(T_VARIABLE, T_DNUMBER, T_LNUMBER, T_STRING, T_ISSET, T_EMPTY)) { break; } - $_exp .= $tokens->getAndNext(); - $term = 0; - } elseif($tokens->is(Tokenizer::MACRO_BINARY)) { + $_exp[] = $tokens->getAndNext(); + $term = false; + } elseif($tokens->is(Tokenizer::MACRO_BINARY)) { // like binary operator, see Tokenizer::MACRO_BINARY if(!$term) { throw new UnexpectedTokenException($tokens); } @@ -628,37 +669,19 @@ class Template extends Render { } elseif ($tokens->is(Tokenizer::MACRO_BOOLEAN)) { $cond = false; } - $_exp .= " ".$tokens->getAndNext()." "; + $_exp[] = " ".$tokens->getAndNext()." "; $term = 0; - } elseif($tokens->is(Tokenizer::MACRO_INCDEC)) { - if($term === 2) { - $term = 1; - } elseif(!$tokens->isNext(T_VARIABLE)) { - break; - } - $_exp .= $tokens->getAndNext(); - } elseif($term && !$cond && !$tokens->isLast()) { - if($tokens->is(Tokenizer::MACRO_EQUALS) && $term === 2) { - $_exp .= ' '.$tokens->getAndNext().' '; - $term = 0; - } else { - break; - } } else { break; } } - if($term === 0) { throw new UnexpectedTokenException($tokens); } - if($brackets) { - throw new TokenizeException("Brackets don't match"); - } - if($required && $_exp === "") { + if($required && !$_exp) { throw new UnexpectedTokenException($tokens); } - return $_exp; + return implode('',$_exp); } /** @@ -678,12 +701,8 @@ class Template extends Render { if($tokens->is(T_VARIABLE)) { $key = "[ ".$this->parseVariable($tokens, self::DENY_ARRAY)." ]"; } elseif($tokens->is(Tokenizer::MACRO_STRING)) { - if($tokens->isNext("(")) { - $key = "[".$this->parseExp($tokens)."]"; - } else { - $key = '["'.$key.'"]'; - $tokens->next(); - } + $key = '["'.$key.'"]'; + $tokens->next(); } elseif($tokens->is(Tokenizer::MACRO_SCALAR, '"')) { $key = "[".$this->parseScalar($tokens, false)."]"; } else { @@ -707,54 +726,73 @@ class Template extends Render { $_var .= $key; } elseif($t === T_DNUMBER) { $_var .= '['.substr($tokens->getAndNext(), 1).']'; + } elseif($t === T_OBJECT_OPERATOR) { + $_var .= "->".$tokens->getNext(T_STRING); + $tokens->next(); } else { break; } } - return $_var; + if($this->_options & Fenom::FORCE_VERIFY) { + return 'isset('.$_var.') ? '.$_var.' : null'; + } else { + return $_var; + } } /** - * Parse variable + * Parse complex variable * $var.foo[bar]["a"][1+3/$var]|mod:3:"w":$var3|mod3 + * ++$var|mod + * $var--|mod * * @see parseModifier * @static * @param Tokenizer $tokens - * @param int $deny set limitations - * @param bool $pure_var will be FALSE if variable modified + * @param int $options set parser options + * @param string $var already parsed plain variable * @throws \LogicException - * @throws UnexpectedTokenException + * @throws InvalidUsageException * @return string */ - public function parseVariable(Tokenizer $tokens, $deny = 0, &$pure_var = true) { - $_var = $this->parseVar($tokens, $deny); - $pure_var = true; - while($t = $tokens->key()) { - if($t === "|" && !($deny & self::DENY_MODS)) { - $pure_var = false; - return $this->parseModifier($tokens, $_var); - } elseif($t === T_OBJECT_OPERATOR) { - $prop = $tokens->getNext(T_STRING); - if($tokens->isNext("(")) { - if($this->_options & Fenom::DENY_METHODS) { - throw new \LogicException("Forbidden to call methods"); - } - $pure_var = false; - $tokens->next(); - $_var .= '->'.$prop.$this->parseArgs($tokens); - } else { - $tokens->next(); - $_var .= '->'.$prop; - } - } elseif($t === "?" || $t === "!") { - $pure_var = false; - return $this->parseTernary($tokens, $_var, $t); + public function parseVariable(Tokenizer $tokens, $options = 0, $var = null) { + $stained = false; + if(!$var) { + if($tokens->is(Tokenizer::MACRO_INCDEC)) { + $stained = true; + $var = $tokens->getAndNext().$this->parseVar($tokens, $options); } else { - break; + $var = $this->parseVar($tokens, $options); + } + if($tokens->is(T_OBJECT_OPERATOR)) { // parse + $var .= '->'.$tokens->getNext(T_STRING); + $tokens->next(); } } - return $_var; + + if($tokens->is("(") && $tokens->hasBackList(T_STRING, T_OBJECT_OPERATOR)) { + if($stained) { + throw new InvalidUsageException("Can not increment or decrement of the method result"); + } + if($this->_options & Fenom::DENY_METHODS) { + throw new \LogicException("Forbidden to call methods"); + } + $var .= $this->parseArgs($tokens); + $stained = true; + } + if($tokens->is('?', '!')) { + return $this->parseTernary($tokens, $var, $tokens->current()); + } + if($tokens->is(Tokenizer::MACRO_INCDEC)) { + if($stained) { + throw new InvalidUsageException("Can not use two increments and/or decrements for one variable"); + } + $var .= $tokens->getAndNext(); + } + if($tokens->is('|') && !($options & self::DENY_MODS)) { + return $this->parseModifier($tokens, $var); + } + return $var; } /** @@ -796,6 +834,127 @@ class Template extends Render { } } + /** + * Parse 'is' and 'is not' operator + * @see $_checkers + * @param Tokenizer $tokens + * @param string $value + * @param bool $variable + * @throws InvalidUsageException + * @return string + */ + public function parseIs(Tokenizer $tokens, $value, $variable = false) { + $tokens->next(); + if($tokens->current() == 'not'){ + $invert = '!'; + $equal = '!='; + $tokens->next(); + } else { + $invert = ''; + $equal = '=='; + } + if($tokens->is(Tokenizer::MACRO_STRING)) { + $action = $tokens->current(); + if(!$variable && ($action == "set" || $action == "empty")) { + $action = "_$action"; + $tokens->next(); + return $invert.sprintf(self::$_checkers[$action], $value); + } elseif(isset(self::$_checkers[$action])) { + $tokens->next(); + return $invert.sprintf(self::$_checkers[$action], $value); + } elseif($tokens->isSpecialVal()) { + $tokens->next(); + return '('.$value.' '.$equal.'= '.$action.')'; + } + return $invert.'('.$value.' instanceof \\'.$this->parseName($tokens).')'; + } elseif($tokens->is(T_VARIABLE)) { + return '('.$value.' '.$equal.'= '.$this->parseVariable($tokens).')'; + } elseif($tokens->is(Tokenizer::MACRO_SCALAR)) { + return '('.$value.' '.$equal.'= '.$this->parseScalar($tokens).')'; + } elseif($tokens->is('[')) { + return '('.$value.' '.$equal.'= '.$this->parseArray($tokens).')'; + } elseif($tokens->is(T_NS_SEPARATOR)) { // + return $invert.'('.$value.' instanceof \\'.$this->parseName($tokens).')'; + } else { + throw new InvalidUsageException("Unknown argument"); + } + } + + /** + * Parse 'in' and 'not in' operators + * @param Tokenizer $tokens + * @param string $value + * @throws InvalidUsageException + * @throws UnexpectedTokenException + * @return string + */ + public function parseIn(Tokenizer $tokens, $value) { + $checkers = array( + "string" => 'is_int(strpos(%2$s, %1$s))', + "list" => "in_array(%s, %s)", + "keys" => "array_key_exists(%s, %s)", + "auto" => '\Fenom\Modifier::in(%s, %s)' + ); + $checker = null; + $invert = ''; + if($tokens->current() == 'not'){ + $invert = '!'; + $tokens->next(); + } + if($tokens->current() !== "in") { + throw new UnexpectedTokenException($tokens); + } + $tokens->next(); + if($tokens->is(Tokenizer::MACRO_STRING)) { + $checker = $tokens->current(); + if(!isset($checkers[$checker])) { + throw new UnexpectedTokenException($tokens); + } + $tokens->next(); + } + if($tokens->is('[')) { + if($checker == "string") { + throw new InvalidUsageException("Can not use string operation for array"); + } elseif(!$checker) { + $checker = "list"; + } + return $invert.sprintf($checkers[$checker], $value, $this->parseArray($tokens)); + } elseif($tokens->is('"', T_ENCAPSED_AND_WHITESPACE, T_CONSTANT_ENCAPSED_STRING)) { + if(!$checker) { + $checker = "string"; + } elseif($checker != "string") { + throw new InvalidUsageException("Can not use array operation for string"); + } + return $invert.sprintf($checkers[$checker], "strval($value)", $this->parseScalar($tokens)); + } elseif($tokens->is(T_VARIABLE, Tokenizer::MACRO_INCDEC)) { + if(!$checker) { + $checker = "auto"; + } + return $invert.sprintf($checkers[$checker], $value, $this->parseVariable($tokens)); + } else { + throw new UnexpectedTokenException($tokens); + } + } + + /** + * Parse method, class or constant name + * + * @param Tokenizer $tokens + * @return string + */ + public function parseName(Tokenizer $tokens) { + $tokens->skipIf(T_NS_SEPARATOR); + $name = ""; + if($tokens->is(T_STRING)) { + $name .= $tokens->getAndNext(); + while($tokens->is(T_NS_SEPARATOR)) { + $name .= '\\'.$tokens->next()->get(T_STRING); + $tokens->next(); + } + } + return $name; + } + /** * Parse scalar values * @@ -919,7 +1078,7 @@ class Template extends Render { } elseif($tokens->is('"', '`', T_ENCAPSED_AND_WHITESPACE)) { $args[] = $this->parseSubstr($tokens); } elseif($tokens->is('(')) { - $args[] = $this->parseExp($tokens); + $args[] = $this->parseExp($tokens, true); } elseif($tokens->is('[')) { $args[] = $this->parseArray($tokens); } elseif($tokens->is(T_STRING) && $tokens->isNext('(')) { @@ -929,7 +1088,6 @@ class Template extends Render { } } - if(!is_string($mods)) { // dynamic modifier $mods = 'call_user_func($tpl->getStorage()->getModifier("'.$modifier_name.'"), '; } else { diff --git a/src/Fenom/Tokenizer.php b/src/Fenom/Tokenizer.php index bc3556d..9c2aedf 100644 --- a/src/Fenom/Tokenizer.php +++ b/src/Fenom/Tokenizer.php @@ -284,6 +284,13 @@ class Tokenizer { return $this->current(); } + /** + * @param $token + * @return bool + */ + public function isNextToken($token) { + return $this->next ? $this->next[1] == $token : false; + } /** * Return substring. This method doesn't move pointer. @@ -374,6 +381,22 @@ class Tokenizer { return $this; } + /** + * @param $token1 + * @return bool + */ + public function hasBackList($token1 /*, $token2 ...*/) { + $tokens = func_get_args(); + $c = $this->p; + foreach($tokens as $token) { + $c--; + if($c < 0 || $this->tokens[$c][0] !== $token) { + return false; + } + } + return true; + } + /** * Lazy load properties * diff --git a/tests/TestCase.php b/tests/TestCase.php index e88ea3a..f021df8 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -9,20 +9,45 @@ class TestCase extends \PHPUnit_Framework_TestCase { public $fenom; public $values = array( - "one" => 1, + "zero" => 0, + "one" => 1, "two" => 2, "three" => 3, + "float" => 4.5, + "bool" => true, + 0 => "empty value", 1 => "one value", 2 => "two value", 3 => "three value", ); + public static function getVars() { + return array( + "zero" => 0, + "one" => 1, + "two" => 2, + "three" => 3, + "float" => 4.5, + "bool" => true, + "obj" => new \StdClass, + "list" => array( + "a" => 1, + "b" => 2 + ), + 0 => "empty value", + 1 => "one value", + 2 => "two value", + 3 => "three value", + ); + } + public function setUp() { if(!file_exists(FENOM_RESOURCES.'/compile')) { mkdir(FENOM_RESOURCES.'/compile', 0777, true); } else { FS::clean(FENOM_RESOURCES.'/compile/'); } + $this->fenom = Fenom::factory(FENOM_RESOURCES.'/template', FENOM_RESOURCES.'/compile'); $this->fenom->addModifier('dots', __CLASS__.'::dots'); $this->fenom->addModifier('concat', __CLASS__.'::concat'); diff --git a/tests/cases/Fenom/AutoEscapeTest.php b/tests/cases/Fenom/AutoEscapeTest.php index 2b6840e..5044335 100644 --- a/tests/cases/Fenom/AutoEscapeTest.php +++ b/tests/cases/Fenom/AutoEscapeTest.php @@ -13,27 +13,35 @@ class AutoEscapeTest extends TestCase { ); return array( // variable - array('{$html}', $html, $vars, 0), - array('{$html}', $escaped, $vars, \Fenom::AUTO_ESCAPE), - array('{raw $html}', $html, $vars, \Fenom::AUTO_ESCAPE), + array('{$html}, {$html}', "$html, $html", $vars, 0), + array('{$html}, {$html}', "$escaped, $escaped", $vars, \Fenom::AUTO_ESCAPE), array('{raw $html}, {$html}', "$html, $escaped", $vars, \Fenom::AUTO_ESCAPE), - array('{raw "{$html|up}"}', strtoupper($html), $vars, \Fenom::AUTO_ESCAPE), + array('{raw $html}, {$html}', "$html, $escaped", $vars, \Fenom::AUTO_ESCAPE), + array('{raw "{$html|up}"}, {$html}', strtoupper($html).", $escaped", $vars, \Fenom::AUTO_ESCAPE), array('{autoescape true}{$html}{/autoescape}, {$html}', "$escaped, $html", $vars, 0), array('{autoescape false}{$html}{/autoescape}, {$html}', "$html, $escaped", $vars, \Fenom::AUTO_ESCAPE), array('{autoescape true}{$html}{/autoescape}, {$html}', "$escaped, $escaped", $vars, \Fenom::AUTO_ESCAPE), array('{autoescape false}{$html}{/autoescape}, {$html}', "$html, $html", $vars, 0), + array('{autoescape true}{raw $html}{/autoescape}, {$html}', "$html, $html", $vars, 0), + array('{autoescape false}{raw $html}{/autoescape}, {$html}', "$html, $escaped", $vars, \Fenom::AUTO_ESCAPE), + array('{autoescape true}{raw $html}{/autoescape}, {$html}', "$html, $escaped", $vars, \Fenom::AUTO_ESCAPE), + array('{autoescape false}{raw $html}{/autoescape}, {$html}', "$html, $html", $vars, 0), // inline function - array('{test_function text=$html}', $html, $vars, 0), - array('{test_function text=$html}', $escaped, $vars, \Fenom::AUTO_ESCAPE), - array('{raw:test_function text=$html}', $html, $vars, \Fenom::AUTO_ESCAPE), - array('{raw:test_function text="{$html|up}"}', strtoupper($html), $vars, \Fenom::AUTO_ESCAPE), + array('{test_function text=$html}, {$html}', "$html, $html", $vars, 0), + array('{test_function text=$html}, {$html}', "$escaped, $escaped", $vars, \Fenom::AUTO_ESCAPE), + array('{raw:test_function text=$html}, {$html}', "$html, $escaped", $vars, \Fenom::AUTO_ESCAPE), + array('{raw:test_function text="{$html|up}"}, {$html}', strtoupper($html).", $escaped", $vars, \Fenom::AUTO_ESCAPE), array('{autoescape true}{test_function text=$html}{/autoescape}, {test_function text=$html}', "$escaped, $html", $vars, 0), array('{autoescape false}{test_function text=$html}{/autoescape}, {test_function text=$html}', "$html, $escaped", $vars, \Fenom::AUTO_ESCAPE), array('{autoescape true}{test_function text=$html}{/autoescape}, {test_function text=$html}', "$escaped, $escaped", $vars, \Fenom::AUTO_ESCAPE), array('{autoescape false}{test_function text=$html}{/autoescape}, {test_function text=$html}', "$html, $html", $vars, 0), + array('{autoescape true}{raw:test_function text=$html}{/autoescape}, {test_function text=$html}', "$html, $html", $vars, 0), + array('{autoescape false}{raw:test_function text=$html}{/autoescape}, {test_function text=$html}', "$html, $escaped", $vars, \Fenom::AUTO_ESCAPE), + array('{autoescape true}{raw:test_function text=$html}{/autoescape}, {test_function text=$html}', "$html, $escaped", $vars, \Fenom::AUTO_ESCAPE), + array('{autoescape false}{raw:test_function text=$html}{/autoescape}, {test_function text=$html}', "$html, $html", $vars, 0), - // block function. Has bug, disable for vacation + // block function. Have bugs // array('{test_block_function}{$html}{/test_block_function}', $html, $vars, 0), // array('{test_block_function}{$html}{/test_block_function}', $escaped, $vars, \Fenom::AUTO_ESCAPE), // array('{raw:test_block_function}{$html}{/test_block_function}', $html, $vars, \Fenom::AUTO_ESCAPE), diff --git a/tests/cases/Fenom/TagsTest.php b/tests/cases/Fenom/TagsTest.php index 61ada8c..111483d 100644 --- a/tests/cases/Fenom/TagsTest.php +++ b/tests/cases/Fenom/TagsTest.php @@ -7,7 +7,7 @@ class TagsTest extends TestCase { public function _testSandbox() { try { - var_dump($this->fenom->compileCode('{for $i=0 to=5}{cycle ["one", "two"]}, {/for}')->getBody()); + var_dump($this->fenom->compileCode('{var $a=Fenom\TestCase::dots("asd")}')->getBody()); } catch(\Exception $e) { echo "$e"; } diff --git a/tests/cases/Fenom/TemplateTest.php b/tests/cases/Fenom/TemplateTest.php index c0c7c5f..e61e935 100644 --- a/tests/cases/Fenom/TemplateTest.php +++ b/tests/cases/Fenom/TemplateTest.php @@ -11,10 +11,16 @@ use Fenom\Template, */ class TemplateTest extends TestCase { + public function setUp() { + parent::setUp(); + $this->tpl('welcome.tpl', 'Welcome, {$username} ({$email})'); + } + public static function providerVars() { $a = array("a" => "World"); $obj = new \stdClass; $obj->name = "Object"; + $obj->list = $a; $obj->c = "c"; $b = array("b" => array("c" => "Username", "c_char" => "c", "mcp" => "Master", 'm{$c}p' => "Unknown", 'obj' => $obj), "c" => "c"); $c = array_replace_recursive($b, array("b" => array(3 => $b["b"], 4 => "Mister"))); @@ -51,16 +57,20 @@ class TemplateTest extends TestCase { array('hello, {$b[ "m{$b.c_char}p" ]} and {$b.3[$b.c_char]}!', $c, 'hello, Master and Username!'), array('hello, {$b.obj->name}!', $c, 'hello, Object!'), + array('hello, {$b.obj->list.a}!', $c, 'hello, World!'), array('hello, {$b[obj]->name}!', $c, 'hello, Object!'), array('hello, {$b["obj"]->name}!', $c, 'hello, Object!'), + array('hello, {$b."obj"->name}!', $c, 'hello, Object!'), array('hello, {$b.obj->name|upper}!', $c, 'hello, OBJECT!'), + array('hello, {$b.obj->list.a|upper}!', + $c, 'hello, WORLD!'), array('hello, {$b[ $b.obj->c ]}!', $b, 'hello, Username!'), array('hello, {$b[ "{$b.obj->c}" ]}!', $b, 'hello, Username!'), array('hello, {"World"}!', $a, 'hello, World!'), - //array('hello, {"W{$a}d"}!', $a, 'hello, WWorldd!'), + array('hello, {"W{$a}d"}!', $a, 'hello, WWorldd!'), ); } @@ -99,17 +109,6 @@ class TemplateTest extends TestCase { array('hello, {$b.c|upper}!', $b, 'hello, USERNAME!'), array('hello, {$b."c"|upper}!', $b, 'hello, USERNAME!'), array('hello, {$b["C"|lower]|upper}!', $b, 'hello, USERNAME!'), -// array('Mod: {$lorem|truncate:16}!', $b, 'Mod: Lorem ipsum...!'), -// array('Mod: {$lorem|truncate:max(4,16)}!', $b, 'Mod: Lorem ipsum...!'), -// array('Mod: {$lorem|truncate:16|upper}!', $b, 'Mod: LOREM IPSUM...!'), -// array('Mod: {$lorem|truncate:16:"->"}!', $b, 'Mod: Lorem ipsum->!'), -// array('Mod: {$lorem|truncate:20:$next}!', $b, 'Mod: Lorem ipsum next -->!'), -// array('Mod: {$lorem|truncate:20:$next|upper}!', $b, 'Mod: LOREM IPSUM NEXT -->!'), -// array('Mod: {$lorem|truncate:(20-5):$next}!', $b, 'Mod: Lorem next -->!'), -// array('Mod: {$lorem|truncate:20:($next|upper)}!', -// $b, 'Mod: Lorem ipsum NEXT -->!'), -// array('Mod: {$lorem|truncate:max(4,20):($next|upper)}!', -// $b, 'Mod: Lorem ipsum NEXT -->!'), array('Mod: {$rescue|escape}!', $b, 'Mod: Chip & Dale!'), array('Mod: {$rescue|escape:"html"}!', $b, 'Mod: Chip & Dale!'), array('Mod: {$rescue|escape:"url"}!', $b, 'Mod: Chip+%26+Dale!'), @@ -316,7 +315,7 @@ class TemplateTest extends TestCase { array('Create: {var $v = 1++} Result: {$v} end', 'Fenom\CompileException', "Unexpected token '++'"), array('Create: {var $v = c} Result: {$v} end', 'Fenom\CompileException', "Unexpected token 'c'"), array('Create: {var $v = ($a)++} Result: {$v} end', 'Fenom\CompileException', "Unexpected token '++'"), - array('Create: {var $v = --$a++} Result: {$v} end', 'Fenom\CompileException', "Can not use two increments and decrements for one variable"), + array('Create: {var $v = --$a++} Result: {$v} end', 'Fenom\CompileException', "Can not use two increments and/or decrements for one variable"), array('Create: {var $v = $a|upper++} Result: {$v} end', 'Fenom\CompileException', "Unexpected token '++'"), array('Create: {var $v = max($a,2)++} Result: {$v} end', 'Fenom\CompileException', "Unexpected token '++'"), array('Create: {var $v = max($a,2)} Result: {$v} end', 'Fenom\CompileException', "Modifier max not found", Fenom::DENY_INLINE_FUNCS), @@ -568,6 +567,88 @@ class TemplateTest extends TestCase { ); } + public static function providerIsOperator() { + return array( + // is {$type} + array('{if $one is int} block1 {else} block2 {/if}', 'block1'), + array('{if $one && $one is int} block1 {else} block2 {/if}', 'block1'), + array('{if $zero && $one is int} block1 {else} block2 {/if}', 'block2'), + array('{if $one is 1} block1 {else} block2 {/if}', 'block1'), + array('{if $one is 2} block1 {else} block2 {/if}', 'block2'), + array('{if $one is not int} block1 {else} block2 {/if}', 'block2'), + array('{if $one is not 1} block1 {else} block2 {/if}', 'block2'), + array('{if $one is not 2} block1 {else} block2 {/if}', 'block1'), + array('{if $one is $one} block1 {else} block2 {/if}', 'block1'), + array('{if $float is float} block1 {else} block2 {/if}', 'block1'), + array('{if $float is not float} block1 {else} block2 {/if}', 'block2'), + array('{if $obj is object} block1 {else} block2 {/if}', 'block1'), + array('{if $obj is $obj} block1 {else} block2 {/if}', 'block1'), + array('{if $list is array} block1 {else} block2 {/if}', 'block1'), + array('{if $list is iterable} block1 {else} block2 {/if}', 'block1'), + array('{if $list is not scalar} block1 {else} block2 {/if}', 'block1'), + array('{if $list is $list} block1 {else} block2 {/if}', 'block1'), + array('{if $one is scalar} block1 {else} block2 {/if}', 'block1'), + // is set + array('{if $one is set} block1 {else} block2 {/if}', 'block1'), + array('{if $one is not set} block1 {else} block2 {/if}', 'block2'), + array('{if $unexists is set} block1 {else} block2 {/if}', 'block2'), + array('{if $unexists is not set} block1 {else} block2 {/if}', 'block1'), + array('{if 5 is set} block1 {else} block2 {/if}', 'block1'), + array('{if time() is set} block1 {else} block2 {/if}', 'block1'), + array('{if null is set} block1 {else} block2 {/if}', 'block2'), + array('{if 0 is empty} block1 {else} block2 {/if}', 'block1'), + array('{if "" is empty} block1 {else} block2 {/if}', 'block1'), + array('{if "data" is empty} block1 {else} block2 {/if}', 'block2'), + array('{if time() is not empty} block1 {else} block2 {/if}', 'block1'), + // is empty + array('{if $one is empty} block1 {else} block2 {/if}', 'block2'), + array('{if $one is not empty} block1 {else} block2 {/if}', 'block1'), + array('{if $unexists is empty} block1 {else} block2 {/if}', 'block1'), + array('{if $unexists is not empty} block1 {else} block2 {/if}', 'block2'), + array('{if $zero is empty} block1 {else} block2 {/if}', 'block1'), + array('{if $zero is not empty} block1 {else} block2 {/if}', 'block2'), + // instaceof + array('{if $obj is StdClass} block1 {else} block2 {/if}', 'block1'), + array('{if $obj is \StdClass} block1 {else} block2 {/if}', 'block1'), + array('{if $obj is not \My\StdClass} block1 {else} block2 {/if}', 'block1'), + // event, odd + array('{if $one is odd} block1 {else} block2 {/if}', 'block1'), + array('{if $one is even} block1 {else} block2 {/if}', 'block2'), + array('{if $two is even} block1 {else} block2 {/if}', 'block1'), + array('{if $two is odd} block1 {else} block2 {/if}', 'block2'), + // template + array('{if "welcome.tpl" is template} block1 {else} block2 {/if}', 'block1'), + array('{if "welcome2.tpl" is template} block1 {else} block2 {/if}', 'block2'), + + ); + } + + public static function providerInOperator() { + return array( + array('{if $one in "qwertyuiop 1"} block1 {else} block2 {/if}', 'block1'), + array('{if $one in string "qwertyuiop 1"} block1 {else} block2 {/if}', 'block1'), + array('{if $one in "qwertyuiop"} block1 {else} block2 {/if}', 'block2'), + array('{if $one not in "qwertyuiop 1"} block1 {else} block2 {/if}', 'block2'), + array('{if $one not in "qwertyuiop"} block1 {else}v block2 {/if}', 'block1'), + + array('{if $one in [1, 2, 3]} block1 {else} block2 {/if}', 'block1'), + array('{if $one in list [1, 2, 3]} block1 {else} block2 {/if}', 'block1'), + array('{if $one in ["one", "two", "three"]} block1 {else} block2 {/if}', 'block2'), + array('{if $one in keys [1 => "one", 2 => "two", 3 => "three"]} block1 {else} block2 {/if}', 'block1'), + + array('{if $one in $two} block1 {else} block2 {/if}', 'block2'), + ); + } + + public function _testSandbox() { + try { + var_dump($this->fenom->compileCode('{$one.two->three[e]()}')->getBody()); + } catch(\Exception $e) { + print_r($e->getMessage()."\n".$e->getTraceAsString()); + } + exit; + } + /** * @dataProvider providerVars */ @@ -615,7 +696,7 @@ class TemplateTest extends TestCase { * @group include * @dataProvider providerInclude */ - public function _testInclude($code, $vars, $result) { // fixme, addTemplate removed + public function testInclude($code, $vars, $result) { $this->exec($code, $vars, $result); } @@ -732,5 +813,21 @@ class TemplateTest extends TestCase { public function testLayersInvalid($code, $exception, $message, $options = 0) { $this->execError($code, $exception, $message, $options); } + + /** + * @group is_operator + * @dataProvider providerIsOperator + */ + public function testIsOperator($code, $result) { + $this->exec($code, self::getVars(), $result); + } + + /** + * @group in_operator + * @dataProvider providerInOperator + */ + public function testInOperator($code, $result) { + $this->exec($code, self::getVars(), $result); + } }