diff --git a/benchmark/run.php b/benchmark/run.php index b364a76..f28ac5b 100644 --- a/benchmark/run.php +++ b/benchmark/run.php @@ -28,7 +28,7 @@ Benchmark::runs("twig", 'inheritance/twig/b100.tpl', __DIR__.'/templates/foreach Benchmark::runs("aspect", 'inheritance/smarty/b100.tpl', __DIR__.'/templates/foreach/data.json'); echo "\nDone. Cleanup.\n"; -passthru("rm -rf ".__DIR__."/compile/*"); +//passthru("rm -rf ".__DIR__."/compile/*"); passthru("rm -f ".__DIR__."/templates/inheritance/smarty/*"); passthru("rm -f ".__DIR__."/templates/inheritance/twig/*"); diff --git a/docs/about.md b/docs/about.md index 6b87c1d..a746e26 100644 --- a/docs/about.md +++ b/docs/about.md @@ -1,5 +1,5 @@ -About Aspect -============ +About Aspect [RU] +================= Aspect - самый быстрый, гибкий и тонкий шаблонизатор для PHP, унаследовавший синтаксис от Smarty3 и улучшив его. Пожалуй это единственный шаблонизатор, который не использет ни регулярные выражения, как Twig, ни лексер от BISON, как Smarty3. diff --git a/docs/ext/mods.md b/docs/ext/mods.md index a859741..49d04f1 100644 --- a/docs/ext/mods.md +++ b/docs/ext/mods.md @@ -1,11 +1,22 @@ -Модификаторы +Модификаторы [RU] ============ -Добавить модификатор: -```php -$aspect->addModifier($modifier, $callback); +``` +$aspect->addModifier(string $modifier, callable $callback); ``` -* `$modifier` - имя модификатора -* `$callback` - строка с именем функции \ No newline at end of file +* `$modifier` - название модификатора, которое будет использоваться в шаблоне +* `$callback` - коллбек, который будет вызван для изменения данных + +For example: + +```smarty +{$variable|my_modifier:$param1:$param2} +``` + +```php +$aspect->addModifier('my_modifier', function ($variable, $param1, $param2) { + // ... +}); +``` diff --git a/docs/ext/tags.md b/docs/ext/tags.md index f857bda..0c4289e 100644 --- a/docs/ext/tags.md +++ b/docs/ext/tags.md @@ -1,20 +1,68 @@ -Теги -==== +Tags [RU] +========= -Теги делятся на компилеры и функции. -Компилеры формируют синтаксис языка шаблона, добавляя такой функционал как foreach, if, while и т.д. В то время как функции - обычный вызов некоторой именованной функции +В шаблонизаторе принято различать два типа тегов: компиляторы и функции. +Компиляторы вызываются во время преобразования кода шаблона в PHP код и возвращяю PHP код который будет вставлен вместо тега. +А функции вызываются непременно в момент выполнения шаблона и возвращают непосредственно данные которые будут отображены. +Среди тегов как и в HTML есть строчные и блоковые теги. -Добавить компилер: +## Inline function + +Примитивное добавление функции можно осуществить следующим образом: ```php -$aspect->addCompiler($compiler, $parser); +$aspect->addFunction(string $function_name, callable $callback[, callable $parser]); ``` -* `$compiler` - имя модификатора -* `$parser` - функция разбора тега в формате function (MF\Tokenizer $tokens, MF\Aspect\Template $tpl) {} - -Добавить блочный компилер: +В данном случае запускается стандартный парсер, который автоматически разберет аргументы тега, которые должны быть в формате HTML аттрибутов и отдаст их в функцию ассоциативным массивом. +В данном случае вы можете переопределить парсер на произвольный в формате `function (Aspect\Tokenizer $tokenizer, Aspect\Template $template)` +Существует более совершенный способ добавления функции: ```php -$aspect->addBlockCompiler($compiler, $parsers, $tags); -``` \ No newline at end of file +$aspect->addFunctionSmarty(string $function_name, callable $callback); +``` + +В данном случае парсер просканирует список аргументов коллбека и попробует сопоставить с аргументами из тега. Таким образом вы успешно можете добавлять Ваши штатные функции. + +## Block function + +Добавление блоковой функции аналогичен добавлению строковой за исключением того что есть возможность указать парсер для закрывающего тега. + +```php +$aspect->addBlockFunction(string $function_name, callable $callback[, callable $parser_open[, callable $parser_close]]); +``` + +Сам коллбек принимает первым аргументом контент между открывающим и закрывающим тегом, а вторым аргументом - ассоциативный массив из аргуметов тега. + +## Inline compiler + +Добавление строчного компилятора осуществляеться очень просто: + +```php +$aspect->addCompiler(string $compiler, callable $parser); +``` + +Парсер должен принимать `Aspect\Tokenizer $tokenizer`, `Aspect\Template $template` и возвращать PHP код. +Компилятор так же можно импортировать из класса автоматически + +```php +$aspect->addCompilerSmart(string $compiler, $storage); +``` + +`$storage` может быть как классом так и объектом. В данном случае шаблонизатор будет искать метод `tag{$compiler}`, который будет взят в качестве парсера тега. + +## Block compiler + +Добавление блочного компилятора осуществяется двум способами. Первый + +```php +$aspect->addBlockCompiler(string $compiler, array $parsers, array $tags); +``` + +где `$parser` ассоциативный массив `["open" => parser, "close" => parser]`, сождержащий парсер на открывающий и на закрывающий тег, а `$tags` содержит список внутренних тегов в формате `["tag_name"] => parser`, которые могут быть использованы только с этим компилятором. +Второй способ добавления парсера через импортирование из класса или объекта методов: + +```php +$aspect->addBlockCompilerSmart(string $compiler, $storage, array $tags, array $floats); +``` + diff --git a/docs/install.md b/docs/install.md index 55e3fdf..de30690 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,5 +1,5 @@ Installation -============================= +============ For installation use [composer](http://getcompoer.org). Add in your `composer.json` requirements: ```json diff --git a/docs/operators.md b/docs/operators.md index 3938176..9a47f33 100644 --- a/docs/operators.md +++ b/docs/operators.md @@ -7,7 +7,7 @@ Math Bitwize -`| & <<` +`| & << >> |= &= <<= >>=` Unary @@ -15,12 +15,8 @@ Unary Boolean -`|| && and or < > <= >= == === !== != ≥ ≤ ≠` +`|| && and or < > <= >= == === !== !=` Ternar `? :` - -Test - -`is in like` \ No newline at end of file diff --git a/docs/settings.md b/docs/settings.md index 0ac4184..9a8cd6b 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -1,5 +1,5 @@ -Настройка -========= +Settings [RU] +============= ### Engine settings diff --git a/docs/syntax.md b/docs/syntax.md index e4bf9f5..21393cd 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -1,5 +1,5 @@ -Syntax -====== +Syntax [RU] +=========== ### Output variables diff --git a/docs/tags/for.md b/docs/tags/for.md index 9bb5540..d5aa1ee 100644 --- a/docs/tags/for.md +++ b/docs/tags/for.md @@ -2,7 +2,7 @@ Tag {for} ========= ```smarty -{for $counter= [to=] [step=] [index=$index] [first=$first] [last=$last]} +{for $counter= to= [step=] [index=$index] [first=$first] [last=$last]} {* ...code... *} {break} {* ...code... *} diff --git a/docs/tags/macro.md b/docs/tags/macro.md index 5ca3cbe..a449a3b 100644 --- a/docs/tags/macro.md +++ b/docs/tags/macro.md @@ -1,7 +1,12 @@ -Tag {macro} -============ +Tag {macro} [RU] +================ -Declare macro +Макросы - фрагмент шаблона который можно повторить сколь угодно раз и в каком угодно месте. +Макросы не имеют общего пространства имен с шаблоном и могут оперировать только переданными переменными. + +### {macro} + +Обявление макроса происходит при помощи блочного тега `{macro}` ```smarty {macro plus(x, y, z=0)} @@ -9,7 +14,7 @@ Declare macro {/macro} ``` -Invoke macro +Вызов макроса происходит при помощи строкового тега `{macro}`. Аргументы передаются стандартно, как атрибуты в HTML тегах ```smarty {macro.plus x=$num y=100} @@ -17,14 +22,23 @@ Invoke macro ### {import} -Import [macro](./macro.md) from another template +Для использования маросов в другом шаблоне необходимо их импортировать при помощи тега `{import}` ```smarty {import 'math.tpl'} ``` +При импорте можно указать дргое пространство имен что бы можно было использовать одноименные макросы из разных шаблонов + ```smarty {import 'math.tpl' as math} ... {math.plus x=5 y=100} ``` + +Пространство имен макросов может совпадать с названием какого-либо тега, в данном случае ничего плохого не произойдет: будет вызван макрос, а тег не исчезнит +При необходимости можно импортировать только необходимые макросы, явно указав в теге `{import}` + +```smarty +{import [plus, minus, exp] from 'math.tpl' as math} +``` \ No newline at end of file diff --git a/src/Aspect.php b/src/Aspect.php index 1fc1a62..de826d0 100644 --- a/src/Aspect.php +++ b/src/Aspect.php @@ -25,7 +25,7 @@ class Aspect { const DEFAULT_CLOSE_COMPILER = 'Aspect\Compiler::stdClose'; const DEFAULT_FUNC_PARSER = 'Aspect\Compiler::stdFuncParser'; const DEFAULT_FUNC_OPEN = 'Aspect\Compiler::stdFuncOpen'; - const DEFAULT_FUNC_CLOSE = 'Aspect\Compiler::stdFuncOpen'; + const DEFAULT_FUNC_CLOSE = 'Aspect\Compiler::stdFuncClose'; const SMART_FUNC_PARSER = 'Aspect\Compiler::smartFuncParser'; /** @@ -320,7 +320,18 @@ class Aspect { return $this; } - public function addCompilerSmart($class) { + /** + * @param string $compiler + * @param string|object $storage + * @return $this + */ + public function addCompilerSmart($compiler, $storage) { + if(method_exists($storage, "tag".$compiler)) { + $this->_actions[$compiler] = array( + 'type' => self::INLINE_COMPILER, + 'parser' => array($storage, "tag".$compiler) + ); + } return $this; } @@ -329,7 +340,7 @@ class Aspect { * * @param string $compiler * @param callable $open_parser - * @param callable $close_parser + * @param callable|string $close_parser * @param array $tags * @return Aspect */ @@ -344,12 +355,40 @@ class Aspect { } /** - * @param string $class + * @param $compiler + * @param $storage * @param array $tags * @param array $floats + * @throws LogicException * @return Aspect */ - public function addBlockCompilerSmart($class, array $tags, array $floats = array()) { + public function addBlockCompilerSmart($compiler, $storage, array $tags, array $floats = array()) { + $c = array( + 'type' => self::BLOCK_COMPILER, + "tags" => array(), + "float_tags" => array() + ); + if(method_exists($storage, $compiler."Open")) { + $c["open"] = $compiler."Open"; + } else { + throw new \LogicException("Open compiler {$compiler}Open not found"); + } + if(method_exists($storage, $compiler."Close")) { + $c["close"] = $compiler."Close"; + } else { + throw new \LogicException("Close compiler {$compiler}Close not found"); + } + foreach($tags as $tag) { + if(method_exists($storage, "tag".$tag)) { + $c["tags"][ $tag ] = "tag".$tag; + if($floats && in_array($tag, $floats)) { + $c['float_tags'][ $tag ] = 1; + } + } else { + throw new \LogicException("Tag compiler $tag (tag{$compiler}) not found"); + } + } + $this->_actions[$compiler] = $c; return $this; } @@ -362,7 +401,7 @@ class Aspect { public function addFunction($function, $callback, $parser = self::DEFAULT_FUNC_PARSER) { $this->_actions[$function] = array( 'type' => self::INLINE_FUNCTION, - 'parser' => $parser ?: self::DEFAULT_FUNC_PARSER, + 'parser' => $parser, 'function' => $callback, ); return $this; @@ -385,16 +424,16 @@ class Aspect { /** * @param string $function * @param callable $callback - * @param callable $parser_open - * @param callable $parser_close + * @param callable|string $parser_open + * @param callable|string $parser_close * @return Aspect */ - public function addBlockFunction($function, $callback, $parser_open = null, $parser_close = null) { + public function addBlockFunction($function, $callback, $parser_open = self::DEFAULT_FUNC_OPEN, $parser_close = self::DEFAULT_FUNC_CLOSE) { $this->_actions[$function] = array( - 'type' => self::BLOCK_FUNCTION, - 'open' => $parser_open ?: 'Aspect\Compiler::stdFuncOpen', - 'close' => $parser_close ?: 'Aspect\Compiler::stdFuncClose', - 'function' => $callback, + 'type' => self::BLOCK_FUNCTION, + 'open' => $parser_open, + 'close' => $parser_close, + 'function' => $callback, ); return $this; } @@ -564,7 +603,7 @@ class Aspect { return $this->_storage[ $template ]; } } elseif($this->_options & self::FORCE_COMPILE) { - return $this->compile($template, false); + return $this->compile($template, $this->_options & self::DISABLE_CACHE); } else { return $this->_storage[ $template ] = $this->_load($template); } @@ -583,7 +622,7 @@ class Aspect { * * @param string $tpl * @throws \RuntimeException - * @return Aspect\Template|mixed + * @return Aspect\Render */ protected function _load($tpl) { $file_name = $this->_getHash($tpl); @@ -591,7 +630,6 @@ class Aspect { return $this->compile($tpl); } else { $aspect = $this; - /** @var Aspect\Render $tpl */ return include($this->_compile_dir."/".$file_name); } } @@ -604,7 +642,7 @@ class Aspect { */ private function _getHash($tpl) { $hash = $tpl.":".$this->_options; - return sprintf("%s.%u.%d.php", basename($tpl), crc32($hash), strlen($hash)); + return sprintf("%s.%u.%d.php", str_replace(":", "_", basename($tpl)), crc32($hash), strlen($hash)); } /** diff --git a/src/Aspect/Compiler.php b/src/Aspect/Compiler.php index fc96cb7..d4f0439 100644 --- a/src/Aspect/Compiler.php +++ b/src/Aspect/Compiler.php @@ -148,7 +148,6 @@ class Compiler { $before = $before ? implode("; ", $before).";" : ""; $body = $body ? implode("; ", $body).";" : ""; $scope["after"] = $scope["after"] ? implode("; ", $scope["after"]).";" : ""; - if($key) { return "$prepend if($from) { $before foreach($from as $key => $value) { $body"; } else { @@ -161,8 +160,6 @@ class Compiler { * * @param Tokenizer $tokens * @param Scope $scope - * @internal param $ - * @param Scope $scope * @return string */ public static function foreachElse($tokens, Scope $scope) { @@ -385,7 +382,7 @@ class Compiler { return ""; } else { // dynamic extends $tpl->_extends = $tpl_name; - return '$parent = $tpl->getStorage()->getTemplate('.$tpl_name.');'; + return '$parent = $tpl->getStorage()->getTemplate("extend:".'.$tpl_name.');'; } } @@ -561,7 +558,8 @@ class Compiler { */ public static function smartFuncParser($function, Tokenizer $tokens, Template $tpl) { if(strpos($function, "::")) { - $ref = new \ReflectionMethod($function); + list($class, $method) = explode("::", $function, 2); + $ref = new \ReflectionMethod($class, $method); } else { $ref = new \ReflectionFunction($function); } @@ -710,6 +708,24 @@ class Compiler { * @throws ImproperUseException */ public static function tagImport(Tokenizer $tokens, Template $tpl) { + $import = array(); + if($tokens->is('[')) { + $tokens->next(); + while($tokens->valid()) { + if($tokens->is(Tokenizer::MACRO_STRING)) { + $import[ $tokens->current() ] = true; + $tokens->next(); + } elseif($tokens->is(']')) { + $tokens->next(); + break; + } + } + if($tokens->current() != "from") { + throw new UnexpectedException($tokens); + } + $tokens->next(); + } + $tpl->parseFirstArg($tokens, $name); if(!$name) { throw new ImproperUseException("Invalid usage tag {import}"); diff --git a/src/Aspect/Template.php b/src/Aspect/Template.php index 345cdb9..0aeca0d 100644 --- a/src/Aspect/Template.php +++ b/src/Aspect/Template.php @@ -138,8 +138,8 @@ class Template extends Render { while(($start = strpos($this->_src, '{', $pos)) !== false) { // search open-char of tags switch($this->_src[$start + 1]) { // check next char case "\n": case "\r": case "\t": case " ": case "}": // ignore the tag - $pos = $start + 1; // try find tags after the current char - continue 2; + $pos = $start + 1; // try find tags after the current char + continue 2; case "*": // if comment block $end = strpos($this->_src, '*}', $start); // finding end of the comment block $_frag = substr($this->_src, $this->_pos, $start - $end); // read the comment block for precessing @@ -163,7 +163,7 @@ class Template extends Render { $_tag = substr($tag, 1, -1); $_frag = $frag; } - if($this->_ignore) { // check ignore scope + if($this->_ignore) { // check ignore if($_tag === '/ignore') { $this->_ignore = false; $this->_appendText($_frag); @@ -173,7 +173,12 @@ class Template extends Render { } } else { $this->_appendText($_frag); - $this->_appendCode($this->_tag($_tag)); + $tokens = new Tokenizer($_tag); + $this->_appendCode($this->_tag($tokens), $tag); + if($tokens->key()) { // if tokenizer still have tokens + throw new CompileException("Unexpected token '".$tokens->current()."' in {$this} line {$this->_line}, near '{".$tokens->getSnippetAsString(0,0)."' <- there", 0, E_ERROR, $this->_name, $this->_line); + } + } $frag = ""; } @@ -195,7 +200,6 @@ class Template extends Render { call_user_func_array($cb, array(&$this->_body, $this)); } } - /*$this->_body = str_replace(array('?>'.PHP_EOL.'_body);*/ } /** @@ -207,7 +211,7 @@ class Template extends Render { $this->_body .= str_replace("'.PHP_EOL, $text); } - public static function escapeCode($code) { + private function _escapeCode($code) { $c = ""; foreach(token_get_all($code) as $token) { if(is_string($token)) { @@ -225,12 +229,16 @@ class Template extends Render { * Append PHP code to template body * * @param string $code + * @param $source */ - private function _appendCode($code) { + private function _appendCode($code, $source) { if(!$code) { return; } else { - $this->_body .= self::escapeCode($code); + if(strpos($code, '?>') !== false) { + $code = $this->_escapeCode($code); // paste PHP_EOL + } + $this->_body .= "_name}:{$this->_line}: {$source} */\n $code ?>".PHP_EOL; } } @@ -259,7 +267,7 @@ class Template extends Render { return "_name."' compiled at ".date('Y-m-d H:i:s')." */\n". "return new Aspect\\Render(\$aspect, ".$this->_getClosureSource().", ".var_export(array( - //"options" => $this->_options, + "options" => $this->_options, "provider" => $this->_scm, "name" => $this->_name, "base_name" => $this->_base_name, @@ -321,43 +329,28 @@ class Template extends Render { /** * Internal tags router - * @param string $src + * @param Tokenizer $tokens * @throws UnexpectedException * @throws CompileException * @throws SecurityException * @return string executable PHP code */ - private function _tag($src) { - $tokens = new Tokenizer($src); + private function _tag(Tokenizer $tokens) { try { - switch($src[0]) { - case '"': - case '\'': - case '$': - $code = "echo ".$this->parseExp($tokens).";"; - break; - case '#': - $code = "echo ".$this->parseConst($tokens); - break; - case '/': - $code = $this->_end($tokens); - break; - default: - if($tokens->current() === "ignore") { - $this->_ignore = true; - $tokens->next(); - $code = ''; - } else { - $code = $this->_parseAct($tokens); - } - } - if($tokens->key()) { // if tokenizer still have tokens - throw new UnexpectedException($tokens); - } - if(!$code) { - return ""; + if($tokens->is(Tokenizer::MACRO_STRING)) { + if($tokens->current() === "ignore") { + $this->_ignore = true; + $tokens->next(); + return ''; + } else { + return $this->_parseAct($tokens); + } + } elseif ($tokens->is('/')) { + return $this->_end($tokens); + } elseif ($tokens->is('#')) { + return "echo ".$this->parseConst($tokens).';'; } else { - return "_name}:{$this->_line}: {$src} */\n {$code} ?>"; + return $code = "echo ".$this->parseExp($tokens).";"; } } catch (ImproperUseException $e) { throw new CompileException($e->getMessage()." in {$this} line {$this->_line}", 0, E_ERROR, $this->_name, $this->_line, $e); @@ -376,6 +369,7 @@ class Template extends Render { * @throws TokenizeException */ private function _end(Tokenizer $tokens) { + //return "end"; $name = $tokens->getNext(Tokenizer::MACRO_STRING); $tokens->next(); if(!$this->_stack) { @@ -402,7 +396,7 @@ class Template extends Render { if($tokens->is(Tokenizer::MACRO_STRING)) { $action = $tokens->getAndNext(); } else { - return 'echo '.$this->parseExp($tokens).';'; // may be math and boolean expression + return 'echo '.$this->parseExp($tokens).';'; // may be math and/or boolean expression } if($tokens->is("(", T_NAMESPACE, T_DOUBLE_COLON)) { // just invoke function or static method diff --git a/tests/cases/Aspect/TemplateTest.php b/tests/cases/Aspect/TemplateTest.php index 9ffdb87..0e88013 100644 --- a/tests/cases/Aspect/TemplateTest.php +++ b/tests/cases/Aspect/TemplateTest.php @@ -266,7 +266,7 @@ class TemplateTest extends TestCase { public static function providerIncludeInvalid() { return array( array('Include {include} template', 'Aspect\CompileException', "Unexpected end of expression"), - array('Include {include another="welcome.tpl"} template', 'Aspect\CompileException', "Unexpected token '=' in expression"), + array('Include {include another="welcome.tpl"} template', 'Aspect\CompileException', "Unexpected token '='"), ); } diff --git a/tests/cases/AspectTest.php b/tests/cases/AspectTest.php index b82ce24..7b30d35 100644 --- a/tests/cases/AspectTest.php +++ b/tests/cases/AspectTest.php @@ -68,6 +68,9 @@ class AspectTest extends \Aspect\TestCase { $this->assertSame("Custom modifier (myMod)Custom(/myMod)", $this->aspect->fetch('custom.tpl', array("a" => "Custom"))); } + /** + * @group add_functions + */ public function testSetFunctions() { $this->aspect->setOptions(Aspect::FORCE_COMPILE); $this->aspect->addFunction("myfunc", "myFunc");