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