Support whitespace deletion

Need more test :)
fixes #61
This commit is contained in:
fzerorubigd 2014-07-04 03:36:14 +04:30
parent cd8cec42a0
commit a68318f4c5
No known key found for this signature in database
GPG Key ID: D6EE858AF9D2999A
5 changed files with 82 additions and 20 deletions

View File

@ -115,6 +115,7 @@ class Helpers
* @param array $args The arguments passed the the helper
* @param string $source The source
*
* @throws \InvalidArgumentException
* @return mixed The helper return value
*/
public function call($name, Template $template, Context $context, $args, $source)

View File

@ -92,7 +92,7 @@ class Parser
array_unshift($newNodes, $result);
}
} while (true);
break;
// There is no break here, since we need the end token to handle the whitespace trim
default:
array_push($stack, $token);
}
@ -101,7 +101,6 @@ class Parser
} while ($tokens->valid());
return $stack;
}
}

View File

@ -145,6 +145,7 @@ class Template
list($index, $tree, $stop) = $topTree;
$buffer = '';
$rTrim = false;
while (array_key_exists($index, $tree)) {
$current = $tree[$index];
$index++;
@ -155,44 +156,60 @@ class Template
) {
break;
}
if (isset($current[Tokenizer::TRIM_LEFT]) && $current[Tokenizer::TRIM_LEFT]) {
$buffer = rtrim($buffer);
}
$tmp = '';
switch ($current[Tokenizer::TYPE]) {
case Tokenizer::T_END_SECTION:
break; // Its here just for handling whitespace trim.
case Tokenizer::T_SECTION :
$newStack = isset($current[Tokenizer::NODES])
? $current[Tokenizer::NODES] : array();
array_push($this->_stack, array(0, $newStack, false));
$buffer .= $this->_section($context, $current);
$tmp = $this->_section($context, $current);
array_pop($this->_stack);
break;
case Tokenizer::T_INVERTED :
$newStack = isset($current[Tokenizer::NODES]) ?
$current[Tokenizer::NODES] : array();
array_push($this->_stack, array(0, $newStack, false));
$buffer .= $this->_inverted($context, $current);
$tmp = $this->_inverted($context, $current);
array_pop($this->_stack);
break;
case Tokenizer::T_COMMENT :
$buffer .= '';
$tmp = '';
break;
case Tokenizer::T_PARTIAL:
case Tokenizer::T_PARTIAL_2:
$buffer .= $this->_partial($context, $current);
$tmp = $this->_partial($context, $current);
break;
case Tokenizer::T_UNESCAPED:
case Tokenizer::T_UNESCAPED_2:
$buffer .= $this->_get($context, $current, false);
$tmp = $this->_get($context, $current, false);
break;
case Tokenizer::T_ESCAPED:
$buffer .= $this->_get($context, $current, true);
$tmp = $this->_get($context, $current, true);
break;
case Tokenizer::T_TEXT:
$buffer .= $current[Tokenizer::VALUE];
$tmp = $current[Tokenizer::VALUE];
break;
default:
throw new \RuntimeException(
'Invalid node type : ' . json_encode($current)
);
}
if ($rTrim) {
$tmp = ltrim($tmp);
}
$buffer .= $tmp;
// Some time, there is more than one string token (first is empty),
//so we need to trim all of them in one shot
$rTrim = (empty($tmp) && $rTrim) ||
isset($current[Tokenizer::TRIM_RIGHT]) && $current[Tokenizer::TRIM_RIGHT];
}
if ($stop) {
//Ok break here, the helper should be aware of this.
@ -273,25 +290,25 @@ class Template
// subexpression parsing loop
$subexprs = array(); // will contain all subexpressions inside outermost brackets
$inside_of = array( 'single' => false, 'double' => false );
$insideOf = array( 'single' => false, 'double' => false );
$lvl = 0;
$cur_start = 0;
for ($i=0; $i < strlen($current[Tokenizer::ARGS]); $i++) {
$cur = substr($current[Tokenizer::ARGS], $i, 1);
if ($cur == "'" ) {
$inside_of['single'] = ! $inside_of['single'];
$insideOf['single'] = ! $insideOf['single'];
}
if ($cur == '"' ) {
$inside_of['double'] = ! $inside_of['double'];
$insideOf['double'] = ! $insideOf['double'];
}
if ($cur == '(' && ! $inside_of['single'] && ! $inside_of['double']) {
if ($cur == '(' && ! $insideOf['single'] && ! $insideOf['double']) {
if ($lvl == 0) {
$cur_start = $i+1;
}
$lvl++;
continue;
}
if ($cur == ')' && ! $inside_of['single'] && ! $inside_of['double']) {
if ($cur == ')' && ! $insideOf['single'] && ! $insideOf['double']) {
$lvl--;
if ($lvl == 0) {
$subexprs[] = substr($current[Tokenizer::ARGS], $cur_start, $i - $cur_start);

View File

@ -60,6 +60,7 @@ class Tokenizer
const T_ESCAPE = "\\";
const T_SINGLE_Q = "'";
const T_DOUBLE_Q = "\"";
const T_TRIM = "~";
// Valid token types
private static $_tagTypes = array(
@ -93,6 +94,8 @@ class Tokenizer
const NODES = 'nodes';
const VALUE = 'value';
const ARGS = 'args';
const TRIM_LEFT = 'tleft';
const TRIM_RIGHT = 'rleft';
protected $state;
protected $tagType;
@ -103,6 +106,10 @@ class Tokenizer
protected $lineStart;
protected $otag;
protected $ctag;
protected $escaped;
protected $escaping;
protected $trimLeft;
protected $trimRight;
/**
* Scan and tokenize template source.
@ -132,10 +139,10 @@ class Tokenizer
// To play nice with helpers' arguments quote and apostrophe marks
// should be additionally escaped only when they are not in a tag.
$quote_in_tag = $this->state != self::IN_TEXT
$quoteInTag = $this->state != self::IN_TEXT
&& ($text[$i] == self::T_SINGLE_Q || $text[$i] == self::T_DOUBLE_Q);
if ($this->escaped && $text[$i] != self::T_UNESCAPED && !$quote_in_tag) {
if ($this->escaped && $text[$i] != self::T_UNESCAPED && !$quoteInTag) {
$this->buffer .= "\\";
}
@ -145,6 +152,10 @@ class Tokenizer
$this->buffer .= "{{{";
$i += 2;
continue;
} elseif ($this->tagChange($this->otag. self::T_TRIM, $text, $i) and !$this->escaped) {
$this->flushBuffer();
$this->state = self::IN_TAG_TYPE;
$this->trimLeft = true;
} elseif ($this->tagChange($this->otag, $text, $i) and !$this->escaped) {
$i--;
$this->flushBuffer();
@ -184,6 +195,10 @@ class Tokenizer
break;
default:
if ($this->tagChange(self::T_TRIM . $this->ctag, $text, $i)) {
$this->trimRight = true;
continue;
}
if ($this->tagChange($this->ctag, $text, $i)) {
// Sections (Helpers) can accept parameters
// Same thing for Partials (little known fact)
@ -206,6 +221,8 @@ class Tokenizer
self::INDEX => ($this->tagType == self::T_END_SECTION) ?
$this->seenTag - strlen($this->otag) :
$i + strlen($this->ctag),
self::TRIM_LEFT => $this->trimLeft,
self::TRIM_RIGHT => $this->trimRight
);
if (isset($args)) {
$t[self::ARGS] = $args;
@ -214,6 +231,8 @@ class Tokenizer
unset($t);
unset($args);
$this->buffer = '';
$this->trimLeft = false;
$this->trimRight = false;
$i += strlen($this->ctag) - 1;
$this->state = self::IN_TEXT;
if ($this->tagType == self::T_UNESCAPED) {
@ -262,6 +281,8 @@ class Tokenizer
$this->lineStart = 0;
$this->otag = '{{';
$this->ctag = '}}';
$this->trimLeft = false;
$this->trimRight = false;
}
/**
@ -337,7 +358,7 @@ class Tokenizer
}
/**
* Change the current Mustache delimiters. Set new `otag` and `ctag` values.
* Change the current Handlebars delimiters. Set new `otag` and `ctag` values.
*
* @param string $text Mustache template source
* @param int $index Current tokenizer index
@ -364,7 +385,7 @@ class Tokenizer
* Test whether it's time to change tags.
*
* @param string $tag Current tag name
* @param string $text Mustache template source
* @param string $text Handlebars template source
* @param int $index Current tokenizer index
*
* @return boolean True if this is a closing section tag

View File

@ -283,8 +283,32 @@ class HandlebarsTest extends \PHPUnit_Framework_TestCase
'{{#if 0}}ok{{else}}fail{{/if}}',
array(),
'fail'
)
),
array (
' {{~#if 1}}OK {{~else~}} NO {{~/if~}} END',
array(),
'OKEND'
),
array(
'XX {{~#bindAttr data}} XX',
array(),
'XXdata XX'
),
array(
'{{#each data}}{{#if @last}}the last is
{{~this}}{{/if}}{{/each}}',
array('data' => array('one', 'two', 'three')),
'the last isthree'
),
array(
'{{#with data}}
{{~key~}}
{{/with}}',
array('data' => array('key' => 'result')),
'result'
),
);
}