From 60d593dcabcd4e82c320bab1bfde622cb26aec8a Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 23 Jun 2010 03:16:04 -0400 Subject: [PATCH 1/9] Higher order functions, see: http://bit.ly/9s0dO4 --- Mustache.php | 28 +++--- test/MustacheHigherOrderSectionsTest.php | 117 +++++++++++++++++++++++ 2 files changed, 134 insertions(+), 11 deletions(-) create mode 100644 test/MustacheHigherOrderSectionsTest.php diff --git a/Mustache.php b/Mustache.php index 88727c5..8d843c5 100644 --- a/Mustache.php +++ b/Mustache.php @@ -188,19 +188,25 @@ class Mustache { // regular section case '#': - if ($this->_varIsIterable($val)) { - foreach ($val as $local_context) { - $this->_pushContext($local_context); + if ($val) { + // higher order sections + if (is_callable($val)) { + $content = call_user_func($val, $content); $replace .= $this->_renderTemplate($content); - $this->_popContext(); - } - } else if ($val) { - if (is_array($val) || is_object($val)) { - $this->_pushContext($val); - $replace .= $this->_renderTemplate($content); - $this->_popContext(); + } else if ($this->_varIsIterable($val)) { + foreach ($val as $local_context) { + $this->_pushContext($local_context); + $replace .= $this->_renderTemplate($content); + $this->_popContext(); + } } else { - $replace .= $content; + if (is_array($val) || is_object($val)) { + $this->_pushContext($val); + $replace .= $this->_renderTemplate($content); + $this->_popContext(); + } else { + $replace .= $content; + } } } break; diff --git a/test/MustacheHigherOrderSectionsTest.php b/test/MustacheHigherOrderSectionsTest.php new file mode 100644 index 0000000..63b8d3b --- /dev/null +++ b/test/MustacheHigherOrderSectionsTest.php @@ -0,0 +1,117 @@ +foo = new Foo(); + } + + public function testAnonymousFunctionSectionCallback() { + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + $this->markTestSkipped('Unable to test anonymous function section callbacks in PHP < 5.3'); + return; + } + + $this->foo->wrapper = function($text) { + return sprintf('
%s
', $text); + }; + + $this->assertEquals( + sprintf('
%s
', $this->foo->name), + $this->foo->render('{{#wrapper}}{{name}}{{/wrapper}}') + ); + } + + public function testSectionCallback() { + $this->assertEquals(sprintf('%s', $this->foo->name), $this->foo->render('{{name}}')); + $this->assertEquals(sprintf('%s', $this->foo->name), $this->foo->render('{{#wrap}}{{name}}{{/wrap}}')); + } + + public function testRuntimeSectionCallback() { + $this->foo->double_wrap = array($this->foo, 'wrapWithBoth'); + $this->assertEquals( + sprintf('%s', $this->foo->name), + $this->foo->render('{{#double_wrap}}{{name}}{{/double_wrap}}') + ); + } + + public function testFunctionSectionCallback() { + $this->foo->wrapper = 'make_my_logo_bigger'; + $this->assertEquals( + sprintf('

%s

', $this->foo->name), + $this->foo->render('{{#wrapper}}{{name}}{{/wrapper}}') + ); + } + + public function testStaticSectionCallback() { + $this->foo->trimmer = array(get_class($this->foo), 'staticTrim'); + $this->assertEquals($this->foo->name, $this->foo->render('{{#trimmer}} {{name}} {{/trimmer}}')); + + $this->foo->trimmer = 'Foo::staticTrim'; + $this->assertEquals($this->foo->lorem, $this->foo->render('{{#trimmer}} {{lorem}} {{/trimmer}}')); + } + + public function testViewArraySectionCallback() { + $data = array( + 'name' => 'Bob', + 'wrap' => 'make_my_logo_bigger', + 'trim' => array(get_class($this->foo), 'staticTrim'), + ); + $this->assertEquals( + sprintf('

%s

', $data['name']), + $this->foo->render('{{#wrap}}{{name}}{{/wrap}}', $data) + ); + $this->assertEquals($data['name'], $this->foo->render('{{#trim}} {{name}} {{/trim}}', $data)); + } + + public function testViewArrayAnonymousSectionCallback() { + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + $this->markTestSkipped('Unable to test anonymous function section callbacks in PHP < 5.3'); + return; + } + $data = array( + 'name' => 'Bob', + 'wrap' => 'make_my_logo_bigger', + 'anonywrap' => function($text) { + return array('[[%s]]', $text); + } + ); + $this->assertEquals( + sprintf('

%s

', $data['name']), + $this->foo->render('{{#wrap}}{{name}}{{/wrap}}', $data) + ); + } +} + +class Foo extends Mustache { + public $name = 'Justin'; + public $lorem = 'Lorem ipsum dolor sit amet,'; + public $wrap; + + public function __construct($template = null, $view = null, $partials = null) { + $this->wrap = array($this, 'wrapWithEm'); + parent::__construct($template, $view, $partials); + } + + public function wrapWithEm($text) { + return sprintf('%s', $text); + } + + public function wrapWithStrong($text) { + return sprintf('%s', $text); + } + + public function wrapWithBoth($text) { + return self::wrapWithStrong(self::wrapWithEm($text)); + } + + public static function staticTrim($text) { + return trim($text); + } +} + +function make_my_logo_bigger($text) { + return sprintf('

%s

', $text); +} \ No newline at end of file From 2edd7330a0694ec67b3fa29cbf66e38fa12ec1c8 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 5 Jul 2010 15:27:43 -0400 Subject: [PATCH 2/9] Failing test for higher-order section side effects--issue #4 --- test/MustacheHigherOrderSectionsTest.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/MustacheHigherOrderSectionsTest.php b/test/MustacheHigherOrderSectionsTest.php index 63b8d3b..1d3ac35 100644 --- a/test/MustacheHigherOrderSectionsTest.php +++ b/test/MustacheHigherOrderSectionsTest.php @@ -83,6 +83,18 @@ class MustacheHigherOrderSectionsTest extends PHPUnit_Framework_TestCase { $this->foo->render('{{#wrap}}{{name}}{{/wrap}}', $data) ); } + + public function testMonsters() { + $frank = new Monster(); + $frank->title = 'Dr.'; + $frank->name = 'Frankenstein'; + $this->assertEquals('Dr. Frankenstein', $frank->render()); + + $dracula = new Monster(); + $dracula->title = 'Count'; + $dracula->name = 'Dracula'; + $this->assertEquals('Count Dracula', $dracula->render()); + } } class Foo extends Mustache { @@ -106,7 +118,7 @@ class Foo extends Mustache { public function wrapWithBoth($text) { return self::wrapWithStrong(self::wrapWithEm($text)); } - + public static function staticTrim($text) { return trim($text); } @@ -114,4 +126,10 @@ class Foo extends Mustache { function make_my_logo_bigger($text) { return sprintf('

%s

', $text); +} + +class Monster extends Mustache { + public $_template = '{{#title}}{{title}} {{/title}}{{name}}'; + public $title; + public $name; } \ No newline at end of file From 18bffdaea0964a7f46ac4b9604e47744f295cdfa Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 9 Aug 2010 10:17:13 -0400 Subject: [PATCH 3/9] Only allow anonymous function or class-based callback for higher-order sections. --- Mustache.php | 25 +++++++++++++++++++++--- test/MustacheHigherOrderSectionsTest.php | 24 +++-------------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Mustache.php b/Mustache.php index 6153b27..e3c61a7 100644 --- a/Mustache.php +++ b/Mustache.php @@ -229,7 +229,7 @@ class Mustache { case '#': // higher order sections - if (is_callable($val)) { + if ($this->_sectionIsCallable($val)) { $content = call_user_func($val, $content); $replace .= $this->_renderTemplate($content); } else if ($this->_varIsIterable($val)) { @@ -358,7 +358,6 @@ class Mustache { return (is_array($this->_localPragmas[$pragma_name])) ? $this->_localPragmas[$pragma_name] : array(); } - /** * Check whether this Mustache instance throws a given exception. * @@ -548,7 +547,6 @@ class Mustache { $this->_context = $new; } - /** * Remove the latest context from the stack. * @@ -659,6 +657,27 @@ class Mustache { protected function _varIsIterable($var) { return $var instanceof Traversable || (is_array($var) && !array_diff_key($var, array_keys(array_keys($var)))); } + + /** + * Higher order sections helper: tests whether the section $var is a valid callback. + * + * In Mustache.php, a variable is considered 'callable' if the variable is: + * + * 1. an anonymous function. + * 2. an object and the name of a public function, i.e. `array($SomeObject, 'methodName')` + * 3. a class name and the name of a public static function, i.e. `array('SomeClass', 'methodName')` + * 4. a static function name in the form `'SomeClass::methodName'` + * + * @access protected + * @param mixed $var + * @return bool + */ + protected function _sectionIsCallable($var) { + if (is_string($var) && (strpos($var, '::') == false)) { + return false; + } + return is_callable($var); + } } diff --git a/test/MustacheHigherOrderSectionsTest.php b/test/MustacheHigherOrderSectionsTest.php index 1d3ac35..88077f0 100644 --- a/test/MustacheHigherOrderSectionsTest.php +++ b/test/MustacheHigherOrderSectionsTest.php @@ -37,14 +37,6 @@ class MustacheHigherOrderSectionsTest extends PHPUnit_Framework_TestCase { ); } - public function testFunctionSectionCallback() { - $this->foo->wrapper = 'make_my_logo_bigger'; - $this->assertEquals( - sprintf('

%s

', $this->foo->name), - $this->foo->render('{{#wrapper}}{{name}}{{/wrapper}}') - ); - } - public function testStaticSectionCallback() { $this->foo->trimmer = array(get_class($this->foo), 'staticTrim'); $this->assertEquals($this->foo->name, $this->foo->render('{{#trimmer}} {{name}} {{/trimmer}}')); @@ -56,13 +48,8 @@ class MustacheHigherOrderSectionsTest extends PHPUnit_Framework_TestCase { public function testViewArraySectionCallback() { $data = array( 'name' => 'Bob', - 'wrap' => 'make_my_logo_bigger', 'trim' => array(get_class($this->foo), 'staticTrim'), ); - $this->assertEquals( - sprintf('

%s

', $data['name']), - $this->foo->render('{{#wrap}}{{name}}{{/wrap}}', $data) - ); $this->assertEquals($data['name'], $this->foo->render('{{#trim}} {{name}} {{/trim}}', $data)); } @@ -73,13 +60,12 @@ class MustacheHigherOrderSectionsTest extends PHPUnit_Framework_TestCase { } $data = array( 'name' => 'Bob', - 'wrap' => 'make_my_logo_bigger', - 'anonywrap' => function($text) { - return array('[[%s]]', $text); + 'wrap' => function($text) { + return sprintf('[[%s]]', $text); } ); $this->assertEquals( - sprintf('

%s

', $data['name']), + sprintf('[[%s]]', $data['name']), $this->foo->render('{{#wrap}}{{name}}{{/wrap}}', $data) ); } @@ -124,10 +110,6 @@ class Foo extends Mustache { } } -function make_my_logo_bigger($text) { - return sprintf('

%s

', $text); -} - class Monster extends Mustache { public $_template = '{{#title}}{{title}} {{/title}}{{name}}'; public $title; From 65d6b5a63897f63528802285a61b4534d45ab424 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 15 Dec 2010 10:38:11 -0500 Subject: [PATCH 4/9] Enable (and fix) lambdas spec test. This requires the not-yet-committed PHP version of the lambdas spec to actually do anything besides skip some more tests. But trust me, once you see these hot new lambda spec tests, it'll be a *lot* more interesting. --- test/MustacheSpecTest.php | 56 +++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/test/MustacheSpecTest.php b/test/MustacheSpecTest.php index d26c701..a426bbc 100644 --- a/test/MustacheSpecTest.php +++ b/test/MustacheSpecTest.php @@ -57,20 +57,25 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { $this->assertEquals($expected, $m->render(), $desc); } - // /** - // * @group lambdas - // * @dataProvider loadLambdasSpec - // */ - // public function testLambdasSpec($template, $data, $partials, $expected, $desc) { - // $this->markTestSkipped("Lambdas for PHP haven't made it into the spec yet, so we'll skip them to avoid a bajillion failed tests."); - // - // if (!version_compare(PHP_VERSION, '5.3.0', '>=')) { - // $this->markTestSkipped('Unable to test Lambdas spec with PHP < 5.3.'); - // } - // - // $m = new Mustache($template, $data, $partials); - // $this->assertEquals($expected, $m->render(), $desc); - // } + /** + * @group lambdas + * @dataProvider loadLambdasSpec + */ + public function testLambdasSpec($template, $data, $partials, $expected, $desc) { + if (!isset($data['lambda']['php'])) { + $this->markTestSkipped(sprintf('PHP lambda test not implemented for "%s".', $desc)); + } + + if (!version_compare(PHP_VERSION, '5.3.0', '>=')) { + $this->markTestSkipped('Unable to test Lambdas spec with PHP < 5.3.'); + } + + $func = $data['lambda']['php']; + $data['lambda'] = function($text = null) use ($func) { return eval($func); }; + + $m = new Mustache($template, $data, $partials); + $this->assertEquals($expected, $m->render(), $desc); + } /** * @group partials @@ -106,9 +111,9 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { return $this->loadSpec('inverted'); } - // public function loadLambdasSpec() { - // return $this->loadSpec('lambdas'); - // } + public function loadLambdasSpec() { + return $this->loadSpec('lambdas'); + } public function loadPartialsSpec() { return $this->loadSpec('partials'); @@ -133,12 +138,23 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { } $data = array(); - $yaml = new sfYamlParser(); + $file = file_get_contents($filename); - $spec = $yaml->parse(file_get_contents($filename)); + // @hack: pre-process the 'lambdas' spec so the Symfony YAML parser doesn't complain. + if ($name === 'lambdas') { + $file = str_replace(" !code\n", "\n", $file); + } + + $spec = $yaml->parse($file); foreach ($spec['tests'] as $test) { - $data[] = array($test['template'], $test['data'], isset($test['partials']) ? $test['partials'] : array(), $test['expected'], $test['desc']); + $data[] = array( + $test['template'], + $test['data'], + isset($test['partials']) ? $test['partials'] : array(), + $test['expected'], + $test['desc'] + ); } return $data; } From 6a65bcda03039e90602b95d74e957faf06ece8ac Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 15 Dec 2010 10:47:28 -0500 Subject: [PATCH 5/9] Update (per the lambdas spec) to allow lambdas as "higher-order variables". --- Mustache.php | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Mustache.php b/Mustache.php index 153572d..c0ea3a9 100644 --- a/Mustache.php +++ b/Mustache.php @@ -106,6 +106,11 @@ class Mustache { protected $_localPragmas = array(); + /** + * Interpolated lambdas should be called only once... store 'em for later here + */ + protected $_interpolatedLambdas = array(); + /** * Mustache class constructor. * @@ -226,7 +231,7 @@ class Mustache { case '#': // higher order sections - if ($this->_sectionIsCallable($val)) { + if ($this->_varIsCallable($val)) { $content = call_user_func($val, $content); $replace .= $this->_renderTemplate($content); } else if ($this->_varIsIterable($val)) { @@ -586,7 +591,7 @@ class Mustache { * @return string */ protected function _renderEscaped($tag_name) { - return htmlentities($this->_getVariable($tag_name), ENT_COMPAT, $this->_charset); + return htmlentities($this->_renderUnescaped($tag_name), ENT_COMPAT, $this->_charset); } /** @@ -608,7 +613,17 @@ class Mustache { * @return string */ protected function _renderUnescaped($tag_name) { - return $this->_getVariable($tag_name); + $val = $this->_getVariable($tag_name); + + if ($this->_varIsCallable($val)) { + $key = is_object($val) ? spl_object_hash($val) : serialize($val); + if (!isset($this->_interpolatedLambdas[$key])) { + $this->_interpolatedLambdas[$key] = call_user_func($val); + } + return $this->_renderTemplate($this->_interpolatedLambdas[$key]); + } + + return $val; } /** @@ -783,7 +798,7 @@ class Mustache { * @param mixed $var * @return bool */ - protected function _sectionIsCallable($var) { + protected function _varIsCallable($var) { if (is_string($var) && (strpos($var, '::') == false)) { return false; } From dd6077812429be0e33622bf7169be83f597cb9b9 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 27 Dec 2010 01:01:18 -0500 Subject: [PATCH 6/9] Update spec test to work with v1.0.0rc2 lambdas spec. --- test/MustacheSpecTest.php | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/test/MustacheSpecTest.php b/test/MustacheSpecTest.php index a426bbc..69428f3 100644 --- a/test/MustacheSpecTest.php +++ b/test/MustacheSpecTest.php @@ -62,21 +62,34 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { * @dataProvider loadLambdasSpec */ public function testLambdasSpec($template, $data, $partials, $expected, $desc) { - if (!isset($data['lambda']['php'])) { - $this->markTestSkipped(sprintf('PHP lambda test not implemented for "%s".', $desc)); - } - if (!version_compare(PHP_VERSION, '5.3.0', '>=')) { $this->markTestSkipped('Unable to test Lambdas spec with PHP < 5.3.'); } - $func = $data['lambda']['php']; - $data['lambda'] = function($text = null) use ($func) { return eval($func); }; - + $data = $this->prepareLambdasSpec($data); $m = new Mustache($template, $data, $partials); $this->assertEquals($expected, $m->render(), $desc); } + /** + * Extract and lambdafy any 'lambda' values found in the $data array. + */ + protected function prepareLambdasSpec($data) { + foreach ($data as $key => $val) { + if ($key === 'lambda') { + if (!isset($val['php'])) { + $this->markTestSkipped(sprintf('PHP lambda test not implemented for this test.')); + } + + $func = $val['php']; + $data[$key] = function($text = null) use ($func) { return eval($func); }; + } else if (is_array($val)) { + $data[$key] = $this->prepareLambdasSpec($val); + } + } + return $data; + } + /** * @group partials * @dataProvider loadPartialsSpec @@ -112,7 +125,7 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { } public function loadLambdasSpec() { - return $this->loadSpec('lambdas'); + return $this->loadSpec('~lambdas'); } public function loadPartialsSpec() { @@ -142,7 +155,7 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { $file = file_get_contents($filename); // @hack: pre-process the 'lambdas' spec so the Symfony YAML parser doesn't complain. - if ($name === 'lambdas') { + if ($name === '~lambdas') { $file = str_replace(" !code\n", "\n", $file); } From 14911a158eb2835e2cbf9f654a9bc4e70136979a Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sat, 5 Mar 2011 12:14:02 -0500 Subject: [PATCH 7/9] Per the updated spec, interpolated lambdas should not be cached. --- Mustache.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Mustache.php b/Mustache.php index 69defe3..ad89f24 100644 --- a/Mustache.php +++ b/Mustache.php @@ -72,11 +72,6 @@ class Mustache { protected $_localPragmas = array(); - /** - * Interpolated lambdas should be called only once... store 'em for later here - */ - protected $_interpolatedLambdas = array(); - /** * Mustache class constructor. * @@ -614,10 +609,7 @@ class Mustache { if ($this->_varIsCallable($val)) { $key = is_object($val) ? spl_object_hash($val) : serialize($val); - if (!isset($this->_interpolatedLambdas[$key])) { - $this->_interpolatedLambdas[$key] = call_user_func($val); - } - return $this->_renderTemplate($this->_interpolatedLambdas[$key]); + return $this->_renderTemplate(call_user_func($val)); } return $val; From 4bb4e970de825f09c0b09d5cc3c6073fee77a649 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 12 Jun 2011 23:33:07 -0400 Subject: [PATCH 8/9] Fix missing arguments from renderUnescaped --- Mustache.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mustache.php b/Mustache.php index 414316f..df52755 100644 --- a/Mustache.php +++ b/Mustache.php @@ -617,7 +617,7 @@ class Mustache { * @return string */ protected function _renderEscaped($tag_name, $leading, $trailing) { - return $leading . htmlentities($this->_renderUnescaped($tag_name), ENT_COMPAT, $this->_charset) . $trailing; + return htmlentities($this->_renderUnescaped($tag_name, $leading, $trailing), ENT_COMPAT, $this->_charset); } /** @@ -888,4 +888,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 ac418ad8767473e7b4a1f8d14b041df929e1afd7 Mon Sep 17 00:00:00 2001 From: Damien Tournoud Date: Tue, 7 Dec 2010 11:43:23 -0500 Subject: [PATCH 9/9] Restrict the callable lambda to object calls and anonymous functions. --- Mustache.php | 6 +----- test/MustacheHigherOrderSectionsTest.php | 3 --- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Mustache.php b/Mustache.php index df52755..6f57a25 100644 --- a/Mustache.php +++ b/Mustache.php @@ -847,17 +847,13 @@ class Mustache { * 1. an anonymous function. * 2. an object and the name of a public function, i.e. `array($SomeObject, 'methodName')` * 3. a class name and the name of a public static function, i.e. `array('SomeClass', 'methodName')` - * 4. a static function name in the form `'SomeClass::methodName'` * * @access protected * @param mixed $var * @return bool */ protected function _varIsCallable($var) { - if (is_string($var) && (strpos($var, '::') == false)) { - return false; - } - return is_callable($var); + return !is_string($var) && is_callable($var); } } diff --git a/test/MustacheHigherOrderSectionsTest.php b/test/MustacheHigherOrderSectionsTest.php index 88077f0..87e4142 100644 --- a/test/MustacheHigherOrderSectionsTest.php +++ b/test/MustacheHigherOrderSectionsTest.php @@ -40,9 +40,6 @@ class MustacheHigherOrderSectionsTest extends PHPUnit_Framework_TestCase { public function testStaticSectionCallback() { $this->foo->trimmer = array(get_class($this->foo), 'staticTrim'); $this->assertEquals($this->foo->name, $this->foo->render('{{#trimmer}} {{name}} {{/trimmer}}')); - - $this->foo->trimmer = 'Foo::staticTrim'; - $this->assertEquals($this->foo->lorem, $this->foo->render('{{#trimmer}} {{lorem}} {{/trimmer}}')); } public function testViewArraySectionCallback() {