Fix: cycle tag. Update comments. Add tests.

This commit is contained in:
bzick 2013-06-01 02:00:43 +04:00
parent 7453dbcf2b
commit f42d0cff5d
4 changed files with 66 additions and 37 deletions

View File

@ -14,22 +14,24 @@ use Cytro\Template,
* Cytro Template Engine * Cytro Template Engine
*/ */
class Cytro { class Cytro {
const VERSION = '1.0.1'; const VERSION = '1.0';
/* Compiler types */
const INLINE_COMPILER = 1; const INLINE_COMPILER = 1;
const BLOCK_COMPILER = 2; const BLOCK_COMPILER = 2;
const INLINE_FUNCTION = 3; const INLINE_FUNCTION = 3;
const BLOCK_FUNCTION = 4; const BLOCK_FUNCTION = 4;
const MODIFIER = 5; const MODIFIER = 5;
/* Options */
const DENY_METHODS = 0x10; const DENY_METHODS = 0x10;
const DENY_INLINE_FUNCS = 0x20; const DENY_INLINE_FUNCS = 0x20;
const FORCE_INCLUDE = 0x40; const FORCE_INCLUDE = 0x40;
const AUTO_RELOAD = 0x80; const AUTO_RELOAD = 0x80;
const FORCE_COMPILE = 0xF0; const FORCE_COMPILE = 0xF0;
const DISABLE_CACHE = 0x1F0; const DISABLE_CACHE = 0x1F0;
/* Default parsers */
const DEFAULT_CLOSE_COMPILER = 'Cytro\Compiler::stdClose'; const DEFAULT_CLOSE_COMPILER = 'Cytro\Compiler::stdClose';
const DEFAULT_FUNC_PARSER = 'Cytro\Compiler::stdFuncParser'; const DEFAULT_FUNC_PARSER = 'Cytro\Compiler::stdFuncParser';
const DEFAULT_FUNC_OPEN = 'Cytro\Compiler::stdFuncOpen'; const DEFAULT_FUNC_OPEN = 'Cytro\Compiler::stdFuncOpen';
@ -204,7 +206,11 @@ class Cytro {
'import' => array( 'import' => array(
'type' => self::INLINE_COMPILER, 'type' => self::INLINE_COMPILER,
'parser' => 'Cytro\Compiler::tagImport' 'parser' => 'Cytro\Compiler::tagImport'
) ),
'cycle' => array(
'type' => self::INLINE_COMPILER,
'parser' => 'Cytro\Compiler::tagCycle'
)
); );
/** /**

View File

@ -696,21 +696,31 @@ class Compiler {
* @throws ImproperUseException * @throws ImproperUseException
*/ */
public static function tagCycle(Tokenizer $tokens, Template $tpl) { public static function tagCycle(Tokenizer $tokens, Template $tpl) {
$exp = $tpl->parseExp($tokens, true); if($tokens->is("[")) {
$exp = $tpl->parseArray($tokens);
} else {
$exp = $tpl->parseExp($tokens, true);
}
if($tokens->valid()) { if($tokens->valid()) {
$p = $tpl->parseParams($tokens); $p = $tpl->parseParams($tokens);
if(empty($p["index"])) { if(empty($p["index"])) {
throw new ImproperUseException("Cycle may contain only index attribute"); throw new ImproperUseException("Cycle may contain only index attribute");
} else { } else {
return __CLASS__.'::cycle((array)'.$exp.', '.$p["index"].');'; return 'echo '.__CLASS__.'::cycle('.$exp.', '.$p["index"].')';
} }
} else { } else {
$var = $tpl->tmpVar(); $var = $tpl->tmpVar();
return "is_array($exp) ? ".__CLASS__.'::cycle('.$exp.", isset($var) ? $var++ : ($var = 0) ) : $exp"; return 'echo '.__CLASS__.'::cycle('.$exp.", isset($var) ? ++$var : ($var = 0) )";
} }
} }
public static function cycle($vals, $index) { /**
* Runtime cycle callback
* @param mixed $vals
* @param $index
* @return mixed
*/
public static function cycle($vals, $index) {
return $vals[$index % count($vals)]; return $vals[$index % count($vals)];
} }

View File

@ -18,10 +18,19 @@ use Cytro;
*/ */
class Template extends Render { class Template extends Render {
const DENY_ARRAY = 1; /**
const DENY_MODS = 2; * Disable array parser.
*/
const DENY_ARRAY = 1;
/**
* Disable modifier parser.
*/
const DENY_MODS = 2;
const EXTENDED = 0x1000; /**
* Template was extended
*/
const EXTENDED = 0x1000;
/** /**
* @var int shared counter * @var int shared counter
@ -56,7 +65,6 @@ class Template extends Render {
/** /**
* @var int * @var int
*/ */
private $_pos = 0;
private $_line = 1; private $_line = 1;
private $_post = array(); private $_post = array();
/** /**
@ -131,25 +139,22 @@ class Template extends Render {
* @throws CompileException * @throws CompileException
*/ */
public function compile() { public function compile() {
if(!isset($this->_src)) { // already compiled
return;
}
$end = $pos = 0; $end = $pos = 0;
while(($start = strpos($this->_src, '{', $pos)) !== false) { // search open-symbol of tags while(($start = strpos($this->_src, '{', $pos)) !== false) { // search open-symbol of tags
switch($this->_src[$start + 1]) { // check next character switch($this->_src[$start + 1]) { // check next character
case "\n": case "\r": case "\t": case " ": case "}": // ignore the tag case "\n": case "\r": case "\t": case " ": case "}": // ignore the tag
$pos = $start + 1; // try find tags after the current char $pos = $start + 1;
continue 2; continue 2;
case "*": // if comment block case "*": // if comments
$end = strpos($this->_src, '*}', $start) + 1; // finding end of the comment block $end = strpos($this->_src, '*}', $start) + 1; // find end of the comment block
if($end === false) { if($end === false) {
throw new CompileException("Unclosed comment block in line {$this->_line}", 0, 1, $this->_name, $this->_line); throw new CompileException("Unclosed comment block in line {$this->_line}", 0, 1, $this->_name, $this->_line);
} }
$this->_appendText(substr($this->_src, $pos, $start - $pos)); $this->_appendText(substr($this->_src, $pos, $start - $pos));
$comment = substr($this->_src, $start, $end - $start); // read the comment block for processing $comment = substr($this->_src, $start, $end - $start); // read the comment block for processing
$this->_line += substr_count($comment, "\n"); // count skipped lines in comment block $this->_line += substr_count($comment, "\n"); // count lines in comments
unset($comment); unset($comment); // cleanup
$pos = $end + 1; // seek pointer $pos = $end + 1;
continue 2; continue 2;
} }
$frag = substr($this->_src, $pos, $start - $pos); // variable $frag contains chars after previous '}' and current '{' $frag = substr($this->_src, $pos, $start - $pos); // variable $frag contains chars after previous '}' and current '{'
@ -177,7 +182,7 @@ class Template extends Render {
$tokens = new Tokenizer($_tag); // tokenize the tag $tokens = new Tokenizer($_tag); // tokenize the tag
if($tokens->isIncomplete()) { // all strings finished? if($tokens->isIncomplete()) { // all strings finished?
$from = $end + 1; $from = $end + 1;
goto reparse; // find another close-symbol goto reparse; // need next close-symbol
} }
$this->_appendCode( $this->_tag($tokens) , $tag); // start the tag lexer $this->_appendCode( $this->_tag($tokens) , $tag); // start the tag lexer
$pos = $end + 1; // move search-pointer to end of the tag $pos = $end + 1; // move search-pointer to end of the tag
@ -187,7 +192,7 @@ class Template extends Render {
} }
} }
unset($frag); unset($frag, $_tag, $tag); // cleanup
} }
gc_collect_cycles(); gc_collect_cycles();
$this->_appendText(substr($this->_src, $end ? $end + 1 : 0)); $this->_appendText(substr($this->_src, $end ? $end + 1 : 0));
@ -202,7 +207,7 @@ class Template extends Render {
} }
throw new CompileException("Unclosed tag".(count($_names) == 1 ? "" : "s").": ".implode(", ", $_names), 0, 1, $this->_name, $_line); throw new CompileException("Unclosed tag".(count($_names) == 1 ? "" : "s").": ".implode(", ", $_names), 0, 1, $this->_name, $_line);
} }
unset($this->_src); $this->_src = ""; // cleanup
if($this->_post) { if($this->_post) {
foreach($this->_post as $cb) { foreach($this->_post as $cb) {
call_user_func_array($cb, array(&$this->_body, $this)); call_user_func_array($cb, array(&$this->_body, $this));
@ -1065,5 +1070,4 @@ class Template extends Render {
class CompileException extends \ErrorException {} class CompileException extends \ErrorException {}
class SecurityException extends CompileException {} class SecurityException extends CompileException {}
class ImproperUseException extends \LogicException {} class ImproperUseException extends \LogicException {}
class ReparseTagException extends \Exception {}

View File

@ -5,14 +5,14 @@ namespace Cytro;
class TagsTest extends TestCase { class TagsTest extends TestCase {
// public function _testSandbox() { public function _testSandbox() {
// try { try {
// var_dump($this->cytro->compileCode(" literal: { \$a} end")->getBody()); var_dump($this->cytro->compileCode('{for $i=0 to=5}{cycle ["one", "two"]}, {/for}')->getBody());
// } catch(\Exception $e) { } catch(\Exception $e) {
// echo "$e"; echo "$e";
// } }
// exit; exit;
// } }
/** /**
* @dataProvider providerScalars * @dataProvider providerScalars
@ -35,13 +35,22 @@ class TagsTest extends TestCase {
$this->assertRender("{var \$a|low|dots}before {{$tpl_val}} after{/var}\nVar: {\$a}", "\nVar: ".strtolower("before ".$val." after")."..."); $this->assertRender("{var \$a|low|dots}before {{$tpl_val}} after{/var}\nVar: {\$a}", "\nVar: ".strtolower("before ".$val." after")."...");
} }
public function testCycle() { public function testCycle() {
$this->assertRender('{for $i=0 to=4}{cycle ["one", "two"]}, {/for}', "one, two, one, two, one, ");
} }
/**
*
*/
public function testCycleIndex() {
$this->assertRender('{var $a=["one", "two"]}{for $i=1 to=5}{cycle $a index=$i}, {/for}', "two, one, two, one, two, ");
}
public function testFilter() { /**
* @dataProvider providerScalars
*/
public function testFilter($tpl_val, $val) {
$this->assertRender("{filter|up} before {{$tpl_val}} after {/filter}", strtoupper(" before {$val} after "));
} }
} }