Done #5, dev #66, improve blocks, refectory block's tests

This commit is contained in:
Ivan Shalganov 2014-02-14 15:55:36 +04:00
parent dc287f08b2
commit 52c0858d06
16 changed files with 231 additions and 106 deletions

View File

@ -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

View File

View 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);

View File

@ -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'];
}
/**
@ -1035,4 +996,4 @@ class Compiler
$scope->tpl->escape = $scope["escape"];
}
}
}

View File

@ -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

View File

@ -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');

View 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()));
}
}

View File

@ -0,0 +1 @@
{block 'body'}Child 1 {parent}{/block}

View File

@ -0,0 +1,3 @@
{use 'autoextends/use.tpl'}
{block 'header'}Child 2 header{/block}

View File

@ -0,0 +1 @@
{block 'body'}Child 3 content{/block}

View 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}

View File

@ -0,0 +1,2 @@
{block 'footer'}Footer from use{/block}
{block 'header'}Header from use{/block}

View File

@ -0,0 +1,2 @@
{block 'body'}Child 1 content{/block}

View File

@ -0,0 +1,2 @@
{block 'header'}Child 2 header{/block}

View File

@ -0,0 +1,2 @@
{block 'body'}Child 3 content{/block}

View 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}