Explorar el Código

Merge branch 'feature/higher-order-sections' into dev

Justin Hileman hace 14 años
padre
commit
507f5c631f
Se han modificado 3 ficheros con 193 adiciones y 24 borrados
  1. 30 4
      Mustache.php
  2. 114 0
      test/MustacheHigherOrderSectionsTest.php
  3. 49 20
      test/MustacheSpecTest.php

+ 30 - 4
Mustache.php

@@ -239,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);
@@ -616,7 +619,7 @@ class Mustache {
 	 * @return string
 	 */
 	protected function _renderEscaped($tag_name, $leading, $trailing) {
-		return $leading . htmlentities($this->_getVariable($tag_name), ENT_COMPAT, $this->_charset) . $trailing;
+		return htmlentities($this->_renderUnescaped($tag_name, $leading, $trailing), ENT_COMPAT, $this->_charset);
 	}
 
 	/**
@@ -648,7 +651,14 @@ 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)) {
+			$key = is_object($val) ? spl_object_hash($val) : serialize($val);
+			$val = $this->_renderTemplate(call_user_func($val));
+		}
+
+		return $leading . $val . $trailing;
 	}
 
 	/**
@@ -830,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);
+	}
 }
 
 
@@ -860,4 +887,3 @@ class MustacheException extends Exception {
 	const UNKNOWN_PRAGMA           = 4;
 
 }
-

+ 114 - 0
test/MustacheHigherOrderSectionsTest.php

@@ -0,0 +1,114 @@
+<?php
+
+require_once '../Mustache.php';
+
+class MustacheHigherOrderSectionsTest extends PHPUnit_Framework_TestCase {
+
+	public function setUp() {
+		$this->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('<div class="anonymous">%s</div>', $text);
+		};
+
+		$this->assertEquals(
+			sprintf('<div class="anonymous">%s</div>', $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('<em>%s</em>', $this->foo->name), $this->foo->render('{{#wrap}}{{name}}{{/wrap}}'));
+	}
+
+	public function testRuntimeSectionCallback() {
+		$this->foo->double_wrap = array($this->foo, 'wrapWithBoth');
+		$this->assertEquals(
+			sprintf('<strong><em>%s</em></strong>', $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('<em>%s</em>', $text);
+	}
+
+	public function wrapWithStrong($text) {
+		return sprintf('<strong>%s</strong>', $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;
+}

+ 49 - 20
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);
+
+		// @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_get_contents($filename));
+		$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;
 	}