diff --git a/Mustache.php b/Mustache.php index 8d843c5..6153b27 100644 --- a/Mustache.php +++ b/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)))); } } diff --git a/examples/implicit_iterator/ImplicitIterator.php b/examples/implicit_iterator/ImplicitIterator.php new file mode 100644 index 0000000..c01fef0 --- /dev/null +++ b/examples/implicit_iterator/ImplicitIterator.php @@ -0,0 +1,5 @@ + '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]); + } +} \ No newline at end of file diff --git a/examples/section_iterator_objects/section_iterator_objects.mustache b/examples/section_iterator_objects/section_iterator_objects.mustache new file mode 100644 index 0000000..44dfce4 --- /dev/null +++ b/examples/section_iterator_objects/section_iterator_objects.mustache @@ -0,0 +1,5 @@ +* {{ start }} +{{# middle }} +* {{ item }} +{{/ middle }} +* {{ final }} \ No newline at end of file diff --git a/examples/section_iterator_objects/section_iterator_objects.txt b/examples/section_iterator_objects/section_iterator_objects.txt new file mode 100644 index 0000000..e6b2d7a --- /dev/null +++ b/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. \ No newline at end of file diff --git a/examples/section_magic_objects/SectionMagicObjects.php b/examples/section_magic_objects/SectionMagicObjects.php new file mode 100644 index 0000000..ebb0031 --- /dev/null +++ b/examples/section_magic_objects/SectionMagicObjects.php @@ -0,0 +1,26 @@ + '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]); + } +} \ No newline at end of file diff --git a/examples/section_magic_objects/section_magic_objects.mustache b/examples/section_magic_objects/section_magic_objects.mustache new file mode 100644 index 0000000..9119608 --- /dev/null +++ b/examples/section_magic_objects/section_magic_objects.mustache @@ -0,0 +1,6 @@ +* {{ start }} +{{# middle }} +* {{ foo }} +* {{ bar }} +{{/ middle }} +* {{ final }} \ No newline at end of file diff --git a/examples/section_magic_objects/section_magic_objects.txt b/examples/section_magic_objects/section_magic_objects.txt new file mode 100644 index 0000000..e6b2d7a --- /dev/null +++ b/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. \ No newline at end of file diff --git a/examples/section_objects/SectionObjects.php b/examples/section_objects/SectionObjects.php new file mode 100644 index 0000000..41b7d84 --- /dev/null +++ b/examples/section_objects/SectionObjects.php @@ -0,0 +1,16 @@ +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(); + } +} \ No newline at end of file diff --git a/test/MustachePragmaImplicitIteratorTest.php b/test/MustachePragmaImplicitIteratorTest.php new file mode 100644 index 0000000..bf39091 --- /dev/null +++ b/test/MustachePragmaImplicitIteratorTest.php @@ -0,0 +1,51 @@ +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}}')); + } + +} \ No newline at end of file diff --git a/test/MustacheTest.php b/test/MustacheTest.php index c05b15b..a877c03 100644 --- a/test/MustacheTest.php +++ b/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; + 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 'mustache': + $template = file_get_contents($filename); + break; - case 'txt': - $output = 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();