Răsfoiți Sursa

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

Conflicts:
	Mustache.php
Justin Hileman 15 ani în urmă
părinte
comite
8709a3969d

+ 74 - 20
Mustache.php

@@ -33,7 +33,45 @@ class Mustache {
 	// Override charset passed to htmlentities() and htmlspecialchars(). Defaults to UTF-8.
 	protected $_charset = 'UTF-8';
 
-	const PRAGMA_DOT_NOTATION = 'DOT-NOTATION';
+	/**
+	 * Pragmas are macro-like directives that, when invoked, change the behavior or
+	 * syntax of Mustache.
+	 *
+	 * They should be considered extremely experimental. Most likely their implementation
+	 * will change in the future.
+	 */
+
+	/**
+	 * The {{%DOT-NOTATION}} pragma allows context traversal via dots. Given the following context:
+	 *
+	 *     $context = array('foo' => array('bar' => array('baz' => 'qux')));
+	 *
+	 * One could access nested properties using dot notation:
+	 *
+	 *      {{%DOT-NOTATION}}{{foo.bar.baz}}
+	 *
+	 * Which would render as `qux`.
+	 */
+	const PRAGMA_DOT_NOTATION      = 'DOT-NOTATION';
+
+	/**
+	 * The {{%IMPLICIT-ITERATOR}} pragma allows access to non-associative array data in an
+	 * iterable section:
+	 *
+	 *     $context = array('items' => array('foo', 'bar', 'baz'));
+	 *
+	 * With this template:
+	 *
+	 *     {{%IMPLICIT-ITERATOR}}{{#items}}{{.}}{{/items}}
+	 *
+	 * Would render as `foobarbaz`.
+	 *
+	 * {{%IMPLICIT-ITERATOR}} accepts an optional 'iterator' argument which allows implicit
+	 * iterator tags other than {{.}} ...
+	 *
+	 *     {{%IMPLICIT-ITERATOR iterator=i}}{{#items}}{{i}}{{/items}}
+	 */
+	const PRAGMA_IMPLICIT_ITERATOR = 'IMPLICIT-ITERATOR';
 
 	/**
 	 * The {{%UNESCAPED}} pragma swaps the meaning of the {{normal}} and {{{unescaped}}}
@@ -56,6 +94,7 @@ class Mustache {
 
 	protected $_pragmasImplemented = array(
 		self::PRAGMA_DOT_NOTATION,
+		self::PRAGMA_IMPLICIT_ITERATOR,
 		self::PRAGMA_UNESCAPED
 	);
 
@@ -188,25 +227,40 @@ class Mustache {
 
 				// regular section
 				case '#':
-					if ($val) {
-						// higher order sections
-						if (is_callable($val)) {
-							$content = call_user_func($val, $content);
-							$replace .= $this->_renderTemplate($content);
-						} else if ($this->_varIsIterable($val)) {
-							foreach ($val as $local_context) {
-								$this->_pushContext($local_context);
-								$replace .= $this->_renderTemplate($content);
-								$this->_popContext();
+
+					// higher order sections
+					if (is_callable($val)) {
+						$content = call_user_func($val, $content);
+						$replace .= $this->_renderTemplate($content);
+					} else if ($this->_varIsIterable($val)) {
+						if ($this->_hasPragma(self::PRAGMA_IMPLICIT_ITERATOR)) {
+							if ($opt = $this->_getPragmaOptions(self::PRAGMA_IMPLICIT_ITERATOR)) {
+								$iterator = $opt['iterator'];
+							} else {
+								$iterator = '.';
 							}
 						} else {
-							if (is_array($val) || is_object($val)) {
-								$this->_pushContext($val);
-								$replace .= $this->_renderTemplate($content);
-								$this->_popContext();
+							$iterator = false;
+						}
+
+						foreach ($val as $local_context) {
+							if ($iterator) {
+								$iterator_context = array($iterator => $local_context);
+								$this->_pushContext($iterator_context);
 							} else {
-								$replace .= $content;
+								$this->_pushContext($local_context);
 							}
+
+							$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 {
+							$replace .= $content;
 						}
 					}
 					break;
@@ -297,11 +351,11 @@ class Mustache {
 	 * @throws MustacheException Unknown pragma
 	 */
 	protected function _getPragmaOptions($pragma_name) {
-		if (!$this->_hasPragma()) {
+		if (!$this->_hasPragma($pragma_name)) {
 			throw new MustacheException('Unknown pragma: ' . $pragma_name, MustacheException::UNKNOWN_PRAGMA);
 		}
 
-		return $this->_localPragmas[$pragma_name];
+		return (is_array($this->_localPragmas[$pragma_name])) ? $this->_localPragmas[$pragma_name] : array();
 	}
 
 
@@ -527,7 +581,7 @@ class Mustache {
 	 * @return string
 	 */
 	protected function _getVariable($tag_name) {
-		if ($this->_hasPragma(self::PRAGMA_DOT_NOTATION)) {
+		if ($this->_hasPragma(self::PRAGMA_DOT_NOTATION) && $tag_name != '.') {
 			$chunks = explode('.', $tag_name);
 			$first = array_shift($chunks);
 
@@ -603,7 +657,7 @@ class Mustache {
 	 * @return bool
 	 */
 	protected function _varIsIterable($var) {
-		return is_object($var) || (is_array($var) && !array_diff_key($var, array_keys(array_keys($var))));
+		return $var instanceof Traversable || (is_array($var) && !array_diff_key($var, array_keys(array_keys($var))));
 	}
 }
 

+ 5 - 0
examples/implicit_iterator/ImplicitIterator.php

@@ -0,0 +1,5 @@
+<?php
+
+class ImplicitIterator extends Mustache {
+	protected $data = array('Donkey Kong', 'Luigi', 'Mario', 'Peach', 'Yoshi');
+}

+ 4 - 0
examples/implicit_iterator/implicit_iterator.mustache

@@ -0,0 +1,4 @@
+{{%IMPLICIT-ITERATOR}}
+{{#data}}
+* {{.}}
+{{/data}}

+ 5 - 0
examples/implicit_iterator/implicit_iterator.txt

@@ -0,0 +1,5 @@
+* Donkey Kong
+* Luigi
+* Mario
+* Peach
+* Yoshi

+ 40 - 0
examples/section_iterator_objects/SectionIteratorObjects.php

@@ -0,0 +1,40 @@
+<?php
+
+class SectionIteratorObjects extends Mustache {
+	public $start = "It worked the first time.";
+
+	public function middle() {
+		return new IteratorObject();
+	}
+
+	public $final = "Then, surprisingly, it worked the final time.";
+}
+
+class IteratorObject implements Iterator {
+	protected $_position = 0;
+
+	protected $_data = array(
+		array('item' => 'And it worked the second time.'),
+		array('item' => 'As well as the third.'),
+	);
+
+	public function rewind() {
+		$this->_position = 0;
+	}
+
+	public function current() {
+		return $this->_data[$this->_position];
+	}
+
+	public function key() {
+		return $this->_position;
+	}
+
+	public function next() {
+		++$this->_position;
+	}
+
+	public function valid() {
+		return isset($this->_data[$this->_position]);
+	}
+}

+ 5 - 0
examples/section_iterator_objects/section_iterator_objects.mustache

@@ -0,0 +1,5 @@
+* {{ start }}
+{{# middle }}
+* {{ item }}
+{{/ middle }}
+* {{ final }}

+ 4 - 0
examples/section_iterator_objects/section_iterator_objects.txt

@@ -0,0 +1,4 @@
+* It worked the first time.
+* And it worked the second time.
+* As well as the third.
+* Then, surprisingly, it worked the final time.

+ 26 - 0
examples/section_magic_objects/SectionMagicObjects.php

@@ -0,0 +1,26 @@
+<?php
+
+class SectionMagicObjects extends Mustache {
+	public $start = "It worked the first time.";
+
+	public function middle() {
+		return new MagicObject();
+	}
+
+	public $final = "Then, surprisingly, it worked the final time.";
+}
+
+class MagicObject {
+	protected $_data = array(
+		'foo' => 'And it worked the second time.',
+		'bar' => 'As well as the third.'
+	);
+
+	public function __get($key) {
+		return isset($this->_data[$key]) ? $this->_data[$key] : NULL;
+	}
+
+	public function __isset($key) {
+		return isset($this->_data[$key]);
+	}
+}

+ 6 - 0
examples/section_magic_objects/section_magic_objects.mustache

@@ -0,0 +1,6 @@
+* {{ start }}
+{{# middle }}
+* {{ foo }}
+* {{ bar }}
+{{/ middle }}
+* {{ final }}

+ 4 - 0
examples/section_magic_objects/section_magic_objects.txt

@@ -0,0 +1,4 @@
+* It worked the first time.
+* And it worked the second time.
+* As well as the third.
+* Then, surprisingly, it worked the final time.

+ 16 - 0
examples/section_objects/SectionObjects.php

@@ -0,0 +1,16 @@
+<?php
+
+class SectionObjects extends Mustache {
+	public $start = "It worked the first time.";
+
+	public function middle() {
+		return new SectionObject;
+	}
+
+	public $final = "Then, surprisingly, it worked the final time.";
+}
+
+class SectionObject {
+	public $foo = 'And it worked the second time.';
+	public $bar = 'As well as the third.';
+}

+ 6 - 0
examples/section_objects/section_objects.mustache

@@ -0,0 +1,6 @@
+* {{ start }}
+{{# middle }}
+* {{ foo }}
+* {{ bar }}
+{{/ middle }}
+* {{ final }}

+ 4 - 0
examples/section_objects/section_objects.txt

@@ -0,0 +1,4 @@
+* It worked the first time.
+* And it worked the second time.
+* As well as the third.
+* Then, surprisingly, it worked the final time.

+ 56 - 0
test/MustacheObjectSectionTest.php

@@ -0,0 +1,56 @@
+<?php
+
+require_once '../Mustache.php';
+
+class MustacheObjectSectionTest extends PHPUnit_Framework_TestCase {
+	public function testBasicObject() {
+		$alpha = new Alpha();
+		$this->assertEquals('Foo', $alpha->render('{{#foo}}{{name}}{{/foo}}'));
+	}
+
+	public function testObjectWithGet() {
+		$beta = new Beta();
+		$this->assertEquals('Foo', $beta->render('{{#foo}}{{name}}{{/foo}}'));
+	}
+
+	public function testSectionObjectWithGet() {
+		$gamma = new Gamma();
+		$this->assertEquals('Foo', $gamma->render('{{#bar}}{{#foo}}{{name}}{{/foo}}{{/bar}}'));
+	}
+}
+
+class Alpha extends Mustache {
+	public $foo;
+
+	public function __construct() {
+		$this->foo = new StdClass();
+		$this->foo->name = 'Foo';
+		$this->foo->number = 1;
+	}
+}
+
+class Beta extends Mustache {
+	protected $_data = array();
+
+	public function __construct() {
+		$this->_data['foo'] = new StdClass();
+		$this->_data['foo']->name = 'Foo';
+		$this->_data['foo']->number = 1;
+	}
+
+	public function __isset($name) {
+		return array_key_exists($name, $this->_data);
+	}
+
+	public function __get($name) {
+		return $this->_data[$name];
+	}
+}
+
+class Gamma extends Mustache {
+	public $bar;
+
+	public function __construct() {
+		$this->bar = new Beta();
+	}
+}

+ 51 - 0
test/MustachePragmaImplicitIteratorTest.php

@@ -0,0 +1,51 @@
+<?php
+
+require_once '../Mustache.php';
+require_once 'PHPUnit/Framework.php';
+
+class MustachePragmaImplicitIteratorTest extends PHPUnit_Framework_TestCase {
+
+	public function testEnablePragma() {
+		$m = $this->getMock('Mustache', array('_renderPragma'), array('{{%IMPLICIT-ITERATOR}}'));
+		$m->expects($this->exactly(1))
+			->method('_renderPragma')
+			->with(array('{{%IMPLICIT-ITERATOR}}', 'IMPLICIT-ITERATOR', null));
+		$m->render();
+	}
+
+	public function testImplicitIterator() {
+		$m1 = new Mustache('{{%IMPLICIT-ITERATOR}}{{#items}}{{.}}{{/items}}', array('items' => array('a', 'b', 'c')));
+		$this->assertEquals('abc', $m1->render());
+
+		$m2 = new Mustache('{{%IMPLICIT-ITERATOR}}{{#items}}{{.}}{{/items}}', array('items' => array(1, 2, 3)));
+		$this->assertEquals('123', $m2->render());
+	}
+
+	public function testDotNotationCollision() {
+		$m = new Mustache(null, array('items' => array('foo', 'bar', 'baz')));
+
+		$this->assertEquals('foobarbaz', $m->render('{{%IMPLICIT-ITERATOR}}{{%DOT-NOTATION}}{{#items}}{{.}}{{/items}}'));
+		$this->assertEquals('foobarbaz', $m->render('{{%DOT-NOTATION}}{{%IMPLICIT-ITERATOR}}{{#items}}{{.}}{{/items}}'));
+	}
+
+	public function testCustomIterator() {
+		$m = new Mustache(null, array('items' => array('foo', 'bar', 'baz')));
+
+		$this->assertEquals('foobarbaz', $m->render('{{%IMPLICIT-ITERATOR}}{{#items}}{{.}}{{/items}}'));
+		$this->assertEquals('foobarbaz', $m->render('{{%IMPLICIT-ITERATOR iterator=i}}{{#items}}{{i}}{{/items}}'));
+		$this->assertEquals('foobarbaz', $m->render('{{%IMPLICIT-ITERATOR iterator=items}}{{#items}}{{items}}{{/items}}'));
+	}
+
+	public function testDotNotationContext() {
+		$m = new Mustache(null, array('items' => array(
+			array('index' => 1, 'name' => 'foo'),
+			array('index' => 2, 'name' => 'bar'),
+			array('index' => 3, 'name' => 'baz'),
+		)));
+
+		$this->assertEquals('foobarbaz', $m->render('{{%IMPLICIT-ITERATOR}}{{#items}}{{#.}}{{name}}{{/.}}{{/items}}'));
+		$this->assertEquals('123', $m->render('{{%IMPLICIT-ITERATOR iterator=i}}{{%DOT-NOTATION}}{{#items}}{{i.index}}{{/items}}'));
+		$this->assertEquals('foobarbaz', $m->render('{{%IMPLICIT-ITERATOR iterator=i}}{{%DOT-NOTATION}}{{#items}}{{i.name}}{{/items}}'));
+	}
+
+}

+ 19 - 15
test/MustacheTest.php

@@ -262,26 +262,30 @@ class MustacheTest extends PHPUnit_Framework_TestCase {
 				foreach ($children as $file) {
 					if (!$file->isFile()) continue;
 
-					$filename = $file->getPathInfo();
+					$filename = $file->getPathname();
 					$info = pathinfo($filename);
 
-					switch($info['extension']) {
-						case 'php':
-							$class = $info['filename'];
-							include_once($filename);
-							break;
-
-						case 'mustache':
-							$template = file_get_contents($filename);
-							break;
-
-						case 'txt':
-							$output = file_get_contents($filename);
-							break;
+					if (isset($info['extension'])) {
+						switch($info['extension']) {
+							case 'php':
+								$class = $info['filename'];
+								include_once($filename);
+								break;
+
+							case 'mustache':
+								$template = file_get_contents($filename);
+								break;
+
+							case 'txt':
+								$output = file_get_contents($filename);
+								break;
+						}
 					}
 				}
 
-				$ret[$example] = array($class, $template, $output);
+				if (!empty($class)) {
+					$ret[$example] = array($class, $template, $output);
+				}
 			}
 
 			$files->next();