From 31ba792e33110bbd018177d38ed769b22c142753 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Tue, 26 Apr 2011 18:55:05 -0400 Subject: [PATCH 01/11] update spec to 1.1.2 --- test/spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec b/test/spec index 3383fa6..bf6288e 160000 --- a/test/spec +++ b/test/spec @@ -1 +1 @@ -Subproject commit 3383fa66e808a07fde1c291aa16a588d0a1a2a6d +Subproject commit bf6288ed6bd0ce8ccea6f1dac070b3d779132c3b From 60e7fbbb34484f4f1ba1dd53f7874409ead8217b Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Tue, 26 Apr 2011 18:56:03 -0400 Subject: [PATCH 02/11] Fix whitespace bugs in partials, interpolation and comments. --- Mustache.php | 124 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 93 insertions(+), 31 deletions(-) diff --git a/Mustache.php b/Mustache.php index 4455b1e..4c12603 100644 --- a/Mustache.php +++ b/Mustache.php @@ -463,9 +463,10 @@ class Mustache { * @param string $ctag * @return string */ - protected function _prepareTagRegEx($otag, $ctag) { + protected function _prepareTagRegEx($otag, $ctag, $first = false) { return sprintf( - '/(?P(?<=\\n)[ \\t]*)?%s(?P[%s]?)(?P.+?)(?:\\2|})?%s(?:\\s*(?=\\n))?/s', + '/(?P(?:%s\\r?\\n)[ \\t]*)?%s(?P[%s]?)(?P.+?)(?:\\2|})?%s(?P\\s*(?:\\r?\\n|\\Z))?/s', + ($first ? '\\A|' : ''), preg_quote($otag, '/'), self::TAG_TYPES, preg_quote($ctag, '/') @@ -487,7 +488,8 @@ class Mustache { $otag_orig = $this->_otag; $ctag_orig = $this->_ctag; - $this->_tagRegEx = $this->_prepareTagRegEx($this->_otag, $this->_ctag); + $first = true; + $this->_tagRegEx = $this->_prepareTagRegEx($this->_otag, $this->_ctag, true); $html = ''; $matches = array(); @@ -497,10 +499,16 @@ class Mustache { $modifier = $matches['type'][0]; $tag_name = trim($matches['tag_name'][0]); - if (isset($matches['whitespace']) && $matches['whitespace'][1] > -1) { - $whitespace = $matches['whitespace'][0]; + if (isset($matches['leading']) && $matches['leading'][1] > -1) { + $leading = $matches['leading'][0]; } else { - $whitespace = null; + $leading = null; + } + + if (isset($matches['trailing']) && $matches['trailing'][1] > -1) { + $trailing = $matches['trailing'][0]; + } else { + $trailing = null; } $html .= substr($template, 0, $offset); @@ -511,7 +519,12 @@ class Mustache { } $template = substr($template, $next_offset); - $html .= $this->_renderTag($modifier, $tag_name, $whitespace); + $html .= $this->_renderTag($modifier, $tag_name, $leading, $trailing); + + if ($first == true) { + $first = false; + $this->_tagRegEx = $this->_prepareTagRegEx($this->_otag, $this->_ctag); + } } $this->_otag = $otag_orig; @@ -529,20 +542,22 @@ class Mustache { * @access protected * @param string $modifier * @param string $tag_name + * @param string $leading Whitespace + * @param string $trailing Whitespace * @throws MustacheException Unmatched section tag encountered. * @return string */ - protected function _renderTag($modifier, $tag_name, $whitespace) { + protected function _renderTag($modifier, $tag_name, $leading, $trailing) { switch ($modifier) { case '=': - return $this->_changeDelimiter($tag_name); + return $this->_changeDelimiter($tag_name, $leading, $trailing); break; case '!': - return $this->_renderComment($tag_name); + return $this->_renderComment($tag_name, $leading, $trailing); break; case '>': case '<': - return $this->_renderPartial($tag_name, $whitespace); + return $this->_renderPartial($tag_name, $leading, $trailing); break; case '{': // strip the trailing } ... @@ -551,24 +566,41 @@ class Mustache { } case '&': if ($this->_hasPragma(self::PRAGMA_UNESCAPED)) { - return $this->_renderEscaped($tag_name); + return $this->_renderEscaped($tag_name, $leading, $trailing); } else { - return $this->_renderUnescaped($tag_name); + return $this->_renderUnescaped($tag_name, $leading, $trailing); } break; case '#': case '^': case '/': // remove any leftovers from _renderSections - return ''; + return $leading . $trailing; + break; + default: + if ($this->_hasPragma(self::PRAGMA_UNESCAPED)) { + return $this->_renderUnescaped($modifier . $tag_name, $leading, $trailing); + } else { + return $this->_renderEscaped($modifier . $tag_name, $leading, $trailing); + } break; } + } - if ($this->_hasPragma(self::PRAGMA_UNESCAPED)) { - return $this->_renderUnescaped($modifier . $tag_name); - } else { - return $this->_renderEscaped($modifier . $tag_name); + /** + * Returns true if any of its args contains the "\r" character. + * + * @access protected + * @param string $str + * @return boolean + */ + protected function _stringHasR($str) { + foreach (func_get_args() as $arg) { + if (strpos($arg, "\r") !== false) { + return true; + } } + return false; } /** @@ -576,10 +608,12 @@ class Mustache { * * @access protected * @param string $tag_name + * @param string $leading Whitespace + * @param string $trailing Whitespace * @return string */ - protected function _renderEscaped($tag_name) { - return htmlentities($this->_getVariable($tag_name), ENT_COMPAT, $this->_charset); + protected function _renderEscaped($tag_name, $leading, $trailing) { + return $leading . htmlentities($this->_getVariable($tag_name), ENT_COMPAT, $this->_charset) . $trailing; } /** @@ -587,10 +621,18 @@ class Mustache { * * @access protected * @param string $tag_name + * @param string $leading Whitespace + * @param string $trailing Whitespace * @return string */ - protected function _renderComment($tag_name) { - return ''; + protected function _renderComment($tag_name, $leading, $trailing) { + if ($leading !== null && $trailing !== null) { + if (strpos($leading, "\n") === false) { + return ''; + } + return $this->_stringHasR($leading, $trailing) ? "\r\n" : "\n"; + } + return $leading . $trailing; } /** @@ -598,10 +640,12 @@ class Mustache { * * @access protected * @param string $tag_name + * @param string $leading Whitespace + * @param string $trailing Whitespace * @return string */ - protected function _renderUnescaped($tag_name) { - return $this->_getVariable($tag_name); + protected function _renderUnescaped($tag_name, $leading, $trailing) { + return $leading . $this->_getVariable($tag_name) . $trailing; } /** @@ -609,14 +653,24 @@ class Mustache { * * @access protected * @param string $tag_name + * @param string $leading Whitespace + * @param string $trailing Whitespace * @return string */ - protected function _renderPartial($tag_name, $whitespace = '') { + protected function _renderPartial($tag_name, $leading, $trailing) { + $partial = $this->_getPartial($tag_name); + if ($leading !== null && $trailing !== null) { + $whitespace = trim($leading, "\r\n"); + $partial = preg_replace('/(\\r?\\n)(?!$)/s', "\\1" . $whitespace, $partial); + } + $view = clone($this); - - $partial = $whitespace . preg_replace('/\n(?!$)/s', "\n" . $whitespace, $this->_getPartial($tag_name)); - - return $view->render($partial); + + if ($leading !== null && $trailing !== null) { + return $leading . $view->render($partial); + } else { + return $leading . $view->render($partial) . $trailing; + } } /** @@ -625,16 +679,24 @@ class Mustache { * * @access protected * @param string $tag_name + * @param string $leading Whitespace + * @param string $trailing Whitespace * @return string */ - protected function _changeDelimiter($tag_name) { + protected function _changeDelimiter($tag_name, $leading, $trailing) { list($otag, $ctag) = explode(' ', $tag_name); $this->_otag = $otag; $this->_ctag = $ctag; $this->_tagRegEx = $this->_prepareTagRegEx($this->_otag, $this->_ctag); - return ''; + if ($leading !== null && $trailing !== null) { + if (strpos($leading, "\n") === false) { + return ''; + } + return $this->_stringHasR($leading, $trailing) ? "\r\n" : "\n"; + } + return $leading . $trailing; } /** From 9c73e5bb4f3f4220a20917df906777ca7d6e2c58 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Tue, 26 Apr 2011 18:59:22 -0400 Subject: [PATCH 03/11] Add whitespace bug to known issues. --- README.markdown | 1 + 1 file changed, 1 insertion(+) diff --git a/README.markdown b/README.markdown index 3662d3c..d49f2fd 100644 --- a/README.markdown +++ b/README.markdown @@ -81,6 +81,7 @@ 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. From 7d3f312b4c9c39e189c22b8ef22057cf8583052b Mon Sep 17 00:00:00 2001 From: KevBurnsJr Date: Sun, 12 Jun 2011 17:20:03 -0700 Subject: [PATCH 04/11] Adding test for delimiter switching in partials --- test/MustacheTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/MustacheTest.php b/test/MustacheTest.php index 21714a7..3bbb0e3 100644 --- a/test/MustacheTest.php +++ b/test/MustacheTest.php @@ -147,6 +147,15 @@ 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 partials + */ + public function testRenderDelimitersInPartials() { + $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'))); + $this->assertEquals('{{first_name}} {{last_name}}', $m->render('{{last_name}}, {{first_name}}', array('first_name' => 'Frank', 'last_name' => 'Zappa'))); + } /** * Mustache should allow newlines (and other whitespace) in comments and all other tags. From c2f1201c5c5fdce2d3d405d46e77635813e9e479 Mon Sep 17 00:00:00 2001 From: KevBurnsJr Date: Sun, 12 Jun 2011 17:27:05 -0700 Subject: [PATCH 05/11] Oops, fixing test. --- test/MustacheTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/MustacheTest.php b/test/MustacheTest.php index 3bbb0e3..a1fdded 100644 --- a/test/MustacheTest.php +++ b/test/MustacheTest.php @@ -153,7 +153,7 @@ class MustacheTest extends PHPUnit_Framework_TestCase { */ public function testRenderDelimitersInPartials() { $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'))); + $this->assertEquals('{{first_name}} {{last_name}}', $m->render(null, array('first_name' => 'Charlie', 'last_name' => 'Chaplin'))); $this->assertEquals('{{first_name}} {{last_name}}', $m->render('{{last_name}}, {{first_name}}', array('first_name' => 'Frank', 'last_name' => 'Zappa'))); } From 1acae92de2c9d18cbc68c51e68cad31143734ff2 Mon Sep 17 00:00:00 2001 From: KevBurnsJr Date: Sun, 12 Jun 2011 20:26:38 -0700 Subject: [PATCH 06/11] Creating new Test to prove error --- test/MustacheTest.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/MustacheTest.php b/test/MustacheTest.php index a1fdded..ad81c0c 100644 --- a/test/MustacheTest.php +++ b/test/MustacheTest.php @@ -151,10 +151,13 @@ class MustacheTest extends PHPUnit_Framework_TestCase { /** * @group partials */ - public function testRenderDelimitersInPartials() { - $m = new Mustache('{{>stache}}', null, array('stache' => '{{=<% %>=}}{{first_name}} {{last_name}}<%={{ }}=%>')); - $this->assertEquals('{{first_name}} {{last_name}}', $m->render(null, array('first_name' => 'Charlie', 'last_name' => 'Chaplin'))); - $this->assertEquals('{{first_name}} {{last_name}}', $m->render('{{last_name}}, {{first_name}}', array('first_name' => 'Frank', 'last_name' => 'Zappa'))); + public function testRenderDelimitersInSections() { + $m = new Mustache('{{#a}}{{=<% %>=}}{{b}} c<%={{ }}=%>{{/a}}'); + $this->assertEquals('{{b}} c', $m->render(null, array( + 'a' => array( + array('b' => 'Do Not Render') + ) + ))); } /** From 61a0b7f2f2b83d79f3eb2da851eb9abb6d1b67b0 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 12 Jun 2011 23:57:29 -0400 Subject: [PATCH 07/11] Provide another interpolation failure test case. --- test/MustacheTest.php | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/test/MustacheTest.php b/test/MustacheTest.php index 41f5b16..70622b4 100644 --- a/test/MustacheTest.php +++ b/test/MustacheTest.php @@ -197,15 +197,27 @@ class MustacheTest extends PHPUnit_Framework_TestCase { } /** - * @group partials + * @group interpolation + * @dataProvider interpolationData */ - public function testRenderDelimitersInSections() { - $m = new Mustache('{{#a}}{{=<% %>=}}{{b}} c<%={{ }}=%>{{/a}}'); - $this->assertEquals('{{b}} c', $m->render(null, array( - 'a' => array( - array('b' => 'Do Not Render') - ) - ))); + 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}}' + ), + ); } /** @@ -438,4 +450,4 @@ class MustacheExposedOptionsStub extends Mustache { public function getDelimiters() { return array($this->_otag, $this->_ctag); } -} \ No newline at end of file +} From 9af6b2033edf8a494fd5556b06fd3741688b4fd8 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 13 Jun 2011 01:05:48 -0400 Subject: [PATCH 08/11] Work in progress. Fixing double-rendering of sections, mustache injection, etc. --- Mustache.php | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/Mustache.php b/Mustache.php index 4c12603..57ea064 100644 --- a/Mustache.php +++ b/Mustache.php @@ -211,20 +211,10 @@ class Mustache { * @return string Rendered Mustache template. */ protected function _renderTemplate($template) { - $template = $this->_renderSections($template); - return $this->_renderTags($template); - } - - /** - * 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; + if ($section = $this->_findSection($template)) { + list($section, $offset, $type, $tag_name, $content) = $section; + $before = substr($template, 0, $offset); + $after = substr($template, $offset + strlen($section)); $replace = ''; $val = $this->_getVariable($tag_name); @@ -256,10 +246,10 @@ class Mustache { break; } - $template = substr_replace($template, $replace, $offset, strlen($section)); + return $this->_renderTags($before) . $replace . $this->_renderTemplate($after); } - return $template; + return $this->_renderTags($template); } /** @@ -856,4 +846,4 @@ class MustacheException extends Exception { // which can't be handled by this Mustache instance. const UNKNOWN_PRAGMA = 4; -} \ No newline at end of file +} From f95f8c09a89809bd61a76ec59d01285fed0b801d Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 13 Jun 2011 01:23:19 -0400 Subject: [PATCH 09/11] More efficient return from _findSection. --- Mustache.php | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Mustache.php b/Mustache.php index 57ea064..6df106d 100644 --- a/Mustache.php +++ b/Mustache.php @@ -212,9 +212,7 @@ class Mustache { */ protected function _renderTemplate($template) { if ($section = $this->_findSection($template)) { - list($section, $offset, $type, $tag_name, $content) = $section; - $before = substr($template, 0, $offset); - $after = substr($template, $offset + strlen($section)); + list($before, $type, $tag_name, $content, $after) = $section; $replace = ''; $val = $this->_getVariable($tag_name); @@ -276,7 +274,7 @@ class Mustache { * * @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); @@ -316,10 +314,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; } @@ -847,3 +849,4 @@ class MustacheException extends Exception { const UNKNOWN_PRAGMA = 4; } + From e452b4cb73fac99aa1140757e5e5d47f0cfea3be Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 13 Jun 2011 02:03:00 -0400 Subject: [PATCH 10/11] Fix the last couple of regressions with section rendering. --- Mustache.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Mustache.php b/Mustache.php index 6df106d..19e9cbb 100644 --- a/Mustache.php +++ b/Mustache.php @@ -214,13 +214,13 @@ class Mustache { if ($section = $this->_findSection($template)) { list($before, $type, $tag_name, $content, $after) = $section; - $replace = ''; + $renderedContent = ''; $val = $this->_getVariable($tag_name); switch($type) { // inverted section case '^': if (empty($val)) { - $replace .= $content; + $renderedContent = $this->_renderTemplate($content); } break; @@ -229,22 +229,22 @@ class Mustache { if ($this->_varIsIterable($val)) { foreach ($val as $local_context) { $this->_pushContext($local_context); - $replace .= $this->_renderTemplate($content); + $renderedContent .= $this->_renderTemplate($content); $this->_popContext(); } } else if ($val) { if (is_array($val) || is_object($val)) { $this->_pushContext($val); - $replace .= $this->_renderTemplate($content); + $renderedContent = $this->_renderTemplate($content); $this->_popContext(); } else { - $replace .= $content; + $renderedContent = $this->_renderTemplate($content); } } break; } - return $this->_renderTags($before) . $replace . $this->_renderTemplate($after); + return $this->_renderTags($before) . $renderedContent . $this->_renderTemplate($after); } return $this->_renderTags($template); @@ -268,9 +268,7 @@ 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 @@ -566,7 +564,7 @@ class Mustache { case '#': case '^': case '/': - // remove any leftovers from _renderSections + // remove any leftover section tags return $leading . $trailing; break; default: From 0a10c5bcfe1280a19a754328f423d4fb12c2a3d0 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 13 Jun 2011 02:16:48 -0400 Subject: [PATCH 11/11] update known issues in README --- README.markdown | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.markdown b/README.markdown index 52ee11f..c3f6831 100644 --- a/README.markdown +++ b/README.markdown @@ -81,11 +81,10 @@ 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 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