diff --git a/Mustache.php b/Mustache.php index 7171acd..4455b1e 100644 --- a/Mustache.php +++ b/Mustache.php @@ -38,38 +38,6 @@ class Mustache { * 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}}} * Mustache tags. That is, once this pragma is activated the {{normal}} tag will not be @@ -88,8 +56,8 @@ class Mustache { const SECTION_TYPES = '\^#\/'; const TAG_TYPES = '#\^\/=!<>\\{&'; - public $_otag = '{{'; - public $_ctag = '}}'; + protected $_otag = '{{'; + protected $_ctag = '}}'; protected $_tagRegEx; @@ -99,8 +67,6 @@ class Mustache { protected $_pragmas = array(); protected $_pragmasImplemented = array( - self::PRAGMA_DOT_NOTATION, - self::PRAGMA_IMPLICIT_ITERATOR, self::PRAGMA_UNESCAPED ); @@ -112,16 +78,64 @@ class Mustache { * This method accepts a $template string and a $view object. Optionally, pass an associative * array of partials as well. * + * Passing an $options array allows overriding certain Mustache options during instantiation: + * + * $options = array( + * // `charset` -- must be supported by `htmlspecialentities()`. defaults to 'UTF-8' + * 'charset' => 'ISO-8859-1', + * + * // opening and closing delimiters, as an array or a space-separated string + * 'delimiters' => '<% %>', + * + * // an array of pragmas to enable + * 'pragmas' => array( + * Mustache::PRAGMA_UNESCAPED + * ), + * ); + * * @access public * @param string $template (default: null) * @param mixed $view (default: null) * @param array $partials (default: null) + * @param array $options (default: array()) * @return void */ - public function __construct($template = null, $view = null, $partials = null) { + public function __construct($template = null, $view = null, $partials = null, array $options = null) { if ($template !== null) $this->_template = $template; if ($partials !== null) $this->_partials = $partials; if ($view !== null) $this->_context = array($view); + if ($options !== null) $this->_setOptions($options); + } + + /** + * Helper function for setting options from constructor args. + * + * @access protected + * @param array $options + * @return void + */ + protected function _setOptions(array $options) { + if (isset($options['charset'])) { + $this->_charset = $options['charset']; + } + + if (isset($options['delimiters'])) { + $delims = $options['delimiters']; + if (!is_array($delims)) { + $delims = array_map('trim', explode(' ', $delims, 2)); + } + $this->_otag = $delims[0]; + $this->_ctag = $delims[1]; + } + + if (isset($options['pragmas'])) { + foreach ($options['pragmas'] as $pragma_name) { + if (!in_array($pragma_name, $this->_pragmasImplemented)) { + throw new MustacheException('Unknown pragma: ' . $pragma_name, MustacheException::UNKNOWN_PRAGMA); + } + } + $this->_pragmas = $options['pragmas']; + } } /** @@ -225,24 +239,8 @@ class Mustache { // regular section case '#': 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 { - $iterator = false; - } - foreach ($val as $local_context) { - - if ($iterator) { - $iterator_context = array($iterator => $local_context); - $this->_pushContext($iterator_context); - } else { - $this->_pushContext($local_context); - } + $this->_pushContext($local_context); $replace .= $this->_renderTemplate($content); $this->_popContext(); } @@ -615,8 +613,10 @@ class Mustache { */ protected function _renderPartial($tag_name, $whitespace = '') { $view = clone($this); - - return $whitespace . preg_replace('/\n(?!$)/s', "\n" . $whitespace, $view->render($this->_getPartial($tag_name))); + + $partial = $whitespace . preg_replace('/\n(?!$)/s', "\n" . $whitespace, $this->_getPartial($tag_name)); + + return $view->render($partial); } /** @@ -685,7 +685,9 @@ class Mustache { * @return string */ protected function _getVariable($tag_name) { - if ($tag_name != '.' && strpos($tag_name, '.') !== false && $this->_hasPragma(self::PRAGMA_DOT_NOTATION)) { + if ($tag_name === '.') { + return $this->_context[0]; + } else if (strpos($tag_name, '.') !== false) { $chunks = explode('.', $tag_name); $first = array_shift($chunks); diff --git a/README.markdown b/README.markdown index 3662d3c..fc88e02 100644 --- a/README.markdown +++ b/README.markdown @@ -83,10 +83,12 @@ Known Issues * Things get weird when you change delimiters inside a section -- `delimiters` example currently fails with an "unclosed section" exception. + * The current spec test exposes several whitespace bugs (which are mostly instances of the exact same whitespace + bug) ... Despite these failing tests, this version is actually *closer* to correct than previous releases. See Also -------- * [Readme for the Ruby Mustache implementation](http://github.com/defunkt/mustache/blob/master/README.md). - * [mustache(1)](http://defunkt.github.com/mustache/mustache.1.html) and [mustache(5)](http://defunkt.github.com/mustache/mustache.5.html) man pages. \ No newline at end of file + * [mustache(1)](http://defunkt.github.com/mustache/mustache.1.html) and [mustache(5)](http://defunkt.github.com/mustache/mustache.5.html) man pages. diff --git a/examples/dot_notation/dot_notation.mustache b/examples/dot_notation/dot_notation.mustache index 4831386..0566867 100644 --- a/examples/dot_notation/dot_notation.mustache +++ b/examples/dot_notation/dot_notation.mustache @@ -1,4 +1,3 @@ -{{%DOT-NOTATION}} * {{person.name.first}} {{person.name.last}} * {{person.age}} * {{person.hometown.city}}, {{person.hometown.state}} diff --git a/examples/implicit_iterator/implicit_iterator.mustache b/examples/implicit_iterator/implicit_iterator.mustache index 94b82e1..b8d58ff 100644 --- a/examples/implicit_iterator/implicit_iterator.mustache +++ b/examples/implicit_iterator/implicit_iterator.mustache @@ -1,4 +1,3 @@ -{{%IMPLICIT-ITERATOR}} {{#data}} * {{.}} {{/data}} \ No newline at end of file diff --git a/test/MustachePragmaDotNotationTest.php b/test/MustachePragmaDotNotationTest.php deleted file mode 100644 index 56f6f11..0000000 --- a/test/MustachePragmaDotNotationTest.php +++ /dev/null @@ -1,63 +0,0 @@ - array('bar' => 'this worked'))); - - $this->assertEquals($m->render('{{foo.bar}}'), '', - 'Dot notation not enabled, variable should have been replaced with nothing'); - $this->assertEquals($m->render('{{%DOT-NOTATION}}{{foo.bar}}'), 'this worked', - 'Dot notation enabled, variable should have been replaced by "this worked"'); - } - - public function testDeepTraversal() { - $data = array( - 'foo' => array('bar' => array('baz' => array('qux' => array('quux' => 'WIN!')))), - 'a' => array('b' => array('c' => array('d' => array('e' => 'abcs')))), - 'one' => array( - 'one' => 'one-one', - 'two' => 'one-two', - 'three' => 'one-three', - ), - ); - - $m = new Mustache('', $data); - $this->assertEquals($m->render('{{%DOT-NOTATION}}{{foo.bar.baz.qux.quux}}'), 'WIN!'); - $this->assertEquals($m->render('{{%DOT-NOTATION}}{{a.b.c.d.e}}'), 'abcs'); - $this->assertEquals($m->render('{{%DOT-NOTATION}}{{one.one}}|{{one.two}}|{{one.three}}'), 'one-one|one-two|one-three'); - } - - public function testDotNotationContext() { - $data = array('parent' => array('items' => array( - array('item' => array('index' => 1)), - array('item' => array('index' => 2)), - array('item' => array('index' => 3)), - array('item' => array('index' => 4)), - array('item' => array('index' => 5)), - ))); - - $m = new Mustache('', $data); - $this->assertEquals('12345', $m->render('{{%DOT-NOTATION}}{{#parent}}{{#items}}{{item.index}}{{/items}}{{/parent}}')); - } - - public function testDotNotationSectionNames() { - $data = array('parent' => array('items' => array( - array('item' => array('index' => 1)), - array('item' => array('index' => 2)), - array('item' => array('index' => 3)), - array('item' => array('index' => 4)), - array('item' => array('index' => 5)), - ))); - - $m = new Mustache('', $data); - $this->assertEquals('.....', $m->render('{{%DOT-NOTATION}}{{#parent.items}}.{{/parent.items}}')); - $this->assertEquals('12345', $m->render('{{%DOT-NOTATION}}{{#parent.items}}{{item.index}}{{/parent.items}}')); - $this->assertEquals('12345', $m->render('{{%DOT-NOTATION}}{{#parent.items}}{{#item}}{{index}}{{/item}}{{/parent.items}}')); - } -} \ No newline at end of file diff --git a/test/MustachePragmaImplicitIteratorTest.php b/test/MustachePragmaImplicitIteratorTest.php deleted file mode 100644 index 507620d..0000000 --- a/test/MustachePragmaImplicitIteratorTest.php +++ /dev/null @@ -1,87 +0,0 @@ -getMock('Mustache', array('_renderPragma'), array('{{%IMPLICIT-ITERATOR}}')); - $m->expects($this->exactly(1)) - ->method('_renderPragma') - ->with(array( - 0 => '{{%IMPLICIT-ITERATOR}}', - 1 => 'IMPLICIT-ITERATOR', 'pragma_name' => 'IMPLICIT-ITERATOR', - 2 => null, 'options_string' => 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}}')); - } - - /** - * @dataProvider recursiveSectionData - */ - public function testRecursiveSections($template, $view, $result) { - $m = new Mustache(); - $this->assertEquals($result, $m->render($template, $view)); - } - - public function recursiveSectionData() { - return array( - array( - '{{%IMPLICIT-ITERATOR}}{{#items}}{{#.}}{{.}}{{/.}}{{/items}}', - array('items' => array(array('a', 'b', 'c'), array('d', 'e', 'f'))), - 'abcdef' - ), - array( - '{{%IMPLICIT-ITERATOR}}{{#items}}{{#.}}{{#.}}{{.}}{{/.}}{{/.}}{{/items}}', - array('items' => array(array(array('a', 'b'), array('c')), array(array('d'), array('e', 'f')))), - 'abcdef' - ), - array( - '{{%IMPLICIT-ITERATOR}}{{#items}}{{#.}}{{#items}}{{.}}{{/items}}{{/.}}{{/items}}', - array('items' => array( - array('items' => array('a', 'b', 'c')), - array('items' => array('d', 'e', 'f')), - )), - 'abcdef' - ), - ); - } -} \ No newline at end of file diff --git a/test/MustachePragmaTest.php b/test/MustachePragmaTest.php index 8be6e66..7952b03 100644 --- a/test/MustachePragmaTest.php +++ b/test/MustachePragmaTest.php @@ -22,23 +22,23 @@ class MustachePragmaTest extends PHPUnit_Framework_TestCase { public function testPragmaReplace() { $m = new Mustache(); - $this->assertEquals('', $m->render('{{%DOT-NOTATION}}'), 'Pragma tag not removed'); + $this->assertEquals('', $m->render('{{%UNESCAPED}}'), 'Pragma tag not removed'); } public function testPragmaReplaceMultiple() { $m = new Mustache(); - $this->assertEquals('', $m->render('{{% DOT-NOTATION }}'), 'Pragmas should allow whitespace'); - $this->assertEquals('', $m->render('{{% DOT-NOTATION foo=bar }}'), 'Pragmas should allow whitespace'); - $this->assertEquals('', $m->render("{{%DOT-NOTATION}}\n{{%DOT-NOTATION}}"), 'Multiple pragma tags not removed'); - $this->assertEquals(' ', $m->render('{{%DOT-NOTATION}} {{%DOT-NOTATION}}'), 'Multiple pragma tags not removed'); + $this->assertEquals('', $m->render('{{% UNESCAPED }}'), 'Pragmas should allow whitespace'); + $this->assertEquals('', $m->render('{{% UNESCAPED foo=bar }}'), 'Pragmas should allow whitespace'); + $this->assertEquals('', $m->render("{{%UNESCAPED}}\n{{%UNESCAPED}}"), 'Multiple pragma tags not removed'); + $this->assertEquals(' ', $m->render('{{%UNESCAPED}} {{%UNESCAPED}}'), 'Multiple pragma tags not removed'); } public function testPragmaReplaceNewline() { $m = new Mustache(); - $this->assertEquals('', $m->render("{{%DOT-NOTATION}}\n"), 'Trailing newline after pragma tag not removed'); - $this->assertEquals("\n", $m->render("\n{{%DOT-NOTATION}}\n"), 'Too many newlines removed with pragma tag'); - $this->assertEquals("1\n23", $m->render("1\n2{{%DOT-NOTATION}}\n3"), 'Wrong newline removed with pragma tag'); + $this->assertEquals('', $m->render("{{%UNESCAPED}}\n"), 'Trailing newline after pragma tag not removed'); + $this->assertEquals("\n", $m->render("\n{{%UNESCAPED}}\n"), 'Too many newlines removed with pragma tag'); + $this->assertEquals("1\n23", $m->render("1\n2{{%UNESCAPED}}\n3"), 'Wrong newline removed with pragma tag'); } public function testPragmaReset() { diff --git a/test/MustacheSpecTest.php b/test/MustacheSpecTest.php index d26c701..9f7f8b7 100644 --- a/test/MustacheSpecTest.php +++ b/test/MustacheSpecTest.php @@ -25,7 +25,7 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { * @group comments * @dataProvider loadCommentSpec */ - public function testCommentSpec($template, $data, $partials, $expected, $desc) { + public function testCommentSpec($desc, $template, $data, $partials, $expected) { $m = new Mustache($template, $data, $partials); $this->assertEquals($expected, $m->render(), $desc); } @@ -34,7 +34,7 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { * @group delimiters * @dataProvider loadDelimitersSpec */ - public function testDelimitersSpec($template, $data, $partials, $expected, $desc) { + public function testDelimitersSpec($desc, $template, $data, $partials, $expected) { $m = new Mustache($template, $data, $partials); $this->assertEquals($expected, $m->render(), $desc); } @@ -43,7 +43,7 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { * @group interpolation * @dataProvider loadInterpolationSpec */ - public function testInterpolationSpec($template, $data, $partials, $expected, $desc) { + public function testInterpolationSpec($desc, $template, $data, $partials, $expected) { $m = new Mustache($template, $data, $partials); $this->assertEquals($expected, $m->render(), $desc); } @@ -52,7 +52,7 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { * @group inverted-sections * @dataProvider loadInvertedSpec */ - public function testInvertedSpec($template, $data, $partials, $expected, $desc) { + public function testInvertedSpec($desc, $template, $data, $partials, $expected) { $m = new Mustache($template, $data, $partials); $this->assertEquals($expected, $m->render(), $desc); } @@ -61,7 +61,7 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { // * @group lambdas // * @dataProvider loadLambdasSpec // */ - // public function testLambdasSpec($template, $data, $partials, $expected, $desc) { + // 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', '>=')) { @@ -76,7 +76,7 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { * @group partials * @dataProvider loadPartialsSpec */ - public function testPartialsSpec($template, $data, $partials, $expected, $desc) { + public function testPartialsSpec($desc, $template, $data, $partials, $expected) { $m = new Mustache($template, $data, $partials); $this->assertEquals($expected, $m->render(), $desc); } @@ -85,7 +85,7 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { * @group sections * @dataProvider loadSectionsSpec */ - public function testSectionsSpec($template, $data, $partials, $expected, $desc) { + public function testSectionsSpec($desc, $template, $data, $partials, $expected) { $m = new Mustache($template, $data, $partials); $this->assertEquals($expected, $m->render(), $desc); } @@ -138,7 +138,7 @@ class MustacheSpecTest extends PHPUnit_Framework_TestCase { $spec = $yaml->parse(file_get_contents($filename)); foreach ($spec['tests'] as $test) { - $data[] = array($test['template'], $test['data'], isset($test['partials']) ? $test['partials'] : array(), $test['expected'], $test['desc']); + $data[] = array($test['name'] . ': ' . $test['desc'], $test['template'], $test['data'], isset($test['partials']) ? $test['partials'] : array(), $test['expected']); } return $data; } diff --git a/test/MustacheTest.php b/test/MustacheTest.php index 21714a7..6bb19d7 100644 --- a/test/MustacheTest.php +++ b/test/MustacheTest.php @@ -72,6 +72,54 @@ class MustacheTest extends PHPUnit_Framework_TestCase { $this->assertEquals($output, $m4->render($template)); } + /** + * @dataProvider constructorOptions + */ + public function testConstructorOptions($options, $charset, $delimiters, $pragmas) { + $mustache = new MustacheExposedOptionsStub(null, null, null, $options); + $this->assertEquals($charset, $mustache->getCharset()); + $this->assertEquals($delimiters, $mustache->getDelimiters()); + $this->assertEquals($pragmas, $mustache->getPragmas()); + } + + public function constructorOptions() { + return array( + array( + array(), + 'UTF-8', + array('{{', '}}'), + array(), + ), + array( + array( + 'charset' => 'UTF-8', + 'delimiters' => '<< >>', + 'pragmas' => array(Mustache::PRAGMA_UNESCAPED) + ), + 'UTF-8', + array('<<', '>>'), + array(Mustache::PRAGMA_UNESCAPED), + ), + array( + array( + 'charset' => 'cp866', + 'delimiters' => array('[[[[', ']]]]'), + 'pragmas' => array(Mustache::PRAGMA_UNESCAPED) + ), + 'cp866', + array('[[[[', ']]]]'), + array(Mustache::PRAGMA_UNESCAPED), + ), + ); + } + + /** + * @expectedException MustacheException + */ + public function testConstructorInvalidPragmaOptionsThrowExceptions() { + $mustache = new Mustache(null, null, null, array('pragmas' => array('banana phone'))); + } + /** * Test __toString() function. * @@ -366,4 +414,16 @@ class MustacheTest extends PHPUnit_Framework_TestCase { $m = new Mustache($template, $view); $this->assertEquals('{{win}}', $m->render()); } +} + +class MustacheExposedOptionsStub extends Mustache { + public function getPragmas() { + return $this->_pragmas; + } + public function getCharset() { + return $this->_charset; + } + public function getDelimiters() { + return array($this->_otag, $this->_ctag); + } } \ No newline at end of file diff --git a/test/spec b/test/spec index 2d9859f..3383fa6 160000 --- a/test/spec +++ b/test/spec @@ -1 +1 @@ -Subproject commit 2d9859f72c8a80e1e528004a18721c97e8411fc9 +Subproject commit 3383fa66e808a07fde1c291aa16a588d0a1a2a6d