From e0b9b6c522c84af4c1895c7dfada0183bb77fa69 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 26 Mar 2010 04:05:56 -0400 Subject: [PATCH 01/14] Added HandlebarMustache, an extended Mustache class which contains file handling for templates and partials. This will most likely be merged in with the main Mustache class once it's a little more solid. --- HandlebarMustache.php | 111 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 HandlebarMustache.php diff --git a/HandlebarMustache.php b/HandlebarMustache.php new file mode 100644 index 0000000..8382a1e --- /dev/null +++ b/HandlebarMustache.php @@ -0,0 +1,111 @@ +templateBase)) { + $this->setTemplateBase(dirname(__FILE__)); + } + } + + /** + * Override the current templateBase. + * + * @access public + * @param string $dir + * @return void + */ + public function setTemplateBase($dir) { + if (substr($dir, -1) !== '/') { + $dir .= '/'; + } + $this->templateBase = $dir; + } + + /** + * 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 $file + * @return void + */ + public function loadTemplate($file) { + if (strpos($file, '.') === false) { + $file .= '.mustache'; + } + + $filename = $this->templateBase . $file; + 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 ''; + } + } + } +} \ No newline at end of file From b8d4321e4aefb45a8d9ba93e8eae4eb6975f5a55 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 26 Mar 2010 04:07:08 -0400 Subject: [PATCH 02/14] Added TraversableMustache, a Mustache subclass that allows traversal of variables via dots... they approximately map to ->, as in ->bar->baz means the same thing as {{foo.bar.baz}} --- TraversableMustache.php | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 TraversableMustache.php diff --git a/TraversableMustache.php b/TraversableMustache.php new file mode 100644 index 0000000..a898740 --- /dev/null +++ b/TraversableMustache.php @@ -0,0 +1,49 @@ + 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 98422c110e471bfeb5ec6e8b503828281dded121 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Tue, 30 Mar 2010 14:00:10 -0400 Subject: [PATCH 03/14] Changed 'file' variable name to more appropriate 'name' variable. --- HandlebarMustache.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/HandlebarMustache.php b/HandlebarMustache.php index 8382a1e..0484375 100644 --- a/HandlebarMustache.php +++ b/HandlebarMustache.php @@ -57,15 +57,15 @@ class HandlebarMustache extends Mustache { * A '.mustache' file extension is assumed if none is provided in $file. * * @access public - * @param string $file + * @param string $name * @return void */ - public function loadTemplate($file) { - if (strpos($file, '.') === false) { - $file .= '.mustache'; + public function loadTemplate($name) { + if (strpos($name, '.') === false) { + $name .= '.mustache'; } - $filename = $this->templateBase . $file; + $filename = $this->templateBase . $name; if (file_exists($filename)) { $this->template = file_get_contents($filename); } else { From 1c40e4982545fd6132293a7af4018e118c73394d Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Tue, 30 Mar 2010 14:03:16 -0400 Subject: [PATCH 04/14] Added default template name based on underscorified class name. I.e. a class named FooBarBaz would try to load a template named foo_bar_baz.mustache in the current directory. --- HandlebarMustache.php | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/HandlebarMustache.php b/HandlebarMustache.php index 0484375..489a20a 100644 --- a/HandlebarMustache.php +++ b/HandlebarMustache.php @@ -20,6 +20,16 @@ class HandlebarMustache extends Mustache { */ protected $templateBase; + /** + * templateName. + * + * If none is specified, this will default to an underscorified version of the class name. + * + * @var string + * @access protected + */ + protected $templateName; + /** * HandlebarMustache class constructor. * @@ -36,6 +46,11 @@ class HandlebarMustache extends Mustache { if (!isset($this->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. @@ -108,4 +134,24 @@ class HandlebarMustache extends Mustache { } } } + + /** + * 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 88f67fc53eff6e3077138ba7849007f29db131e7 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Tue, 6 Apr 2010 01:07:13 -0400 Subject: [PATCH 05/14] added notes on purpose of the mustache subclasses. --- HandlebarMustache.php | 3 +++ TraversableMustache.php | 2 ++ 2 files changed, 5 insertions(+) diff --git a/HandlebarMustache.php b/HandlebarMustache.php index 489a20a..7dbdb8b 100644 --- a/HandlebarMustache.php +++ b/HandlebarMustache.php @@ -6,6 +6,9 @@ * This is an extended Mustache class which contains file handling for templates * and partial templates. * + * Once it's a bit more complete, this class will be merged with the parent Mustache class + * to provide a full-featured view layer... + * * @extends Mustache */ class HandlebarMustache extends Mustache { diff --git a/TraversableMustache.php b/TraversableMustache.php index a898740..79dfc8e 100644 --- a/TraversableMustache.php +++ b/TraversableMustache.php @@ -3,6 +3,8 @@ /** * TraversableMustache class. * + * This is an implementaiton of the DOT_NOTATION pragma. + * * A Mustache subclass which allows variable traversal via dots, i.e.: * * @code From 6e508d5b482819ec33eb2f67a31c991a60f85d68 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 25 Apr 2010 17:53:51 -0400 Subject: [PATCH 06/14] Rudimentary unit test script. --- test/MustacheTest.php | 110 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 test/MustacheTest.php diff --git a/test/MustacheTest.php b/test/MustacheTest.php new file mode 100644 index 0000000..ac02ada --- /dev/null +++ b/test/MustacheTest.php @@ -0,0 +1,110 @@ +assertEquals($m->render($template), $output); + } + + + /** + * Data provider for testExamples method. + * + * Assumes that an `examples` directory exists inside parent directory. + * This examples directory should contain any number of subdirectories, each of which contains + * three files: one Mustache class (.php), one Mustache template (.mustache), and one output file + * (.txt). + * + * This whole mess will be refined later to be more intuitive and less prescriptive, but it'll + * do for now. Especially since it means we can have unit tests :) + * + * @access public + * @return array + */ + public function getExamples() { + $basedir = dirname(__FILE__) . '/../examples/'; + + $ret = array(); + + $files = new RecursiveDirectoryIterator($basedir); + while ($files->valid()) { + + if ($files->hasChildren() && $children = $files->getChildren()) { + $example = $files->getSubPathname(); + $class = null; + $template = null; + $output = null; + + foreach ($children as $file) { + if (!$file->isFile()) continue; + + $filename = $file->getPathInfo(); + $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; + } + } + + $ret[$example] = array($class, $template, $output); + } + + $files->next(); + } + return $ret; + } +} \ No newline at end of file From 3108aff56d3fae25c098cb64efa5f440bd75c3d8 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Sun, 25 Apr 2010 17:55:43 -0400 Subject: [PATCH 07/14] fixing documentation for unit tests --- test/MustacheTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/MustacheTest.php b/test/MustacheTest.php index ac02ada..3cff074 100644 --- a/test/MustacheTest.php +++ b/test/MustacheTest.php @@ -27,7 +27,7 @@ require_once 'PHPUnit/Framework.php'; * * 1. {@link http://www.phpunit.de/manual/current/en/installation.html Install PHPUnit} * 2. run phpunit from the `test` directory: - * `phpunit MustacheTest` or just `phpunit` + * `phpunit MustacheTest` * 3. Fix bugs. Lather, rinse, repeat. * * @extends PHPUnit_Framework_TestCase From 5400cd91a03809537b6e64e1bc226c2415e8d897 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 23 Apr 2010 11:48:29 -0400 Subject: [PATCH 08/14] 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 fc170fca1103d68a4b1ae31cbfb4a9c4daf64105 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 26 Apr 2010 18:58:58 -0400 Subject: [PATCH 09/14] Add inverted section. --- Mustache.php | 35 +++++++++++++------ examples/inverted_section/InvertedSection.php | 5 +++ .../inverted_section.mustache | 2 ++ .../inverted_section/inverted_section.txt | 1 + 4 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 examples/inverted_section/InvertedSection.php create mode 100644 examples/inverted_section/inverted_section.mustache create mode 100644 examples/inverted_section/inverted_section.txt diff --git a/Mustache.php b/Mustache.php index d8d42f6..1b53c39 100644 --- a/Mustache.php +++ b/Mustache.php @@ -106,7 +106,7 @@ class Mustache { } /** - * Render boolean and enumerable sections. + * Render boolean, enumerable and inverted sections. * * @access protected * @param string $template @@ -120,23 +120,36 @@ class Mustache { $otag = $this->prepareRegEx($this->otag); $ctag = $this->prepareRegEx($this->ctag); - $regex = '/' . $otag . '\\#(.+?)' . $ctag . '\\s*([\\s\\S]+?)' . $otag . '\\/\\1' . $ctag . '\\s*/m'; + $regex = '/' . $otag . '(\\^|\\#)(.+?)' . $ctag . '\\s*([\\s\\S]+?)' . $otag . '\\/\\2' . $ctag . '\\s*/m'; $matches = array(); while (preg_match($regex, $template, $matches, PREG_OFFSET_CAPTURE)) { $section = $matches[0][0]; $offset = $matches[0][1]; - $tag_name = trim($matches[1][0]); - $content = $matches[2][0]; + $type = $matches[1][0]; + $tag_name = trim($matches[2][0]); + $content = $matches[3][0]; $replace = ''; $val = $this->getVariable($tag_name, $context); - if (is_array($val)) { - foreach ($val as $local_context) { - $replace .= $this->_render($content, $this->getContext($context, $local_context)); - } - } else if ($val) { - $replace .= $content; + switch($type) { + // inverted section + case '^': + if (empty($val)) { + $replace .= $content; + } + break; + + // regular section + case '#': + if (is_array($val)) { + foreach ($val as $local_context) { + $replace .= $this->_render($content, $this->getContext($context, $local_context)); + } + } else if ($val) { + $replace .= $content; + } + break; } $template = substr_replace($template, $replace, $offset, strlen($section)); @@ -293,7 +306,7 @@ class Mustache { $otag = $this->prepareRegEx($this->otag); $ctag = $this->prepareRegEx($this->ctag); - $this->tagRegEx = '/' . $otag . "(#|\/|=|!|>|\\{|&)?([^\/#]+?)\\1?" . $ctag . "+/"; + $this->tagRegEx = '/' . $otag . "(#|\/|=|!|>|\\{|&)?([^\/#\^]+?)\\1?" . $ctag . "+/"; return ''; } diff --git a/examples/inverted_section/InvertedSection.php b/examples/inverted_section/InvertedSection.php new file mode 100644 index 0000000..18eeb86 --- /dev/null +++ b/examples/inverted_section/InvertedSection.php @@ -0,0 +1,5 @@ +{{name}}{{/repo}} +{{^repo}}No repos :({{/repo}} \ No newline at end of file diff --git a/examples/inverted_section/inverted_section.txt b/examples/inverted_section/inverted_section.txt new file mode 100644 index 0000000..2b9ed3f --- /dev/null +++ b/examples/inverted_section/inverted_section.txt @@ -0,0 +1 @@ +No repos :( \ No newline at end of file From 57a6aafff1ebe15490a1c257fe84c0e6d9499cc0 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 26 Apr 2010 19:01:57 -0400 Subject: [PATCH 10/14] Fixed complex example to use inverted sections rather than fancy ruby-ish context swizzling (which never worked in PHP anyway). --- README.markdown | 1 - examples/complex/complex.mustache | 4 ++-- examples/complex/complex.php | 5 ----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/README.markdown b/README.markdown index bc50bf5..0634a0f 100644 --- a/README.markdown +++ b/README.markdown @@ -82,7 +82,6 @@ Known Issues ------------ * 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. diff --git a/examples/complex/complex.mustache b/examples/complex/complex.mustache index f3d8fea..4bf1687 100644 --- a/examples/complex/complex.mustache +++ b/examples/complex/complex.mustache @@ -5,9 +5,9 @@ {{#current}}
  • {{name}}
  • {{/current}} - {{#isLink}} + {{^current}}
  • {{name}}
  • - {{/isLink}} + {{/current}} {{/item}} {{/notEmpty}} diff --git a/examples/complex/complex.php b/examples/complex/complex.php index ca32ed7..ea55418 100644 --- a/examples/complex/complex.php +++ b/examples/complex/complex.php @@ -9,11 +9,6 @@ class Complex extends Mustache { array('name' => 'blue', 'current' => false, 'url' => '#Blue'), ); - public function isLink() { - // Exploit the fact that the current iteration item is at the top of the context stack. - return $this->getVariable('current', $this->context) != true; - } - public function notEmpty() { return !($this->isEmpty()); } From 6350e6180b6010820c6c39bcdbd0e9a6d1d898a3 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 26 Apr 2010 19:24:29 -0400 Subject: [PATCH 11/14] Inverting expected/actual param order in MustacheTest so the unit test output makes sense. --- test/MustacheTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/MustacheTest.php b/test/MustacheTest.php index 3cff074..b6a6384 100644 --- a/test/MustacheTest.php +++ b/test/MustacheTest.php @@ -46,7 +46,7 @@ class MustacheTest extends PHPUnit_Framework_TestCase { */ public function testExamples($class, $template, $output) { $m = new $class; - $this->assertEquals($m->render($template), $output); + $this->assertEquals($output, $m->render($template)); } From bb4905dd1b2081b38fa63e6b8329858e20e94be6 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 26 Apr 2010 20:35:46 -0400 Subject: [PATCH 12/14] Added known issue for missing child context in sections. --- README.markdown | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 0634a0f..5ee9dae 100644 --- a/README.markdown +++ b/README.markdown @@ -81,7 +81,10 @@ And render it: Known Issues ------------ - * Sections don't respect delimiter changes -- `delimiters` example currently fails with an "unclosed section" exception. + * Sections don't respect delimiter changes -- `delimiters` example currently fails with an + "unclosed section" exception. + * Sections don't have access to child context, i.e. `{{#foo}}{{bar}}{{/foo}}' fails if the context + is `array('foo' => array('bar' => 'baz'));` * Test coverage is incomplete. From 2fbc599c261746723e774ad8e0615ebf20a2cd47 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 26 Apr 2010 21:07:47 -0400 Subject: [PATCH 13/14] fixing markup typo in README --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 5ee9dae..e4334b5 100644 --- a/README.markdown +++ b/README.markdown @@ -83,7 +83,7 @@ Known Issues * Sections don't respect delimiter changes -- `delimiters` example currently fails with an "unclosed section" exception. - * Sections don't have access to child context, i.e. `{{#foo}}{{bar}}{{/foo}}' fails if the context + * Sections don't have access to child context, i.e. `{{#foo}}{{bar}}{{/foo}}` fails if the context is `array('foo' => array('bar' => 'baz'));` * Test coverage is incomplete. From 0450bcfb464b8b87cd37371f98cbcb398d452f07 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Mon, 26 Apr 2010 21:10:55 -0400 Subject: [PATCH 14/14] Removing traversable mustache class. This was moved into pragma-dot-notation feature branch. --- TraversableMustache.php | 51 ----------------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 TraversableMustache.php 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