Merge remote-tracking branch 'refs/remotes/origin/develop'

This commit is contained in:
bzick 2013-08-23 00:56:55 +04:00
commit 2d871b2dd7
16 changed files with 499 additions and 206 deletions

4
.gitignore vendored
View File

@ -10,4 +10,6 @@ tests/resources/template/*
!.gitkeep
benchmark/compile/*
benchmark/templates/inheritance/smarty
benchmark/templates/inheritance/twig
benchmark/templates/inheritance/twig
benchmark/sandbox/compiled/*
!.gitkeep

View File

@ -1,6 +1,14 @@
CHANGELOG
=========
## 1.3.0
- Feature #41: Add system variable `$`.
- Fix bug when recursive macros doesn't work in Template
- Recognize variable parser
- Tests++
- Docs--
### 1.2.2 (2013-08-07)
- Fix bug in setOptions method

View File

View File

@ -1,3 +1,11 @@
<?php
require_once __DIR__.'/../../vendor/autoload.php';
require_once __DIR__.'/../../vendor/autoload.php';
$fenom = Fenom::factory(__DIR__.'/templates', __DIR__.'/compiled', Fenom::FORCE_COMPILE);
$fenom->display("greeting.tpl", array(
"user" => array(
"name" => "Ivka"
)
));

View File

@ -0,0 +1,8 @@
Greeting,
{if $user}
{$user.name}!
{else}
anonymous?
{/if}
3

5
docs/dev/git.md Normal file
View File

@ -0,0 +1,5 @@
Git conversation
================
Ветка `master` содержит стабильную последнюю версию проекта. В ветку `master` может сливаться новая версия проекта из `develop` или исправления.
Ветка `develop`, для разработки, содержит не стабильную версию проекта. Принимает все новшевства, изменения и исправления.

88
docs/dev/internal.md Normal file
View File

@ -0,0 +1,88 @@
How it work
===========
## Терминология
* Исходный шаблон - изначальный вид шаблона в специальном синтаксисе
* Код шаблона - резальтат компиляции шаблона, PHP код.
* Провайдер - объект, источник исходных шаблонов.
## Классы
* `Fenom` - является хранилищем
* [шаблонов](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L88)
* [модификаторов](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L112)
* [фильтров](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L73)
* [тегов](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L140)
* [провайдеров](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L107)
* [настройки](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L98) - маска из [опций](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L29)
* [список](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L131) разрешенных функций
а также обладает соответсвующими setter-ами и getter-ами для настройки.
* `Fenom\Tokenizer` - разбирает, при помощи [tokens_get_all](http://docs.php.net/manual/en/function.token-get-all.php), строку на токены, которые хранит [массивом](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Tokenizer.php#L84).
Обладает методами для обработки токенов, работающими как с [конкретными токенами](http://docs.php.net/manual/en/tokens.php) так и с их [группами](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Tokenizer.php#L94).
* `Fenom\Render` - простейший шаблон. Хранит
* `Closure` с [PHP кодом](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Render.php#L30) шаблона
* [настройки](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Render.php#L19)
* [зависимости](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Render.php#L59)
* `Fenom\Template` - шаблон с функцией компиляции, расширен от `Fenom\Render`. Содержит различные методы для разбора выражений при помощи `Fenom\Tokenizer`.
* `Fenom\Compiler` - набор правил разбора различных тегов.
* `Fenom\Modifier` - набор модификаторов.
* `Fenom\Scope` - абстрактный уровень блочного тега.
* `Fenom\ProviderInterface` - интерфейс провадеров шаблонов
* `Fenom\Provider` - примитивный провайдер шаблонов с файловой системы.
## Процесс работы
При вызове метода `Fenom::display($template, $vars)` шаблонизатор [ищет](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L712) в своем хранилище уже загруженный шаблон.
Если шаблона [нет](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L727) - либо [загружает](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L762) код шаблона с файловой системыб либо [инициирует](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L759) его [компиляцию](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L788).
### Компиляция шаблонов
* [Создается](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L660) "пустой" `Fenom\Template`
* В него [загружется](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L157) исходный шаблон [из провайдера](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L167)
* Исходный шаблон проходит [pre-фильтры](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L200).
* Начинается [разбор](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L196) исходного шаблона.
* [Ищется](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L204) первый открывающий тег символ - `{`
* [Смотрятся](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L205) следующий за `{` символ.
* Если `}` или пробельный символ - ищется следующий символ `{`
* Если `*` - ищется `*}`, текст до которого, в последствии, вырезается.
* Ищется символ `}`. Полученный фрагмент шаблона считается тегом.
* Если [был тег](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L238) `{ignore}` название тега проверяется на закрытие этого тега.
* Для тега [создается](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L245) токенайзер и отдается в [диспетчер](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L488) тегов
* Диспетчер тега вызывает различные парсеры выражений, компилятор тега и возвращает PHP код (см ниже).
* Полученный фрагмент PHP кода [обрабатывается и прикрепляется](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L362) к коду шаблона.
* Ищется следующий `{` символ...
* ...
* В конце проверяется токенайзер на наличие не используемых токенов, если таковые есть - выбрасывается ошибка.
* [Проверяется](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L264) стек на наличие не закрытых блоковых тегов
* PHP код проходит [post-фильтры](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L282)
* Код шаблона [сохраняеться](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L799) на файлувую систему
* Код шаблона выполняется для использования
### Как работает токенайзер
Объек токенайзера принимает на вход любую строчку и разбирает ее при помощи функции token_get_all(). Полученные токен складываются в массив. Каждый токен прдсатвляет из себя числовой массив из 4-х элементов:
* Код токена. Это либо число либо один символ.
* Тело токена. Содержимое токена.
* Номер строки в исходной строке
* Пробельные символы, идущие за токеном
Токенайзер обладает внутренним указателем на "текущий" токен, передвигая указатель можно получить доступ к токенам через специальные функции-проверки. Почти все функции-проверки проверяют текущее значение на соответствие кода токену. Вместо кода может быть отдан код группы токенов.
### Как работает диспетчер тегов
* Проверяет, не является выражение в токенайзере [тегом ignore](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L492).
* Проверяет, не является выражение в токенайзере [закрывающим тегом](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L499).
* Проверяет, не является выражение в токенайзере [скалярным значением](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L566).
* По имени тега из [списка тегов](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom.php#L140) выбирается массив и запускается [соответсвующий](https://github.com/bzick/fenom/blob/1.2.2/src/Fenom/Template.php#L582) парсер.
* Парсер возвращает PHP код
### Как работают парсеры
Парсер всегда получает объект токенайзера. Курсор токенайзера установлен на токен с которого начинается выражение, которое должен разобрать парсер.
Таким образом, по завершению разбора выражения, парсер должен установить курсор токенайзера на первый незнакомый ему символ.
Для примера рассмортим парсер переменной `Fenom\Template::parseVar()`.
В шаблоне имеется тег {$list.one.c|modifier:1.2}. В парсер будет отдан объект токенайзера `new Tokenizer('$list.one.c|modifier:1.2')` с токенами `$list` `.` `one` `.` `c` `|` `modifier` `:` `1.2`.
Указатель курсора установлен на токен `$list`. После разбора токенов, курсор будет установлен на `|` так как это не знакомый парсеру переменных токен. Следующий парсер может быть вызван `Fenom\Template::parseModifier()`, который распарсит модификатор.

View File

@ -3,4 +3,4 @@ Develop
If you want to discuss the enhancement of the Fenom, create an issue on Github or submit a pull request.
For questions: a.cobest@gmail.com (English, Russian languages)
For questions: a.cobest@gmail.com (English, Russian languages)

View File

@ -1,11 +1,11 @@
Syntax [RU]
===========
Fenom have [Smarty](http://www.smarty.net/) like syntax.
Fenom implement [Smarty](http://www.smarty.net/) syntax with some improvements
## Variable
### Get/print value
### Use variables
```smarty
{$foo}
@ -20,7 +20,31 @@ Fenom have [Smarty](http://www.smarty.net/) like syntax.
{$foo.$bar}
{$foo[$bar]}
{$foo->bar}
{$foo->bar()}
{$foo->bar.buz}
```
### System variable
Unnamed system variable starts with `$.` and allow access to global variables and system info:
* `$.get` is `$_GET`.
* `$.post` is `$_POST`.
* `$.cookie` is `$_COOKIE`.
* `$.session` is `$_SESSION`.
* `$.globals` is `$GLOBALS`.
* `$.request` is `$_REQUEST`.
* `$.files` is `$_FILES`.
* `$.server` is `$_SERVER`.
* `$.env` is `$_ENV`.
* `$.tpl.name` returns current template name.
* `$.tpl.schema` returns current schema of the template.
* `$.version` returns version of the Fenom.
* `$.const` paste constant.
```smarty
{if $.get.debug? && $.const.DEBUG}
...
{/if}
```
### Multidimensional value support

View File

@ -17,7 +17,7 @@ use Fenom\Template;
*/
class Fenom
{
const VERSION = '1.2';
const VERSION = '1.3';
/* Actions */
const INLINE_COMPILER = 1;
@ -27,6 +27,7 @@ class Fenom
const MODIFIER = 5;
/* Options */
const DENY_ACCESSOR = 0x8;
const DENY_METHODS = 0x10;
const DENY_NATIVE_FUNCS = 0x20;
const FORCE_INCLUDE = 0x40;
@ -55,6 +56,7 @@ class Fenom
* @see setOptions
*/
private static $_options_list = array(
"disable_accessor" => self::DENY_ACCESSOR,
"disable_methods" => self::DENY_METHODS,
"disable_native_funcs" => self::DENY_NATIVE_FUNCS,
"disable_cache" => self::DISABLE_CACHE,
@ -129,7 +131,7 @@ class Fenom
* @var array of allowed PHP functions
*/
protected $_allowed_funcs = array(
"count" => 1, "is_string" => 1, "is_array" => 1, "is_numeric" => 1, "is_int" => 1,
"count" => 1, "is_string" => 1, "is_array" => 1, "is_numeric" => 1, "is_int" => 1, 'constant' => 1,
"is_object" => 1, "strtotime" => 1, "gettype" => 1, "is_double" => 1, "json_encode" => 1, "json_decode" => 1,
"ip2long" => 1, "long2ip" => 1, "strip_tags" => 1, "nl2br" => 1, "explode" => 1, "implode" => 1
);
@ -755,12 +757,15 @@ class Fenom
protected function _load($tpl, $opts)
{
$file_name = $this->_getCacheName($tpl, $opts);
if (!is_file($this->_compile_dir . "/" . $file_name)) {
return $this->compile($tpl, true, $opts);
} else {
if (is_file($this->_compile_dir . "/" . $file_name)) {
$fenom = $this;
return include($this->_compile_dir . "/" . $file_name);
$_tpl = include($this->_compile_dir . "/" . $file_name);
/* @var Fenom\Render $tpl */
if($_tpl->isValid()) {
return $_tpl;
}
}
return $this->compile($tpl, true, $opts);
}
/**

View File

@ -33,14 +33,6 @@ class Compiler
public static function tagInclude(Tokenizer $tokens, Template $tpl)
{
$name = false;
// if($tokens->is('[')) {
// $tokens->next();
// if(!$name && $tokens->is(T_CONSTANT_ENCAPSED_STRING)) {
// if($tpl->getStorage()->templateExists($_name = substr($tokens->getAndNext(), 1, -1))) {
// $name = $_name;
// }
// }
// }
$cname = $tpl->parsePlainArg($tokens, $name);
$p = $tpl->parseParams($tokens);
if ($p) { // if we have additionally variables
@ -74,7 +66,7 @@ class Compiler
public static function ifOpen(Tokenizer $tokens, Scope $scope)
{
$scope["else"] = false;
return 'if(' . $scope->tpl->parseExp($tokens, true) . ') {';
return 'if(' . $scope->tpl->parseExpr($tokens) . ') {';
}
/**
@ -91,7 +83,7 @@ class Compiler
if ($scope["else"]) {
throw new InvalidUsageException('Incorrect use of the tag {elseif}');
}
return '} elseif(' . $scope->tpl->parseExp($tokens, true) . ') {';
return '} elseif(' . $scope->tpl->parseExpr($tokens) . ') {';
}
/**
@ -137,11 +129,11 @@ class Compiler
}
$tokens->get(T_AS);
$tokens->next();
$value = $scope->tpl->parseVariable($tokens, Template::DENY_MODS | Template::DENY_ARRAY);
$value = $scope->tpl->parseVar($tokens);
if ($tokens->is(T_DOUBLE_ARROW)) {
$tokens->next();
$key = $value;
$value = $scope->tpl->parseVariable($tokens, Template::DENY_MODS | Template::DENY_ARRAY);
$value = $scope->tpl->parseVar($tokens);
}
$scope["after"] = array();
@ -154,7 +146,7 @@ class Compiler
}
$tokens->getNext("=");
$tokens->next();
$p[$param] = $scope->tpl->parseVariable($tokens, Template::DENY_MODS | Template::DENY_ARRAY);
$p[$param] = $scope->tpl->parseVar($tokens);
}
if ($p["index"]) {
@ -228,7 +220,7 @@ class Compiler
$var = $scope->tpl->parseVariable($tokens, Template::DENY_MODS);
$tokens->get("=");
$tokens->next();
$val = $scope->tpl->parseExp($tokens, true);
$val = $scope->tpl->parseExpr($tokens);
$p = $scope->tpl->parseParams($tokens, $p);
if (is_numeric($p["step"])) {
@ -305,7 +297,7 @@ class Compiler
*/
public static function whileOpen(Tokenizer $tokens, Scope $scope)
{
return 'while(' . $scope->tpl->parseExp($tokens, true) . ') {';
return 'while(' . $scope->tpl->parseExpr($tokens) . ') {';
}
/**
@ -319,7 +311,7 @@ class Compiler
public static function switchOpen(Tokenizer $tokens, Scope $scope)
{
$scope["no-break"] = $scope["no-continue"] = true;
$scope["switch"] = 'switch(' . $scope->tpl->parseExp($tokens, true) . ') {';
$scope["switch"] = 'switch(' . $scope->tpl->parseExpr($tokens) . ') {';
// lazy init
return '';
}
@ -334,7 +326,7 @@ class Compiler
*/
public static function tagCase(Tokenizer $tokens, Scope $scope)
{
$code = 'case ' . $scope->tpl->parseExp($tokens, true) . ': ';
$code = 'case ' . $scope->tpl->parseExpr($tokens) . ': ';
if ($scope["switch"]) {
unset($scope["no-break"], $scope["no-continue"]);
$code = $scope["switch"] . "\n" . $code;
@ -706,14 +698,14 @@ class Compiler
*/
public static function varOpen(Tokenizer $tokens, Scope $scope)
{
$var = $scope->tpl->parseVariable($tokens, Template::DENY_MODS);
$var = $scope->tpl->parseVar($tokens);
if ($tokens->is('=')) { // inline tag {var ...}
$scope->is_closed = true;
$tokens->next();
if ($tokens->is("[")) {
return $var . '=' . $scope->tpl->parseArray($tokens);
} else {
return $var . '=' . $scope->tpl->parseExp($tokens, true);
return $var . '=' . $scope->tpl->parseExpr($tokens);
}
} else {
$scope["name"] = $var;
@ -771,7 +763,7 @@ class Compiler
if ($tokens->is("[")) {
$exp = $tpl->parseArray($tokens);
} else {
$exp = $tpl->parseExp($tokens, true);
$exp = $tpl->parseExpr($tokens);
}
if ($tokens->valid()) {
$p = $tpl->parseParams($tokens);
@ -944,7 +936,7 @@ class Compiler
throw new InvalidUsageException("Raw mode allow for expressions or functions");
}
} else {
$code = $tpl->out($tpl->parseExp($tokens, true));
$code = $tpl->out($tpl->parseExpr($tokens));
}
$tpl->escape = $escape;
return $code;

View File

@ -9,6 +9,7 @@
*/
namespace Fenom;
use Fenom;
use Symfony\Component\Yaml\Exception\RuntimeException;
/**
* Primitive template
@ -69,6 +70,11 @@ class Render extends \ArrayObject
*/
protected $_provider;
/**
* @var \Closure[]
*/
protected $_macros;
/**
* @param Fenom $fenom
* @param callable $code template body
@ -190,7 +196,11 @@ class Render extends \ArrayObject
* @param $name
* @return mixed
*/
public function getMacro($name) {
public function getMacro($name)
{
if(empty($this->_macros[$name])) {
throw new RuntimeException('macro not found');
}
return $this->_macros[$name];
}
@ -234,4 +244,22 @@ class Render extends \ArrayObject
{
throw new \BadMethodCallException("Unknown method " . $method);
}
public function __get($name)
{
if($name == 'info') {
return array(
'name' => $this->_name,
'schema' => $this->_scm,
'time' => $this->_time
);
} else {
return null;
}
}
public function __isset($name)
{
return $name == 'info';
}
}

View File

@ -99,7 +99,7 @@ class Template extends Render
private $_filters = array();
private static $_checkers = array(
protected static $_tests = array(
'integer' => 'is_int(%s)',
'int' => 'is_int(%s)',
'float' => 'is_float(%s)',
@ -223,6 +223,10 @@ class Template extends Render
unset($comment); // cleanup
break;
default:
// var_dump($this->_src[$pos]);
if($this->_src[$pos] === "\n") {
$pos++;
}
$this->_appendText(substr($this->_src, $pos, $start - $pos));
$end = $start + 1;
do {
@ -260,6 +264,9 @@ class Template extends Render
}
gc_collect_cycles();
if($end < strlen($this->_src) && $this->_src[$end + 1] === "\n") {
$end++;
}
$this->_appendText(substr($this->_src, $end ? $end + 1 : 0)); // append tail of the template
if ($this->_stack) {
$_names = array();
@ -397,33 +404,40 @@ class Template extends Render
*/
public function getTemplateCode()
{
if($this->macros) {
$macros = array();
foreach($this->macros as $m) {
if($m["recursive"]) {
$macros[] = "\t\t'".$m["name"]."' => function (\$tpl) {\n?>".$m["body"]."<?php\n}";
}
}
$macros = "\n".implode(",\n", $macros);
} else {
$macros = "";
}
$before = $this->_before ? $this->_before . "\n" : "";
return "<?php \n" .
"/** Fenom template '" . $this->_name . "' compiled at " . date('Y-m-d H:i:s') . " */\n" .
$before . // some code 'before' template
"return new Fenom\\Render(\$fenom, " . $this->_getClosureSource() . ", array(\n".
"\t'options' => {$this->_options},\n".
"\t'provider' => ".var_export($this->_scm, true).",\n".
"\t'name' => ".var_export($this->_name, true).",\n".
"\t'base_name' => ".var_export($this->_base_name, true).",\n".
"\t'time' => {$this->_time},\n".
"\t'depends' => ".var_export($this->_base_name, true).",\n".
"\t'macros' => array({$macros}),
"return new Fenom\\Render(\$fenom, " . $this->_getClosureSource() . ", array(\n" .
"\t'options' => {$this->_options},\n" .
"\t'provider' => " . var_export($this->_scm, true) . ",\n" .
"\t'name' => " . var_export($this->_name, true) . ",\n" .
"\t'base_name' => " . var_export($this->_base_name, true) . ",\n" .
"\t'time' => {$this->_time},\n" .
"\t'depends' => " . var_export($this->_base_name, true) . ",\n" .
"\t'macros' => " . $this->_getMacrosArray() . ",\n
));\n";
}
/**
* Make array with macros code
* @return string
*/
private function _getMacrosArray()
{
if ($this->macros) {
$macros = array();
foreach ($this->macros as $m) {
if ($m["recursive"]) {
$macros[] = "\t\t'" . $m["name"] . "' => function (\$tpl) {\n?>" . $m["body"] . "<?php\n}";
}
}
return "array(\n" . implode(",\n", $macros).")";
} else {
return 'array()';
}
}
/**
* Return closure code
* @return string
@ -444,7 +458,7 @@ class Template extends Render
{
if (!$this->_code) {
// evaluate template's code
eval("\$this->_code = " . $this->_getClosureSource() . ";");
eval("\$this->_code = " . $this->_getClosureSource() . ";\n\$this->_macros = ".$this->_getMacrosArray() .';');
if (!$this->_code) {
throw new CompileException("Fatal error while creating the template");
}
@ -459,6 +473,7 @@ class Template extends Render
*/
public function addDepend(Render $tpl)
{
// var_dump($tpl->getScm(),"$tpl", (new \Exception())->getTraceAsString() );
$this->_depends[$tpl->getScm()][$tpl->getName()] = $tpl->getTime();
}
@ -498,10 +513,8 @@ class Template extends Render
}
} elseif ($tokens->is('/')) {
return $this->parseEndTag($tokens);
} elseif ($tokens->is('#')) {
return $this->out($this->parseConst($tokens), $tokens);
} else {
return $this->out($this->parseExp($tokens), $tokens);
return $this->out($this->parseExpr($tokens), $tokens);
}
} catch (InvalidUsageException $e) {
throw new CompileException($e->getMessage() . " in {$this} line {$this->_line}", 0, E_ERROR, $this->_name, $this->_line, $e);
@ -563,11 +576,11 @@ class Template extends Render
if ($tokens->is(Tokenizer::MACRO_STRING)) {
$action = $tokens->getAndNext();
} else {
return $this->out($this->parseExp($tokens)); // may be math and/or boolean expression
return $this->out($this->parseExpr($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));
return $this->out($this->parseExpr($tokens));
}
if ($tokens->is('.')) {
@ -616,14 +629,13 @@ class Template extends Render
}
/**
* Parse expressions. The mix of operations and terms.
* Parse expressions. The mix of operators and terms.
*
* @param Tokenizer $tokens
* @param bool $required
* @return string
* @throws Error\UnexpectedTokenException
*/
public function parseExp(Tokenizer $tokens, $required = false)
public function parseExpr(Tokenizer $tokens)
{
$exp = array();
$var = false; // last term was: true - variable, false - mixed
@ -673,15 +685,15 @@ class Template extends Render
}
} elseif ($tokens->is('~')) { // string concatenation operator: 'asd' ~ $var
$concat = array(array_pop($exp));
while($tokens->is('~')) {
while ($tokens->is('~')) {
$tokens->next();
if($tokens->is(T_LNUMBER, T_DNUMBER)) {
$concat[] = "strval(".$this->parseTerm($tokens).")";
if ($tokens->is(T_LNUMBER, T_DNUMBER)) {
$concat[] = "strval(" . $this->parseTerm($tokens) . ")";
} else {
$concat[] = $this->parseTerm($tokens);
$concat[] = $this->parseTerm($tokens);
}
}
$exp[] = "(".implode(".", $concat).")";
$exp[] = "(" . implode(".", $concat) . ")";
} else {
break;
}
@ -690,17 +702,14 @@ class Template extends Render
}
}
if ($op) {
throw new UnexpectedTokenException($tokens);
}
if ($required && !$exp) {
if ($op || !$exp) {
throw new UnexpectedTokenException($tokens);
}
return implode(' ', $exp);
}
/**
* Parse any term: -2, ++$var, 'adf'|mod:4
* Parse any term of expression: -2, ++$var, 'adf'|mod:4
*
* @param Tokenizer $tokens
* @param bool $is_var
@ -712,132 +721,98 @@ class Template extends Render
public function parseTerm(Tokenizer $tokens, &$is_var = false)
{
$is_var = false;
$unary = "";
term: {
if ($tokens->is(T_LNUMBER, T_DNUMBER)) {
return $unary . $this->parseScalar($tokens, true);
} elseif ($tokens->is(T_CONSTANT_ENCAPSED_STRING, '"', T_ENCAPSED_AND_WHITESPACE)) {
if ($unary) {
throw new UnexpectedTokenException($tokens->back());
}
return $this->parseScalar($tokens, true);
} elseif ($tokens->is(T_VARIABLE)) {
$var = $this->parseVar($tokens);
if ($tokens->is(Tokenizer::MACRO_INCDEC, "|", "!", "?")) {
return $unary . $this->parseVariable($tokens, 0, $var);
} elseif ($tokens->is("(") && $tokens->hasBackList(T_STRING)) { // method call
return $unary . $this->parseVariable($tokens, 0, $var);
} elseif ($unary) {
return $unary . $var;
} else {
$is_var = true;
return $var;
}
} elseif ($tokens->is(Tokenizer::MACRO_INCDEC)) {
return $unary . $this->parseVariable($tokens);
} elseif ($tokens->is("(")) {
$tokens->next();
$exp = $unary . "(" . $this->parseExp($tokens, true) . ")";
$tokens->need(")")->next();
return $exp;
} elseif ($tokens->is(Tokenizer::MACRO_UNARY)) {
if ($unary) {
throw new UnexpectedTokenException($tokens);
}
$unary = $tokens->getAndNext();
goto term;
} elseif ($tokens->is(T_STRING)) {
if ($tokens->isSpecialVal()) {
return $unary . $tokens->getAndNext();
} elseif ($tokens->isNext("(") && !$tokens->getWhitespace()) {
$func = $this->_fenom->getModifier($tokens->current(), $this);
if (!$func) {
throw new \Exception("Function " . $tokens->getAndNext() . " not found");
}
$tokens->next();
$func = $func . $this->parseArgs($tokens);
if ($tokens->is('|')) {
return $unary . $this->parseModifier($tokens, $func);
} else {
return $unary . $func;
}
} else {
return false;
}
} elseif ($tokens->is(T_ISSET, T_EMPTY)) {
$func = $tokens->getAndNext();
if ($tokens->is("(") && $tokens->isNext(T_VARIABLE)) {
$tokens->next();
$exp = $func . "(" . $this->parseVar($tokens) . ")";
$tokens->need(')')->next();
return $unary . $exp;
} else {
throw new TokenizeException("Unexpected token " . $tokens->getNext() . ", isset() and empty() accept only variables");
}
} elseif ($tokens->is('[')) {
if ($unary) {
throw new UnexpectedTokenException($tokens->back());
}
return $this->parseArray($tokens);
if ($tokens->is(Tokenizer::MACRO_UNARY)) {
$unary = $tokens->getAndNext();
} else {
$unary = "";
}
if ($tokens->is(T_LNUMBER, T_DNUMBER)) {
return $unary . $this->parseScalar($tokens, true);
} elseif ($tokens->is(T_CONSTANT_ENCAPSED_STRING, '"', T_ENCAPSED_AND_WHITESPACE)) {
if ($unary) {
throw new UnexpectedTokenException($tokens->back());
}
return $this->parseScalar($tokens, true);
} elseif ($tokens->is(T_VARIABLE)) {
$var = $this->parseVar($tokens);
if ($tokens->is(Tokenizer::MACRO_INCDEC, "|", "!", "?")) {
return $unary . $this->parseVariable($tokens, 0, $var);
} elseif ($tokens->is("(") && $tokens->hasBackList(T_STRING)) { // method call
return $unary . $this->parseVariable($tokens, 0, $var);
} elseif ($unary) {
$tokens->back();
throw new UnexpectedTokenException($tokens);
return $unary . $var;
} else {
$is_var = true;
return $var;
}
} elseif ($tokens->is('$')) {
$var = $this->parseAccessor($tokens, $is_var);
if ($tokens->is(Tokenizer::MACRO_INCDEC, "|", "!", "?")) {
return $unary . $this->parseVariable($tokens, 0, $var);
} else {
return $unary . $var;
}
} elseif ($tokens->is(Tokenizer::MACRO_INCDEC)) {
return $unary . $this->parseVariable($tokens);
} elseif ($tokens->is("(")) {
$tokens->next();
$exp = $unary . "(" . $this->parseExpr($tokens) . ")";
$tokens->need(")")->next();
return $exp;
} elseif ($tokens->is(T_STRING)) {
if ($tokens->isSpecialVal()) {
return $unary . $tokens->getAndNext();
} elseif ($tokens->isNext("(") && !$tokens->getWhitespace()) {
$func = $this->_fenom->getModifier($tokens->current(), $this);
if (!$func) {
throw new \Exception("Function " . $tokens->getAndNext() . " not found");
}
$tokens->next();
$func = $func . $this->parseArgs($tokens);
if ($tokens->is('|')) {
return $unary . $this->parseModifier($tokens, $func);
} else {
return $unary . $func;
}
} else {
return false;
}
} elseif ($tokens->is(T_ISSET, T_EMPTY)) {
$func = $tokens->getAndNext();
if ($tokens->is("(") && $tokens->isNext(T_VARIABLE)) {
$tokens->next();
$exp = $func . "(" . $this->parseVar($tokens) . ")";
$tokens->need(')')->next();
return $unary . $exp;
} else {
throw new TokenizeException("Unexpected token " . $tokens->getNext() . ", isset() and empty() accept only variables");
}
} elseif ($tokens->is('[')) {
if ($unary) {
throw new UnexpectedTokenException($tokens->back());
}
return $this->parseArray($tokens);
} elseif ($unary) {
$tokens->back();
throw new UnexpectedTokenException($tokens);
} else {
return false;
}
}
/**
* Parse simple variable (without modifier etc)
*
* @param Tokenizer $tokens
* @param int $options
* @return string
*/
public function parseVar(Tokenizer $tokens, $options = 0)
public function parseVar(Tokenizer $tokens)
{
$var = $tokens->get(T_VARIABLE);
$_var = '$tpl["' . substr($var, 1) . '"]';
$tokens->next();
while ($t = $tokens->key()) {
if ($t === "." && !($options & self::DENY_ARRAY)) {
$key = $tokens->getNext();
if ($tokens->is(T_VARIABLE)) {
$key = "[ " . $this->parseVariable($tokens, self::DENY_ARRAY) . " ]";
} elseif ($tokens->is(Tokenizer::MACRO_STRING)) {
$key = '["' . $key . '"]';
$tokens->next();
} elseif ($tokens->is(Tokenizer::MACRO_SCALAR, '"')) {
$key = "[" . $this->parseScalar($tokens, false) . "]";
} else {
break;
}
$_var .= $key;
} elseif ($t === "[" && !($options & self::DENY_ARRAY)) {
$tokens->next();
if ($tokens->is(Tokenizer::MACRO_STRING)) {
if ($tokens->isNext("(")) {
$key = "[" . $this->parseExp($tokens) . "]";
} else {
$key = '["' . $tokens->current() . '"]';
$tokens->next();
}
} else {
$key = "[" . $this->parseExp($tokens, true) . "]";
}
$tokens->get("]");
$tokens->next();
$_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;
}
}
$_var = $this->_var($tokens, $_var);
if ($this->_options & Fenom::FORCE_VERIFY) {
return 'isset(' . $_var . ') ? ' . $_var . ' : null';
} else {
@ -845,6 +820,56 @@ class Template extends Render
}
}
/**
* @param Tokenizer $tokens
* @param $var
* @return string
* @throws Error\UnexpectedTokenException
*/
protected function _var(Tokenizer $tokens, $var)
{
while ($t = $tokens->key()) {
if ($t === ".") {
$tokens->next();
if ($tokens->is(T_VARIABLE)) {
$key = '[ $tpl["' . substr($tokens->getAndNext(), 1) . '"] ]';
} elseif ($tokens->is(Tokenizer::MACRO_STRING)) {
$key = '["' . $tokens->getAndNext() . '"]';
} elseif ($tokens->is(Tokenizer::MACRO_SCALAR)) {
$key = "[" . $tokens->getAndNext() . "]";
} elseif ($tokens->is('"')) {
$key = "[" . $this->parseQuote($tokens) . "]";
} else {
throw new UnexpectedTokenException($tokens);
}
$var .= $key;
} elseif ($t === "[") {
$tokens->next();
if ($tokens->is(Tokenizer::MACRO_STRING)) {
if ($tokens->isNext("(")) {
$key = "[" . $this->parseExpr($tokens) . "]";
} else {
$key = '["' . $tokens->current() . '"]';
$tokens->next();
}
} else {
$key = "[" . $this->parseExpr($tokens) . "]";
}
$tokens->get("]");
$tokens->next();
$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;
}
/**
* Parse complex variable
* $var.foo[bar]["a"][1+3/$var]|mod:3:"w":$var3|mod3
@ -901,6 +926,55 @@ class Template extends Render
return $var;
}
/**
* Parse accessor
*/
public function parseAccessor(Tokenizer $tokens, &$is_var)
{
$is_var = false;
$vars = array(
'get' => '$_GET',
'post' => '$_POST',
'session' => '$_SESSION',
'cookie' => '$_COOKIE',
'request' => '$_REQUEST',
'files' => '$_FILES',
'globals' => '$GLOBALS',
'server' => '$_SERVER',
'env' => '$_ENV',
'tpl' => '$tpl->info'
);
if ($this->_options & Fenom::DENY_ACCESSOR) {
throw new \LogicException("Accessor are disabled");
}
$key = $tokens->need('$')->next()->need('.')->next()->current();
$tokens->next();
if (isset($vars[$key])) {
$is_var = true;
return $this->_var($tokens, $vars[$key]);
}
switch ($key) {
case 'const':
$tokens->need('.')->next();
$var = $this->parseName($tokens);
if (!defined($var)) {
$var = 'constant(' . var_export($var, true) . ')';
}
break;
case 'version':
$var = '\Fenom::VERSION';
break;
default:
throw new UnexpectedTokenException($tokens);
}
if ($tokens->is('|')) {
return $this->parseModifier($tokens, $var);
} else {
return $var;
}
}
/**
* Parse ternary operator
*
@ -917,9 +991,9 @@ class Template extends Render
if ($tokens->is(":")) {
$tokens->next();
if ($empty) {
return '(empty(' . $var . ') ? (' . $this->parseExp($tokens, true) . ') : ' . $var . ')';
return '(empty(' . $var . ') ? (' . $this->parseExpr($tokens) . ') : ' . $var . ')';
} else {
return '(isset(' . $var . ') ? ' . $var . ' : (' . $this->parseExp($tokens, true) . '))';
return '(isset(' . $var . ') ? ' . $var . ' : (' . $this->parseExpr($tokens) . '))';
}
} elseif ($tokens->is(Tokenizer::MACRO_BINARY, Tokenizer::MACRO_BOOLEAN, Tokenizer::MACRO_MATH) || !$tokens->valid()) {
if ($empty) {
@ -928,9 +1002,9 @@ class Template extends Render
return 'isset(' . $var . ')';
}
} else {
$expr1 = $this->parseExp($tokens, true);
$expr1 = $this->parseExpr($tokens);
$tokens->need(':')->skip();
$expr2 = $this->parseExp($tokens, true);
$expr2 = $this->parseExpr($tokens);
if ($empty) {
return '(empty(' . $var . ') ? ' . $expr2 . ' : ' . $expr1 . ')';
} else {
@ -941,7 +1015,7 @@ class Template extends Render
/**
* Parse 'is' and 'is not' operators
* @see $_checkers
* @see _tests
* @param Tokenizer $tokens
* @param string $value
* @param bool $variable
@ -964,10 +1038,10 @@ class Template extends Render
if (!$variable && ($action == "set" || $action == "empty")) {
$action = "_$action";
$tokens->next();
return $invert . sprintf(self::$_checkers[$action], $value);
} elseif (isset(self::$_checkers[$action])) {
return $invert . sprintf(self::$_tests[$action], $value);
} elseif (isset(self::$_tests[$action])) {
$tokens->next();
return $invert . sprintf(self::$_checkers[$action], $value);
return $invert . sprintf(self::$_tests[$action], $value);
} elseif ($tokens->isSpecialVal()) {
$tokens->next();
return '(' . $value . ' ' . $equal . '= ' . $action . ')';
@ -1133,7 +1207,7 @@ class Template extends Render
$_str = "";
}
$tokens->getNext(T_VARIABLE);
$_str .= '(' . $this->parseExp($tokens) . ')';
$_str .= '(' . $this->parseExpr($tokens) . ')';
if ($tokens->is($stop)) {
$tokens->next();
return $_str;
@ -1202,7 +1276,7 @@ class Template extends Render
} elseif ($tokens->is('"', '`', T_ENCAPSED_AND_WHITESPACE)) {
$args[] = $this->parseQuote($tokens);
} elseif ($tokens->is('(')) {
$args[] = $this->parseExp($tokens, true);
$args[] = $this->parseExpr($tokens);
} elseif ($tokens->is('[')) {
$args[] = $this->parseArray($tokens);
} elseif ($tokens->is(T_STRING) && $tokens->isNext('(')) {
@ -1246,7 +1320,7 @@ class Template extends Render
$val = false;
$_arr .= $tokens->getAndNext() . ' ';
} elseif ($tokens->is(Tokenizer::MACRO_SCALAR, T_VARIABLE, T_STRING, T_EMPTY, T_ISSET, "(", "#") && !$val) {
$_arr .= $this->parseExp($tokens, true);
$_arr .= $this->parseExpr($tokens);
$key = false;
$val = true;
} elseif ($tokens->is('"') && !$val) {
@ -1311,11 +1385,11 @@ class Template extends Render
$n = $this->i++;
if ($recursive) {
$recursive['recursive'] = true;
$body = '$tpl->getMacro("'.$name.'")->__invoke($tpl);';
$body = '$tpl->getMacro("' . $name . '")->__invoke($tpl);';
} else {
$body = '?>'.$macro["body"].'<?php';
$body = '?>' . $macro["body"] . '<?php';
}
return '$_tpl'.$n.' = $tpl->exchangeArray(' . Compiler::toArray($args) . ');' . PHP_EOL . $body . PHP_EOL . '$tpl->exchangeArray($_tpl'.$n.'); unset($_tpl'.$n.');';
return '$_tpl' . $n . ' = $tpl->exchangeArray(' . Compiler::toArray($args) . ');' . PHP_EOL . $body . PHP_EOL . '$tpl->exchangeArray($_tpl' . $n . '); unset($_tpl' . $n . ');';
}
/**
@ -1334,7 +1408,7 @@ class Template extends Render
$arg = $colon = false;
while ($tokens->valid()) {
if (!$arg && $tokens->is(T_VARIABLE, T_STRING, "(", Tokenizer::MACRO_SCALAR, '"', Tokenizer::MACRO_UNARY, Tokenizer::MACRO_INCDEC)) {
$_args .= $this->parseExp($tokens, true);
$_args .= $this->parseExpr($tokens);
$arg = true;
$colon = false;
} elseif (!$arg && $tokens->is('[')) {
@ -1367,7 +1441,7 @@ class Template extends Render
{
if ($tokens->is(T_CONSTANT_ENCAPSED_STRING)) {
if ($tokens->isNext('|')) {
return $this->parseExp($tokens, true);
return $this->parseExpr($tokens);
} else {
$str = $tokens->getAndNext();
$static = stripslashes(substr($str, 1, -1));
@ -1377,7 +1451,7 @@ class Template extends Render
$static = $tokens->getAndNext();
return '"' . addslashes($static) . '"';
} else {
return $this->parseExp($tokens, true);
return $this->parseExpr($tokens);
}
}
@ -1403,12 +1477,12 @@ class Template extends Render
}
if ($tokens->is("=")) {
$tokens->next();
$params[$key] = $this->parseExp($tokens);
$params[$key] = $this->parseExpr($tokens);
} else {
$params[$key] = 'true';
}
} elseif ($tokens->is(Tokenizer::MACRO_SCALAR, '"', '`', T_VARIABLE, "[", '(')) {
$params[] = $this->parseExp($tokens);
$params[] = $this->parseExpr($tokens);
} else {
break;
}

View File

@ -20,7 +20,7 @@ defined('T_TRAIT_C') || define('T_TRAIT_C', 365);
/**
* for PHP <5.5 compatible
*/
defined('T_YIELD') || define('T_YIELD', 390);
defined('T_YIELD') || define('T_YIELD', 267);
/**
* Each token have structure

View File

@ -52,15 +52,21 @@ class MacrosTest extends TestCase
{/macro}
{macro.factorial num=10}');
$this->tpl("macro_recursive_import.tpl", '
{import "macro_recursive.tpl" as math}
{math.factorial num=10}');
}
public function _testSandbox()
{
try {
$this->fenom->compile("macro_recursive.tpl");
// $this->fenom->compile("macro_recursive.tpl")->display([]);
// $this->fenom->flush();
// var_dump($this->fenom->fetch("macro_recursive.tpl", []));
var_dump( $this->fenom->compile("macro_recursive.tpl")->getTemplateCode());
var_dump( $this->fenom->compile("macro_recursive_import.tpl")->display([]));
var_dump( $this->fenom->display("macro_recursive_import.tpl", []));
} catch (\Exception $e) {
var_dump($e->getMessage() . ": " . $e->getTraceAsString());
}
@ -107,4 +113,12 @@ class MacrosTest extends TestCase
$tpl = $this->fenom->getTemplate('macro_recursive.tpl');
$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');
$this->fenom->flush();
$tpl = $this->fenom->getTemplate('macro_recursive.tpl');
$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));
}
}

View File

@ -16,6 +16,15 @@ class TemplateTest extends TestCase
{
parent::setUp();
$this->tpl('welcome.tpl', '<b>Welcome, {$username} ({$email})</b>');
$_GET['one'] = 'get1';
$_POST['one'] = 'post1';
$_REQUEST['one'] = 'request1';
$_FILES['one'] = 'files1';
$_SERVER['one'] = 'server1';
$_SESSION['one'] = 'session1';
$GLOBALS['one'] = 'globals1';
$_ENV['one'] = 'env1';
$_COOKIE['one'] = 'cookie1';
}
public static function providerVars()
@ -687,10 +696,29 @@ class TemplateTest extends TestCase
);
}
public static function providerAccessor() {
return array(
array('{$.get.one}', 'get1'),
array('{$.post.one}', 'post1'),
array('{$.request.one}', 'request1'),
array('{$.session.one}', 'session1'),
array('{$.files.one}', 'files1'),
array('{$.globals.one}', 'globals1'),
array('{$.cookie.one}', 'cookie1'),
array('{$.server.one}', 'server1'),
array('{$.const.PHP_EOL}', PHP_EOL),
array('{$.version}', Fenom::VERSION),
array('{$.get.one?}', '1'),
array('{$.get.one is set}', '1'),
array('{$.get.two is empty}', '1'),
);
}
public function _testSandbox()
{
try {
var_dump($this->fenom->compileCode('{if max(2, 4) > 1 && max(2, 3) < 1} block1 {else} block2 {/if}')->getBody());
var_dump($this->fenom->compileCode('{$.const.access?}')->getBody());
} catch (\Exception $e) {
print_r($e->getMessage() . "\n" . $e->getTraceAsString());
}
@ -910,5 +938,14 @@ class TemplateTest extends TestCase
{
$this->exec($code, self::getVars(), $result);
}
/**
* @group accessor
* @dataProvider providerAccessor
*/
public function testAccessor($code, $result)
{
$this->exec($code, self::getVars(), $result);
}
}