Add recursive macros support (#28)

This commit is contained in:
bzick 2013-07-29 14:53:21 +04:00
parent 97e891895a
commit 4edd0e1708
8 changed files with 135 additions and 49 deletions

View File

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

View File

@ -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": [
{

View File

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

View File

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

View File

@ -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.'<?php $tpl->exchangeArray($_tpl); unset($_tpl);';
return '$_tpl = (array)$tpl; $tpl->exchangeArray('.self::toArray($p).'+$_tpl); ?>'.$inc->getBody().'<?php $tpl->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.'<?php $tpl->exchangeArray($_tpl); unset($_tpl);';
return '$_tpl = (array)$tpl; ?>'.$inc->getBody().'<?php $tpl->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"] = '<?php '.$stack.' = array(); macro_'.$scope["macro"]['id'].': ?>'.$scope->cutContent().'<?php if('.$stack.') {'.PHP_EOL.
'$call = array_pop('.$stack.');'.PHP_EOL.
'$tpl = $call["tpl"];'.PHP_EOL.
$switch.PHP_EOL.
'unset($call, '.$stack.');'.PHP_EOL.
'} ?>';
} else {
$scope["macro"]["body"] = $scope->cutContent();
}
$scope->tpl->macros[ $scope["name"] ] = $scope["macro"];
}
/**

View File

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

View File

@ -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 "<?php \n".
"/** Fenom template '".$this->_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"].'<?php $tpl = $_tpl; unset($_tpl);';
if(!$macro) {
throw new InvalidUsageException("Undefined macro '$name'");
}
}
$tokens->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"].'<?php $tpl = $_tpl; unset($_tpl);';
}
}
@ -1270,6 +1290,7 @@ class Template extends Render {
}
}
/**
* Parse parameters as $key=$value
* param1=$var param2=3 ...

View File

@ -42,6 +42,25 @@ class MacrosTest extends TestCase {
a: {macro.plus x=5 y=3}.
');
$this->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));
}
}