diff --git a/Mustache.php b/Mustache.php index 4fded45..a41502b 100644 --- a/Mustache.php +++ b/Mustache.php @@ -191,7 +191,7 @@ class Mustache { * @return string Rendered Mustache template. */ protected function _renderTemplate($template) { - $template = $this->_renderSection($template); + $template = $this->_renderSections($template); return $this->_renderTags($template); } @@ -202,18 +202,9 @@ class Mustache { * @param string $template * @return string */ - protected function _renderSection($template) { - $otag = preg_quote($this->_otag, '/'); - $ctag = preg_quote($this->_ctag, '/'); - $regex = '/' . $otag . '(\\^|\\#)\\s*(.+?)\\s*' . $ctag . '\\s*([\\s\\S]+?)' . $otag . '\\/\\s*\\2\\s*' . $ctag . '\\s*/ms'; - - $matches = array(); - while (preg_match($regex, $template, $matches, PREG_OFFSET_CAPTURE)) { - $section = $matches[0][0]; - $offset = $matches[0][1]; - $type = $matches[1][0]; - $tag_name = trim($matches[2][0]); - $content = $matches[3][0]; + protected function _renderSections($template) { + while ($section_data = $this->_findSection($template)) { + list($section, $offset, $type, $tag_name, $content) = $section_data; $replace = ''; $val = $this->_getVariable($tag_name); @@ -267,6 +258,71 @@ class Mustache { return $template; } + /** + * Extract a section from $template. + * + * This is a helper function to find sections needed by _renderSections. + * + * @access protected + * @param string $template + * @return array $section, $offset, $type, $tag_name and $content + */ + protected function _findSection($template) { + $otag = preg_quote($this->_otag, '/'); + $ctag = preg_quote($this->_ctag, '/'); + $regex = '/' . $otag . '([\\^\\#\\/])\\s*(.+?)\\s*' . $ctag . '\\s*/ms'; + + $section_start = null; + $section_type = null; + $content_start = null; + + $search_offset = 0; + + $section_stack = array(); + $matches = array(); + while (preg_match($regex, $template, $matches, PREG_OFFSET_CAPTURE, $search_offset)) { + + $match = $matches[0][0]; + $offset = $matches[0][1]; + $type = $matches[1][0]; + $tag_name = trim($matches[2][0]); + + $search_offset = $offset + strlen($match); + + switch ($type) { + case '^': + case '#': + if (empty($section_stack)) { + $section_start = $offset; + $section_type = $type; + $content_start = $search_offset; + } + array_push($section_stack, $tag_name); + break; + case '/': + if (empty($section_stack) || ($tag_name !== array_pop($section_stack))) { + if ($this->_throwsException(MustacheException::UNEXPECTED_CLOSE_SECTION)) { + throw new MustacheException('Unexpected close section: ' . $tag_name, MustacheException::UNEXPECTED_CLOSE_SECTION); + } + } + + if (empty($section_stack)) { + $section = substr($template, $section_start, $search_offset - $section_start); + $content = substr($template, $content_start, $offset - $content_start); + + return array($section, $section_start, $section_type, $tag_name, $content); + } + break; + } + } + + if (!empty($section_stack)) { + if ($this->_throwsException(MustacheException::UNCLOSED_SECTION)) { + throw new MustacheException('Unclosed section: ' . $section_stack[0], MustacheException::UNCLOSED_SECTION); + } + } + } + /** * Initialize pragmas and remove all pragma tags. * @@ -353,7 +409,6 @@ class Mustache { return (is_array($this->_localPragmas[$pragma_name])) ? $this->_localPragmas[$pragma_name] : array(); } - /** * Check whether this Mustache instance throws a given exception. * diff --git a/test/MustacheTest.php b/test/MustacheTest.php index fba0327..69a2029 100644 --- a/test/MustacheTest.php +++ b/test/MustacheTest.php @@ -330,4 +330,24 @@ class MustacheTest extends PHPUnit_Framework_TestCase { $this->assertEquals('success', $m->render('{{=<< >>=}}<< result >>')); $this->assertEquals('success', $m->render('{{=<% %>=}}<% result %>')); } + + /** + * @group sections + * @dataProvider poorlyNestedSections + * @expectedException MustacheException + */ + public function testPoorlyNestedSections($template) { + $m = new Mustache($template); + $m->render(); + } + + public function poorlyNestedSections() { + return array( + array('{{#foo}}'), + array('{{#foo}}{{/bar}}'), + array('{{#foo}}{{#bar}}{{/foo}}'), + array('{{#foo}}{{#bar}}{{/foo}}{{/bar}}'), + array('{{#foo}}{{/bar}}{{/foo}}'), + ); + } } \ No newline at end of file