From db4ff1058d8fb5a834285bf0408e4f89d1a20770 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 23 Apr 2010 11:57:24 -0400 Subject: [PATCH 01/10] Merging changes from TraversableMustache --- Mustache.php | 26 ++++++++++++++++++++- TraversableMustache.php | 51 ----------------------------------------- 2 files changed, 25 insertions(+), 52 deletions(-) delete mode 100644 TraversableMustache.php diff --git a/Mustache.php b/Mustache.php index d8d42f6..806e76a 100644 --- a/Mustache.php +++ b/Mustache.php @@ -24,7 +24,7 @@ class Mustache { // Override charset passed to htmlentities() and htmlspecialchars(). Defaults to UTF-8. protected $charset = 'UTF-8'; - + protected $tagRegEx; protected $template = ''; @@ -333,6 +333,30 @@ class Mustache { * @return string */ protected function getVariable($tag_name, &$context) { + $chunks = explode('.', $tag_name); + $first = array_shift($chunks); + + $ret = $this->_getVariable($first, $context); + while ($next = array_shift($chunks)) { + // Slice off a chunk of context for dot notation traversal. + $c = array($ret); + $ret = $this->_getVariable($next, $c); + } + + return $ret; + } + + /** + * Get a variable from the context array. Internal helper used by getVariable() to abstract + * variable traversal for dot notation. + * + * @access protected + * @param string $tag_name + * @param array &$context + * @throws MustacheException Unknown variable name. + * @return string + */ + protected function _getVariable($tag_name, &$context) { foreach ($context as $view) { if (is_object($view)) { if (isset($view->$tag_name)) { diff --git a/TraversableMustache.php b/TraversableMustache.php deleted file mode 100644 index 79dfc8e..0000000 --- a/TraversableMustache.php +++ /dev/null @@ -1,51 +0,0 @@ - array( - * 'three' => 'wheee!' - * ) - * ); - * - * protected $template = '{{one.two.three}}'; - * } - * $foo = new Foo; - * print $foo; - * @endcode - * - * (The above code prints 'wheee!') - * - * @extends Mustache - */ -class TraversableMustache extends Mustache { - - /** - * Override default getVariable method to allow object traversal via dots. - * This might be cool. Also, might be heinous. - * - * @access protected - * @param string $tag_name - * @param array &$context - * @return string - */ - protected function getVariable($tag_name, &$context) { - $chunks = explode('.', $tag_name); - $first = array_shift($chunks); - - $ret = parent::getVariable($first, $context); - while ($next = array_shift($chunks)) { - $c = array($ret); - $ret = parent::getVariable($next, $c); - } - - return $ret; - } -} \ No newline at end of file From d2ff6fa1accb292ceeb1d1f46af410f33adae532 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 23 Apr 2010 13:46:39 -0400 Subject: [PATCH 02/10] Example for dot notation pragma. --- examples/dot_notation/DotNotation.php | 14 ++++++++++++++ examples/dot_notation/dot_notation.mustache | 1 + examples/dot_notation/dot_notation.txt | 1 + 3 files changed, 16 insertions(+) create mode 100644 examples/dot_notation/DotNotation.php create mode 100644 examples/dot_notation/dot_notation.mustache create mode 100644 examples/dot_notation/dot_notation.txt diff --git a/examples/dot_notation/DotNotation.php b/examples/dot_notation/DotNotation.php new file mode 100644 index 0000000..d5f1dd0 --- /dev/null +++ b/examples/dot_notation/DotNotation.php @@ -0,0 +1,14 @@ + array( + 'baz' => 'Qux', + ) + ); +} diff --git a/examples/dot_notation/dot_notation.mustache b/examples/dot_notation/dot_notation.mustache new file mode 100644 index 0000000..85873f7 --- /dev/null +++ b/examples/dot_notation/dot_notation.mustache @@ -0,0 +1 @@ +Grandchild is "{{foo.bar.baz}}". \ No newline at end of file diff --git a/examples/dot_notation/dot_notation.txt b/examples/dot_notation/dot_notation.txt new file mode 100644 index 0000000..c7b8cd2 --- /dev/null +++ b/examples/dot_notation/dot_notation.txt @@ -0,0 +1 @@ +Grandchild is "Qux". \ No newline at end of file From 818a07a64ae05e45b25cd75b41539fef082000f4 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 23 Apr 2010 14:14:06 -0400 Subject: [PATCH 03/10] Added more complex dot notation example (from defunkt/mustache). --- examples/dot_notation/DotNotation.php | 13 +++++++++---- examples/dot_notation/dot_notation.mustache | 7 ++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/examples/dot_notation/DotNotation.php b/examples/dot_notation/DotNotation.php index d5f1dd0..0274e19 100644 --- a/examples/dot_notation/DotNotation.php +++ b/examples/dot_notation/DotNotation.php @@ -2,13 +2,18 @@ /** * DotNotation example class. Uses DOT_NOTATION pragma. - * + * * @extends Mustache */ class DotNotation extends Mustache { - public $foo = array( - 'bar' => array( - 'baz' => 'Qux', + public $person = array( + 'name' => array('first' => 'Chris', 'last' => 'Firescythe'), + 'age' => 24, + 'hometown' => array( + 'city' => 'Cincinnati', + 'state' => 'OH', ) ); + + public $normal = 'Normal'; } diff --git a/examples/dot_notation/dot_notation.mustache b/examples/dot_notation/dot_notation.mustache index 85873f7..12cbf7f 100644 --- a/examples/dot_notation/dot_notation.mustache +++ b/examples/dot_notation/dot_notation.mustache @@ -1 +1,6 @@ -Grandchild is "{{foo.bar.baz}}". \ No newline at end of file +* {{person.name.first}} {{person.name.last}} +* {{person.age}} +* {{person.hometown.city}}, {{person.hometown.state}} +* {{#person}}{{hometown.city}}, {{hometown.state}}{{/person}} +* {{#person}}{{#hometown}}{{city}}, {{state}}{{/hometown}}{{/person}} +* {{normal}} \ No newline at end of file From 8cbf5ebb0fffc1010c3e23cb48479c1ae370f60e Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 25 Apr 2010 18:05:54 -0400 Subject: [PATCH 04/10] Added known issue for current pragma limitation. --- README.markdown | 1 + 1 file changed, 1 insertion(+) diff --git a/README.markdown b/README.markdown index bc50bf5..4282594 100644 --- a/README.markdown +++ b/README.markdown @@ -81,6 +81,7 @@ And render it: Known Issues ------------ + * There's no way to toggle a pragma other than checking out the feature branch for each pragma... Need a clean way to do this. * Sections don't respect delimiter changes -- `delimiters` example currently fails with an "unclosed section" exception. * Since `complex` example emulates some fancy swizzling available in Ruby, it fails. Need to convert example to PHPisms (In PHP, Mustache class doesn't maintain current context stack -- available context is passed to methods via params). * Test coverage is incomplete. From f5b4c7ddf1d3c42051c8fac824476e2a123e766d Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 23 Apr 2010 11:48:29 -0400 Subject: [PATCH 05/10] Removing now-deprecated HandlebarMustache class. --- HandlebarMustache.php | 160 ------------------------------------------ 1 file changed, 160 deletions(-) delete mode 100644 HandlebarMustache.php diff --git a/HandlebarMustache.php b/HandlebarMustache.php deleted file mode 100644 index 7dbdb8b..0000000 --- a/HandlebarMustache.php +++ /dev/null @@ -1,160 +0,0 @@ -templateBase)) { - $this->setTemplateBase(dirname(__FILE__)); - } - - // default template name is the underscorified class name. - if (!isset($this->templateName)) { - $this->templateName = strtolower(preg_replace('#(?templateBase = $dir; - } - - /** - * Override the default templateName. - * - * @access public - * @param string $name - * @return void - */ - public function setTemplateName($name) { - $this->templateName = $name; - } - - /** - * Load a template file. This file will be relative to $this->templateBase. - * A '.mustache' file extension is assumed if none is provided in $file. - * - * @access public - * @param string $name - * @return void - */ - public function loadTemplate($name) { - if (strpos($name, '.') === false) { - $name .= '.mustache'; - } - - $filename = $this->templateBase . $name; - if (file_exists($filename)) { - $this->template = file_get_contents($filename); - } else { - $this->template = null; - } - } - - /** - * Load a partial, either from $this->partials or from a file in the templateBase - * directory. - * - * @access protected - * @param string $tag_name - * @return string Partial template. - */ - protected function getPartial($tag_name) { - try { - if ($result = parent::getPartial($tag_name)) { - return $result; - } - } catch (MustacheException $e) { - // Ignore the UNKNOWN_PARTIAL exceptions, we'll just look for a template file. - if ($e->getCode() !== MustacheException::UNKNOWN_PARTIAL) { - throw $e; - } - } - - $filename = $this->templateBase . $tag_name . '.mustache'; - if (file_exists($filename)) { - $this->partials[$tag_name] = file_get_contents($filename); - return $this->partials[$tag_name]; - } else { - if ($this->throwPartialExceptions) { - throw new MustacheException( - 'Unknown partial: ' . $tag_name, - MustacheException::UNKNOWN_PARTIAL - ); - } else { - return ''; - } - } - } - - /** - * Render the given template and view object. - * - * Defaults to the template and view passed to the class constructor unless a new one is provided. - * Optionally, pass an associative array of partials as well. - * - * @access public - * @param string $template (default: null) - * @param mixed $view (default: null) - * @param array $partials (default: null) - * @return string Rendered Mustache template. - */ - public function render($template = null, $view = null, $partials = null) { - if ($template === null && !isset($this->template)) { - $this->loadTemplate($this->templateName); - } - - return parent::render($template, $view, $partials); - } -} \ No newline at end of file From 81fa51c1af71e501debbd996f536edd3aa910e50 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 26 Apr 2010 20:33:54 -0400 Subject: [PATCH 06/10] Simplified dot notation example, fixed sample output. --- examples/dot_notation/dot_notation.mustache | 2 -- examples/dot_notation/dot_notation.txt | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/dot_notation/dot_notation.mustache b/examples/dot_notation/dot_notation.mustache index 12cbf7f..0566867 100644 --- a/examples/dot_notation/dot_notation.mustache +++ b/examples/dot_notation/dot_notation.mustache @@ -1,6 +1,4 @@ * {{person.name.first}} {{person.name.last}} * {{person.age}} * {{person.hometown.city}}, {{person.hometown.state}} -* {{#person}}{{hometown.city}}, {{hometown.state}}{{/person}} -* {{#person}}{{#hometown}}{{city}}, {{state}}{{/hometown}}{{/person}} * {{normal}} \ No newline at end of file diff --git a/examples/dot_notation/dot_notation.txt b/examples/dot_notation/dot_notation.txt index c7b8cd2..f8cf1fa 100644 --- a/examples/dot_notation/dot_notation.txt +++ b/examples/dot_notation/dot_notation.txt @@ -1 +1,4 @@ -Grandchild is "Qux". \ No newline at end of file +* Chris Firescythe +* 24 +* Cincinnati, OH +* Normal \ No newline at end of file From 537ebc44c8df4ba45f5a2f9c28887a47122b5c07 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 28 Apr 2010 21:51:54 -0400 Subject: [PATCH 07/10] Initial work adding pragma detection to Mustache.php Currently stores pragmas and options, but does nothing with them. Also throws exceptions when it encounters unknown pragmas. Test coverage only for exceptions and pragma tag removal. --- Mustache.php | 66 +++++++++++++++++++++++++++++++++++-- test/MustachePragmaTest.php | 40 ++++++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 test/MustachePragmaTest.php diff --git a/Mustache.php b/Mustache.php index e1f69cd..6bdc6a4 100644 --- a/Mustache.php +++ b/Mustache.php @@ -30,6 +30,9 @@ class Mustache { protected $template = ''; protected $context = array(); protected $partials = array(); + protected $pragmas = array(); + + protected $pragmasImplemented = array('DOT-NOTATION'); /** * Mustache class constructor. @@ -101,6 +104,7 @@ class Mustache { * @return string Rendered Mustache template. */ protected function _render($template, &$context) { + $template = $this->renderPragmas($template, $context); $template = $this->renderSection($template, $context); return $this->renderTags($template, $context); } @@ -162,6 +166,60 @@ class Mustache { return $template; } + /** + * Initialize pragmas and remove all pragma tags. + * + * @access protected + * @param string $template + * @param array &$context + * @return string + */ + protected function renderPragmas($template, &$context) { + // no pragmas + if (strpos($template, $this->otag . '%') === false) { + return $template; + } + + $otag = $this->prepareRegEx($this->otag); + $ctag = $this->prepareRegEx($this->ctag); + $regex = '/' . $otag . '%([\\w_-]+)((?: [\\w]+=[\\w]+)*)' . $ctag . '\\n?/'; + return preg_replace_callback($regex, array($this, 'renderPragma'), $template); + } + + /** + * A preg_replace helper to remove {{%PRAGMA}} tags and enable requested pragma. + * + * @access protected + * @param mixed $matches + * @return void + * @throws MustacheException unknown pragma + */ + protected function renderPragma($matches) { + $pragma = $matches[0]; + $pragma_name = $matches[1]; + $options_string = $matches[2]; + + if (!in_array($pragma_name, $this->pragmasImplemented)) { + throw new MustacheException('Unknown pragma: ' . $pragma_name, MustacheException::UNKNOWN_PRAGMA); + } + + $options = array(); + foreach (explode(' ', trim($options_string)) as $o) { + if ($p = trim($o)) { + $p = explode('=', trim($p)); + $options[$p[0]] = $p[1]; + } + } + + if (empty($options)) { + $this->pragmas[$pragma_name] = true; + } else { + $this->pragmas[$pragma_name] = $options; + } + + return ''; + } + /** * Loop through and render individual Mustache tags. * @@ -175,8 +233,8 @@ class Mustache { return $template; } - $otag = $this->prepareRegEx($this->otag); - $ctag = $this->prepareRegEx($this->ctag); + $otag = $this->prepareRegEx($this->otag); + $ctag = $this->prepareRegEx($this->ctag); $this->tagRegEx = '/' . $otag . "(#|\/|=|!|>|\\{|&)?([^\/#]+?)\\1?" . $ctag . "+/"; $html = ''; $matches = array(); @@ -442,4 +500,8 @@ class MustacheException extends Exception { // with no associated partial. const UNKNOWN_PARTIAL = 3; + // An UNKNOWN_PRAGMA exception is thrown whenever a {{%PRAGMA}} tag appears + // which can't be handled by this Mustache instance. + const UNKNOWN_PRAGMA = 4; + } \ No newline at end of file diff --git a/test/MustachePragmaTest.php b/test/MustachePragmaTest.php new file mode 100644 index 0000000..49937d1 --- /dev/null +++ b/test/MustachePragmaTest.php @@ -0,0 +1,40 @@ +render('{{%I-HAVE-THE-GREATEST-MUSTACHE}}'); + } catch (Exception $e) { + $caught_exception = $e; + } + + $this->assertNotNull($caught_exception, 'No exception caught'); + $this->assertType('MustacheException', $caught_exception); + $this->assertEquals($caught_exception->getCode(), MustacheException::UNKNOWN_PRAGMA, 'Caught exception code was not MustacheException::UNKNOWN_PRAGMA'); + } + + public function testPragmaReplace() { + $m = new Mustache(); + $this->assertEquals($m->render('{{%DOT-NOTATION}}'), '', 'Pragma tag not removed'); + } + + public function testPragmaReplaceMultiple() { + $m = new Mustache(); + $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'); + } + + public function testPragmaReplaceNewline() { + $m = new Mustache(); + $this->assertEquals($m->render("{{%DOT-NOTATION}}\n"), '', 'Trailing newline after pragma tag not removed'); + $this->assertEquals($m->render("\n{{%DOT-NOTATION}}\n"), "\n", 'Too many newlines removed with pragma tag'); + $this->assertEquals($m->render("1\n2{{%DOT-NOTATION}}\n3"), "1\n23", 'Wrong newline removed with pragma tag'); + } +} \ No newline at end of file From e962cbac8c1e90574ae64c37f15afffe114f57c5 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 29 Apr 2010 19:24:44 -0400 Subject: [PATCH 08/10] Cleaned up UNKNOWN PRAGMA exception test. --- test/MustachePragmaTest.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/MustachePragmaTest.php b/test/MustachePragmaTest.php index 49937d1..db21269 100644 --- a/test/MustachePragmaTest.php +++ b/test/MustachePragmaTest.php @@ -7,17 +7,15 @@ class MustachePragmaTest extends PHPUnit_Framework_TestCase { public function testUnknownPragmaException() { $m = new Mustache(); - $caught_exception = null; try { $m->render('{{%I-HAVE-THE-GREATEST-MUSTACHE}}'); - } catch (Exception $e) { - $caught_exception = $e; + } catch (MustacheException $e) { + $this->assertEquals(MustacheException::UNKNOWN_PRAGMA, $e->getCode(), 'Caught exception code was not MustacheException::UNKNOWN_PRAGMA'); + return; } - $this->assertNotNull($caught_exception, 'No exception caught'); - $this->assertType('MustacheException', $caught_exception); - $this->assertEquals($caught_exception->getCode(), MustacheException::UNKNOWN_PRAGMA, 'Caught exception code was not MustacheException::UNKNOWN_PRAGMA'); + $this->fail('Mustache should have thrown an unknown pragma exception'); } public function testPragmaReplace() { @@ -37,4 +35,5 @@ class MustachePragmaTest extends PHPUnit_Framework_TestCase { $this->assertEquals($m->render("\n{{%DOT-NOTATION}}\n"), "\n", 'Too many newlines removed with pragma tag'); $this->assertEquals($m->render("1\n2{{%DOT-NOTATION}}\n3"), "1\n23", 'Wrong newline removed with pragma tag'); } + } \ No newline at end of file From 2ed23003e7b7cbad29fc83a710307424ba9aa3df Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 29 Apr 2010 20:46:12 -0400 Subject: [PATCH 09/10] Made dot-notation pragma toggleable, added test cases. --- Mustache.php | 43 +++++++++++++++++++------- test/MustachePragmaDotNotationTest.php | 34 ++++++++++++++++++++ 2 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 test/MustachePragmaDotNotationTest.php diff --git a/Mustache.php b/Mustache.php index d30de53..e47e532 100644 --- a/Mustache.php +++ b/Mustache.php @@ -25,6 +25,8 @@ class Mustache { // Override charset passed to htmlentities() and htmlspecialchars(). Defaults to UTF-8. protected $charset = 'UTF-8'; + const PRAGMA_DOT_NOTATION = 'DOT-NOTATION'; + protected $tagRegEx; protected $template = ''; @@ -32,7 +34,9 @@ class Mustache { protected $partials = array(); protected $pragmas = array(); - protected $pragmasImplemented = array('DOT-NOTATION'); + protected $pragmasImplemented = array( + self::PRAGMA_DOT_NOTATION + ); /** * Mustache class constructor. @@ -168,7 +172,7 @@ class Mustache { /** * Initialize pragmas and remove all pragma tags. - * + * * @access protected * @param string $template * @param array &$context @@ -220,6 +224,20 @@ class Mustache { return ''; } + protected function hasPragma($pragma_name) { + if (array_key_exists($pragma_name, $this->pragmas) && $this->pragmas[$pragma_name]) { + return true; + } + } + + protected function getPragmaOptions($pragma_name) { + if (!$this->hasPragma()) { + throw new MustacheException('Unknown pragma: ' . $pragma_name, MustacheException::UNKNOWN_PRAGMA); + } + + return $this->pragmas[$pragma_name]; + } + /** * Loop through and render individual Mustache tags. * @@ -408,17 +426,20 @@ class Mustache { * @return string */ protected function getVariable($tag_name, &$context) { - $chunks = explode('.', $tag_name); - $first = array_shift($chunks); + if ($this->hasPragma(self::PRAGMA_DOT_NOTATION)) { + $chunks = explode('.', $tag_name); + $first = array_shift($chunks); - $ret = $this->_getVariable($first, $context); - while ($next = array_shift($chunks)) { - // Slice off a chunk of context for dot notation traversal. - $c = array($ret); - $ret = $this->_getVariable($next, $c); + $ret = $this->_getVariable($first, $context); + while ($next = array_shift($chunks)) { + // Slice off a chunk of context for dot notation traversal. + $c = array($ret); + $ret = $this->_getVariable($next, $c); + } + return $ret; + } else { + return $this->_getVariable($tag_name, $context); } - - return $ret; } /** diff --git a/test/MustachePragmaDotNotationTest.php b/test/MustachePragmaDotNotationTest.php new file mode 100644 index 0000000..d6d31b7 --- /dev/null +++ b/test/MustachePragmaDotNotationTest.php @@ -0,0 +1,34 @@ + 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'); + } + +} \ No newline at end of file From 0ff5a335059b982e987d1e2a7db594eb4a539dcb Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 29 Apr 2010 20:46:35 -0400 Subject: [PATCH 10/10] Updated dot-notation example to include {{%PRAGMA}} tag. --- examples/dot_notation/dot_notation.mustache | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/dot_notation/dot_notation.mustache b/examples/dot_notation/dot_notation.mustache index 0566867..4831386 100644 --- a/examples/dot_notation/dot_notation.mustache +++ b/examples/dot_notation/dot_notation.mustache @@ -1,3 +1,4 @@ +{{%DOT-NOTATION}} * {{person.name.first}} {{person.name.last}} * {{person.age}} * {{person.hometown.city}}, {{person.hometown.state}}