Fix: reparse close tag, custom provider, simplify compile method, add providers and tests

This commit is contained in:
bzick 2013-05-30 19:00:00 +04:00
parent 707a13cd08
commit 32ccaa91f0
13 changed files with 482 additions and 347 deletions

View File

@ -1,4 +1,4 @@
<h1>Вывод 10 полей из 1000 элементов в цикле<h1>
<h1>Вывод 10 полей из 1000 элементов в цикле</h1>
{foreach $array as $item}
{$item.id} {$item.title} {$item.var1} {$item.var2} {$item.var3} {$item.var4} {$item.var5} {$item.var6} {$item.var5} {$item.var6}
{/foreach}
{/foreach}

View File

@ -12,6 +12,8 @@ Documentation
### Modifiers
[Usage](./syntax.md#modifiers)
* [upper](./mods/upper.md) aka `up`
* [lower](./mods/lower.md) aka `low`
* [date_format](./mods/date_format.md)
@ -20,15 +22,16 @@ Documentation
* [escape](./mods/escape.md) aka `e`
* [unescape](./mods/unescape.md)
* [strip](./mods/strip.md)
* [length](./mods/lenght.md)
* [length](./mods/length.md)
* [in](./mods/in.md)
* allowed functions: `json_encode`, `json_decode`, `count`, `is_string`, `is_array`, `is_numeric`, `is_int`, `is_object`,
`strtotime`, `gettype`, `is_double`, `ip2long`, `long2ip`, `strip_tags`, `nl2br`
[Using](./syntax.md#modifiers) and [addition](./ext/mods.md) of modifiers.
* or [add](./ext/mods.md) your own
### Tags
[Usage](./syntax.md#tags)
* [var](./tags/var.md)
* [if](./tags/if.md), `elseif` and `else`
* [foreach](./tags/foreach.md), `foreaelse`, `break` and `continue`
@ -37,12 +40,11 @@ Documentation
* [cycle](./tags/cycle.md)
* [include](./tags/include.md)
* [extends](./tags/extends.md), `use`, `block` and `parent`
* [capture](./tags/capture.md)
* [filter](./tags/filter.md)
* [ignore](./tags/ignore.md)
* [macro](./tags/macro.md) and `import`
[Using](./syntax.md#tags) and [addition](./ext/tags.md) of tags.
* [autotrim](./tags/autotrim.md)
* or [add](./ext/tags.md) your own
### Extends

View File

@ -91,6 +91,7 @@ class Cytro {
"e" => 'Cytro\Modifier::escape', // alias of escape
"unescape" => 'Cytro\Modifier::unescape',
"strip" => 'Cytro\Modifier::strip',
"length" => 'Cytro\Modifier::length',
"default" => 'Cytro\Modifier::defaultValue'
);
@ -224,6 +225,7 @@ class Cytro {
throw new InvalidArgumentException("Source must be a valid path or provider object");
}
$cytro = new static($provider);
/* @var Cytro $cytro */
$cytro->setCompileDir($compile_dir);
if($options) {
$cytro->setOptions($options);
@ -525,8 +527,8 @@ class Cytro {
*/
public function getProvider($scm = false) {
if($scm) {
if(isset($this->_provider[$scm])) {
return $this->_provider[$scm];
if(isset($this->_providers[$scm])) {
return $this->_providers[$scm];
} else {
throw new InvalidArgumentException("Provider for '$scm' not found");
}
@ -565,6 +567,23 @@ class Cytro {
return $this->getTemplate($template)->fetch($vars);
}
/**
*
*
* @param string $template name of template
* @param array $vars
* @param $callback
* @param float $chunk
* @return \Cytro\Render
* @example $cytro->pipe("products.yml.tpl", $iterators, [new SplFileObject("/tmp/products.yml"), "fwrite"], 512*1024)
*/
public function pipe($template, array $vars, $callback, $chunk = 1e6) {
ob_start($callback, $chunk, true);
$this->getTemplate($template)->display($vars);
ob_end_flush();
}
/**
* Get template by name
*

View File

@ -411,6 +411,7 @@ class Compiler {
*/
public static function extendBody(&$body, $tpl) {
$t = $tpl;
// var_dump("$tpl: ".$tpl->getBody());
while(isset($t->_extends)) {
$t = $t->_extends;
if(is_object($t)) {
@ -484,7 +485,7 @@ class Compiler {
if($scope["name"]) { // is scalar name
if($tpl->_compatible) { // is compatible mode
$scope->replaceContent(
'<?php /* Block '.$tpl.': '.$scope["cname"].' */'.PHP_EOL.' if(empty($tpl->b['.$scope["cname"].'])) { '.
'<?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 };".
@ -493,7 +494,7 @@ class Compiler {
} elseif(!isset($tpl->blocks[ $scope["name"] ])) { // is block not registered
$tpl->blocks[ $scope["name"] ] = $scope->getContent();
$scope->replaceContent(
'<?php /* Block '.$tpl.': '.$scope["cname"].' '.$tpl->_compatible.' */'.PHP_EOL.' $tpl->b['.$scope["cname"].'] = function($tpl) { ?>'.PHP_EOL.
'<?php /* 2) Block '.$tpl.': '.$scope["cname"].' '.$tpl->_compatible.' */'.PHP_EOL.' $tpl->b['.$scope["cname"].'] = function($tpl) { ?>'.PHP_EOL.
$scope->getContent().
"<?php }; ?>".PHP_EOL
);
@ -501,7 +502,7 @@ class Compiler {
} else { // dynamic name
$tpl->_compatible = true; // enable compatible mode
$scope->replaceContent(
'<?php /* Block '.$tpl.': '.$scope["cname"].' */'.PHP_EOL.' if(empty($tpl->b['.$scope["cname"].'])) { '.
'<?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 };".
@ -512,7 +513,7 @@ class Compiler {
if(isset($tpl->blocks[ $scope["name"] ])) { // has block
if($tpl->_compatible) { // compatible mode enabled
$scope->replaceContent(
'<?php /* Block '.$tpl.': '.$scope["cname"].' */'.PHP_EOL.' if(isset($tpl->b['.$scope["cname"].'])) { echo $tpl->b['.$scope["cname"].']->__invoke($tpl); } else {?>'.PHP_EOL.
'<?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
);
@ -520,9 +521,11 @@ class Compiler {
} else {
$scope->replaceContent($tpl->blocks[ $scope["name"] ]);
}
// } elseif(isset($tpl->_extended) || !empty($tpl->_compatible)) {
} elseif(isset($tpl->_extended) && $tpl->_compatible || empty($tpl->_extended)) {
// var_dump("$tpl: exxx");
$scope->replaceContent(
'<?php /* Block '.$tpl.': '.$scope["cname"].' */'.PHP_EOL.' if(isset($tpl->b['.$scope["cname"].'])) { echo $tpl->b['.$scope["cname"].']->__invoke($tpl); } else {?>'.PHP_EOL.
'<?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
);
@ -629,74 +632,59 @@ class Compiler {
return 'array('.implode(",", $_code).')';
}
public static function varOpen(Tokenizer $tokens, Scope $scope) {
$scope->is_closed = true;
return self::setVar($tokens, $scope->tpl).';';
/**
* @param Tokenizer $tokens
* @param Scope $scope
* @return string
*/
public static function varOpen(Tokenizer $tokens, Scope $scope) {
$var = $scope->tpl->parseVariable($tokens, Template::DENY_MODS);
if($tokens->is('=')) { // inline tag {var ...}
$scope->is_closed = true;
$tokens->next();
if($tokens->is("[")) {
return $var.'='.$scope->tpl->parseArray($tokens);
} else {
return $var.'='.$scope->tpl->parseExp($tokens, true);
}
} else {
$scope["name"] = $var;
if($tokens->is('|')) {
$scope["value"] = $scope->tpl->parseModifier($tokens, "ob_get_clean()");
} else {
$scope["value"] = "ob_get_clean()";
}
return 'ob_start();';
}
}
public static function varClose() {
return '';
/**
* @param Tokenizer $tokens
* @param Scope $scope
* @return string
*/
public static function varClose(Tokenizer $tokens, Scope $scope) {
return $scope["name"].'='.$scope["value"].';';
}
/**
* Tag {var ...}
*
* @static
* @param Tokenizer $tokens
* @param Template $tpl
* @return string
*/
//public static function assign(Tokenizer $tokens, Template $tpl) {
// return self::setVar($tokens, $tpl).';';
//}
/**
* Set variable expression
* @param Tokenizer $tokens
* @param Template $tpl
* @param bool $allow_array
* @return string
*/
public static function setVar(Tokenizer $tokens, Template $tpl, $allow_array = true) {
$var = $tpl->parseVariable($tokens, $tpl::DENY_MODS);
$tokens->get('=');
$tokens->next();
if($tokens->is("[") && $allow_array) {
return $var.'='.$tpl->parseArray($tokens);
} else {
return $var.'='.$tpl->parseExp($tokens, true);
}
}
public static function filterOpen(Tokenizer $tokens, Scope $scope) {
$scope["filter"] = $scope->tpl->parseModifier($tokens, "ob_get_clean()");
return "ob_start();";
}
public static function filterClose($tokens, Scope $scope) {
return "echo ".$scope["filter"].";";
}
/**
* @param Tokenizer $tokens
* @param Scope $scope
* @return string
*/
public static function captureOpen(Tokenizer $tokens, Scope $scope) {
if($tokens->is("|")) {
$scope["value"] = $scope->tpl->parseModifier($tokens, "ob_get_clean()");
} else {
$scope["value"] = "ob_get_clean()";
}
$scope["var"] = $scope->tpl->parseVariable($tokens, Template::DENY_MODS);
public static function filterOpen(Tokenizer $tokens, Scope $scope) {
$scope["filter"] = $scope->tpl->parseModifier($tokens, "ob_get_clean()");
return "ob_start();";
}
public static function captureClose($tokens, Scope $scope) {
return $scope["var"]." = ".$scope["value"].";";
/**
* @param $tokens
* @param Scope $scope
* @return string
*/
public static function filterClose($tokens, Scope $scope) {
return "echo ".$scope["filter"].";";
}
/**

View File

@ -80,7 +80,7 @@ class Modifier {
}
/**
* Crop string by length (support unicode)
* Crop string to specific length (support unicode)
*
* @param string $string text witch will be truncate
* @param int $length maximum symbols of result string
@ -134,7 +134,7 @@ class Modifier {
* @return int
*/
public static function length($item) {
if(is_scalar($item)) {
if(is_string($item)) {
return strlen(preg_replace('#[\x00-\x7F]|[\x80-\xDF][\x00-\xBF]|[\xE0-\xEF][\x00-\xBF]{2}#s', ' ', $item));
} elseif (is_array($item)) {
return count($item);

View File

@ -97,12 +97,12 @@ class Template extends Render {
$this->_name = $name;
if($provider = strstr($name, ":", true)) {
$this->_scm = $provider;
$this->_base_name = substr($name, strlen($provider));
$this->_base_name = substr($name, strlen($provider) + 1);
} else {
$this->_base_name = $name;
$this->_base_name = $name;
}
$this->_provider = $this->_cytro->getProvider($provider);
$this->_src = $this->_provider->getSource($name, $this->_time);
$this->_provider = $this->_cytro->getProvider($provider);
$this->_src = $this->_provider->getSource($this->_base_name, $this->_time);
if($compile) {
$this->compile();
}
@ -134,56 +134,61 @@ class Template extends Render {
if(!isset($this->_src)) { // already compiled
return;
}
$pos = 0;
$frag = "";
$end = $pos = 0;
while(($start = strpos($this->_src, '{', $pos)) !== false) { // search open-symbol of tags
switch($this->_src[$start + 1]) { // check next char
switch($this->_src[$start + 1]) { // check next character
case "\n": case "\r": case "\t": case " ": case "}": // ignore the tag
$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
if($end === false) {
throw new CompileException("Unclosed comment block in line {$this->_line}", 0, 1, $this->_name, $this->_line);
}
$_frag = substr($this->_src, $this->_pos, $start - $end); // read the comment block for precessing
$this->_line += substr_count($_frag, "\n"); // count skipped lines
$pos = $end + 1; // trying finding tags after the comment block
$this->_line += substr_count($_frag, "\n"); // count skipped lines in comment block
$pos = $end + 1; // seek pointer
continue 2;
}
$end = strpos($this->_src, '}', $start); // search close-symbol of the tag
if(!$end) { // if unexpected end of template
throw new CompileException("Unclosed tag in line {$this->_line}", 0, 1, $this->_name, $this->_line);
}
$frag .= substr($this->_src, $this->_pos, $start - $this->_pos); // variable $frag contains chars after previous '}' and current '{'
$tag = substr($this->_src, $start, $end - $start + 1); // variable $tag contains cytro tag '{...}'
$this->_line += substr_count($this->_src, "\n", $this->_pos, $end - $start + 1); // count lines in $frag and $tag (using original text $code)
$pos = $this->_pos = $end + 1; // move search-pointer to end of the tag
$frag = substr($this->_src, $pos, $start - $pos); // variable $frag contains chars after previous '}' and current '{'
$this->_appendText($frag);
if($tag[strlen($tag) - 2] === "-") { // check right trim flag
$_tag = substr($tag, 1, -2);
$_frag = rtrim($frag);
} else {
$_tag = substr($tag, 1, -1);
$_frag = $frag;
}
if($this->_ignore) { // check ignore
if($_tag === '/ignore') {
$this->_ignore = false;
$this->_appendText($_frag);
} else { // still ignore
$frag .= $tag;
continue;
}
} else {
$this->_appendText($_frag);
$tokens = new Tokenizer($_tag);
$this->_appendCode($this->_tag($tokens), $tag);
if($tokens->key()) { // if tokenizer have tokens - throws exceptions
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);
$from = $start;
reparse: { // yep, i use goto operator. For this algorithm it is good choice
$end = strpos($this->_src, '}', $from); // search close-symbol of the tag
if($end === false) { // if unexpected end of template
throw new CompileException("Unclosed tag in line {$this->_line}", 0, 1, $this->_name, $this->_line);
}
$tag = substr($this->_src, $start, $end - $start + 1); // variable $tag contains cytro tag '{...}'
}
$frag = "";
$_tag = substr($tag, 1, -1); // strip delimiters '{' and '}'
if($this->_ignore) { // check ignore
if($_tag === '/ignore') { // turn off ignore
$this->_ignore = false;
} else { // still ignore
$this->_appendText($tag);
}
$pos = $start + strlen($tag);
continue;
} else {
$tokens = new Tokenizer($_tag); // tokenize the tag
if($tokens->isIncomplete()) { // all strings finished?
$from = $end + 1;
goto reparse; // find another close-symbol
}
$this->_appendCode( $this->_tag($tokens) , $tag); // start the tag lexer
$pos = $end + 1; // move search-pointer to end of the tag
if($tokens->key()) { // if tokenizer have tokens - throws exceptions
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);
}
}
}
unset($frag);
}
$this->_appendText(substr($this->_src, $this->_pos));
gc_collect_cycles();
$this->_appendText(substr($this->_src, $end ? $end + 1 : 0));
if($this->_stack) {
$_names = array();
$_line = 0;
@ -191,9 +196,9 @@ class Template extends Render {
if(!$_line) {
$_line = $scope->line;
}
$_names[] = '{'.$scope->name.'} defined on line '.$scope->line;
$_names[] = '{'.$scope->name.'} opened on line '.$scope->line;
}
throw new CompileException("Unclosed tag(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);
if($this->_post) {
@ -217,6 +222,7 @@ class Template extends Render {
* @param string $text
*/
private function _appendText($text) {
$this->_line += substr_count($text, "\n");
if($this->_filter) {
if(strpos($text, "<?") === false) {
$this->_body .= $text;
@ -262,9 +268,11 @@ class Template extends Render {
* @param $source
*/
private function _appendCode($code, $source) {
if(!$code) {
return;
} else {
$this->_line += substr_count($source, "\n");
if(strpos($code, '?>') !== false) {
$code = $this->_escapeCode($code); // paste PHP_EOL
}
@ -357,14 +365,14 @@ class Template extends Render {
return parent::fetch($values);
}
/**
* Internal tags router
* @param Tokenizer $tokens
* @throws UnexpectedTokenException
* @throws CompileException
* @throws SecurityException
* @return string executable PHP code
*/
/**
* Internal tags router
* @param Tokenizer $tokens
*
* @throws SecurityException
* @throws CompileException
* @return string executable PHP code
*/
private function _tag(Tokenizer $tokens) {
try {
if($tokens->is(Tokenizer::MACRO_STRING)) {
@ -742,104 +750,68 @@ class Template extends Render {
return $_scalar;
}
/**
* Parse string with or without variable
*
* @param Tokenizer $tokens
* @throws UnexpectedTokenException
* @return string
*/
/**
* Parse string with or without variable
*
* @param Tokenizer $tokens
* @throws UnexpectedTokenException
* @return string
*/
public function parseSubstr(Tokenizer $tokens) {
ref: {
if($tokens->is('"',"`")) {
$p = $tokens->p;
$stop = $tokens->current();
$_str = '"';
$tokens->next();
while($t = $tokens->key()) {
if($t === T_ENCAPSED_AND_WHITESPACE) {
$_str .= $tokens->current();
$tokens->next();
} elseif($t === T_VARIABLE) {
if(strlen($_str) > 1) {
$_str .= '".';
} else {
$_str = "";
}
$_str .= '$tpl["'.substr($tokens->current(), 1).'"]';
$tokens->next();
if($tokens->is($stop)) {
$tokens->skip();
return $_str;
} else {
$_str .= '."';
}
} elseif($t === T_CURLY_OPEN) {
if(strlen($_str) > 1) {
$_str .= '".';
} else {
$_str = "";
}
$tokens->getNext(T_VARIABLE);
$_str .= '('.$this->parseExp($tokens).')';
/*if(!$tokens->valid()) {
$more = $this->_getMoreSubstr($stop);
//var_dump($more); exit;
$tokens->append("}".$more, $p);
var_dump("Curly", $more, $tokens->getSnippetAsString());
exit;
}*/
//$tokens->skip('}');
if($tokens->is($stop)) {
$tokens->next();
return $_str;
} else {
$_str .= '."';
}
} elseif($t === "}") {
$tokens->next();
} elseif($t === $stop) {
$tokens->next();
return $_str.'"';
if($tokens->is('"',"`")) {
$stop = $tokens->current();
$_str = '"';
$tokens->next();
while($t = $tokens->key()) {
if($t === T_ENCAPSED_AND_WHITESPACE) {
$_str .= $tokens->current();
$tokens->next();
} elseif($t === T_VARIABLE) {
if(strlen($_str) > 1) {
$_str .= '".';
} else {
break;
$_str = "";
}
}
if($more = $this->_getMoreSubstr($stop)) {
$tokens->append("}".$more, $p);
goto ref;
}
throw new UnexpectedTokenException($tokens);
} elseif($tokens->is(T_CONSTANT_ENCAPSED_STRING)) {
return $tokens->getAndNext();
} elseif($tokens->is(T_ENCAPSED_AND_WHITESPACE)) {
$p = $tokens->p;
if($more = $this->_getMoreSubstr($tokens->curr[1][0])) {
$tokens->append("}".$more, $p);
goto ref;
}
throw new UnexpectedTokenException($tokens);
} else {
return "";
}
}
}
$_str .= '$tpl["'.substr($tokens->current(), 1).'"]';
$tokens->next();
if($tokens->is($stop)) {
$tokens->skip();
return $_str;
} else {
$_str .= '."';
}
} elseif($t === T_CURLY_OPEN) {
if(strlen($_str) > 1) {
$_str .= '".';
} else {
$_str = "";
}
$tokens->getNext(T_VARIABLE);
$_str .= '('.$this->parseExp($tokens).')';
if($tokens->is($stop)) {
$tokens->next();
return $_str;
} else {
$_str .= '."';
}
} elseif($t === "}") {
$tokens->next();
} elseif($t === $stop) {
$tokens->next();
return $_str.'"';
} else {
/**
* @param string $after
* @return bool|string
*/
private function _getMoreSubstr($after) {
$end = strpos($this->_src, $after, $this->_pos);
$end = strpos($this->_src, "}", $end);
if(!$end) {
return false;
break;
}
}
throw new UnexpectedTokenException($tokens);
} elseif($tokens->is(T_CONSTANT_ENCAPSED_STRING)) {
return $tokens->getAndNext();
} elseif($tokens->is(T_ENCAPSED_AND_WHITESPACE)) {
throw new UnexpectedTokenException($tokens);
} else {
return "";
}
$fragment = substr($this->_src, $this->_pos, $end - $this->_pos);
$this->_pos = $end + 1;
return $fragment;
}
/**
@ -1091,4 +1063,5 @@ class Template extends Render {
class CompileException extends \ErrorException {}
class SecurityException extends CompileException {}
class ImproperUseException extends \LogicException {}
class ImproperUseException extends \LogicException {}
class ReparseTagException extends \Exception {}

View File

@ -79,6 +79,7 @@ class Tokenizer {
public $tokens;
public $p = 0;
public $quotes = 0;
private $_max = 0;
private $_last_no = 0;
@ -100,7 +101,7 @@ class Tokenizer {
\T_NEW => 1, \T_PRINT => 1, \T_PRIVATE => 1, \T_PUBLIC => 1, \T_PROTECTED => 1, \T_REQUIRE => 1,
\T_REQUIRE_ONCE => 1,\T_RETURN => 1, \T_RETURN => 1, \T_STRING => 1, \T_SWITCH => 1, \T_THROW => 1,
\T_TRAIT => 1, \T_TRAIT_C => 1, \T_TRY => 1, \T_UNSET => 1, \T_UNSET => 1, \T_VAR => 1,
\T_WHILE => 1
\T_WHILE => 1, \T_YIELD => 1
),
self::MACRO_INCDEC => array(
\T_INC => 1, \T_DEC => 1
@ -142,52 +143,56 @@ class Tokenizer {
'true' => 1, 'false' => 1, 'null' => 1, 'TRUE' => 1, 'FALSE' => 1, 'NULL' => 1
);
/**
* Translate expression to tokens list.
*
* @static
* @param string $query
* @return array
*/
public static function decode($query) {
$tokens = array(-1 => array(\T_WHITESPACE, '', '', 1));
$_tokens = token_get_all("<?php ".$query);
$line = 1;
array_shift($_tokens);
$i = 0;
foreach($_tokens as &$token) {
if(is_string($token)) {
$tokens[] = array(
$token,
$token,
"",
$line,
);
$i++;
} elseif ($token[0] === \T_WHITESPACE) {
$tokens[$i-1][2] = $token[1];
} else {
$tokens[] = array(
$token[0],
$token[1],
"",
$line = $token[2],
);
$i++;
}
/**
* @param $query
*/
public function __construct($query) {
$tokens = array(-1 => array(\T_WHITESPACE, '', '', 1));
$_tokens = token_get_all("<?php ".$query);
$line = 1;
array_shift($_tokens);
$i = 0;
foreach($_tokens as $token) {
if(is_string($token)) {
if($token === '"' || $token === "'" || $token === "`") {
$this->quotes++;
}
$tokens[] = array(
$token,
$token,
"",
$line,
);
$i++;
} elseif ($token[0] === \T_WHITESPACE) {
$tokens[$i-1][2] = $token[1];
} else {
$tokens[] = array(
$token[0],
$token[1],
"",
$line = $token[2],
token_name($token[0]) // debug
);
$i++;
}
}
return $tokens;
}
public function __construct($query, $decode = 0) {
$this->tokens = self::decode($query, $decode);
unset($this->tokens[-1]);
}
unset($tokens[-1]);
$this->tokens = $tokens;
$this->_max = count($this->tokens) - 1;
$this->_last_no = $this->tokens[$this->_max][3];
}
/**
* Is incomplete mean some string not closed
*
* @return int
*/
public function isIncomplete() {
return ($this->quotes % 2) || ($this->tokens[$this->_max][0] === T_ENCAPSED_AND_WHITESPACE);
}
/**
* Return the current element
*
@ -468,7 +473,7 @@ class Tokenizer {
}
/**
* Get tokens near current token
* Get tokens near current position
* @param int $before count tokens before current token
* @param int $after count tokens after current token
* @return array
@ -553,30 +558,6 @@ class Tokenizer {
public function getLine() {
return $this->curr ? $this->curr[3] : $this->_last_no;
}
/**
* Parse code and append tokens. This method move pointer to offset.
*
* @param string $code
* @param int $offset if not -1 replace tokens from position $offset
* @return Tokenizer
*/
public function append($code, $offset = -1) {
if($offset != -1) {
$code = $this->getSubstr($offset).$code;
if($this->p > $offset) {
$this->p = $offset;
}
$this->tokens = array_slice($this->tokens, 0, $offset);
}
$tokens = self::decode($code);
unset($tokens[-1], $this->prev, $this->curr, $this->next);
$this->tokens = array_merge($this->tokens, $tokens);
$this->_max = count($this->tokens) - 1;
$this->_last_no = $this->tokens[$this->_max][3];
return $this;
}
}
/**

View File

@ -24,6 +24,11 @@ class TestCase extends \PHPUnit_Framework_TestCase {
FS::clean(CYTRO_RESOURCES.'/compile/');
}
$this->cytro = Cytro::factory(CYTRO_RESOURCES.'/template', CYTRO_RESOURCES.'/compile');
$this->cytro->addModifier('dots', __CLASS__.'::dots');
}
public static function dots($value) {
return $value."...";
}
public static function setUpBeforeClass() {
@ -85,10 +90,105 @@ class TestCase extends \PHPUnit_Framework_TestCase {
$this->fail("Code $code must be invalid");
}
public function assertRender($tpl, $result) {
public function assertRender($tpl, $result, $debug = false) {
$template = $this->cytro->compileCode($tpl);
if($debug) {
print_r("$tpl:\n".$template->getBody());
}
$this->assertSame($result, $template->fetch($this->values));
}
public static function providerNumbers() {
return array(
array('77', 77),
array('-33', -33),
array('0.2', 0.2),
array('-0.3', -0.3),
array('1e6', 1e6),
array('-2e6', -2e6),
);
}
public static function providerStrings() {
return array(
array('"str"', 'str'),
array('"str\nand\nmany\nlines"', "str\nand\nmany\nlines"),
array('"str and \'substr\'"', "str and 'substr'"),
array('"str and \"substr\""', 'str and "substr"'),
array("'str'", 'str'),
array("'str\\nin\\none\\nline'", 'str\nin\none\nline'),
array("'str and \"substr\"'", 'str and "substr"'),
array("'str and \'substr\''", "str and 'substr'"),
array('"$one"', '1'),
array('"$one $two"', '1 2'),
array('"$one and $two"', '1 and 2'),
array('"a $one and $two b"', 'a 1 and 2 b'),
array('"{$one}"', '1'),
array('"a {$one} b"', 'a 1 b'),
array('"{$one + 2}"', '3'),
array('"{$one * $two + 1}"', '3'),
array('"{$one} and {$two}"', '1 and 2'),
array('"$one and {$two}"', '1 and 2'),
array('"{$one} and $two"', '1 and 2'),
array('"a {$one} and {$two} b"', 'a 1 and 2 b'),
array('"{$one+1} and {$two-1}"', '2 and 1'),
array('"a {$one+1} and {$two-1} b"', 'a 2 and 1 b'),
array('"a {$one|dots} and {$two|dots} b"', 'a 1... and 2... b'),
array('"a {$one|dots} and $two b"', 'a 1... and 2 b'),
array('"a $one and {$two|dots} b"', 'a 1 and 2... b'),
);
}
public function providerVariables() {
return array();
}
public static function providerObjects() {
return array();
}
public static function providerArrays() {
$scalars = array();
$data = array(
array('[]', array()),
array('[[],[]]', array(array(), array())),
);
foreach(self::providerScalars() as $scalar) {
$scalars[0][] = $scalar[0];
$scalars[1][] = $scalar[1];
$data[] = array(
"[".$scalar[0]."]",
array($scalar[1])
);
$data[] = array(
"['some_key' =>".$scalar[0]."]",
array('some_key' => $scalar[1])
);
}
$data[] = array(
"[".implode(", ", $scalars[0])."]",
$scalars[1]
);
return $data;
}
public static function providerScalars() {
return array_merge(
self::providerNumbers(),
self::providerStrings()
);
}
public static function providerValues() {
return array_merge(
self::providerScalars(),
self::providerArrays(),
self::providerVariables(),
self::providerObjects()
);
}
}
class Fake implements \ArrayAccess {
@ -113,4 +213,8 @@ class Fake implements \ArrayAccess {
public function offsetUnset($offset) {
unset($this->vars[$offset]);
}
public function proxy() {
return implode(", ", func_get_args());
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace Cytro;
class CustomProvider extends TestCase {
public function setUp() {
$this->setUp();
$this->cytro->addProvider("my", new FSProvider(CYTRO_RESOURCES.'/provider'));
}
public function testCustom() {
$this->render("start: {include 'my:include.tpl'}", 'start: include template');
$this->render("start: {import 'my:macros.tpl' as ops} {ops.add a=3 b=6}");
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Cytro;
class CustomProviderTest extends TestCase {
public function setUp() {
parent::setUp();
$this->cytro->addProvider("my", new FSProvider(CYTRO_RESOURCES.'/provider'));
}
public function testCustom() {
$this->assertRender("start: {include 'my:include.tpl'}", 'start: include template');
//$this->assertRender("start: {import 'my:macros.tpl' as ops} {ops.add a=3 b=6}");
}
}

View File

@ -5,8 +5,8 @@ namespace Cytro;
class ModifiersTest extends TestCase {
public static function providerTruncate() {
$lorem = 'Lorem ipsum dolor sit amet';
$uni = 'Лорем ипсум долор сит амет';
$lorem = 'Lorem ipsum dolor sit amet'; // en
$uni = 'Лорем ипсум долор сит амет'; // ru
return array(
// ascii chars
array($lorem, 'Lorem ip...', 8),
@ -23,13 +23,15 @@ class ModifiersTest extends TestCase {
);
}
/**
* @dataProvider providerTruncate
* @param $in
* @param $out
* @param $count
* @param string $delim
* @param bool $by_word
* @param bool $by_words
* @param bool $middle
*/
public function testTruncate($in, $out, $count, $delim = '...', $by_words = false, $middle = false) {
$tpl = $this->cytro->compileCode('{$text|truncate:$count:$delim:$by_words:$middle}');
@ -41,4 +43,58 @@ class ModifiersTest extends TestCase {
"middle" => $middle
)));
}
public static function providerUpLow() {
return array(
array("up", "lorem", "LOREM"),
array("up", "Lorem", "LOREM"),
array("up", "loREM", "LOREM"),
array("up", "223a", "223A"),
array("low", "lorem", "lorem"),
array("low", "Lorem", "lorem"),
array("low", "loREM", "lorem"),
array("low", "223A", "223a"),
);
}
/**
* @dataProvider providerUpLow
* @param $modifier
* @param $in
* @param $out
*/
public function testUpLow($modifier, $in, $out) {
$tpl = $this->cytro->compileCode('{$text|'.$modifier.'}');
$this->assertEquals($out, $tpl->fetch(array(
"text" => $in,
)));
}
public static function providerLength() {
return array(
array("length", 6),
array("длина", 5),
array("length - длина", 14),
array(array(1, 33, "c" => 4), 3),
array(new \ArrayIterator(array(1, "c" => 4)), 2),
array(true, 0),
array(new \stdClass(), 0),
array(5, 0)
);
}
/**
* @dataProvider providerLength
* @param $in
* @param $in
* @param $out
*/
public function testLength($in, $out) {
$tpl = $this->cytro->compileCode('{$data|length}');
$this->assertEquals($out, $tpl->fetch(array(
"data" => $in,
)));
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Cytro;
class TagsTest extends TestCase {
// public function _testSandbox() {
// try {
// var_dump($this->cytro->compileCode(" literal: { \$a} end")->getBody());
// } catch(\Exception $e) {
// echo "$e";
// }
// exit;
// }
/**
* @dataProvider providerScalars
*/
public function testVar($tpl_val, $val) {
$this->assertRender("{var \$a=$tpl_val}\nVar: {\$a}", "\nVar: ".$val);
}
/**
* @dataProvider providerScalars
*/
public function testVarBlock($tpl_val, $val) {
$this->assertRender("{var \$a}before {{$tpl_val}} after{/var}\nVar: {\$a}", "\nVar: before ".$val." after");
}
/**
* @dataProvider providerScalars
*/
public function testVarBlockModified($tpl_val, $val) {
$this->assertRender("{var \$a|low|dots}before {{$tpl_val}} after{/var}\nVar: {\$a}", "\nVar: ".strtolower("before ".$val." after")."...");
}
public function testCycle() {
}
public function testFilter() {
}
}

View File

@ -4,6 +4,11 @@ use Cytro\Template,
Cytro,
Cytro\Render;
/**
* Test template parsing
*
* @package Cytro
*/
class TemplateTest extends TestCase {
public function setUp() {
@ -15,11 +20,6 @@ class TemplateTest extends TestCase {
)));
}
/*public function testSandbox() {
var_dump($this->cytro->compileCode('{"$s:{$b+1}f d {$d}"}')->_body);
exit;
}*/
public static function providerVars() {
$a = array("a" => "World");
$obj = new \stdClass;
@ -73,41 +73,6 @@ class TemplateTest extends TestCase {
);
}
public static function providerScalars() {
return array(
array('77', 77),
array('-33', -33),
array('0.2', 0.2),
array('-0.3', -0.3),
array('1e6', 1e6),
array('-2e6', -2e6),
array('"str"', 'str'),
array('"str\nand\nmany\nlines"', "str\nand\nmany\nlines"),
array('"str and \'substr\'"', "str and 'substr'"),
array('"str and \"substr\""', 'str and "substr"'),
array("'str'", 'str'),
array("'str\\nin\\none\\nline'", 'str\nin\none\nline'),
array("'str and \"substr\"'", 'str and "substr"'),
array("'str and \'substr\''", "str and 'substr'"),
array('"$one"', '1'),
array('"$one $two"', '1 2'),
array('"$one and $two"', '1 and 2'),
array('"a $one and $two b"', 'a 1 and 2 b'),
array('"{$one}"', '1'),
array('"a {$one} b"', 'a 1 b'),
array('"{$one + 2}"', '3'),
array('"{$one * $two + 1}"', '3'),
array('"{$one} and {$two}"', '1 and 2'),
array('"$one and {$two}"', '1 and 2'),
array('"{$one} and $two"', '1 and 2'),
array('"a {$one} and {$two} b"', 'a 1 and 2 b'),
array('"{$one+1} and {$two-1}"', '2 and 1'),
array('"a {$one+1} and {$two-1} b"', 'a 2 and 1 b'),
array('"a {$one|dots} and {$two|dots} b"', 'a 1... and 2... b'),
array('"a {$one|dots} and $two b"', 'a 1... and 2 b'),
array('"a $one and {$two|dots} b"', 'a 1 and 2... b'),
);
}
public static function providerVarsInvalid() {
return array(
@ -347,7 +312,7 @@ class TemplateTest extends TestCase {
public static function providerCreateVarInvalid() {
return array(
array('Create: {var $v} Result: {$v} end', 'Cytro\CompileException', "Unexpected end of expression"),
array('Create: {var $v} Result: {$v} end', 'Cytro\CompileException', "Unclosed tag: {var} opened"),
array('Create: {var $v = } Result: {$v} end', 'Cytro\CompileException', "Unexpected end of expression"),
array('Create: {var $v = 1++} Result: {$v} end', 'Cytro\CompileException', "Unexpected token '++'"),
array('Create: {var $v = c} Result: {$v} end', 'Cytro\CompileException', "Unexpected token 'c'"),
@ -586,7 +551,7 @@ class TemplateTest extends TestCase {
array('Layers: {for $a=4 to=6} block1 {if 1} {/for} {/if} end', 'Cytro\CompileException', "Unexpected closing of the tag 'for'"),
array('Layers: {switch 1} {if 1} {case 1} {/if} {/switch} end', 'Cytro\CompileException', "Unexpected tag 'case' (this tag can be used with 'switch')"),
array('Layers: {/switch} end', 'Cytro\CompileException', "Unexpected closing of the tag 'switch'"),
array('Layers: {if 1} end', 'Cytro\CompileException', "Unclosed tag(s): {if}"),
array('Layers: {if 1} end', 'Cytro\CompileException', "Unclosed tag: {if}"),
);
}