diff --git a/.gitignore b/.gitignore index 735129a..53f7e1a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ tests/resources/template/* !.gitkeep benchmark/compile/* benchmark/templates/inheritance/smarty -benchmark/templates/inheritance/twig \ No newline at end of file +benchmark/templates/inheritance/twig +benchmark/sandbox/compiled/* +!.gitkeep \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b4e9a..a0f3856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/benchmark/sandbox/compiled/.gitkeep b/benchmark/sandbox/compiled/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/benchmark/sandbox/fenom.php b/benchmark/sandbox/fenom.php index f162d74..ef220e5 100644 --- a/benchmark/sandbox/fenom.php +++ b/benchmark/sandbox/fenom.php @@ -1,3 +1,11 @@ display("greeting.tpl", array( + "user" => array( + "name" => "Ivka" + ) +)); \ No newline at end of file diff --git a/benchmark/sandbox/templates/greeting.tpl b/benchmark/sandbox/templates/greeting.tpl new file mode 100644 index 0000000..0b97ab3 --- /dev/null +++ b/benchmark/sandbox/templates/greeting.tpl @@ -0,0 +1,8 @@ +Greeting, +{if $user} + {$user.name}! +{else} + anonymous? +{/if} + +3 \ No newline at end of file diff --git a/docs/dev/git.md b/docs/dev/git.md new file mode 100644 index 0000000..c9e656b --- /dev/null +++ b/docs/dev/git.md @@ -0,0 +1,5 @@ +Git conversation +================ + +Ветка `master` содержит стабильную последнюю версию проекта. В ветку `master` может сливаться новая версия проекта из `develop` или исправления. +Ветка `develop`, для разработки, содержит не стабильную версию проекта. Принимает все новшевства, изменения и исправления. diff --git a/docs/dev/internal.md b/docs/dev/internal.md new file mode 100644 index 0000000..58bac38 --- /dev/null +++ b/docs/dev/internal.md @@ -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()`, который распарсит модификатор. diff --git a/docs/develop.md b/docs/dev/readme.md similarity index 66% rename from docs/develop.md rename to docs/dev/readme.md index c84934c..c097f37 100644 --- a/docs/develop.md +++ b/docs/dev/readme.md @@ -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) \ No newline at end of file +For questions: a.cobest@gmail.com (English, Russian languages) diff --git a/docs/syntax.md b/docs/syntax.md index 763eea0..e9de09f 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -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 diff --git a/src/Fenom.php b/src/Fenom.php index dd02de9..02a10a7 100644 --- a/src/Fenom.php +++ b/src/Fenom.php @@ -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); } /** diff --git a/src/Fenom/Compiler.php b/src/Fenom/Compiler.php index 904e5ee..146bf66 100644 --- a/src/Fenom/Compiler.php +++ b/src/Fenom/Compiler.php @@ -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; diff --git a/src/Fenom/Render.php b/src/Fenom/Render.php index f01bd53..7cf7b31 100644 --- a/src/Fenom/Render.php +++ b/src/Fenom/Render.php @@ -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'; + } } diff --git a/src/Fenom/Template.php b/src/Fenom/Template.php index 6902792..033e32f 100644 --- a/src/Fenom/Template.php +++ b/src/Fenom/Template.php @@ -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"]."_before ? $this->_before . "\n" : ""; return "_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"] . "_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"].'' . $macro["body"] . '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; } diff --git a/src/Fenom/Tokenizer.php b/src/Fenom/Tokenizer.php index 1acf388..d07c758 100644 --- a/src/Fenom/Tokenizer.php +++ b/src/Fenom/Tokenizer.php @@ -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 diff --git a/tests/cases/Fenom/MacrosTest.php b/tests/cases/Fenom/MacrosTest.php index 7b5c32c..06551ef 100644 --- a/tests/cases/Fenom/MacrosTest.php +++ b/tests/cases/Fenom/MacrosTest.php @@ -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)); + } } diff --git a/tests/cases/Fenom/TemplateTest.php b/tests/cases/Fenom/TemplateTest.php index b007dc8..d501f68 100644 --- a/tests/cases/Fenom/TemplateTest.php +++ b/tests/cases/Fenom/TemplateTest.php @@ -16,6 +16,15 @@ class TemplateTest extends TestCase { parent::setUp(); $this->tpl('welcome.tpl', 'Welcome, {$username} ({$email})'); + $_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); + } }