diff --git a/docs/syntax.md b/docs/syntax.md index 8707ab6..f77cfe4 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -25,7 +25,7 @@ Fenom implement [Smarty](http://www.smarty.net/) syntax with some improvements ### System variable -Unnamed system variable starts with `$.` and allow access to global variables and system info: +Unnamed system variable starts with `$.` and allow access to global variables and system info (fix doc): * `$.get` is `$_GET`. * `$.post` is `$_POST`. @@ -135,7 +135,7 @@ See also [{var}](./tags/var.md) documentation. ### Strings When the string in double quotation marks, all the expressions in the string will be run. -The result of the expression will be inserted into the string instead it. +The result of expressions will be inserted into the string instead it. ```smarty {var $foo="Username"} @@ -168,9 +168,9 @@ but if use single quote any template expressions will be on display as it is ### Modifiers -* Модификаторы позволяют изменить значение переменной перед выводом или использованием в выражении -* To apply a modifier, specify the value followed by a | (pipe) and the modifier name. -* A modifier may accept additional parameters that affect its behavior. These parameters follow the modifier name and are separated by a : (colon). +* Modifiers allows change some value before output or using. +* To apply a modifier, specify the value followed by a `|` (pipe) and the modifier name. +* A modifier may accept additional parameters that affect its behavior. These parameters follow the modifier name and are separated by a `:` (colon). ```smarty {var $foo="User"} @@ -187,13 +187,14 @@ but if use single quote any template expressions will be on display as it is ### Tags -Каждый тэг шаблонизатора либо выводит переменную, либо вызывает какую-либо функцию. (переписать) -Тег вызова функции начинается с названия функции и содержит список аргументов: +Basically, tag seems like ```smarty {FUNCNAME attr1 = "val1" attr2 = $val2} ``` +Tags starts with name and may have attributes + Это общий формат функций, но могут быть исключения, например функция [{var}](./tags/var.md), использованная выше. ```smarty diff --git a/sandbox/templates/extends/parent.tpl b/sandbox/templates/extends/parent.tpl new file mode 100644 index 0000000..e69de29 diff --git a/src/Fenom.php b/src/Fenom.php index db8e5d9..7fb7090 100644 --- a/src/Fenom.php +++ b/src/Fenom.php @@ -99,6 +99,11 @@ class Fenom */ protected $_compile_dir = "/tmp"; + /** + * @var string[] compile directory for custom provider + */ + protected $_compiles = array(); + /** * @var int masked options */ @@ -213,8 +218,7 @@ class Fenom 'type' => self::BLOCK_COMPILER, 'open' => 'Fenom\Compiler::tagBlockOpen', 'close' => 'Fenom\Compiler::tagBlockClose', - 'tags' => array(// 'parent' => 'Fenom\Compiler::tagParent' // not implemented yet - ), + 'tags' => array('parent' => 'Fenom\Compiler::tagParent'), 'float_tags' => array('parent' => 1) ), 'extends' => array( // {extends ...} @@ -355,7 +359,7 @@ class Fenom } /** - * @param callable $cbcd + * @param callable $cb * @return self */ public function addTagFilter($cb) @@ -620,11 +624,15 @@ class Fenom * * @param string $scm scheme name * @param Fenom\ProviderInterface $provider provider object + * @param string $compile_path * @return $this */ - public function addProvider($scm, \Fenom\ProviderInterface $provider) + public function addProvider($scm, \Fenom\ProviderInterface $provider, $compile_path = null) { $this->_providers[$scm] = $provider; + if($compile_path) { + $this->_compiles[$scm] = $compile_path; + } return $this; } @@ -730,7 +738,11 @@ class Fenom public function getTemplate($template, $options = 0) { $options |= $this->_options; - $key = dechex($options) . "@" . $template; + if(is_array($template)) { + $key = dechex($options) . "@" . implode(",", $template); + } else { + $key = dechex($options) . "@" . $template; + } if (isset($this->_storage[$key])) { /** @var Fenom\Template $tpl */ $tpl = $this->_storage[$key]; @@ -766,13 +778,13 @@ class Fenom /** * Load template from cache or create cache if it doesn't exists. * - * @param string $tpl + * @param string $template * @param int $opts * @return Fenom\Render */ - protected function _load($tpl, $opts) + protected function _load($template, $opts) { - $file_name = $this->_getCacheName($tpl, $opts); + $file_name = $this->_getCacheName($template, $opts); if (is_file($this->_compile_dir . "/" . $file_name)) { $fenom = $this; // used in template $_tpl = include($this->_compile_dir . "/" . $file_name); @@ -781,7 +793,7 @@ class Fenom return $_tpl; } } - return $this->compile($tpl, true, $opts); + return $this->compile($template, true, $opts); } /** @@ -793,14 +805,22 @@ class Fenom */ private function _getCacheName($tpl, $options) { - $hash = $tpl . ":" . $options; - return sprintf("%s.%x.%x.php", str_replace(":", "_", basename($tpl)), crc32($hash), strlen($hash)); + if(is_array($tpl)) { + $hash = implode(".", $tpl) . ":" . $options; + foreach($tpl as &$t) { + $t = str_replace(":", "_", basename($t)); + } + return implode("~", $tpl).".".sprintf("%x.%x.php", crc32($hash), strlen($hash)); + } else { + $hash = $tpl . ":" . $options; + return sprintf("%s.%x.%x.php", str_replace(":", "_", basename($tpl)), crc32($hash), strlen($hash)); + } } /** * Compile and save template * - * @param string $tpl + * @param string|array $tpl * @param bool $store store template on disk * @param int $options * @throws RuntimeException @@ -809,7 +829,15 @@ class Fenom public function compile($tpl, $store = true, $options = 0) { $options = $this->_options | $options; - $template = $this->getRawTemplate()->load($tpl); + if(is_string($tpl)) { + $template = $this->getRawTemplate()->load($tpl); + } else { + $template = $this->getRawTemplate()->load($tpl[0], false); + unset($tpl[0]); + foreach($tpl as $t) { + $template->extend($t); + } + } if ($store) { $cache = $this->_getCacheName($tpl, $options); $tpl_tmp = tempnam($this->_compile_dir, $cache); diff --git a/src/Fenom/Compiler.php b/src/Fenom/Compiler.php index a871128..56a9e72 100644 --- a/src/Fenom/Compiler.php +++ b/src/Fenom/Compiler.php @@ -549,33 +549,13 @@ class Compiler public static function tagUse(Tokenizer $tokens, Template $tpl) { if ($tpl->getStackSize()) { - throw new InvalidUsageException("Tags {use} can not be nested"); + throw new InvalidUsageException("Tag {use} can not be nested"); } - $cname = $tpl->parsePlainArg($tokens, $name); + $tpl->parsePlainArg($tokens, $name); if ($name) { - $donor = $tpl->getStorage()->getRawTemplate()->load($name, false); - $donor->_extended = true; - $donor->_extends = $tpl; - $donor->_compatible = & $tpl->_compatible; - //$donor->blocks = &$tpl->blocks; - $donor->compile(); - $blocks = $donor->blocks; - foreach ($blocks as $name => $code) { - if (isset($tpl->blocks[$name])) { - $tpl->blocks[$name] = $code; - unset($blocks[$name]); - } - } - $tpl->uses = $blocks + $tpl->uses; - $tpl->addDepend($donor); - return '?>' . $donor->getBody() . 'importBlocks($name); } else { throw new InvalidUsageException('template name must be given explicitly yet'); - // under construction -// $tpl->_compatible = true; -// return '$donor = $tpl->getStorage()->getTemplate(' . $cname . ', \Fenom\Template::EXTENDED);' . PHP_EOL . -// '$donor->fetch((array)$tpl);' . PHP_EOL . -// '$tpl->b += (array)$donor->b'; } } @@ -583,8 +563,8 @@ class Compiler * Tag {block ...} * @param Tokenizer $tokens * @param Scope $scope + * @throws \RuntimeException * @return string - * @throws InvalidUsageException */ public static function tagBlockOpen(Tokenizer $tokens, Scope $scope) { @@ -592,77 +572,58 @@ class Compiler $scope->tpl->_compatible = true; } $scope["cname"] = $scope->tpl->parsePlainArg($tokens, $name); + if(!$name) { + throw new \RuntimeException("Only static names for blocks allowed"); + } $scope["name"] = $name; + $scope["use_parent"] = false; } /** - * Close tag {/block} + * @param Tokenizer $tokens + * @param Scope $scope + */ + public static function tagBlockClose($tokens, Scope $scope) + { + $tpl = $scope->tpl; + $name = $scope["name"]; + if(isset($tpl->blocks[$name])) { // block defined + $block = &$tpl->blocks[$name]; + if($block['use_parent']) { + $parent = $scope->getContent(); + + $block['block'] = str_replace($block['use_parent']." ?>", "?>".$parent, $block['block']); + } + if(!$block["import"]) { // not from {use} - redefine block + $scope->replaceContent($block["block"]); + return; + } elseif($block["import"] != $tpl->getName()) { // tag {use} was in another template + $tpl->blocks[$scope["name"]]["import"] = false; + $scope->replaceContent($block["block"]); + } + } + $tpl->blocks[$scope["name"]] = [ + "from" => $tpl->getName(), + "import" => false, + "use_parent" => $scope["use_parent"], + "block" => $scope->getContent() + ]; + } + + /** + * Tag {parent} + * * @param Tokenizer $tokens * @param Scope $scope * @return string */ - public static function tagBlockClose($tokens, Scope $scope) - { - - $tpl = $scope->tpl; - if (isset($tpl->_extends)) { // is child - if ($scope["name"]) { // is scalar name - if ($tpl->_compatible) { // is compatible mode - $scope->replaceContent( - 'b[' . $scope["cname"] . '])) { ' . - '$tpl->b[' . $scope["cname"] . '] = function($tpl) { ?>' . PHP_EOL . - $scope->getContent() . - "" . PHP_EOL - ); - } elseif (!isset($tpl->blocks[$scope["name"]])) { // is block not registered - $tpl->blocks[$scope["name"]] = $scope->getContent(); - $scope->replaceContent( - '_compatible . ' */' . PHP_EOL . ' $tpl->b[' . $scope["cname"] . '] = function($tpl) { ?>' . PHP_EOL . - $scope->getContent() . - "" . PHP_EOL - ); - } - } else { // dynamic name - $tpl->_compatible = true; // enable compatible mode - $scope->replaceContent( - 'b[' . $scope["cname"] . '])) { ' . - '$tpl->b[' . $scope["cname"] . '] = function($tpl) { ?>' . PHP_EOL . - $scope->getContent() . - "" . PHP_EOL - ); - } - } else { // is parent - if (isset($tpl->blocks[$scope["name"]])) { // has block - if ($tpl->_compatible) { // compatible mode enabled - $scope->replaceContent( - 'b[' . $scope["cname"] . '])) { echo $tpl->b[' . $scope["cname"] . ']->__invoke($tpl); } else {?>' . PHP_EOL . - $tpl->blocks[$scope["name"]] . - '' . PHP_EOL - ); - - } else { - $scope->replaceContent($tpl->blocks[$scope["name"]]); - } -// } elseif(isset($tpl->_extended) || !empty($tpl->_compatible)) { - } elseif (isset($tpl->_extended) && $tpl->_compatible || empty($tpl->_extended)) { - $scope->replaceContent( - 'b[' . $scope["cname"] . '])) { echo $tpl->b[' . $scope["cname"] . ']->__invoke($tpl); } else {?>' . PHP_EOL . - $scope->getContent() . - '' . PHP_EOL - ); - } - } - return ''; - - } - public static function tagParent($tokens, Scope $scope) { - if (empty($scope->tpl->_extends)) { - throw new InvalidUsageException("Tag {parent} may be declared in children"); + $block_scope = $scope->tpl->getParentScope('block'); + if(!$block_scope['use_parent']) { + $block_scope['use_parent'] = "/* %%parent#".mt_rand(0, 1e6)."%% */"; } + return $block_scope['use_parent']; } /** @@ -1035,4 +996,4 @@ class Compiler $scope->tpl->escape = $scope["escape"]; } -} +} \ No newline at end of file diff --git a/src/Fenom/Template.php b/src/Fenom/Template.php index e33dfa8..fe2f885 100644 --- a/src/Fenom/Template.php +++ b/src/Fenom/Template.php @@ -155,6 +155,21 @@ class Template extends Render return count($this->_stack); } + /** + * @param string $tag + * @return bool|\Fenom\Scope + */ + public function getParentScope($tag) + { + for($i = count($this->_stack) - 1; $i>=0; $i--) { + if($this->_stack[$i]->name == $tag) { + return $this->_stack[$i]; + } + } + + return false; + } + /** * Load source from provider * @param string $name @@ -443,6 +458,7 @@ class Template extends Render // evaluate template's code eval("\$this->_code = " . $this->_getClosureSource() . ";\n\$this->_macros = " . $this->_getMacrosArray() . ';'); if (!$this->_code) { + var_dump($this->getBody());exit; throw new CompileException("Fatal error while creating the template"); } } @@ -474,6 +490,36 @@ class Template extends Render } } + /** + * Import block from another template + * @param string $tpl + */ + public function importBlocks($tpl) { + $donor = $this->_fenom->compile($tpl, false); + foreach($donor->blocks as $name => $block) { + if(!isset($this->blocks[ $name ])) { + $block['import'] = $this->getName(); + $this->blocks[ $name ] = $block; + } + } + $this->addDepend($donor); + } + + /** + * Extends the template + * @param string $tpl + */ + public function extend($tpl) { + $this->compile(); + $parent = $this->_fenom->getRawTemplate()->load($tpl, false); + $parent->blocks = &$this->blocks; + $parent->_options = $this->_options; + $parent->compile(); + $this->_body = $parent->_body; + $this->_src = $parent->_src; + $this->addDepend($parent); + } + /** * Tag router * @param Tokenizer $tokens diff --git a/tests/TestCase.php b/tests/TestCase.php index cedf302..3adcbfe 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,6 +4,7 @@ use Fenom, Fenom\Provider as FS; class TestCase extends \PHPUnit_Framework_TestCase { + public $template_path = 'template'; /** * @var Fenom */ @@ -53,7 +54,7 @@ class TestCase extends \PHPUnit_Framework_TestCase FS::clean(FENOM_RESOURCES . '/compile/'); } - $this->fenom = Fenom::factory(FENOM_RESOURCES . '/template', FENOM_RESOURCES . '/compile'); + $this->fenom = Fenom::factory(FENOM_RESOURCES . '/' . $this->template_path, FENOM_RESOURCES . '/compile'); $this->fenom->addModifier('dots', __CLASS__ . '::dots'); $this->fenom->addModifier('concat', __CLASS__ . '::concat'); $this->fenom->addModifier('append', __CLASS__ . '::append'); diff --git a/tests/cases/Fenom/ExtendsTest.php b/tests/cases/Fenom/ExtendsTest.php new file mode 100644 index 0000000..f60bab5 --- /dev/null +++ b/tests/cases/Fenom/ExtendsTest.php @@ -0,0 +1,63 @@ +fenom->getTemplate([ + 'autoextends/child.3.tpl', + 'autoextends/child.2.tpl', + 'autoextends/child.1.tpl', + 'autoextends/parent.tpl', + ])->getBody()); +// var_dump($this->fenom->compileCode('{block "main"}a {parent} b{/block}')->getBody()); +// $child = $this->fenom->getRawTemplate()->load('autoextends/child.1.tpl', false); +// $child->extend('autoextends/parent.tpl'); +// $child->compile(); +// print_r($child->getBody()); + } catch (\Exception $e) { + echo "$e"; + } + exit; + } + + public function testManualExtends() + { + $child = $this->fenom->getRawTemplate()->load('autoextends/child.1.tpl', false); + $child->extend('autoextends/parent.tpl'); + $child->compile(); + $result = "Before header +Content of the header +Before body +Child 1 Body +Before footer +Content of the footer"; + $this->assertSame($result, $child->fetch(array())); + } + + /** + * @group testAutoExtends + */ + public function testAutoExtends() { + $result = "Before header +Child 2 header +Before body +Child 3 content +Before footer +Footer from use"; + $this->assertSame($result, $this->fenom->fetch([ + 'autoextends/child.3.tpl', + 'autoextends/child.2.tpl', + 'autoextends/child.1.tpl', + 'autoextends/parent.tpl', + ], array())); + } + +} + diff --git a/tests/resources/provider/autoextends/child.1.tpl b/tests/resources/provider/autoextends/child.1.tpl new file mode 100644 index 0000000..a8bbf5b --- /dev/null +++ b/tests/resources/provider/autoextends/child.1.tpl @@ -0,0 +1 @@ +{block 'body'}Child 1 {parent}{/block} \ No newline at end of file diff --git a/tests/resources/provider/autoextends/child.2.tpl b/tests/resources/provider/autoextends/child.2.tpl new file mode 100644 index 0000000..958b425 --- /dev/null +++ b/tests/resources/provider/autoextends/child.2.tpl @@ -0,0 +1,3 @@ + +{use 'autoextends/use.tpl'} +{block 'header'}Child 2 header{/block} diff --git a/tests/resources/provider/autoextends/child.3.tpl b/tests/resources/provider/autoextends/child.3.tpl new file mode 100644 index 0000000..9461db2 --- /dev/null +++ b/tests/resources/provider/autoextends/child.3.tpl @@ -0,0 +1 @@ +{block 'body'}Child 3 content{/block} \ No newline at end of file diff --git a/tests/resources/provider/autoextends/parent.tpl b/tests/resources/provider/autoextends/parent.tpl new file mode 100644 index 0000000..d23349b --- /dev/null +++ b/tests/resources/provider/autoextends/parent.tpl @@ -0,0 +1,6 @@ +Before header +{block 'header'}Content of the header{/block} +Before body +{block 'body'}Body{/block} +Before footer +{block 'footer'}Content of the footer{/block} \ No newline at end of file diff --git a/tests/resources/provider/autoextends/use.tpl b/tests/resources/provider/autoextends/use.tpl new file mode 100644 index 0000000..6d6147d --- /dev/null +++ b/tests/resources/provider/autoextends/use.tpl @@ -0,0 +1,2 @@ +{block 'footer'}Footer from use{/block} +{block 'header'}Header from use{/block} \ No newline at end of file diff --git a/tests/resources/provider/staticextends/child.1.tpl b/tests/resources/provider/staticextends/child.1.tpl new file mode 100644 index 0000000..6191eda --- /dev/null +++ b/tests/resources/provider/staticextends/child.1.tpl @@ -0,0 +1,2 @@ + +{block 'body'}Child 1 content{/block} \ No newline at end of file diff --git a/tests/resources/provider/staticextends/child.2.tpl b/tests/resources/provider/staticextends/child.2.tpl new file mode 100644 index 0000000..0dbdcf7 --- /dev/null +++ b/tests/resources/provider/staticextends/child.2.tpl @@ -0,0 +1,2 @@ + +{block 'header'}Child 2 header{/block} \ No newline at end of file diff --git a/tests/resources/provider/staticextends/child.3.tpl b/tests/resources/provider/staticextends/child.3.tpl new file mode 100644 index 0000000..5644c48 --- /dev/null +++ b/tests/resources/provider/staticextends/child.3.tpl @@ -0,0 +1,2 @@ + +{block 'body'}Child 3 content{/block} \ No newline at end of file diff --git a/tests/resources/provider/staticextends/parent.tpl b/tests/resources/provider/staticextends/parent.tpl new file mode 100644 index 0000000..d23349b --- /dev/null +++ b/tests/resources/provider/staticextends/parent.tpl @@ -0,0 +1,6 @@ +Before header +{block 'header'}Content of the header{/block} +Before body +{block 'body'}Body{/block} +Before footer +{block 'footer'}Content of the footer{/block} \ No newline at end of file