Merge pull request #170 from fenom-template/develop

2.7.0
This commit is contained in:
Ivan Shalganov 2015-06-03 11:57:35 +03:00
commit 56dfcfc71f
11 changed files with 187 additions and 124 deletions

View File

@ -18,7 +18,7 @@
var data = { "time": obj.ts };
```
Так же для игнорирования синтаксиса Fenom можно использовать модификатор `:ignore` для любого блочного тега.
Так же для игнорирования синтаксиса Fenom можно использовать опцию `:ignore` для любого блочного тега.
```smarty
{if:ignore $cdn.yandex}
var item = {cdn: "//yandex.st/"};

View File

@ -6,7 +6,9 @@ require_once __DIR__.'/../tests/tools.php';
\Fenom::registerAutoload();
$fenom = Fenom::factory(__DIR__.'/templates', __DIR__.'/compiled');
$fenom->setOptions(Fenom::AUTO_RELOAD);
var_dump($fenom->compileCode('{set $z = "A"~~"B"}')->getBody());
//$fenom->display("blocks/second.tpl", []);
$fenom->setOptions(Fenom::AUTO_RELOAD | Fenom::FORCE_COMPILE);
$fenom->addAccessorSmart('g', 'App::$q->get', Fenom::ACCESSOR_CALL);
var_dump($fenom->compileCode('{$.g("env")}')->getBody());
//var_dump($fenom->compile("bug158/main.tpl", [])->getTemplateCode());
//var_dump($fenom->display("bug158/main.tpl", []));
// $fenom->getTemplate("problem.tpl");

View File

@ -0,0 +1,3 @@
{* Отображаемый шаблон *}
{import [test] from "bug158/test.tpl" as test}
{test.test}

View File

@ -0,0 +1,7 @@
{* template:test.tpl *}
{macro test($break = false)}
Test macro recursive
{if $break?}
{macro.test break = true}
{/if}
{/macro}

View File

@ -18,7 +18,8 @@ use Fenom\Template;
*/
class Fenom
{
const VERSION = '2.6';
const VERSION = '2.7';
const REV = 1;
/* Actions */
const INLINE_COMPILER = 1;
const BLOCK_COMPILER = 5;
@ -53,6 +54,12 @@ class Fenom
const MAX_MACRO_RECURSIVE = 32;
const ACCESSOR_CUSTOM = null;
const ACCESSOR_VAR = 'Fenom\Accessor::parserVar';
const ACCESSOR_CALL = 'Fenom\Accessor::parserCall';
public static $charset = "UTF-8";
/**
* @var int[] of possible options, as associative array
* @see setOptions
@ -805,6 +812,21 @@ class Fenom
return $this;
}
/**
* Add global accessor ($.)
* @param string $name
* @param callable|string $accessor
* @param string $parser
* @return Fenom
*/
public function addAccessorSmart($name, $accessor, $parser) {
$this->_accessors[$name] = array(
"accessor" => $accessor,
"parser" => $parser
);
return $this;
}
/**
* Remove accessor
* @param string $name

View File

@ -28,10 +28,21 @@ class Accessor {
'env' => '$_ENV'
);
public static function parserVar($var, Tokenizer $tokens, Template $tpl, &$is_var) {
$is_var = true;
return $tpl->parseVariable($tokens, $var);
}
public static function parserCall($call, Tokenizer $tokens, Template $tpl) {
return $call.$tpl->parseArgs($tokens);
}
/**
* Accessor for global variables
* @param Tokenizer $tokens
* @param Template $tpl
* @return string
*/
public static function getVar(Tokenizer $tokens, Template $tpl)
{
@ -47,6 +58,7 @@ class Accessor {
/**
* Accessor for template information
* @param Tokenizer $tokens
* @return string
*/
public static function tpl(Tokenizer $tokens)
{

View File

@ -741,7 +741,16 @@ class Compiler
*/
public static function setOpen(Tokenizer $tokens, Tag $scope)
{
$var = $scope->tpl->parseVariable($tokens);
if($tokens->is(T_VARIABLE)) {
$var = $scope->tpl->parseVariable($tokens);
} elseif($tokens->is('$')) {
$var = $scope->tpl->parseAccessor($tokens, $is_var);
if(!$is_var) {
throw new InvalidUsageException("Accessor is not writable");
}
} else {
throw new InvalidUsageException("{set} and {add} accept only variable");
}
$before = $after = "";
if($scope->name == 'add') {
$before = "if(!isset($var)) {\n";

View File

@ -58,13 +58,13 @@ class Modifier
* @param string $charset
* @return string
*/
public static function escape($text, $type = 'html', $charset = 'UTF-8')
public static function escape($text, $type = 'html', $charset = null)
{
switch (strtolower($type)) {
case "url":
return urlencode($text);
case "html";
return htmlspecialchars($text, ENT_COMPAT, $charset);
return htmlspecialchars($text, ENT_COMPAT, $charset ? $charset : \Fenom::$charset);
case "js":
return json_encode($text, 64 | 256); // JSON_UNESCAPED_SLASHES = 64, JSON_UNESCAPED_UNICODE = 256
default:

View File

@ -428,9 +428,9 @@ class Template extends Render
{
if ($this->macros) {
$macros = array();
foreach ($this->macros as $m) {
foreach ($this->macros as $name => $m) {
if ($m["recursive"]) {
$macros[] = "\t\t'" . $m["name"] . "' => function (\$var, \$tpl) {\n?>" . $m["body"] . "<?php\n}";
$macros[] = "\t\t'" . $name . "' => function (\$var, \$tpl) {\n?>" . $m["body"] . "<?php\n}";
}
}
return "array(\n" . implode(",\n", $macros) . ")";
@ -490,7 +490,7 @@ class Template extends Render
$escape = $this->_options & Fenom::AUTO_ESCAPE;
}
if ($escape) {
return "echo htmlspecialchars($data, ENT_COMPAT, 'UTF-8');";
return "echo htmlspecialchars($data, ENT_COMPAT, ".var_export(Fenom::$charset, true).");";
} else {
return "echo $data;";
}
@ -782,86 +782,107 @@ class Template extends Render
} else {
$unary = "";
}
if ($tokens->is(T_LNUMBER, T_DNUMBER)) {
$code = $unary . $this->parseScalar($tokens, true);
} elseif ($tokens->is(T_CONSTANT_ENCAPSED_STRING, '"', T_ENCAPSED_AND_WHITESPACE)) {
if ($unary) {
throw new UnexpectedTokenException($tokens->back());
}
$code = $this->parseScalar($tokens, true);
} elseif ($tokens->is(T_VARIABLE)) {
$code = $this->parseVariable($tokens);
if ($tokens->is("(") && $tokens->hasBackList(T_STRING, T_OBJECT_OPERATOR)) {
if ($this->_options & Fenom::DENY_METHODS) {
throw new \LogicException("Forbidden to call methods");
switch($tokens->key()) {
case T_LNUMBER:
case T_DNUMBER:
$code = $unary . $this->parseScalar($tokens, true);
break;
case T_CONSTANT_ENCAPSED_STRING:
case '"':
case T_ENCAPSED_AND_WHITESPACE:
if ($unary) {
throw new UnexpectedTokenException($tokens->back());
}
$code = $unary . $this->parseChain($tokens, $code);
} elseif ($tokens->is(Tokenizer::MACRO_INCDEC)) {
if($this->_options & Fenom::FORCE_VERIFY) {
$code = $unary . '(isset(' . $code . ') ? ' . $code . $tokens->getAndNext() . ' : null)';
} else {
$code = $unary . $code . $tokens->getAndNext();
}
} else {
if($this->_options & Fenom::FORCE_VERIFY) {
$code = $unary . '(isset(' . $code . ') ? ' . $code . ' : null)';
} else {
$is_var = true;
$code = $this->parseScalar($tokens, true);
break;
case '$':
$code = $this->parseAccessor($tokens, $is_var);
if(!$is_var) {
$code = $unary . $code;
break;
}
}
} elseif ($tokens->is('$')) {
$is_var = false;
$code = $unary . $this->parseAccessor($tokens);
} elseif ($tokens->is(Tokenizer::MACRO_INCDEC)) {
if($this->_options & Fenom::FORCE_VERIFY) {
$var = $this->parseVariable($tokens);
$code = $unary . '(isset(' . $var . ') ? ' . $tokens->getAndNext() . $this->parseVariable($tokens).' : null)';
} else {
$code = $unary . $tokens->getAndNext() . $this->parseVariable($tokens);
}
} elseif ($tokens->is("(")) {
$tokens->next();
$code = $unary . "(" . $this->parseExpr($tokens) . ")";
$tokens->need(")")->next();
} elseif ($tokens->is(T_STRING)) {
if ($tokens->isSpecialVal()) {
$code = $unary . $tokens->getAndNext();
} elseif ($tokens->isNext("(") && !$tokens->getWhitespace()) {
$func = $this->_fenom->getModifier($modifier = $tokens->current(), $this);
if (!$func) {
throw new \Exception("Function " . $tokens->getAndNext() . " not found");
case T_VARIABLE:
if(!isset($code)) {
$code = $this->parseVariable($tokens);
}
if (!is_string($func)) { // dynamic modifier
$call = 'call_user_func_array($tpl->getStorage()->getModifier("' . $modifier . '"), array'.$this->parseArgs($tokens->next()).')'; // @todo optimize
if ($tokens->is("(") && $tokens->hasBackList(T_STRING, T_OBJECT_OPERATOR)) {
if ($this->_options & Fenom::DENY_METHODS) {
throw new \LogicException("Forbidden to call methods");
}
$code = $unary . $this->parseChain($tokens, $code);
} elseif ($tokens->is(Tokenizer::MACRO_INCDEC)) {
if($this->_options & Fenom::FORCE_VERIFY) {
$code = $unary . '(isset(' . $code . ') ? ' . $code . $tokens->getAndNext() . ' : null)';
} else {
$code = $unary . $code . $tokens->getAndNext();
}
} else {
$call = $func . $this->parseArgs($tokens->next());
if($this->_options & Fenom::FORCE_VERIFY) {
$code = $unary . '(isset(' . $code . ') ? ' . $code . ' : null)';
} else {
$is_var = true;
$code = $unary . $code;
}
}
break;
case T_DEC:
case T_INC:
if($this->_options & Fenom::FORCE_VERIFY) {
$var = $this->parseVariable($tokens);
$code = $unary . '(isset(' . $var . ') ? ' . $tokens->getAndNext() . $this->parseVariable($tokens).' : null)';
} else {
$code = $unary . $tokens->getAndNext() . $this->parseVariable($tokens);
}
break;
case '(':
$tokens->next();
$code = $unary . "(" . $this->parseExpr($tokens) . ")";
$tokens->need(")")->next();
break;
case T_STRING:
if ($tokens->isSpecialVal()) {
$code = $unary . $tokens->getAndNext();
} elseif ($tokens->isNext("(") && !$tokens->getWhitespace()) {
$func = $this->_fenom->getModifier($modifier = $tokens->current(), $this);
if (!$func) {
throw new \Exception("Function " . $tokens->getAndNext() . " not found");
}
if (!is_string($func)) { // dynamic modifier
$call = 'call_user_func_array($tpl->getStorage()->getModifier("' . $modifier . '"), array'.$this->parseArgs($tokens->next()).')'; // @todo optimize
} else {
$call = $func . $this->parseArgs($tokens->next());
}
$code = $unary . $this->parseChain($tokens, $call);
} elseif ($tokens->isNext(T_NS_SEPARATOR, T_DOUBLE_COLON)) {
$method = $this->parseStatic($tokens);
$args = $this->parseArgs($tokens);
$code = $unary . $this->parseChain($tokens, $method . $args);
} else {
return false;
}
break;
case T_ISSET:
case T_EMPTY:
$func = $tokens->getAndNext();
if ($tokens->is("(") && $tokens->isNext(T_VARIABLE)) {
$code = $unary . $func . "(" . $this->parseVariable($tokens->next()) . ")";
$tokens->need(')')->next();
} else {
throw new TokenizeException("Unexpected token " . $tokens->getNext() . ", isset() and empty() accept only variables");
}
break;
case '[':
if ($unary) {
throw new UnexpectedTokenException($tokens->back());
}
$code = $this->parseArray($tokens);
break;
default:
if ($unary) {
throw new UnexpectedTokenException($tokens->back());
} else {
return false;
}
$code = $unary . $this->parseChain($tokens, $call);
} elseif ($tokens->isNext(T_NS_SEPARATOR, T_DOUBLE_COLON)) {
$method = $this->parseStatic($tokens);
$args = $this->parseArgs($tokens);
$code = $unary . $this->parseChain($tokens, $method . $args);
} else {
return false;
}
} elseif ($tokens->is(T_ISSET, T_EMPTY)) {
$func = $tokens->getAndNext();
if ($tokens->is("(") && $tokens->isNext(T_VARIABLE)) {
$code = $unary . $func . "(" . $this->parseVariable($tokens->next()) . ")";
$tokens->need(')')->next();
} else {
throw new TokenizeException("Unexpected token " . $tokens->getNext() . ", isset() and empty() accept only variables");
}
} elseif ($tokens->is('[')) {
if ($unary) {
throw new UnexpectedTokenException($tokens->back());
}
$code = $this->parseArray($tokens);
} elseif ($unary) {
throw new UnexpectedTokenException($tokens->back());
} else {
return false;
}
if (($allows & self::TERM_MODS) && $tokens->is('|')) {
$code = $this->parseModifier($tokens, $code);
@ -958,14 +979,20 @@ class Template extends Render
/**
* Parse accessor
* @param Tokenizer $tokens
* @param bool $is_var
* @return string
*/
public function parseAccessor(Tokenizer $tokens)
public function parseAccessor(Tokenizer $tokens, &$is_var = false)
{
$accessor = $tokens->need('$')->next()->need('.')->next()->current();
$callback = $this->getStorage()->getAccessor($accessor);
if($callback) {
return call_user_func($callback, $tokens->next(), $this);
$parser = $this->getStorage()->getAccessor($accessor);
$is_var = false;
if($parser) {
if(is_string($parser)) {
return call_user_func_array($parser, array($tokens->next(), $this, &$is_var));
} else {
return call_user_func_array($parser['parser'], array($parser['accessor'], $tokens->next(), $this, &$is_var));
}
} else {
throw new \RuntimeException("Unknown accessor '$accessor'");
}
@ -1342,6 +1369,7 @@ class Template extends Render
{
$recursive = false;
$macro = false;
if (isset($this->macros[$name])) {
$macro = $this->macros[$name];
$recursive = $macro['recursive'];
@ -1414,7 +1442,6 @@ class Template extends Render
* (1 + 2.3, 'string', $var, [2,4])
*
* @param Tokenizer $tokens
* @param bool $as_string
* @return string
*/
public function parseArgs(Tokenizer $tokens)

View File

@ -77,34 +77,12 @@ class MacrosTest extends TestCase
$this->tpl(
"macro_recursive_import.tpl",
'
{import "macro_recursive.tpl" as math}
{import [factorial] from "macro_recursive.tpl" as math}
{math.factorial num=10}'
);
}
public function _testSandbox()
{
try {
// $this->fenom->compile("macro_recursive.tpl")->display([]);
// $this->fenom->flush();
// var_dump($this->fenom->fetch("macro_recursive.tpl", []));
var_dump(
$this->fenom->compileCode(
'{macro factorial(num)}
{if $num}
{$num} {macro.factorial num=$num-1} {$num}
{/if}
{/macro}'
)->getBody()
);
// var_dump($this->fenom->display("macro_recursive_import.tpl", array()));
} catch (\Exception $e) {
var_dump($e->getMessage() . ": " . $e->getTraceAsString());
}
exit;
}
/**
* @throws \Exception
* @group macros
@ -162,6 +140,7 @@ class MacrosTest extends TestCase
$this->assertSame("10 9 8 7 6 5 4 3 2 1 1 2 3 4 5 6 7 8 9 10", Modifier::strip($tpl->fetch(array()), true));
}
public function testImportRecursive()
{
$this->fenom->compile('macro_recursive_import.tpl');

View File

@ -10,23 +10,25 @@ class SandboxTest extends TestCase {
*/
public function test()
{
return;
$this->fenom->setOptions(\Fenom::FORCE_VERIFY);
$this->fenom->addAccessorSmart('q', 'Navi::$q', \Fenom::ACCESSOR_VAR);
// $this->assertEquals([1, 2, 4, "as" => 767, "df" => ["qert"]], [1, 2, 4, "as" => 767, "df" => ["qet"]]);
// $this->fenom->addBlockCompiler('php', 'Fenom\Compiler::nope', function ($tokens, Tag $tag) {
// return '<?php ' . $tag->cutContent();
// });
// $this->tpl('welcome.tpl', '{$a}');
// var_dump($this->fenom->compileCode('{set $a=$one|min:0..$three|max:4}')->getBody());
// try {
// var_dump($this->fenom->compileCode('{foreach $a as $k => $v} {/foreach}')->getBody());
// } catch (\Exception $e) {
// print_r($e->getMessage() . "\n" . $e->getTraceAsString());
// while ($e->getPrevious()) {
// $e = $e->getPrevious();
// print_r("\n\n" . $e->getMessage() . " in {$e->getFile()}:{$e->getLine()}\n" . $e->getTraceAsString());
// }
// }
// exit;
try {
var_dump($this->fenom->compileCode('{set $.q.ddqd->d() = 333}')->getBody());
} catch (\Exception $e) {
print_r($e->getMessage() . "\n" . $e->getTraceAsString());
while ($e->getPrevious()) {
$e = $e->getPrevious();
print_r("\n\n" . $e->getMessage() . " in {$e->getFile()}:{$e->getLine()}\n" . $e->getTraceAsString());
}
}
exit;
}
}