Add range support. Tag {for} now deprecated

This commit is contained in:
bzick 2015-02-12 12:13:35 +03:00
parent a9b9c89f88
commit ed860a49cb
10 changed files with 191 additions and 57 deletions

View File

@ -112,7 +112,7 @@
```smarty
{var $b |= $flags}
{set $b |= $flags}
```
### Операторы инкремента и декремента
@ -135,6 +135,39 @@ Fenom поддерживает префиксные и постфиксные о
* `$a ~~ $b` - возвращает результат объединения сток `$a` и `$b` через пробел
* `$a ~= $b` - присвоение с объединением
### Оператор интервала
Оператор `..` позволяет создать массив данных, не выходящие за указанные ограничения.
```smarty
{set $a = 1..4}
```
создаст массив `[1,2,3,4]`
```smarty
{set $a = 'a'..'f'}
```
создаст массив `['a','b','c','d','e','f']`
```smarty
{set $a = 'a'|up..'f'|up}
```
создаст массив `['A','B','C','D','E','F']`
```smarty
{set $a = $min..$max}
```
создаст массив из значений где первый и минимальный элемент будет значение `$min`,
а максимальный и последний элемент будет занчение `$max`
**Замечание:**
оганичения должны быть одного типа, интервал `1..'f'` преобразует `f` в `0` и будет сгенерировано `[1,0]`
### Тернарные операторы
Еще одним условным оператором являются тернарные операторы `?:` и `!:`.

View File

@ -1,7 +1,8 @@
Тег {foreach}
=============
Тег `foreach` предоставляет простой способ перебора массивов.
Тег `foreach` предоставляет простой способ перебора массивов.
`Foreach` работает только с массивами, объектами и интервалами.
```smarty
{foreach $list as [$key =>] $value [index=$index] [first=$first] [last=$last]}
@ -17,15 +18,19 @@
### {foreach}
Перебор значений массива $list
Перебор значений массива $list:
```smarty
{foreach $list as $value}
<div>{$value}</div>
{/foreach}
{foreach 1..7 as $value} {* так же хорошо работает и с интрвелами *}
<div>№{$value}</div>
{/foreach}
```
Перебор ключей и значений массива $list
Перебор ключей и значений массива $list:
```smarty
{foreach $list as $key => $value}
@ -60,13 +65,19 @@
Переменная `$last` будет иметь значение **TRUE**, если текущая итерация является последней.
**Замечание:**
Использование `last` требует от `$list` быть **countable**.
### {break}
Тег `{break}` используется для выхода из цикла до достижения последней итерации. Если в цикле встречается тег `{break}`, цикл завершает свою работу, и далее, выполняется код, следующий сразу за блоком цикла
Тег `{break}` используется для выхода из цикла до достижения последней итерации.
Если в цикле встречается тег `{break}`, цикл завершает свою работу, и далее, выполняется код, следующий сразу за блоком цикла
### {continue}
Тег `{continue}` используется для прерывания текущей итерации. Если в цикле встречается тег `{continue}`, часть цикла, следующая после тега, не выполняется, и начинается следующая итерация. Если текущая итерация была последней, цикл завершается.
Тег `{continue}` используется для прерывания текущей итерации.
Если в цикле встречается тег `{continue}`, часть цикла, следующая после тега, не выполняется, и начинается следующая итерация.
Если текущая итерация была последней, цикл завершается.
### {foreachelse}
@ -81,7 +92,4 @@
{/foreach}
```
В блоке `{foreachelse}...{/foreach}` использование `{break}`, `{continue}` выбросит исключение `Fenom\CompileException` при компиляции
**Замечание:**
Использование last требует от `$list` быть **countable**.
В блоке `{foreachelse}...{/foreach}` использование `{break}`, `{continue}` выбросит исключение `Fenom\CompileException` при компиляции

View File

@ -7,6 +7,6 @@ require_once __DIR__.'/../tests/tools.php';
$fenom = Fenom::factory(__DIR__.'/templates', __DIR__.'/compiled');
$fenom->setOptions(Fenom::AUTO_RELOAD);
var_dump($fenom->compile("blocks/second.tpl", false)->getBody());
var_dump($fenom->compileCode('{$b == 5 ? "5" : "?"}')->getBody());
//$fenom->display("blocks/second.tpl", []);
// $fenom->getTemplate("problem.tpl");

View File

@ -239,6 +239,7 @@ class Compiler
*/
public static function forOpen(Tokenizer $tokens, Tag $scope)
{
trigger_error("Fenom: tag {for} deprecated, use {foreach 1..4 as \$value}", E_USER_DEPRECATED);
$p = array(
"index" => false,
"first" => false,

View File

@ -279,4 +279,15 @@ class Modifier
return "";
}
}
/**
* @param string|int $from
* @param string|int $to
* @param int $step
* @return array
*/
public static function range($from, $to, $step = 1) {
$v = range($from, $to, $step);
return $v ? $v : array();
}
}

View File

@ -35,6 +35,14 @@ class Template extends Render
* Disable modifier parser.
*/
const DENY_MODS = 2;
/**
* Allow parse modifiers with term
*/
const TERM_MODS = 1;
/**
* Allow parse range with term
*/
const TERM_RANGE = 1;
/**
* @var int shared counter
*/
@ -660,12 +668,8 @@ class Template extends Render
$cond = false; // was comparison operator
while ($tokens->valid()) {
// parse term
$term = $this->parseTerm($tokens, $var); // term of the expression
$term = $this->parseTerm($tokens, $var, -1); // term of the expression
if ($term !== false) {
if ($tokens->is('|')) {
$term = $this->parseModifier($tokens, $term);
$var = false;
}
if ($tokens->is('?', '!')) {
if($cond) {
$term = array_pop($exp) . ' ' . $term;
@ -717,7 +721,7 @@ class Template extends Render
break;
}
} elseif ($tokens->is('~')) { // string concatenation operator: 'asd' ~ $var
if($tokens->isNext('=')) { // ~=
if ($tokens->isNext('=')) { // ~=
$exp[] = ".=";
$tokens->next()->next();
} else {
@ -728,11 +732,11 @@ class Template extends Render
if ($tokens->is(T_LNUMBER, T_DNUMBER)) {
$concat[] = "strval(" . $this->parseTerm($tokens) . ")";
} else {
if($tokens->is('~')) {
if ($tokens->is('~')) {
$tokens->next();
$concat[] = " ";
}
if(!$concat[] = $this->parseTerm($tokens)) {
if (!$concat[] = $this->parseTerm($tokens)) {
throw new UnexpectedTokenException($tokens);
}
}
@ -758,12 +762,11 @@ class Template extends Render
*
* @param Tokenizer $tokens
* @param bool $is_var is parsed term - plain variable
* @throws Error\UnexpectedTokenException
* @throws Error\TokenizeException
* @param int $allows
* @throws \Exception
* @return bool|string
*/
public function parseTerm(Tokenizer $tokens, &$is_var = false)
public function parseTerm(Tokenizer $tokens, &$is_var = false, $allows = -1)
{
$is_var = false;
if ($tokens->is(Tokenizer::MACRO_UNARY)) {
@ -772,62 +775,60 @@ class Template extends Render
$unary = "";
}
if ($tokens->is(T_LNUMBER, T_DNUMBER)) {
return $unary . $this->parseScalar($tokens, true);
$code = $unary . $this->parseScalar($tokens, true);
} elseif ($tokens->is(T_CONSTANT_ENCAPSED_STRING, '"', T_ENCAPSED_AND_WHITESPACE)) {
if ($unary) {
throw new UnexpectedTokenException($tokens->back());
}
return $this->parseScalar($tokens, true);
$code = $this->parseScalar($tokens, true);
} elseif ($tokens->is(T_VARIABLE)) {
$code = $this->parseVariable($tokens);
if ($tokens->is("(") && $tokens->hasBackList(T_STRING, T_OBJECT_OPERATOR)) {
if ($this->_options & Fenom::DENY_METHODS) {
throw new \LogicException("Forbidden to call methods");
}
return $unary . $this->parseChain($tokens, $code);
$code = $unary . $this->parseChain($tokens, $code);
} elseif ($tokens->is(Tokenizer::MACRO_INCDEC)) {
if($this->_options & Fenom::FORCE_VERIFY) {
return $unary . '(isset(' . $code . ') ? ' . $code . $tokens->getAndNext() . ' : null)';
$code = $unary . '(isset(' . $code . ') ? ' . $code . $tokens->getAndNext() . ' : null)';
} else {
return $unary . $code . $tokens->getAndNext();
$code = $unary . $code . $tokens->getAndNext();
}
} else {
if($this->_options & Fenom::FORCE_VERIFY) {
return $unary . '(isset(' . $code . ') ? ' . $code . ' : null)';
$code = $unary . '(isset(' . $code . ') ? ' . $code . ' : null)';
} else {
$is_var = true;
return $unary . $code;
$code = $unary . $code;
}
}
} elseif ($tokens->is('$')) {
$is_var = false;
$var = $this->parseAccessor($tokens);
return $unary . $var;
$code = $unary . $this->parseAccessor($tokens);
} elseif ($tokens->is(Tokenizer::MACRO_INCDEC)) {
if($this->_options & Fenom::FORCE_VERIFY) {
$var = $this->parseVariable($tokens);
return $unary . '(isset(' . $var . ') ? ' . $tokens->getAndNext() . $this->parseVariable($tokens).' : null)';
$code = $unary . '(isset(' . $var . ') ? ' . $tokens->getAndNext() . $this->parseVariable($tokens).' : null)';
} else {
return $unary . $tokens->getAndNext() . $this->parseVariable($tokens);
$code = $unary . $tokens->getAndNext() . $this->parseVariable($tokens);
}
} elseif ($tokens->is("(")) {
$tokens->next();
$code = $unary . "(" . $this->parseExpr($tokens) . ")";
$tokens->need(")")->next();
return $code;
} elseif ($tokens->is(T_STRING)) {
if ($tokens->isSpecialVal()) {
return $unary . $tokens->getAndNext();
$code = $unary . $tokens->getAndNext();
} elseif ($tokens->isNext("(") && !$tokens->getWhitespace()) {
$func = $this->_fenom->getModifier($tokens->current(), $this);
if (!$func) {
throw new \Exception("Function " . $tokens->getAndNext() . " not found");
}
return $unary . $this->parseChain($tokens, $func . $this->parseArgs($tokens->next()));
$code = $unary . $this->parseChain($tokens, $func . $this->parseArgs($tokens->next()));
} elseif ($tokens->isNext(T_NS_SEPARATOR, T_DOUBLE_COLON)) {
$method = $this->parseStatic($tokens);
$args = $this->parseArgs($tokens);
return $unary . $this->parseChain($tokens, $method . $args);
$code = $unary . $this->parseChain($tokens, $method . $args);
} else {
return false;
}
@ -836,7 +837,6 @@ class Template extends Render
if ($tokens->is("(") && $tokens->isNext(T_VARIABLE)) {
$code = $unary . $func . "(" . $this->parseVariable($tokens->next()) . ")";
$tokens->need(')')->next();
return $code;
} else {
throw new TokenizeException("Unexpected token " . $tokens->getNext() . ", isset() and empty() accept only variables");
}
@ -844,12 +844,22 @@ class Template extends Render
if ($unary) {
throw new UnexpectedTokenException($tokens->back());
}
return $this->parseArray($tokens);
$code = $this->parseArray($tokens);
} elseif ($unary) {
throw new UnexpectedTokenException($tokens->back());
} else {
return false;
}
if (($allows & self::TERM_MODS) && $tokens->is('|')) {
$code = $this->parseModifier($tokens, $code);
$is_var = false;
}
if(($allows & self::TERM_RANGE) && $tokens->is('.') && $tokens->isNext('.')) {
$tokens->next()->next();
$code = 'range('.$code.', '.$this->parseTerm($tokens, $var, self::TERM_MODS).')';
$is_var = false;
}
return $code;
}
/**
@ -895,6 +905,9 @@ class Template extends Render
$key = "[" . $tokens->getAndNext() . "]";
} elseif ($tokens->is('"')) {
$key = "[" . $this->parseQuote($tokens) . "]";
} elseif($tokens->is('.')) {
$tokens->back();
break;
} else {
throw new UnexpectedTokenException($tokens);
}
@ -1147,16 +1160,15 @@ class Template extends Render
case T_LNUMBER:
case T_DNUMBER:
return $tokens->getAndNext();
break;
case T_ENCAPSED_AND_WHITESPACE:
case '"':
return $this->parseQuote($tokens);
break;
default:
throw new UnexpectedTokenException($tokens);
}
}
/**
* Parse string with or without variable
*
@ -1248,7 +1260,7 @@ class Template extends Render
$args = array();
while ($tokens->is(":")) {
if (($args[] = $this->parseTerm($tokens->next())) === false) {
if (($args[] = $this->parseTerm($tokens->next(), $is_var, 0)) === false) {
throw new UnexpectedTokenException($tokens);
}
}

View File

@ -186,25 +186,48 @@ class Tokenizer
if ($token === '"' || $token === "'" || $token === "`") {
$this->quotes++;
}
$tokens[] = array(
$token = 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++;
continue;
} elseif ($token[0] === \T_DNUMBER) { // fix .1 and 1.
if(strpos($token[1], '.') === 0) {
$tokens[] = array(
'.',
'.',
"",
$line = $token[2]
);
$token = array(
T_LNUMBER,
ltrim($token[1], '.'),
$line = $token[2]
);
} elseif(strpos($token[1], '.') === strlen($token[1]) - 1) {
$tokens[] = array(
T_LNUMBER,
rtrim($token[1], '.'),
"",
$line = $token[2]
);
$token = array(
'.',
'.',
$line = $token[2]
);
}
}
$tokens[] = array(
$token[0],
$token[1],
"",
$line = $token[2]
);
$i++;
}
unset($tokens[-1]);

View File

@ -5,6 +5,9 @@ namespace Fenom;
class SandboxTest extends TestCase {
/**
* @group sb
*/
public function test()
{
@ -13,8 +16,9 @@ class SandboxTest extends TestCase {
// return '<?php ' . $tag->cutContent();
// });
// $this->tpl('welcome.tpl', '{$a}');
// var_dump($this->fenom->compileCode('{set $a=$one|min:0..$three|max:4}')->getBody());
// try {
// var_dump($this->fenom->compileCode('{for $i=0 to 3} {/for}')->getBody());
// var_dump($this->fenom->compileCode('{foreach $a..$b|up as $k => $v} {/foreach}')->getBody());
// } catch (\Exception $e) {
// print_r($e->getMessage() . "\n" . $e->getTraceAsString());
// while ($e->getPrevious()) {

View File

@ -1629,5 +1629,39 @@ class TemplateTest extends TestCase
{
$this->execError($code, $exception, $message, $options);
}
public static function providerRange()
{
return array(
array('{set $a=1..3}', "1,2,3,"),
array('{set $a="a".."f"}', "a,b,c,d,e,f,"),
array('{set $a=1.."f"}', "1,0,"),
array('{set $a="a"..2}', "0,1,2,"),
array('{set $a=$one..$three}', "1,2,3,"),
array('{set $a=$one..3}', "1,2,3,"),
array('{set $a=1..$three}', "1,2,3,"),
array('{set $a=$one..$three++}', "1,2,3,"),
array('{set $a=$one..++$three}', "1,2,3,4,"),
array('{set $a=$one--..$three++}', "1,2,3,"),
array('{set $a=--$one..++$three}', "0,1,2,3,4,"),
array('{set $a="a"|up.."f"|up}', "A,B,C,D,E,F,"),
array('{set $a=$one|min:0..$three|max:4}', "0,1,2,3,4,"),
array('{set $a=$one|min:0..4}', "0,1,2,3,4,"),
array('{set $a=0..$three|max:4}', "0,1,2,3,4,"),
);
}
/**
* @dataProvider providerRange
* @group testRange
* @param string $code
* @param string $result
*/
public function testRange($code, $result)
{
$this->exec($code.'{foreach $a as $v}{$v},{/foreach}', self::getVars(), $result);
}
}

View File

@ -43,8 +43,7 @@ class TokenizerTest extends \PHPUnit_Framework_TestCase
T_STRING,
'please',
' ',
1,
'T_STRING'
1
),
$tokens->curr
);
@ -110,4 +109,13 @@ class TokenizerTest extends \PHPUnit_Framework_TestCase
$this->assertNull($tokens->undef);
}
public function testFixFloats() {
$text = "1..3";
$tokens = new Tokenizer($text);
$this->assertTrue($tokens->is(T_LNUMBER));
$this->assertTrue($tokens->next()->is('.'));
$this->assertTrue($tokens->next()->is('.'));
$this->assertTrue($tokens->next()->is(T_LNUMBER));
}
}