diff --git a/Mustache.php b/Mustache.php index 0656cef..6f6a031 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); @@ -272,6 +263,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. * @@ -389,7 +445,7 @@ class Mustache { $otag = preg_quote($this->_otag, '/'); $ctag = preg_quote($this->_ctag, '/'); - $this->_tagRegEx = '/' . $otag . "([#\^\/=!>\\{&])?(.+?)\\1?" . $ctag . "+/s"; + $this->_tagRegEx = '/' . $otag . "([#\^\/=!<>\\{&])?(.+?)\\1?" . $ctag . "+/s"; $html = ''; $matches = array(); @@ -424,21 +480,6 @@ class Mustache { */ protected function _renderTag($modifier, $tag_name) { switch ($modifier) { - case '#': - case '^': - if ($this->_throwsException(MustacheException::UNCLOSED_SECTION)) { - throw new MustacheException('Unclosed section: ' . $tag_name, MustacheException::UNCLOSED_SECTION); - } else { - return ''; - } - break; - case '/': - if ($this->_throwsException(MustacheException::UNEXPECTED_CLOSE_SECTION)) { - throw new MustacheException('Unexpected close section: ' . $tag_name, MustacheException::UNEXPECTED_CLOSE_SECTION); - } else { - return ''; - } - break; case '=': return $this->_changeDelimiter($tag_name); break; @@ -446,6 +487,7 @@ class Mustache { return $this->_renderComment($tag_name); break; case '>': + case '<': return $this->_renderPartial($tag_name); break; case '{': @@ -456,6 +498,12 @@ class Mustache { return $this->_renderUnescaped($tag_name); } break; + case '#': + case '^': + case '/': + // remove any leftovers from _renderSections + return ''; + break; } if ($this->_hasPragma(self::PRAGMA_UNESCAPED)) { @@ -525,7 +573,7 @@ class Mustache { $otag = preg_quote($this->_otag, '/'); $ctag = preg_quote($this->_ctag, '/'); - $this->_tagRegEx = '/' . $otag . "([#\^\/=!>\\{&])?(.+?)\\1?" . $ctag . "+/s"; + $this->_tagRegEx = '/' . $otag . "([#\^\/=!<>\\{&])?(.+?)\\1?" . $ctag . "+/s"; return ''; } @@ -611,7 +659,7 @@ class Mustache { } else if (isset($view->$tag_name)) { return $view->$tag_name; } - } else if (isset($view[$tag_name])) { + } else if (array_key_exists($tag_name, $view)) { return $view[$tag_name]; } } diff --git a/examples/sections_nested/SectionsNested.php b/examples/sections_nested/SectionsNested.php new file mode 100644 index 0000000..ec01b75 --- /dev/null +++ b/examples/sections_nested/SectionsNested.php @@ -0,0 +1,33 @@ + 'Von Kaiser', + 'enemies' => array( + array('name' => 'Super Macho Man'), + array('name' => 'Piston Honda'), + array('name' => 'Mr. Sandman'), + ) + ), + array( + 'name' => 'Mike Tyson', + 'enemies' => array( + array('name' => 'Soda Popinski'), + array('name' => 'King Hippo'), + array('name' => 'Great Tiger'), + array('name' => 'Glass Joe'), + ) + ), + array( + 'name' => 'Don Flamenco', + 'enemies' => array( + array('name' => 'Bald Bull'), + ) + ), + ); + } +} \ No newline at end of file diff --git a/examples/sections_nested/sections_nested.mustache b/examples/sections_nested/sections_nested.mustache new file mode 100644 index 0000000..9f8007d --- /dev/null +++ b/examples/sections_nested/sections_nested.mustache @@ -0,0 +1,7 @@ +Enemies of {{ name }}: +{{# enemies }} +{{ name }} ... who also has enemies: +{{# enemies }} +--> {{ name }} +{{/ enemies }} +{{/ enemies }} \ No newline at end of file diff --git a/examples/sections_nested/sections_nested.txt b/examples/sections_nested/sections_nested.txt new file mode 100644 index 0000000..72c44d0 --- /dev/null +++ b/examples/sections_nested/sections_nested.txt @@ -0,0 +1,12 @@ +Enemies of Little Mac: +Von Kaiser ... who also has enemies: +--> Super Macho Man +--> Piston Honda +--> Mr. Sandman +Mike Tyson ... who also has enemies: +--> Soda Popinski +--> King Hippo +--> Great Tiger +--> Glass Joe +Don Flamenco ... who also has enemies: +--> Bald Bull diff --git a/test/MustacheExceptionTest.php b/test/MustacheExceptionTest.php index 39a0f5b..98bddce 100644 --- a/test/MustacheExceptionTest.php +++ b/test/MustacheExceptionTest.php @@ -15,6 +15,7 @@ class MustacheExceptionTest extends PHPUnit_Framework_TestCase { } /** + * @group interpolation * @expectedException MustacheException */ public function testThrowsUnknownVariableException() { @@ -22,6 +23,7 @@ class MustacheExceptionTest extends PHPUnit_Framework_TestCase { } /** + * @group sections * @expectedException MustacheException */ public function testThrowsUnclosedSectionException() { @@ -29,6 +31,15 @@ class MustacheExceptionTest extends PHPUnit_Framework_TestCase { } /** + * @group sections + * @expectedException MustacheException + */ + public function testThrowsUnclosedInvertedSectionException() { + $this->pickyMustache->render('{{^unclosed}}'); + } + + /** + * @group sections * @expectedException MustacheException */ public function testThrowsUnexpectedCloseSectionException() { @@ -36,6 +47,7 @@ class MustacheExceptionTest extends PHPUnit_Framework_TestCase { } /** + * @group partials * @expectedException MustacheException */ public function testThrowsUnknownPartialException() { @@ -43,25 +55,36 @@ class MustacheExceptionTest extends PHPUnit_Framework_TestCase { } /** + * @group pragmas * @expectedException MustacheException */ public function testThrowsUnknownPragmaException() { $this->pickyMustache->render('{{%SWEET-MUSTACHE-BRO}}'); } + /** + * @group sections + */ public function testDoesntThrowUnclosedSectionException() { $this->assertEquals('', $this->slackerMustache->render('{{#unclosed}}')); } + /** + * @group sections + */ public function testDoesntThrowUnexpectedCloseSectionException() { $this->assertEquals('', $this->slackerMustache->render('{{/unopened}}')); } + /** + * @group partials + */ public function testDoesntThrowUnknownPartialException() { $this->assertEquals('', $this->slackerMustache->render('{{>impartial}}')); } /** + * @group pragmas * @expectedException MustacheException */ public function testGetPragmaOptionsThrowsExceptionsIfItThinksYouHaveAPragmaButItTurnsOutYouDont() { diff --git a/test/MustacheObjectSectionTest.php b/test/MustacheObjectSectionTest.php index 17ad2e0..82405d6 100644 --- a/test/MustacheObjectSectionTest.php +++ b/test/MustacheObjectSectionTest.php @@ -2,6 +2,9 @@ require_once '../Mustache.php'; +/** + * @group sections + */ class MustacheObjectSectionTest extends PHPUnit_Framework_TestCase { public function testBasicObject() { $alpha = new Alpha(); diff --git a/test/MustachePragmaDotNotationTest.php b/test/MustachePragmaDotNotationTest.php index 488962b..56f6f11 100644 --- a/test/MustachePragmaDotNotationTest.php +++ b/test/MustachePragmaDotNotationTest.php @@ -2,6 +2,9 @@ require_once '../Mustache.php'; +/** + * @group pragmas + */ class MustachePragmaDotNotationTest extends PHPUnit_Framework_TestCase { public function testDotTraversal() { diff --git a/test/MustachePragmaImplicitIteratorTest.php b/test/MustachePragmaImplicitIteratorTest.php index 207a8c6..9a88359 100644 --- a/test/MustachePragmaImplicitIteratorTest.php +++ b/test/MustachePragmaImplicitIteratorTest.php @@ -2,6 +2,9 @@ require_once '../Mustache.php'; +/** + * @group pragmas + */ class MustachePragmaImplicitIteratorTest extends PHPUnit_Framework_TestCase { public function testEnablePragma() { @@ -47,4 +50,34 @@ class MustachePragmaImplicitIteratorTest extends PHPUnit_Framework_TestCase { $this->assertEquals('foobarbaz', $m->render('{{%IMPLICIT-ITERATOR iterator=i}}{{%DOT-NOTATION}}{{#items}}{{i.name}}{{/items}}')); } + /** + * @dataProvider recursiveSectionData + */ + public function testRecursiveSections($template, $view, $result) { + $m = new Mustache(); + $this->assertEquals($result, $m->render($template, $view)); + } + + public function recursiveSectionData() { + return array( + array( + '{{%IMPLICIT-ITERATOR}}{{#items}}{{#.}}{{.}}{{/.}}{{/items}}', + array('items' => array(array('a', 'b', 'c'), array('d', 'e', 'f'))), + 'abcdef' + ), + array( + '{{%IMPLICIT-ITERATOR}}{{#items}}{{#.}}{{#.}}{{.}}{{/.}}{{/.}}{{/items}}', + array('items' => array(array(array('a', 'b'), array('c')), array(array('d'), array('e', 'f')))), + 'abcdef' + ), + array( + '{{%IMPLICIT-ITERATOR}}{{#items}}{{#.}}{{#items}}{{.}}{{/items}}{{/.}}{{/items}}', + array('items' => array( + array('items' => array('a', 'b', 'c')), + array('items' => array('d', 'e', 'f')), + )), + 'abcdef' + ), + ); + } } \ No newline at end of file diff --git a/test/MustachePragmaTest.php b/test/MustachePragmaTest.php index 086e458..8be6e66 100644 --- a/test/MustachePragmaTest.php +++ b/test/MustachePragmaTest.php @@ -2,6 +2,9 @@ require_once '../Mustache.php'; +/** + * @group pragmas + */ class MustachePragmaTest extends PHPUnit_Framework_TestCase { public function testUnknownPragmaException() { diff --git a/test/MustachePragmaUnescapedTest.php b/test/MustachePragmaUnescapedTest.php index 8331312..df6d94c 100644 --- a/test/MustachePragmaUnescapedTest.php +++ b/test/MustachePragmaUnescapedTest.php @@ -2,6 +2,9 @@ require_once '../Mustache.php'; +/** + * @group pragmas + */ class MustachePragmaUnescapedTest extends PHPUnit_Framework_TestCase { public function testPragmaUnescaped() { diff --git a/test/MustacheTest.php b/test/MustacheTest.php index 7269b36..69a2029 100644 --- a/test/MustacheTest.php +++ b/test/MustacheTest.php @@ -132,8 +132,7 @@ class MustacheTest extends PHPUnit_Framework_TestCase { /** * Test render() with data. * - * @access public - * @return void + * @group interpolation */ public function testRenderWithData() { $m = new Mustache('{{first_name}} {{last_name}}'); @@ -141,6 +140,9 @@ class MustacheTest extends PHPUnit_Framework_TestCase { $this->assertEquals('Zappa, Frank', $m->render('{{last_name}}, {{first_name}}', array('first_name' => 'Frank', 'last_name' => 'Zappa'))); } + /** + * @group partials + */ public function testRenderWithPartials() { $m = new Mustache('{{>stache}}', null, array('stache' => '{{first_name}} {{last_name}}')); $this->assertEquals('Charlie Chaplin', $m->render(null, array('first_name' => 'Charlie', 'last_name' => 'Chaplin'))); @@ -150,8 +152,7 @@ class MustacheTest extends PHPUnit_Framework_TestCase { /** * Mustache should allow newlines (and other whitespace) in comments and all other tags. * - * @access public - * @return void + * @group comments */ public function testNewlinesInComments() { $m = new Mustache("{{! comment \n \t still a comment... }}"); @@ -160,9 +161,6 @@ class MustacheTest extends PHPUnit_Framework_TestCase { /** * Mustache should return the same thing when invoked multiple times. - * - * @access public - * @return void */ public function testMultipleInvocations() { $m = new Mustache('x'); @@ -176,8 +174,7 @@ class MustacheTest extends PHPUnit_Framework_TestCase { /** * Mustache should return the same thing when invoked multiple times. * - * @access public - * @return void + * @group interpolation */ public function testMultipleInvocationsWithTags() { $m = new Mustache('{{one}} {{two}}', array('one' => 'foo', 'two' => 'bar')); @@ -190,9 +187,6 @@ class MustacheTest extends PHPUnit_Framework_TestCase { /** * Mustache should not use templates passed to the render() method for subsequent invocations. - * - * @access public - * @return void */ public function testResetTemplateForMultipleInvocations() { $m = new Mustache('Sirve.'); @@ -205,15 +199,14 @@ class MustacheTest extends PHPUnit_Framework_TestCase { } /** - * testClone function. + * Test the __clone() magic function. * * @group examples * @dataProvider getExamples - * @access public + * * @param string $class * @param string $template * @param string $output - * @return void */ public function test__clone($class, $template, $output) { if (isset($this->knownIssues[$class])) { @@ -240,11 +233,10 @@ class MustacheTest extends PHPUnit_Framework_TestCase { * * @group examples * @dataProvider getExamples - * @access public + * * @param string $class * @param string $template * @param string $output - * @return void */ public function testExamples($class, $template, $output) { if (isset($this->knownIssues[$class])) { @@ -266,7 +258,6 @@ class MustacheTest extends PHPUnit_Framework_TestCase { * This whole mess will be refined later to be more intuitive and less prescriptive, but it'll * do for now. Especially since it means we can have unit tests :) * - * @access public * @return array */ public function getExamples() { @@ -317,6 +308,9 @@ class MustacheTest extends PHPUnit_Framework_TestCase { return $ret; } + /** + * @group delimiters + */ public function testCrazyDelimiters() { $m = new Mustache(null, array('result' => 'success')); $this->assertEquals('success', $m->render('{{=[[ ]]=}}[[ result ]]')); @@ -327,10 +321,33 @@ class MustacheTest extends PHPUnit_Framework_TestCase { $this->assertEquals('success', $m->render('{{=// \\\\}}// result \\\\')); } + /** + * @group delimiters + */ public function testResetDelimiters() { $m = new Mustache(null, array('result' => 'success')); $this->assertEquals('success', $m->render('{{=[[ ]]=}}[[ result ]]')); $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 diff --git a/test/phpunit.xml b/test/phpunit.xml index c70e6b0..891fa8e 100644 --- a/test/phpunit.xml +++ b/test/phpunit.xml @@ -3,4 +3,8 @@ ./ + + ../examples + ./ + \ No newline at end of file