Update to 1.1

Read CHANGELOG.md#1.1.0
This commit is contained in:
bzick 2013-07-22 18:03:43 +04:00
parent a68a30bec5
commit c4610a7778
17 changed files with 760 additions and 277 deletions

View File

@ -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

View File

@ -1,5 +1,35 @@
<?php
require_once __DIR__.'/scripts/bootstrap.php';
$opt = getopt("h", array(
/** @var string $message */
"cleanup",
/** @var boolean $stress */
"stress:",
/** @var boolean $auto_reload */
"auto_reload",
/** @vat boolean $help */
/** @vat boolean $h */
"help"
));
$opt += array(
"stress" => 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";
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/*");
}

View File

@ -0,0 +1,3 @@
<?php
require_once __DIR__.'/../../vendor/autoload.php';

View File

@ -3,11 +3,14 @@
require(__DIR__.'/../../vendor/autoload.php');
class Benchmark {
public static $t = "%8s: %-22s %10.4f sec, %10.1f MiB\n";
const OUTPUT = "%8s: %-22s %10.4f sec, %10.1f MiB\n";
public static function smarty3($tpl, $data, $double, $message) {
public static $stress = 0;
public static $auto_reload = false;
public static function smarty3($tpl, $data, $double, $stress = false, $auto_reload = false) {
$smarty = new Smarty();
$smarty->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));
}

View File

@ -1,15 +1,32 @@
<?php
$opt = getopt("", array(
/** @var string $engine */
"engine:",
/** @var string $template */
"template:",
/** @var string $data */
"data:",
/** @var boolean $double */
"double",
"message:"
/** @var string $message */
"message:",
/** @var boolean $stress */
"stress:",
/** @var boolean $auto_reload */
"auto_reload"
));
require_once __DIR__.'/bootstrap.php';
$opt += array(
"message" => "plain",
"stress" => 0,
);
extract($opt);
Benchmark::$engine($template, json_decode(file_get_contents($data), true), isset($double), $message);
$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));

View File

@ -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]}
```

View File

@ -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

View File

@ -16,7 +16,7 @@ use Fenom\Template,
* @author Ivan Shalganov <a.cobest@gmail.com>
*/
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 ];

View File

@ -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;

View File

@ -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;
}

View File

@ -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() {
}
}

View File

@ -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 {

View File

@ -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
*

View File

@ -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');

View File

@ -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),

View File

@ -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";
}

View File

@ -11,10 +11,16 @@ use Fenom\Template,
*/
class TemplateTest extends TestCase {
public function setUp() {
parent::setUp();
$this->tpl('welcome.tpl', '<b>Welcome, {$username} ({$email})</b>');
}
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 &amp; Dale!'),
array('Mod: {$rescue|escape:"html"}!', $b, 'Mod: Chip &amp; 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);
}
}