From a68318f4c55be035f6d82dbca3211f6dc95ccda7 Mon Sep 17 00:00:00 2001 From: fzerorubigd Date: Fri, 4 Jul 2014 03:36:14 +0430 Subject: [PATCH] Support whitespace deletion Need more test :) fixes #61 --- src/Handlebars/Helpers.php | 1 + src/Handlebars/Parser.php | 3 +-- src/Handlebars/Template.php | 43 ++++++++++++++++++++++++---------- src/Handlebars/Tokenizer.php | 29 +++++++++++++++++++---- tests/Xamin/HandlebarsTest.php | 26 +++++++++++++++++++- 5 files changed, 82 insertions(+), 20 deletions(-) diff --git a/src/Handlebars/Helpers.php b/src/Handlebars/Helpers.php index 5bf62dd..216bcb7 100644 --- a/src/Handlebars/Helpers.php +++ b/src/Handlebars/Helpers.php @@ -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) diff --git a/src/Handlebars/Parser.php b/src/Handlebars/Parser.php index a68c9e1..7b14a9a 100755 --- a/src/Handlebars/Parser.php +++ b/src/Handlebars/Parser.php @@ -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; - } } diff --git a/src/Handlebars/Template.php b/src/Handlebars/Template.php index 687bc6f..2ef9ec9 100644 --- a/src/Handlebars/Template.php +++ b/src/Handlebars/Template.php @@ -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); diff --git a/src/Handlebars/Tokenizer.php b/src/Handlebars/Tokenizer.php index df8fd9e..ffa0bb7 100644 --- a/src/Handlebars/Tokenizer.php +++ b/src/Handlebars/Tokenizer.php @@ -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 diff --git a/tests/Xamin/HandlebarsTest.php b/tests/Xamin/HandlebarsTest.php index 01a822e..81155a0 100644 --- a/tests/Xamin/HandlebarsTest.php +++ b/tests/Xamin/HandlebarsTest.php @@ -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' + ), ); }