diff --git a/README.md b/README.md index 7c8f8a9..2993bfc 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ Fenom - Template Engine for PHP * Simple [syntax](./docs/syntax.md) * [Fast](./docs/benchmark.md) * [Secure](./docs/settings.md) -* [Simple](./ideology.md) -* [Flexible](./docs/main.md#extends) +* Simple +* [Flexible](./docs/ext/extensions.md) * [Lightweight](./docs/benchmark.md#stats) * [Powerful](./docs/main.md) * Easy to use: diff --git a/composer.json b/composer.json index 5cc9d88..bec82b2 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,8 @@ { "name": "fenom/fenom", "type": "library", - "description": "Fenom - fast template engine for PHP", - "homepage": "http://bzick.github.io/fenom/", - "keywords": ["fenom", "template", "templating", "cytro"], + "description": "Fenom - excellent template engine for PHP", + "keywords": ["fenom", "template", "templating", "templater"], "license": "BSD-3", "authors": [ { diff --git a/docs/ext/extensions.md b/docs/ext/extensions.md index 9374920..e82ab24 100644 --- a/docs/ext/extensions.md +++ b/docs/ext/extensions.md @@ -1,5 +1,9 @@ Extensions ========== -* [Extra pack](https://github.com/bzick/fenom-extra) basic add-ons for web-base project. -* *Smarty pack* (planned) Smarty3 adapter \ No newline at end of file +* [Extra pack](https://github.com/bzick/fenom-extra) of add-ons for Fenom template engine. + * Tools for static files (css, js). + * Global variables + * Allow more hooks for extending + * Add variable container + * You can only use the necessary add-ons \ No newline at end of file diff --git a/src/Fenom.php b/src/Fenom.php index a8649b7..3e41406 100644 --- a/src/Fenom.php +++ b/src/Fenom.php @@ -47,6 +47,8 @@ class Fenom { const DEFAULT_FUNC_CLOSE = 'Fenom\Compiler::stdFuncClose'; const SMART_FUNC_PARSER = 'Fenom\Compiler::smartFuncParser'; + const MAX_MACRO_RECURSIVE = 32; + /** * @var int[] of possible options, as associative array * @see setOptions diff --git a/src/Fenom/Compiler.php b/src/Fenom/Compiler.php index 1ef5a58..c5b511e 100644 --- a/src/Fenom/Compiler.php +++ b/src/Fenom/Compiler.php @@ -34,7 +34,7 @@ class Compiler { if($name && ($tpl->getStorage()->getOptions() & \Fenom::FORCE_INCLUDE)) { // if FORCE_INCLUDE enabled and template name known $inc = $tpl->getStorage()->compile($name, false); $tpl->addDepend($inc); - return '$_tpl = (array)$tpl; $tpl->exchangeArray('.self::toArray($p).'+$_tpl); ?>'.$inc->_body.'exchangeArray($_tpl); unset($_tpl);'; + return '$_tpl = (array)$tpl; $tpl->exchangeArray('.self::toArray($p).'+$_tpl); ?>'.$inc->getBody().'exchangeArray($_tpl); unset($_tpl);'; } else { return '$tpl->getStorage()->getTemplate('.$cname.')->display('.self::toArray($p).'+(array)$tpl);'; } @@ -42,7 +42,7 @@ class Compiler { if($name && ($tpl->getStorage()->getOptions() & \Fenom::FORCE_INCLUDE)) { // if FORCE_INCLUDE enabled and template name known $inc = $tpl->getStorage()->compile($name, false); $tpl->addDepend($inc); - return '$_tpl = (array)$tpl; ?>'.$inc->_body.'exchangeArray($_tpl); unset($_tpl);'; + return '$_tpl = (array)$tpl; ?>'.$inc->getBody().'exchangeArray($_tpl); unset($_tpl);'; } else { return '$tpl->getStorage()->getTemplate('.$cname.')->display((array)$tpl);'; } @@ -493,7 +493,6 @@ class Compiler { */ public static function tagBlockOpen(Tokenizer $tokens, Scope $scope) { if($scope->level > 0) { - var_dump("".$scope->tpl); $scope->tpl->_compatible = true; } $scope["cname"] = $scope->tpl->parsePlainArg($tokens, $name); @@ -809,7 +808,6 @@ class Compiler { if($alias) { $name = $alias.'.'.$name; } - $tpl->macros[$name] = $macro; } $tpl->addDepend($donor); @@ -827,8 +825,9 @@ class Compiler { */ public static function macroOpen(Tokenizer $tokens, Scope $scope) { $scope["name"] = $tokens->get(Tokenizer::MACRO_STRING); - $scope["args"] = array(); - $scope["defaults"] = array(); + $scope["recursive"] = array(); + $args = array(); + $defaults = array(); if(!$tokens->valid()) { return; } @@ -836,12 +835,12 @@ class Compiler { if($tokens->is(')')) { return; } - while($tokens->is(Tokenizer::MACRO_STRING)) { - $scope["args"][] = $param = $tokens->getAndNext(); + while($tokens->is(Tokenizer::MACRO_STRING, T_VARIABLE)) { + $args[] = $param = $tokens->getAndNext(); if($tokens->is('=')) { $tokens->next(); if($tokens->is(T_CONSTANT_ENCAPSED_STRING, T_LNUMBER, T_DNUMBER) || $tokens->isSpecialVal()) { - $scope["defaults"][ $param ] = $tokens->getAndNext(); + $defaults[ $param ] = $tokens->getAndNext(); } else { throw new InvalidUsageException("Macro parameters may have only scalar defaults"); } @@ -849,7 +848,12 @@ class Compiler { $tokens->skipIf(','); } $tokens->skipIf(')'); - + $scope["macro"] = array( + "id" => $scope->tpl->i++, + "args" => $args, + "defaults" => $defaults, + "body" => "" + ); return; } @@ -858,12 +862,23 @@ class Compiler { * @param Scope $scope */ public static function macroClose(Tokenizer $tokens, Scope $scope) { - $scope->tpl->macros[ $scope["name"] ] = array( - "body" => $content = $scope->getContent(), - "args" => $scope["args"], - "defaults" => $scope["defaults"] - ); - $scope->tpl->_body = substr($scope->tpl->_body, 0, strlen($scope->tpl->_body) - strlen($content)); + if($scope["recursive"]) { + $switch = "switch(\$call['mark']) {\n"; + foreach($scope["recursive"] as $mark) { + $switch .= "case $mark: goto macro_$mark;\n"; + } + $switch .= "}"; + $stack = '$stack_'.$scope["macro"]['id']; + $scope["macro"]["body"] = ''.$scope->cutContent().''; + } else { + $scope["macro"]["body"] = $scope->cutContent(); + } + $scope->tpl->macros[ $scope["name"] ] = $scope["macro"]; } /** diff --git a/src/Fenom/Render.php b/src/Fenom/Render.php index e7d7361..2f08fed 100644 --- a/src/Fenom/Render.php +++ b/src/Fenom/Render.php @@ -72,11 +72,10 @@ class Render extends \ArrayObject { * @param callable $code template body * @param array $props */ - public function __construct(Fenom $fenom, \Closure $code, $props = array()) { + public function __construct(Fenom $fenom, \Closure $code, array $props = array()) { $this->_fenom = $fenom; $props += self::$_props; $this->_name = $props["name"]; -// $this->_provider = $this->_fenom->getProvider($props["scm"]); $this->_scm = $props["scm"]; $this->_time = $props["time"]; $this->_depends = $props["depends"]; @@ -85,28 +84,48 @@ class Render extends \ArrayObject { /** * Get template storage - * @return Fenom + * @return \Fenom */ public function getStorage() { return $this->_fenom; } + /** + * Get depends list + * @return array + */ public function getDepends() { return $this->_depends; } + /** + * Get schema name + * @return string + */ public function getScm() { return $this->_scm; } + /** + * Get provider of template source + * @return ProviderInterface + */ public function getProvider() { return $this->_fenom->getProvider($this->_scm); } + /** + * Get name without schema + * @return string + */ public function getBaseName() { return $this->_base_name; } + /** + * Get parse options + * @return int + */ public function getOptions() { return $this->_options; } diff --git a/src/Fenom/Template.php b/src/Fenom/Template.php index 975e867..3d51ede 100644 --- a/src/Fenom/Template.php +++ b/src/Fenom/Template.php @@ -38,12 +38,6 @@ class Template extends Render { * @var int shared counter */ public $i = 1; - /** - * Template PHP code - * @var string - */ - public $_body; - /** * @var array of macros */ @@ -63,10 +57,17 @@ class Template extends Render { * @var bool */ public $escape = false; + public $_extends; public $_extended = false; public $_compatible; + /** + * Template PHP code + * @var string + */ + private $_body; + /** * Call stack * @var Scope[] @@ -362,9 +363,10 @@ class Template extends Render { * @return string */ public function getTemplateCode() { + $before = $this->_before ? $this->_before."\n" : ""; return "_name."' compiled at ".date('Y-m-d H:i:s')." */\n". - ($this->_before ? $this->_before."\n" : ""). + $before. // some code 'before' template "return new Fenom\\Render(\$fenom, ".$this->_getClosureSource().", ".var_export(array( "options" => $this->_options, "provider" => $this->_scm, @@ -517,7 +519,7 @@ class Template extends Render { if($action !== "macro") { $name = $action.".".$name; } - return $this->parseMacro($tokens, $name); + return $this->parseMacroCall($tokens, $name); } if($tag = $this->_fenom->getTag($action, $this)) { // call some function @@ -839,7 +841,7 @@ class Template extends Render { } /** - * Parse 'is' and 'is not' operator + * Parse 'is' and 'is not' operators * @see $_checkers * @param Tokenizer $tokens * @param string $value @@ -1095,7 +1097,7 @@ class Template extends Render { } if(!is_string($mods)) { // dynamic modifier - $mods = 'call_user_func($tpl->getStorage()->getModifier("'.$modifier_name.'"), '; + $mods = 'call_user_func($tpl->getStorage()->getModifier("'.$mods.'"), '; } else { $mods .= "("; } @@ -1188,24 +1190,42 @@ class Template extends Render { * @return string * @throws InvalidUsageException */ - public function parseMacro(Tokenizer $tokens, $name) { + public function parseMacroCall(Tokenizer $tokens, $name) { + $recursive = false; + $macro = false; if(isset($this->macros[ $name ])) { $macro = $this->macros[ $name ]; - $p = $this->parseParams($tokens); - $args = array(); - foreach($macro["args"] as $arg) { - if(isset($p[ $arg ])) { - $args[ $arg ] = $p[ $arg ]; - } elseif(isset($macro["defaults"][ $arg ])) { - $args[ $arg ] = $macro["defaults"][ $arg ]; - } else { - throw new InvalidUsageException("Macro '$name' require '$arg' argument"); + } else { + foreach($this->_stack as $scope) { + if($scope->name == 'macro' && $scope['name'] == $name) { // invoke recursive + $recursive = $scope; + $macro = $scope['macro']; + break; } } - $args = $args ? '$tpl = '.Compiler::toArray($args).';' : ''; - return '$_tpl = $tpl; '.$args.' ?>'.$macro["body"].'next(); + $p = $this->parseParams($tokens); + $args = array(); + foreach($macro['args'] as $arg) { + if(isset($p[ $arg ])) { + $args[ $arg ] = $p[ $arg ]; + } elseif(isset($macro['defaults'][ $arg ])) { + $args[ $arg ] = $macro['defaults'][ $arg ]; + } else { + throw new InvalidUsageException("Macro '$name' require '$arg' argument"); + } + } + $args = $args ? '$tpl = '.Compiler::toArray($args).';' : ''; + if($recursive) { + $n = $this->i++; + $recursive['recursive'][] = $n; + return '$stack_'.$macro['id'].'[] = array("tpl" => $tpl, "mark" => '.$n.'); '.$args.' goto macro_'.$macro['id'].'; macro_'.$n.':'; } else { - throw new InvalidUsageException("Undefined macro '$name'"); + return '$_tpl = $tpl; '.$args.' ?>'.$macro["body"].'tpl("macro_recursive.tpl", '{macro factorial(num)} + {if $num} + {$num} {macro.factorial num=$num-1} + {/if} + {/macro} + + {macro.factorial num=10}'); + } + + public function _testSandbox() { + try { + $this->fenom->compile("macro_recursive.tpl"); + $this->fenom->flush(); + var_dump($this->fenom->fetch("macro_recursive.tpl", [])); + } catch(\Exception $e) { + var_dump($e->getMessage().": ".$e->getTraceAsString()); + } + exit; } public function testMacros() { @@ -72,4 +91,11 @@ class MacrosTest extends TestCase { $this->assertSame('a: x + y = 3 , x - y - z = 3 , new minus macros .', Modifier::strip($tpl->fetch(array()), true)); } + + public function testRecursive() { + $this->fenom->compile('macro_recursive.tpl'); + $this->fenom->flush(); + $tpl = $this->fenom->getTemplate('macro_recursive.tpl'); + $this->assertSame("10 9 8 7 6 5 4 3 2 1", Modifier::strip($tpl->fetch(array()), true)); + } }