diff --git a/benchmark/templates/echo.php b/benchmark/templates/echo.php index f6c4711..398d6a8 100644 --- a/benchmark/templates/echo.php +++ b/benchmark/templates/echo.php @@ -53,7 +53,7 @@ $template = $twig->loadTemplate('echo/twig.tpl'); $template->render($data); var_dump("Twig cached: ".(microtime(true)-$start)); -$aspect = Aspect::factory(__DIR__, __DIR__."/../compile/", Aspect::CHECK_MTIME); +$aspect = Aspect::factory(__DIR__, __DIR__."/../compile/", Aspect::AUTO_RELOAD); $start = microtime(true); $template = $aspect->fetch('echo/smarty.tpl', $data); diff --git a/benchmark/templates/foreach.php b/benchmark/templates/foreach.php index 0439771..8b1bf3f 100644 --- a/benchmark/templates/foreach.php +++ b/benchmark/templates/foreach.php @@ -38,7 +38,7 @@ $template = $twig->loadTemplate('foreach/twig.tpl'); $template->render($data); var_dump("Twig cached: ".(microtime(true)-$start)); -$aspect = Aspect::factory(__DIR__, __DIR__."/../compile/", Aspect::CHECK_MTIME | Aspect::INCLUDE_SOURCES); +$aspect = Aspect::factory(__DIR__, __DIR__."/../compile/", Aspect::AUTO_RELOAD | Aspect::INCLUDE_SOURCES); $start = microtime(true); $template = $aspect->fetch('foreach/smarty.tpl', $data); diff --git a/benchmark/templates/inheritance.php b/benchmark/templates/inheritance.php index dc0b04b..0872640 100644 --- a/benchmark/templates/inheritance.php +++ b/benchmark/templates/inheritance.php @@ -49,7 +49,7 @@ $template = $twig->loadTemplate('inheritance/twig/b100.tpl'); $template->render($data); var_dump("Twig cached: ".(microtime(true)-$start)); -$aspect = Aspect::factory(__DIR__, __DIR__."/../compile/", Aspect::CHECK_MTIME); +$aspect = Aspect::factory(__DIR__, __DIR__."/../compile/", Aspect::AUTO_RELOAD); $start = microtime(true); $template = $aspect->fetch('inheritance/smarty/b100.tpl', $data); diff --git a/docs/tags/macro.md b/docs/tags/macro.md index a449a3b..bac80f2 100644 --- a/docs/tags/macro.md +++ b/docs/tags/macro.md @@ -20,6 +20,8 @@ Tag {macro} [RU] {macro.plus x=$num y=100} ``` +На данный момент рекурсивный вызов макроса не поддерживается. + ### {import} Для использования маросов в другом шаблоне необходимо их импортировать при помощи тега `{import}` diff --git a/src/Aspect.php b/src/Aspect.php index de826d0..d0bebb0 100644 --- a/src/Aspect.php +++ b/src/Aspect.php @@ -6,27 +6,27 @@ use Aspect\Template, * Aspect Template Engine */ class Aspect { - const VERSION = 1.0; + const VERSION = '1.0.1'; - const INLINE_COMPILER = 1; - const BLOCK_COMPILER = 2; - const INLINE_FUNCTION = 3; - const BLOCK_FUNCTION = 4; - const MODIFIER = 5; + const INLINE_COMPILER = 1; + const BLOCK_COMPILER = 2; + const INLINE_FUNCTION = 3; + const BLOCK_FUNCTION = 4; + const MODIFIER = 5; - const DENY_METHODS = 0x10; + const DENY_METHODS = 0x10; const DENY_INLINE_FUNCS = 0x20; - const FORCE_INCLUDE = 0x40; + const FORCE_INCLUDE = 0x40; - const CHECK_MTIME = 0x80; - const FORCE_COMPILE = 0xF0; - const DISABLE_CACHE = 0x1F0; + const AUTO_RELOAD = 0x80; + const FORCE_COMPILE = 0xF0; + const DISABLE_CACHE = 0x1F0; const DEFAULT_CLOSE_COMPILER = 'Aspect\Compiler::stdClose'; - const DEFAULT_FUNC_PARSER = 'Aspect\Compiler::stdFuncParser'; - const DEFAULT_FUNC_OPEN = 'Aspect\Compiler::stdFuncOpen'; - const DEFAULT_FUNC_CLOSE = 'Aspect\Compiler::stdFuncClose'; - const SMART_FUNC_PARSER = 'Aspect\Compiler::smartFuncParser'; + const DEFAULT_FUNC_PARSER = 'Aspect\Compiler::stdFuncParser'; + const DEFAULT_FUNC_OPEN = 'Aspect\Compiler::stdFuncOpen'; + const DEFAULT_FUNC_CLOSE = 'Aspect\Compiler::stdFuncClose'; + const SMART_FUNC_PARSER = 'Aspect\Compiler::smartFuncParser'; /** * @var array of possible options, as associative array @@ -35,43 +35,12 @@ class Aspect { private static $_option_list = array( "disable_methods" => self::DENY_METHODS, "disable_native_funcs" => self::DENY_INLINE_FUNCS, + "disable_cache" => self::DISABLE_CACHE, "force_compile" => self::FORCE_COMPILE, - "compile_check" => self::CHECK_MTIME, + "auto_reload" => self::AUTO_RELOAD, "force_include" => self::FORCE_INCLUDE, ); - /** - * Default options for functions - * @var array - */ - private static $_actions_defaults = array( - self::BLOCK_FUNCTION => array( - 'type' => self::BLOCK_FUNCTION, - 'open' => self::DEFAULT_FUNC_OPEN, - 'close' => self::DEFAULT_FUNC_CLOSE, - 'function' => null, - ), - self::INLINE_FUNCTION => array( - 'type' => self::INLINE_FUNCTION, - 'parser' => self::DEFAULT_FUNC_PARSER, - 'function' => null, - ), - self::INLINE_COMPILER => array( - 'type' => self::INLINE_COMPILER, - 'open' => null, - 'close' => self::DEFAULT_CLOSE_COMPILER, - 'tags' => array(), - 'float_tags' => array() - ), - self::BLOCK_COMPILER => array( - 'type' => self::BLOCK_COMPILER, - 'open' => null, - 'close' => null, - 'tags' => array(), - 'float_tags' => array() - ) - ); - /** * @var array Templates storage */ @@ -187,8 +156,9 @@ class Aspect { 'parser' => 'Aspect\Compiler::tagInclude' ), 'var' => array( // {var ...} - 'type' => self::INLINE_COMPILER, - 'parser' => 'Aspect\Compiler::assign' + 'type' => self::BLOCK_COMPILER, + 'open' => 'Aspect\Compiler::varOpen', + 'close' => 'Aspect\Compiler::varClose' ), 'block' => array( // {block ...} {parent} {/block} 'type' => self::BLOCK_COMPILER, @@ -597,7 +567,7 @@ class Aspect { if(isset($this->_storage[ $template ])) { /** @var Aspect\Template $tpl */ $tpl = $this->_storage[ $template ]; - if(($this->_options & self::CHECK_MTIME) && !$tpl->isValid()) { + if(($this->_options & self::AUTO_RELOAD) && !$tpl->isValid()) { return $this->_storage[ $template ] = $this->compile($template); } else { return $this->_storage[ $template ]; diff --git a/src/Aspect/Compiler.php b/src/Aspect/Compiler.php index a0b3032..fa4d78d 100644 --- a/src/Aspect/Compiler.php +++ b/src/Aspect/Compiler.php @@ -98,7 +98,7 @@ class Compiler { $key = null; $before = $body = array(); if($tokens->is(T_VARIABLE)) { - $from = $scope->tpl->parseVar($tokens, Template::DENY_MODS); + $from = $scope->tpl->parseVariable($tokens, Template::DENY_MODS); $prepend = ""; } elseif($tokens->is('[')) { $from = $scope->tpl->parseArray($tokens); @@ -110,11 +110,11 @@ class Compiler { } $tokens->get(T_AS); $tokens->next(); - $value = $scope->tpl->parseVar($tokens, Template::DENY_MODS | Template::DENY_ARRAY); + $value = $scope->tpl->parseVariable($tokens, Template::DENY_MODS | Template::DENY_ARRAY); if($tokens->is(T_DOUBLE_ARROW)) { $tokens->next(); $key = $value; - $value = $scope->tpl->parseVar($tokens, Template::DENY_MODS | Template::DENY_ARRAY); + $value = $scope->tpl->parseVariable($tokens, Template::DENY_MODS | Template::DENY_ARRAY); } $scope["after"] = array(); @@ -127,7 +127,7 @@ class Compiler { } $tokens->getNext("="); $tokens->next(); - $p[ $param ] = $scope->tpl->parseVar($tokens, Template::DENY_MODS | Template::DENY_ARRAY); + $p[ $param ] = $scope->tpl->parseVariable($tokens, Template::DENY_MODS | Template::DENY_ARRAY); } if($p["index"]) { @@ -195,7 +195,7 @@ class Compiler { $scope["after"] = $before = $body = array(); $i = array('', ''); $c = ""; - $var = $scope->tpl->parseVar($tokens, Template::DENY_MODS); + $var = $scope->tpl->parseVariable($tokens, Template::DENY_MODS); $tokens->get("="); $tokens->next(); $val = $scope->tpl->parseExp($tokens, true); @@ -382,7 +382,7 @@ class Compiler { return ""; } else { // dynamic extends $tpl->_extends = $tpl_name; - return '$parent = $tpl->getStorage()->getTemplate("extend:".'.$tpl_name.');'; + return '$parent = $tpl->getStorage()->getTemplate('.$tpl_name.', \Aspect\Template::EXTENDED);'; } } @@ -616,6 +616,15 @@ class Compiler { return 'array('.implode(",", $_code).')'; } + public static function varOpen(Tokenizer $tokens, Scope $scope) { + $scope->is_closed = true; + return self::setVar($tokens, $scope->tpl).';'; + } + + public static function varClose() { + return ''; + } + /** * Tag {var ...} * @@ -624,9 +633,9 @@ class Compiler { * @param Template $tpl * @return string */ - public static function assign(Tokenizer $tokens, Template $tpl) { - return self::setVar($tokens, $tpl).';'; - } + //public static function assign(Tokenizer $tokens, Template $tpl) { + // return self::setVar($tokens, $tpl).';'; + //} /** * Set variable expression @@ -636,7 +645,7 @@ class Compiler { * @return string */ public static function setVar(Tokenizer $tokens, Template $tpl, $allow_array = true) { - $var = $tpl->parseVar($tokens, $tpl::DENY_MODS); + $var = $tpl->parseVariable($tokens, $tpl::DENY_MODS); $tokens->get('='); $tokens->next(); @@ -668,7 +677,7 @@ class Compiler { $scope["value"] = "ob_get_clean()"; } - $scope["var"] = $scope->tpl->parseVar($tokens, Template::DENY_MODS); + $scope["var"] = $scope->tpl->parseVariable($tokens, Template::DENY_MODS); return "ob_start();"; } diff --git a/src/Aspect/Scope.php b/src/Aspect/Scope.php index 0cc9b8d..00b43d4 100644 --- a/src/Aspect/Scope.php +++ b/src/Aspect/Scope.php @@ -14,6 +14,7 @@ class Scope extends \ArrayObject { */ public $tpl; public $is_compiler = true; + public $is_closed = false; private $_action; private $_body; private $_offset; diff --git a/src/Aspect/Template.php b/src/Aspect/Template.php index 053430c..f800d11 100644 --- a/src/Aspect/Template.php +++ b/src/Aspect/Template.php @@ -21,6 +21,8 @@ class Template extends Render { const DENY_ARRAY = 1; const DENY_MODS = 2; + const EXTENDED = 0x1000; + /** * @var int shared counter */ @@ -190,9 +192,9 @@ class Template extends Render { if(!$_line) { $_line = $scope->line; } - $_names[] = $scope->name.' defined on line '.$scope->line; + $_names[] = '{'.$scope->name.'} defined on line '.$scope->line; } - throw new CompileException("Unclosed block tags: ".implode(", ", $_names), 0, 1, $this->_name, $_line); + throw new CompileException("Unclosed tag(s): ".implode(", ", $_names), 0, 1, $this->_name, $_line); } unset($this->_src); if($this->_post) { @@ -202,6 +204,10 @@ class Template extends Render { } } + /** + * Generate temporary internal template variable + * @return string + */ public function tmpVar() { return '$t'.($this->i++); } @@ -418,8 +424,11 @@ class Template extends Render { switch($act["type"]) { case Aspect::BLOCK_COMPILER: $scope = new Scope($action, $this, $this->_line, $act, count($this->_stack), $this->_body); - array_push($this->_stack, $scope); - return $scope->open($tokens); + $code = $scope->open($tokens); + if(!$scope->is_closed) { + array_push($this->_stack, $scope); + } + return $code; case Aspect::INLINE_COMPILER: return call_user_func($act["parser"], $tokens, $this); case Aspect::INLINE_FUNCTION: @@ -467,8 +476,9 @@ class Template extends Render { $_exp .= $this->parseScalar($tokens, true); $term = 1; } elseif(!$term && $tokens->is(T_VARIABLE)) { + $pp = $tokens->isPrev(Tokenizer::MACRO_INCDEC); - $_exp .= $this->parseVar($tokens, 0, $only_var); + $_exp .= $this->parseVariable($tokens, 0, $only_var); if($only_var && !$pp) { $term = 2; } else { @@ -562,29 +572,15 @@ class Template extends Render { } - /** - * Parse variable - * $var.foo[bar]["a"][1+3/$var]|mod:3:"w":$var3|mod3 - * - * @see parseModifier - * @static - * @param Tokenizer $tokens - * @param int $deny - * @param bool $pure_var - * @throws \LogicException - * @throws UnexpectedTokenException - * @return string - */ - public function parseVar(Tokenizer $tokens, $deny = 0, &$pure_var = true) { + public function parseVar(Tokenizer $tokens, $options = 0) { $var = $tokens->get(T_VARIABLE); - $pure_var = true; - $_var = '$tpl["'.ltrim($var,'$').'"]'; + $_var = '$tpl["'.substr($var, 1).'"]'; $tokens->next(); while($t = $tokens->key()) { - if($t === "." && !($deny & self::DENY_ARRAY)) { + if($t === "." && !($options & self::DENY_ARRAY)) { $key = $tokens->getNext(); if($tokens->is(T_VARIABLE)) { - $key = "[ ".$this->parseVar($tokens, self::DENY_ARRAY)." ]"; + $key = "[ ".$this->parseVariable($tokens, self::DENY_ARRAY)." ]"; } elseif($tokens->is(Tokenizer::MACRO_STRING)) { if($tokens->isNext("(")) { $key = "[".$this->parseExp($tokens)."]"; @@ -598,7 +594,7 @@ class Template extends Render { break; } $_var .= $key; - } elseif($t === "[" && !($deny & self::DENY_ARRAY)) { + } elseif($t === "[" && !($options & self::DENY_ARRAY)) { $tokens->next(); if($tokens->is(Tokenizer::MACRO_STRING)) { if($tokens->isNext("(")) { @@ -613,7 +609,33 @@ class Template extends Render { $tokens->get("]"); $tokens->next(); $_var .= $key; - } elseif($t === "|" && !($deny & self::DENY_MODS)) { + } elseif($t === T_DNUMBER) { + $_var .= '['.substr($tokens->getAndNext(), 1).']'; + } else { + break; + } + } + return $_var; + } + + /** + * Parse variable + * $var.foo[bar]["a"][1+3/$var]|mod:3:"w":$var3|mod3 + * + * @see parseModifier + * @static + * @param Tokenizer $tokens + * @param int $deny set limitations + * @param bool $pure_var will be FALSE if variable modified + * @throws \LogicException + * @throws UnexpectedTokenException + * @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) { @@ -629,41 +651,9 @@ class Template extends Render { $tokens->next(); $_var .= '->'.$prop; } - } elseif($t === T_DNUMBER) { - $_var .= '['.substr($tokens->getAndNext(), 1).']'; } elseif($t === "?" || $t === "!") { $pure_var = false; - $empty = ($t === "?"); - $tokens->next(); - if($tokens->is(":")) { - $tokens->next(); - if($empty) { - return '(empty('.$_var.') ? ('.$this->parseExp($tokens, true).') : '.$_var.')'; - } else { - return '(isset('.$_var.') ? '.$_var.' : ('.$this->parseExp($tokens, true).'))'; - } - } elseif($tokens->is(Tokenizer::MACRO_BINARY, Tokenizer::MACRO_BOOLEAN, Tokenizer::MACRO_MATH) || !$tokens->valid()) { - if($empty) { - return '!empty('.$_var.')'; - } else { - return 'isset('.$_var.')'; - } - } else { - $expr1 = $this->parseExp($tokens, true); - if(!$tokens->is(":")) { - throw new UnexpectedTokenException($tokens, null, "ternary operator"); - } - $expr2 = $this->parseExp($tokens, true); - if($empty) { - return '(empty('.$_var.') ? '.$expr2.' : '.$expr1.')'; - } else { - return '(isset('.$_var.') ? '.$expr1.' : '.$expr2.')'; - } - } - } elseif($t === "!") { - $pure_var = false; - $tokens->next(); - return 'isset('.$_var.')'; + return $this->parseTernary($tokens, $_var, $t); } else { break; } @@ -671,6 +661,36 @@ class Template extends Render { return $_var; } + public function parseTernary(Tokenizer $tokens, $var, $type) { + $empty = ($type === "?"); + $tokens->next(); + if($tokens->is(":")) { + $tokens->next(); + if($empty) { + return '(empty('.$var.') ? ('.$this->parseExp($tokens, true).') : '.$var.')'; + } else { + return '(isset('.$var.') ? '.$var.' : ('.$this->parseExp($tokens, true).'))'; + } + } elseif($tokens->is(Tokenizer::MACRO_BINARY, Tokenizer::MACRO_BOOLEAN, Tokenizer::MACRO_MATH) || !$tokens->valid()) { + if($empty) { + return '!empty('.$var.')'; + } else { + return 'isset('.$var.')'; + } + } else { + $expr1 = $this->parseExp($tokens, true); + if(!$tokens->is(":")) { + throw new UnexpectedTokenException($tokens, null, "ternary operator"); + } + $expr2 = $this->parseExp($tokens, true); + if($empty) { + return '(empty('.$var.') ? '.$expr2.' : '.$expr1.')'; + } else { + return '(isset('.$var.') ? '.$expr1.' : '.$expr2.')'; + } + } + } + /** * Parse scalar values * @@ -825,7 +845,7 @@ class Template extends Render { $args[] = $token; $tokens->next(); } elseif($tokens->is(T_VARIABLE)) { - $args[] = $this->parseVar($tokens, self::DENY_MODS); + $args[] = $this->parseVariable($tokens, self::DENY_MODS); } elseif($tokens->is('"', '`', T_ENCAPSED_AND_WHITESPACE)) { $args[] = $this->parseSubstr($tokens); } elseif($tokens->is('(')) { diff --git a/tests/cases/Aspect/TemplateTest.php b/tests/cases/Aspect/TemplateTest.php index a31385a..0354162 100644 --- a/tests/cases/Aspect/TemplateTest.php +++ b/tests/cases/Aspect/TemplateTest.php @@ -586,7 +586,7 @@ class TemplateTest extends TestCase { array('Layers: {for $a=4 to=6} block1 {if 1} {/for} {/if} end', 'Aspect\CompileException', "Unexpected closing of the tag 'for'"), array('Layers: {switch 1} {if 1} {case 1} {/if} {/switch} end', 'Aspect\CompileException', "Unexpected tag 'case' (this tag can be used with 'switch')"), array('Layers: {/switch} end', 'Aspect\CompileException', "Unexpected closing of the tag 'switch'"), - array('Layers: {if 1} end', 'Aspect\CompileException', "Unclosed block tags: if"), + array('Layers: {if 1} end', 'Aspect\CompileException', "Unclosed tag(s): {if}"), ); } @@ -630,6 +630,7 @@ class TemplateTest extends TestCase { } /** + * @group expression * @dataProvider providerExpressions */ public function testExpressions($code, $vars, $result) {