diff --git a/Mustache.php b/Mustache.php index 33c9288..022488b 100644 --- a/Mustache.php +++ b/Mustache.php @@ -14,7 +14,8 @@ */ class Mustache { - const VERSION = '0.7.1'; + const VERSION = '0.8.0'; + const SPEC_VERSION = '1.1.2'; /** * Should this Mustache throw exceptions when it finds unexpected tags? @@ -238,7 +239,10 @@ class Mustache { // regular section case '#': - if ($this->_varIsIterable($val)) { + // higher order sections + if ($this->_varIsCallable($val)) { + $rendered_content = $this->_renderTemplate(call_user_func($val, $content)); + } else if ($this->_varIsIterable($val)) { foreach ($val as $local_context) { $this->_pushContext($local_context); $rendered_content .= $this->_renderTemplate($content); @@ -615,7 +619,8 @@ class Mustache { * @return string */ protected function _renderEscaped($tag_name, $leading, $trailing) { - return $leading . htmlentities($this->_getVariable($tag_name), ENT_COMPAT, $this->_charset) . $trailing; + $rendered = htmlentities($this->_renderUnescaped($tag_name, '', ''), ENT_COMPAT, $this->_charset); + return $leading . $rendered . $trailing; } /** @@ -647,7 +652,13 @@ class Mustache { * @return string */ protected function _renderUnescaped($tag_name, $leading, $trailing) { - return $leading . $this->_getVariable($tag_name) . $trailing; + $val = $this->_getVariable($tag_name); + + if ($this->_varIsCallable($val)) { + $val = $this->_renderTemplate(call_user_func($val)); + } + + return $leading . $val . $trailing; } /** @@ -829,6 +840,23 @@ 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')` + * + * @access protected + * @param mixed $var + * @return bool + */ + protected function _varIsCallable($var) { + return !is_string($var) && is_callable($var); + } } @@ -859,4 +887,3 @@ class MustacheException extends Exception { const UNKNOWN_PRAGMA = 4; } - diff --git a/test/MustacheHigherOrderSectionsTest.php b/test/MustacheHigherOrderSectionsTest.php new file mode 100644 index 0000000..87e4142 --- /dev/null +++ b/test/MustacheHigherOrderSectionsTest.php @@ -0,0 +1,114 @@ +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 testStaticSectionCallback() { + $this->foo->trimmer = array(get_class($this->foo), 'staticTrim'); + $this->assertEquals($this->foo->name, $this->foo->render('{{#trimmer}} {{name}} {{/trimmer}}')); + } + + public function testViewArraySectionCallback() { + $data = array( + 'name' => 'Bob', + 'trim' => array(get_class($this->foo), 'staticTrim'), + ); + $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' => function($text) { + return sprintf('[[%s]]', $text); + } + ); + $this->assertEquals( + sprintf('[[%s]]', $data['name']), + $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 { + 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); + } +} + +class Monster extends Mustache { + public $_template = '{{#title}}{{title}} {{/title}}{{name}}'; + public $title; + public $name; +} \ No newline at end of file diff --git a/test/MustacheSpecTest.php b/test/MustacheSpecTest.php index 9f7f8b7..d3a3413 100644 --- a/test/MustacheSpecTest.php +++ b/test/MustacheSpecTest.php @@ -57,20 +57,38 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { $this->assertEquals($expected, $m->render(), $desc); } - // /** - // * @group lambdas - // * @dataProvider loadLambdasSpec - // */ - // public function testLambdasSpec($desc, $template, $data, $partials, $expected) { - // $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($desc, $template, $data, $partials, $expected) { + if (!version_compare(PHP_VERSION, '5.3.0', '>=')) { + $this->markTestSkipped('Unable to test Lambdas spec with PHP < 5.3.'); + } + + $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 @@ -106,9 +124,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 +151,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['name'] . ': ' . $test['desc'], $test['template'], $test['data'], isset($test['partials']) ? $test['partials'] : array(), $test['expected']); + $data[] = array( + $test['name'] . ': ' . $test['desc'], + $test['template'], + $test['data'], + isset($test['partials']) ? $test['partials'] : array(), + $test['expected'], + ); } return $data; }