mirror of
https://github.com/fenom-template/fenom.git
synced 2023-08-10 21:13:07 +03:00
parent
dc287f08b2
commit
52c0858d06
@ -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
|
||||
|
0
sandbox/templates/extends/parent.tpl
Normal file
0
sandbox/templates/extends/parent.tpl
Normal file
@ -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);
|
||||
|
@ -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() . '<?php ';
|
||||
$tpl->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(
|
||||
'<?php /* 1) Block ' . $tpl . ': ' . $scope["cname"] . ' */' . PHP_EOL . ' if(empty($tpl->b[' . $scope["cname"] . '])) { ' .
|
||||
'$tpl->b[' . $scope["cname"] . '] = function($tpl) { ?>' . PHP_EOL .
|
||||
$scope->getContent() .
|
||||
"<?php };" .
|
||||
"} ?>" . PHP_EOL
|
||||
);
|
||||
} elseif (!isset($tpl->blocks[$scope["name"]])) { // is block not registered
|
||||
$tpl->blocks[$scope["name"]] = $scope->getContent();
|
||||
$scope->replaceContent(
|
||||
'<?php /* 2) Block ' . $tpl . ': ' . $scope["cname"] . ' ' . $tpl->_compatible . ' */' . PHP_EOL . ' $tpl->b[' . $scope["cname"] . '] = function($tpl) { ?>' . PHP_EOL .
|
||||
$scope->getContent() .
|
||||
"<?php }; ?>" . PHP_EOL
|
||||
);
|
||||
}
|
||||
} else { // dynamic name
|
||||
$tpl->_compatible = true; // enable compatible mode
|
||||
$scope->replaceContent(
|
||||
'<?php /* 3) Block ' . $tpl . ': ' . $scope["cname"] . ' */' . PHP_EOL . ' if(empty($tpl->b[' . $scope["cname"] . '])) { ' .
|
||||
'$tpl->b[' . $scope["cname"] . '] = function($tpl) { ?>' . PHP_EOL .
|
||||
$scope->getContent() .
|
||||
"<?php };" .
|
||||
"} ?>" . PHP_EOL
|
||||
);
|
||||
}
|
||||
} else { // is parent
|
||||
if (isset($tpl->blocks[$scope["name"]])) { // has block
|
||||
if ($tpl->_compatible) { // compatible mode enabled
|
||||
$scope->replaceContent(
|
||||
'<?php /* 4) Block ' . $tpl . ': ' . $scope["cname"] . ' */' . PHP_EOL . ' if(isset($tpl->b[' . $scope["cname"] . '])) { echo $tpl->b[' . $scope["cname"] . ']->__invoke($tpl); } else {?>' . PHP_EOL .
|
||||
$tpl->blocks[$scope["name"]] .
|
||||
'<?php } ?>' . 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(
|
||||
'<?php /* 5) Block ' . $tpl . ': ' . $scope["cname"] . ' */' . PHP_EOL . ' if(isset($tpl->b[' . $scope["cname"] . '])) { echo $tpl->b[' . $scope["cname"] . ']->__invoke($tpl); } else {?>' . PHP_EOL .
|
||||
$scope->getContent() .
|
||||
'<?php } ?>' . 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'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
|
63
tests/cases/Fenom/ExtendsTest.php
Normal file
63
tests/cases/Fenom/ExtendsTest.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
namespace Fenom;
|
||||
use Fenom, Fenom\TestCase;
|
||||
|
||||
class ExtendsTest extends TestCase
|
||||
{
|
||||
|
||||
public $template_path = 'provider';
|
||||
|
||||
public function _testSandbox()
|
||||
{
|
||||
try {
|
||||
var_dump($this->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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
1
tests/resources/provider/autoextends/child.1.tpl
Normal file
1
tests/resources/provider/autoextends/child.1.tpl
Normal file
@ -0,0 +1 @@
|
||||
{block 'body'}Child 1 {parent}{/block}
|
3
tests/resources/provider/autoextends/child.2.tpl
Normal file
3
tests/resources/provider/autoextends/child.2.tpl
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
{use 'autoextends/use.tpl'}
|
||||
{block 'header'}Child 2 header{/block}
|
1
tests/resources/provider/autoextends/child.3.tpl
Normal file
1
tests/resources/provider/autoextends/child.3.tpl
Normal file
@ -0,0 +1 @@
|
||||
{block 'body'}Child 3 content{/block}
|
6
tests/resources/provider/autoextends/parent.tpl
Normal file
6
tests/resources/provider/autoextends/parent.tpl
Normal file
@ -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}
|
2
tests/resources/provider/autoextends/use.tpl
Normal file
2
tests/resources/provider/autoextends/use.tpl
Normal file
@ -0,0 +1,2 @@
|
||||
{block 'footer'}Footer from use{/block}
|
||||
{block 'header'}Header from use{/block}
|
2
tests/resources/provider/staticextends/child.1.tpl
Normal file
2
tests/resources/provider/staticextends/child.1.tpl
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
{block 'body'}Child 1 content{/block}
|
2
tests/resources/provider/staticextends/child.2.tpl
Normal file
2
tests/resources/provider/staticextends/child.2.tpl
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
{block 'header'}Child 2 header{/block}
|
2
tests/resources/provider/staticextends/child.3.tpl
Normal file
2
tests/resources/provider/staticextends/child.3.tpl
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
{block 'body'}Child 3 content{/block}
|
6
tests/resources/provider/staticextends/parent.tpl
Normal file
6
tests/resources/provider/staticextends/parent.tpl
Normal file
@ -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}
|
Loading…
Reference in New Issue
Block a user