diff --git a/Mustache.php b/Mustache.php index 6f57a25..514b597 100644 --- a/Mustache.php +++ b/Mustache.php @@ -14,6 +14,9 @@ */ class Mustache { + const VERSION = '0.7.1'; + const SPEC_VERSION = '1.1.2'; + /** * Should this Mustache throw exceptions when it finds unexpected tags? * @@ -176,6 +179,9 @@ class Mustache { if ($template === null) $template = $this->_template; if ($partials !== null) $this->_partials = $partials; + $otag_orig = $this->_otag; + $ctag_orig = $this->_ctag; + if ($view) { $this->_context = array($view); } else if (empty($this->_context)) { @@ -183,7 +189,12 @@ class Mustache { } $template = $this->_renderPragmas($template); - return $this->_renderTemplate($template, $this->_context); + $template = $this->_renderTemplate($template, $this->_context); + + $this->_otag = $otag_orig; + $this->_ctag = $ctag_orig; + + return $template; } /** @@ -211,28 +222,18 @@ class Mustache { * @return string Rendered Mustache template. */ protected function _renderTemplate($template) { - $template = $this->_renderSections($template); - return $this->_renderTags($template); - } + if ($section = $this->_findSection($template)) { + list($before, $type, $tag_name, $content, $after) = $section; - /** - * Render boolean, enumerable and inverted sections. - * - * @access protected - * @param string $template - * @return string - */ - protected function _renderSections($template) { - while ($section_data = $this->_findSection($template)) { - list($section, $offset, $type, $tag_name, $content) = $section_data; + $rendered_before = $this->_renderTags($before); - $replace = ''; + $rendered_content = ''; $val = $this->_getVariable($tag_name); switch($type) { // inverted section case '^': if (empty($val)) { - $replace .= $content; + $rendered_content = $this->_renderTemplate($content); } break; @@ -240,30 +241,29 @@ class Mustache { case '#': // higher order sections if ($this->_varIsCallable($val)) { - $content = call_user_func($val, $content); - $replace .= $this->_renderTemplate($content); + $rendered_content = $this->_renderTemplate(call_user_func($val, $content)); } else if ($this->_varIsIterable($val)) { foreach ($val as $local_context) { $this->_pushContext($local_context); - $replace .= $this->_renderTemplate($content); + $rendered_content .= $this->_renderTemplate($content); $this->_popContext(); } } else if ($val) { if (is_array($val) || is_object($val)) { $this->_pushContext($val); - $replace .= $this->_renderTemplate($content); + $rendered_content = $this->_renderTemplate($content); $this->_popContext(); } else { - $replace .= $content; + $rendered_content = $this->_renderTemplate($content); } } break; } - $template = substr_replace($template, $replace, $offset, strlen($section)); + return $rendered_before . $rendered_content . $this->_renderTemplate($after); } - return $template; + return $this->_renderTags($template); } /** @@ -276,7 +276,7 @@ class Mustache { */ protected function _prepareSectionRegEx($otag, $ctag) { return sprintf( - '/(?:(?<=\\n)[ \\t]*)?%s(?P[%s])(?P.+?)%s\\n?/s', + '/(?:(?<=\\n)[ \\t]*)?%s(?:(?P[%s])(?P.+?)|=(?P.*?)=)%s\\n?/s', preg_quote($otag, '/'), self::SECTION_TYPES, preg_quote($ctag, '/') @@ -284,13 +284,11 @@ class Mustache { } /** - * Extract a section from $template. - * - * This is a helper function to find sections needed by _renderSections. + * Extract the first section from $template. * * @access protected * @param string $template - * @return array $section, $offset, $type, $tag_name and $content + * @return array $before, $type, $tag_name, $content and $after */ protected function _findSection($template) { $regEx = $this->_prepareSectionRegEx($this->_otag, $this->_ctag); @@ -304,6 +302,12 @@ class Mustache { $section_stack = array(); $matches = array(); while (preg_match($regEx, $template, $matches, PREG_OFFSET_CAPTURE, $search_offset)) { + if (isset($matches['delims'][0])) { + list($otag, $ctag) = explode(' ', $matches['delims'][0]); + $regEx = $this->_prepareSectionRegEx($otag, $ctag); + $search_offset = $matches[0][1] + strlen($matches[0][0]); + continue; + } $match = $matches[0][0]; $offset = $matches[0][1]; @@ -330,10 +334,14 @@ class Mustache { } 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); + // $before, $type, $tag_name, $content, $after + return array( + substr($template, 0, $section_start), + $section_type, + $tag_name, + substr($template, $content_start, $offset - $content_start), + substr($template, $search_offset), + ); } break; } @@ -489,9 +497,6 @@ class Mustache { return $template; } - $otag_orig = $this->_otag; - $ctag_orig = $this->_ctag; - $first = true; $this->_tagRegEx = $this->_prepareTagRegEx($this->_otag, $this->_ctag, true); @@ -531,9 +536,6 @@ class Mustache { } } - $this->_otag = $otag_orig; - $this->_ctag = $ctag_orig; - return $html . $template; } @@ -578,7 +580,7 @@ class Mustache { case '#': case '^': case '/': - // remove any leftovers from _renderSections + // remove any leftover section tags return $leading . $trailing; break; default: diff --git a/README.markdown b/README.markdown index 52ee11f..830df90 100644 --- a/README.markdown +++ b/README.markdown @@ -81,15 +81,12 @@ And render it: Known Issues ------------ - * As of v1.1.2, there are a couple of whitespace bugs around section tags. - * Things get weird when you change delimiters inside a section -- `delimiters` example currently fails with an - "unclosed section" exception. - * The current spec test exposes several whitespace bugs (which are mostly instances of the exact same whitespace - bug) ... Despite these failing tests, this version is actually *closer* to correct than previous releases. + * As of Mustache spec v1.1.2, there are a couple of whitespace bugs around section tags... Despite these failing tests, this + version is actually *closer* to correct than previous releases. See Also -------- * [Readme for the Ruby Mustache implementation](http://github.com/defunkt/mustache/blob/master/README.md). - * [mustache(1)](http://defunkt.github.com/mustache/mustache.1.html) and [mustache(5)](http://defunkt.github.com/mustache/mustache.5.html) man pages. + * [mustache(1)](http://mustache.github.com/mustache.1.html) and [mustache(5)](http://mustache.github.com/mustache.5.html) man pages. diff --git a/test/MustacheTest.php b/test/MustacheTest.php index 6bb19d7..1a06b46 100644 --- a/test/MustacheTest.php +++ b/test/MustacheTest.php @@ -36,7 +36,7 @@ class MustacheTest extends PHPUnit_Framework_TestCase { const TEST_CLASS = 'Mustache'; protected $knownIssues = array( - 'Delimiters' => "Known issue: sections don't respect delimiter changes", + // Just the whitespace ones... ); /** @@ -195,6 +195,30 @@ class MustacheTest extends PHPUnit_Framework_TestCase { $this->assertEquals('Charlie Chaplin', $m->render(null, array('first_name' => 'Charlie', 'last_name' => 'Chaplin'))); $this->assertEquals('Zappa, Frank', $m->render('{{last_name}}, {{first_name}}', array('first_name' => 'Frank', 'last_name' => 'Zappa'))); } + + /** + * @group interpolation + * @dataProvider interpolationData + */ + public function testDoubleRenderMustacheTags($template, $context, $expected) { + $m = new Mustache($template, $context); + $this->assertEquals($expected, $m->render()); + } + + public function interpolationData() { + return array( + array( + '{{#a}}{{=<% %>=}}{{b}} c<%={{ }}=%>{{/a}}', + array('a' => array(array('b' => 'Do Not Render'))), + '{{b}} c' + ), + array( + '{{#a}}{{b}}{{/a}}', + array('a' => array('b' => '{{c}}'), 'c' => 'FAIL'), + '{{c}}' + ), + ); + } /** * Mustache should allow newlines (and other whitespace) in comments and all other tags. @@ -378,6 +402,17 @@ class MustacheTest extends PHPUnit_Framework_TestCase { $this->assertEquals('success', $m->render('{{=<% %>=}}<% result %>')); } + /** + * @group delimiters + */ + public function testStickyDelimiters() { + $m = new Mustache(null, array('result' => 'FAIL')); + $this->assertEquals('{{ result }}', $m->render('{{=[[ ]]=}}{{ result }}[[={{ }}=]]')); + $this->assertEquals('{{#result}}{{/result}}', $m->render('{{=[[ ]]=}}{{#result}}{{/result}}[[={{ }}=]]')); + $this->assertEquals('{{ result }}', $m->render('{{=[[ ]]=}}[[#result]]{{ result }}[[/result]][[={{ }}=]]')); + $this->assertEquals('{{ result }}', $m->render('{{#result}}{{=[[ ]]=}}{{ result }}[[/result]][[^result]][[={{ }}=]][[ result ]]{{/result}}')); + } + /** * @group sections * @dataProvider poorlyNestedSections @@ -426,4 +461,4 @@ class MustacheExposedOptionsStub extends Mustache { public function getDelimiters() { return array($this->_otag, $this->_ctag); } -} \ No newline at end of file +}