diff --git a/src/Mustache/Tokenizer.php b/src/Mustache/Tokenizer.php index bf1e5d1..fa834d9 100644 --- a/src/Mustache/Tokenizer.php +++ b/src/Mustache/Tokenizer.php @@ -84,6 +84,8 @@ class Mustache_Tokenizer /** * Scan and tokenize template source. * + * @throws Mustache_Exception_SyntaxException when mismatched section tags are encountered. + * * @param string $text Mustache template source to tokenize * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null) * @@ -151,7 +153,7 @@ class Mustache_Tokenizer default: if ($this->tagChange($this->ctag, $this->ctagLen, $text, $i)) { - $this->tokens[] = array( + $token = array( self::TYPE => $this->tagType, self::NAME => trim($this->buffer), self::OTAG => $this->otag, @@ -160,20 +162,39 @@ class Mustache_Tokenizer self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen ); - $this->buffer = ''; - $i += $this->ctagLen - 1; - $this->state = self::IN_TEXT; if ($this->tagType === self::T_UNESCAPED) { + // Clean up `{{{ tripleStache }}}` style tokens. if ($this->ctag === '}}') { - $i++; + if (($i+2 < $len) && $text[$i + 2] === '}') { + $i++; + } else { + $msg = sprintf( + 'Uneven closing tag encountered: on line %d', + $token[self::LINE] + ); + + throw new Mustache_Exception_SyntaxException($msg, $token); + } } else { - // Clean up `{{{ tripleStache }}}` style tokens. - $lastName = $this->tokens[count($this->tokens) - 1][self::NAME]; + $lastName = $token[self::NAME]; if (substr($lastName, -1) === '}') { - $this->tokens[count($this->tokens) - 1][self::NAME] = trim(substr($lastName, 0, -1)); + $token[self::NAME] = trim(substr($lastName, 0, -1)); + } else { + $msg = sprintf( + 'Uneven closing tag encountered: %s on line %d', + substr($lastName, -1), + $token[self::LINE] + ); + + throw new Mustache_Exception_SyntaxException($msg, $token); } } } + + $this->buffer = ''; + $i += $this->ctagLen - 1; + $this->state = self::IN_TEXT; + $this->tokens[] = $token; } else { $this->buffer .= $text[$i]; } diff --git a/test/Mustache/Test/TokenizerTest.php b/test/Mustache/Test/TokenizerTest.php index 42df8e1..67df941 100644 --- a/test/Mustache/Test/TokenizerTest.php +++ b/test/Mustache/Test/TokenizerTest.php @@ -24,6 +24,28 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase $this->assertSame($expected, $tokenizer->scan($text, $delimiters)); } + /** + * @expectedException Mustache_Exception_SyntaxException + */ + public function testUnevenBracesThrowExceptions() + { + $tokenizer = new Mustache_Tokenizer; + + $text = "{{{ name }}"; + $tokenizer->scan($text, null); + } + + /** + * @expectedException Mustache_Exception_SyntaxException + */ + public function testUnevenBracesWithCustomDelimiterThrowExceptions() + { + $tokenizer = new Mustache_Tokenizer; + + $text = "<%{ name %>"; + $tokenizer->scan($text, "<% %>"); + } + public function getTokens() { return array( @@ -188,6 +210,69 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase ), ) ), + + // custom delimiters don't swallow the next character, even if it is a }, }}}, or the same delimiter + array( + "<% a %>} <% b %>%> <% c %>}}}", + "<% %>", + array( + array( + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED, + Mustache_Tokenizer::NAME => 'a', + Mustache_Tokenizer::OTAG => '<%', + Mustache_Tokenizer::CTAG => '%>', + Mustache_Tokenizer::LINE => 0, + Mustache_Tokenizer::INDEX => 7, + ), + array( + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT, + Mustache_Tokenizer::LINE => 0, + Mustache_Tokenizer::VALUE => "} ", + ), + array( + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED, + Mustache_Tokenizer::NAME => 'b', + Mustache_Tokenizer::OTAG => '<%', + Mustache_Tokenizer::CTAG => '%>', + Mustache_Tokenizer::LINE => 0, + Mustache_Tokenizer::INDEX => 16, + ), + array( + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT, + Mustache_Tokenizer::LINE => 0, + Mustache_Tokenizer::VALUE => "%> ", + ), + array( + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED, + Mustache_Tokenizer::NAME => 'c', + Mustache_Tokenizer::OTAG => '<%', + Mustache_Tokenizer::CTAG => '%>', + Mustache_Tokenizer::LINE => 0, + Mustache_Tokenizer::INDEX => 26, + ), + array( + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT, + Mustache_Tokenizer::LINE => 0, + Mustache_Tokenizer::VALUE => "}}}", + ), + ) + ), + + // unescaped custom delimiters are properly parsed + array( + "<%{ a }%>", + "<% %>", + array( + array( + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_UNESCAPED, + Mustache_Tokenizer::NAME => 'a', + Mustache_Tokenizer::OTAG => '<%', + Mustache_Tokenizer::CTAG => '%>', + Mustache_Tokenizer::LINE => 0, + Mustache_Tokenizer::INDEX => 9, + ) + ) + ), ); } }