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;
}