Kenneth Reitz
2010-05-03 16:23:30 -04:00
parent 3eb13e33ea
commit b3f759dc13
43 changed files with 908 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
Mustache.php for WordPress
=================================
[Mustache.php](http://github.com/bobthecow/mustache.php) by Justin Hileman is a [Mustache](http://defunkt.github.com/mustache/) implementation in PHP.
WordPress adaption by Kenneth Reitz.
Usage
-----
A quick example:
<?php
include('Mustache.php');
$m = new Mustache;
echo $m->render('Hello {{planet}}', array('planet' => 'World!'));
// Renders: "Hello World!"
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://mustache.github.com/mustache.5.html) man pages.
View File
+21
View File
@@ -0,0 +1,21 @@
<?php /*
Plugin Name: Mustache
Plugin URI: http://github.com/bobthecow/mustache.php
Description: Defunkt's Mustache is a framework-agnostic logic-less templating language.
Version: 0.8
Author: Justin Hileman
Author URI: http://justinhileman.com
Min WP Version: 2.0
Max WP Version: 3.5
License: MIT License - http://www.opensource.org/licenses/mit-license.php
Mustache is a framework-agnostic logic-less templating language. It enforces separation of view logic from template files. In fact, it is not even possible to embed logic in the template.
*/
try {
require_once(dirname(__FILE__).DIRECTORY_SEPARATOR.'mustache'.DIRECTORY_SEPARATOR.'mustache.php');
} catch (Exception $e) {
// Just in case
}
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2010 Justin Hileman
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
+446
View File
@@ -0,0 +1,446 @@
<?php
/**
* A Mustache implementation in PHP.
*
* {@link http://defunkt.github.com/mustache}
*
* Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
* logic from template files. In fact, it is not even possible to embed logic in the template.
*
* This is very, very rad.
*
* @author Justin Hileman {@link http://justinhileman.com}
*/
class Mustache {
public $otag = '{{';
public $ctag = '}}';
// Should this Mustache throw exceptions when it finds unexpected tags?
protected $throwSectionExceptions = true;
protected $throwPartialExceptions = false;
protected $throwVariableExceptions = false;
// Override charset passed to htmlentities() and htmlspecialchars(). Defaults to UTF-8.
protected $charset = 'UTF-8';
protected $tagRegEx;
protected $template = '';
protected $context = array();
protected $partials = array();
/**
* Mustache class constructor.
*
* This method accepts a $template string and a $view object. 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 void
*/
public function __construct($template = null, $view = null, $partials = null) {
if ($template !== null) $this->template = $template;
if ($partials !== null) $this->partials = $partials;
if ($view !== null) $this->context = array($view);
}
/**
* 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) $template = $this->template;
if ($partials !== null) $this->partials = $partials;
if ($view) {
$this->context = array($view);
} else if (empty($this->context)) {
$this->context = array($this);
}
return $this->_render($template, $this->context);
}
/**
* Wrap the render() function for string conversion.
*
* @access public
* @return string
*/
public function __toString() {
// PHP doesn't like exceptions in __toString.
// catch any exceptions and convert them to strings.
try {
$result = $this->render();
return $result;
} catch (Exception $e) {
return "Error rendering mustache: " . $e->getMessage();
}
}
/**
* Internal render function, used for recursive calls.
*
* @access protected
* @param string $template
* @param array &$context
* @return string Rendered Mustache template.
*/
protected function _render($template, &$context) {
$template = $this->renderSection($template, $context);
return $this->renderTags($template, $context);
}
/**
* Render boolean, enumerable and inverted sections.
*
* @access protected
* @param string $template
* @param array $context
* @return string
*/
protected function renderSection($template, &$context) {
if (strpos($template, $this->otag . '#') === false) {
return $template;
}
$otag = $this->prepareRegEx($this->otag);
$ctag = $this->prepareRegEx($this->ctag);
$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];
$type = $matches[1][0];
$tag_name = trim($matches[2][0]);
$content = $matches[3][0];
$replace = '';
$val = $this->getVariable($tag_name, $context);
switch($type) {
// inverted section
case '^':
if (empty($val)) {
$replace .= $content;
}
break;
// regular section
case '#':
if ($this->varIsIterable($val)) {
foreach ($val as $local_context) {
$c = $this->getContext($context, $local_context);
$replace .= $this->_render($content, $c);
}
} else if ($val) {
if (is_array($val) || is_object($val)) {
$replace .= $this->_render($content, $this->getContext($context, $val));
} else {
$replace .= $content;
}
}
break;
}
$template = substr_replace($template, $replace, $offset, strlen($section));
}
return $template;
}
/**
* Loop through and render individual Mustache tags.
*
* @access protected
* @param string $template
* @param array $context
* @return void
*/
protected function renderTags($template, &$context) {
if (strpos($template, $this->otag) === false) {
return $template;
}
$otag = $this->prepareRegEx($this->otag);
$ctag = $this->prepareRegEx($this->ctag);
$this->tagRegEx = '/' . $otag . "(#|\/|=|!|>|\\{|&)?([^\/#]+?)\\1?" . $ctag . "+/";
$html = '';
$matches = array();
while (preg_match($this->tagRegEx, $template, $matches, PREG_OFFSET_CAPTURE)) {
$tag = $matches[0][0];
$offset = $matches[0][1];
$modifier = $matches[1][0];
$tag_name = trim($matches[2][0]);
$html .= substr($template, 0, $offset);
$html .= $this->renderTag($modifier, $tag_name, $context);
$template = substr($template, $offset + strlen($tag));
}
return $html . $template;
}
/**
* Render the named tag, given the specified modifier.
*
* Accepted modifiers are `=` (change delimiter), `!` (comment), `>` (partial)
* `{` or `&` (don't escape output), or none (render escaped output).
*
* @access protected
* @param string $modifier
* @param string $tag_name
* @param array $context
* @throws MustacheException Unmatched section tag encountered.
* @return string
*/
protected function renderTag($modifier, $tag_name, &$context) {
switch ($modifier) {
case '#':
if ($this->throwSectionExceptions) {
throw new MustacheException('Unclosed section: ' . $tag_name, MustacheException::UNCLOSED_SECTION);
} else {
return '';
}
break;
case '/':
if ($this->throwSectionExceptions) {
throw new MustacheException('Unexpected close section: ' . $tag_name, MustacheException::UNEXPECTED_CLOSE_SECTION);
} else {
return '';
}
break;
case '=':
return $this->changeDelimiter($tag_name, $context);
break;
case '!':
return $this->renderComment($tag_name, $context);
break;
case '>':
return $this->renderPartial($tag_name, $context);
break;
case '{':
case '&':
return $this->renderUnescaped($tag_name, $context);
break;
case '':
default:
return $this->renderEscaped($tag_name, $context);
break;
}
}
/**
* Escape and return the requested tag.
*
* @access protected
* @param string $tag_name
* @param array $context
* @return string
*/
protected function renderEscaped($tag_name, &$context) {
return htmlentities($this->getVariable($tag_name, $context), null, $this->charset);
}
/**
* Render a comment (i.e. return an empty string).
*
* @access protected
* @param string $tag_name
* @param array $context
* @return string
*/
protected function renderComment($tag_name, &$context) {
return '';
}
/**
* Return the requested tag unescaped.
*
* @access protected
* @param string $tag_name
* @param array $context
* @return string
*/
protected function renderUnescaped($tag_name, &$context) {
return $this->getVariable($tag_name, $context);
}
/**
* Render the requested partial.
*
* @access protected
* @param string $tag_name
* @param array $context
* @return string
*/
protected function renderPartial($tag_name, &$context) {
$view = new self($this->getPartial($tag_name), $context, $this->partials);
$view->otag = $this->otag;
$view->ctag = $this->ctag;
return $view->render();
}
/**
* Change the Mustache tag delimiter. This method also replaces this object's current
* tag RegEx with one using the new delimiters.
*
* @access protected
* @param string $tag_name
* @param array $context
* @return string
*/
protected function changeDelimiter($tag_name, &$context) {
$tags = explode(' ', $tag_name);
$this->otag = $tags[0];
$this->ctag = $tags[1];
$otag = $this->prepareRegEx($this->otag);
$ctag = $this->prepareRegEx($this->ctag);
$this->tagRegEx = '/' . $otag . "(#|\/|=|!|>|\\{|&)?([^\/#\^]+?)\\1?" . $ctag . "+/";
return '';
}
/**
* Prepare a new context reference array.
*
* This is used to create context arrays for iterable blocks.
*
* @access protected
* @param array $context
* @param mixed $local_context
* @return void
*/
protected function getContext(&$context, &$local_context) {
$ret = array();
$ret[] =& $local_context;
foreach ($context as $view) {
$ret[] =& $view;
}
return $ret;
}
/**
* Get a variable from the context array.
*
* If the view is an array, returns the value with array key $tag_name.
* If the view is an object, this will check for a public member variable
* named $tag_name. If none is available, this method will execute and return
* any class method named $tag_name. Failing all of the above, this method will
* return an empty string.
*
* @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)) {
return $view->$tag_name;
} else if (method_exists($view, $tag_name)) {
return $view->$tag_name();
}
} else if (isset($view[$tag_name])) {
return $view[$tag_name];
}
}
if ($this->throwVariableExceptions) {
throw new MustacheException("Unknown variable: " . $tag_name, MustacheException::UNKNOWN_VARIABLE);
} else {
return '';
}
}
/**
* Retrieve the partial corresponding to the requested tag name.
*
* Silently fails (i.e. returns '') when the requested partial is not found.
*
* @access protected
* @param string $tag_name
* @throws MustacheException Unknown partial name.
* @return string
*/
protected function getPartial($tag_name) {
if (is_array($this->partials) && isset($this->partials[$tag_name])) {
return $this->partials[$tag_name];
}
if ($this->throwPartialExceptions) {
throw new MustacheException('Unknown partial: ' . $tag_name, MustacheException::UNKNOWN_PARTIAL);
} else {
return '';
}
}
/**
* Check whether the given $var should be iterated (i.e. in a section context).
*
* @access protected
* @param mixed $var
* @return bool
*/
protected function varIsIterable($var) {
return is_object($var) || (is_array($var) && !array_diff_key($var, array_keys(array_keys($var))));
}
/**
* Prepare a string to be used in a regular expression.
*
* @access protected
* @param string $str
* @return string
*/
protected function prepareRegEx($str) {
$replace = array(
'\\' => '\\\\', '^' => '\^', '.' => '\.', '$' => '\$', '|' => '\|', '(' => '\(',
')' => '\)', '[' => '\[', ']' => '\]', '*' => '\*', '+' => '\+', '?' => '\?',
'{' => '\{', '}' => '\}', ',' => '\,'
);
return strtr($str, $replace);
}
}
/**
* MustacheException class.
*
* @extends Exception
*/
class MustacheException extends Exception {
// An UNKNOWN_VARIABLE exception is thrown when a {{variable}} is not found
// in the current context.
const UNKNOWN_VARIABLE = 0;
// An UNCLOSED_SECTION exception is thrown when a {{#section}} is not closed.
const UNCLOSED_SECTION = 1;
// An UNEXPECTED_CLOSE_SECTION exception is thrown when {{/section}} appears
// without a corresponding {{#section}}.
const UNEXPECTED_CLOSE_SECTION = 2;
// An UNKNOWN_PARTIAL exception is thrown whenever a {{>partial}} tag appears
// with no associated partial.
const UNKNOWN_PARTIAL = 3;
}
+93
View File
@@ -0,0 +1,93 @@
Mustache.php
============
A [Mustache](http://defunkt.github.com/mustache/) implementation in PHP.
Usage
-----
A quick example:
<?php
include('Mustache.php');
$m = new Mustache;
echo $m->render('Hello {{planet}}', array('planet' => 'World!'));
// "Hello World!"
?>
And a more in-depth example--this is the canonical Mustache template:
Hello {{name}}
You have just won ${{value}}!
{{#in_ca}}
Well, ${{taxed_value}}, after taxes.
{{/in_ca}}
Along with the associated Mustache class:
<?php
class Chris extends Mustache {
public $name = "Chris";
public $value = 10000;
public function taxed_value() {
return $this->value - ($this->value * 0.4);
}
public $in_ca = true;
}
Render it like so:
<?php
$c = new Chris;
echo $chris->render($template);
?>
Here's the same thing, a different way:
Create a view object--which could also be an associative array, but those don't do functions quite as well:
<?php
class Chris {
public $name = "Chris";
public $value = 10000;
public function taxed_value() {
return $this->value - ($this->value * 0.4);
}
public $in_ca = true;
}
?>
And render it:
<?php
$chris = new Chris;
$m = new Mustache;
echo $m->render($template, $chris);
?>
Known Issues
------------
* Sections don't respect delimiter changes -- `delimiters` example currently fails with an
"unclosed section" exception.
* Test coverage is incomplete.
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.
@@ -0,0 +1,13 @@
<?php
class ChildContext extends Mustache {
public $parent = array(
'child' => 'child works',
);
public $grandparent = array(
'parent' => array(
'child' => 'grandchild works',
),
);
}
@@ -0,0 +1,2 @@
<h1>{{#parent}}{{child}}{{/parent}}</h1>
<h2>{{#grandparent}}{{#parent}}{{child}}{{/parent}}{{/grandparent}}</h2>
@@ -0,0 +1,2 @@
<h1>child works</h1>
<h2>grandchild works</h2>
@@ -0,0 +1,7 @@
<?php
class Comments extends Mustache {
public function title() {
return 'A Comedy of Errors';
}
}
@@ -0,0 +1 @@
<h1>{{title}}{{! just something interesting... or not... }}</h1>
@@ -0,0 +1 @@
<h1>A Comedy of Errors</h1>
@@ -0,0 +1,16 @@
<h1>{{header}}</h1>
{{#notEmpty}}
<ul>
{{#item}}
{{#current}}
<li><strong>{{name}}</strong></li>
{{/current}}
{{^current}}
<li><a href="{{url}}">{{name}}</a></li>
{{/current}}
{{/item}}
</ul>
{{/notEmpty}}
{{#isEmpty}}
<p>The list is empty.</p>
{{/isEmpty}}
+19
View File
@@ -0,0 +1,19 @@
<?php
class Complex extends Mustache {
public $header = 'Colors';
public $item = array(
array('name' => 'red', 'current' => true, 'url' => '#Red'),
array('name' => 'green', 'current' => false, 'url' => '#Green'),
array('name' => 'blue', 'current' => false, 'url' => '#Blue'),
);
public function notEmpty() {
return !($this->isEmpty());
}
public function isEmpty() {
return count($this->item) === 0;
}
}
@@ -0,0 +1,6 @@
<h1>Colors</h1>
<ul>
<li><strong>red</strong></li>
<li><a href="#Green">green</a></li>
<li><a href="#Blue">blue</a></li>
</ul>
@@ -0,0 +1,14 @@
<?php
class Delimiters extends Mustache {
public $start = "It worked the first time.";
public function middle() {
return array(
array('item' => "And it worked the second time."),
array('item' => "As well as the third."),
);
}
public $final = "Then, surprisingly, it worked the final time.";
}
@@ -0,0 +1,8 @@
{{=<% %>=}}
* <% start %>
<%=| |=%>
|# middle |
* | item |
|/ middle |
|={{ }}=|
* {{ final }}
@@ -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.
@@ -0,0 +1,9 @@
<?php
class DoubleSection extends Mustache {
public function t() {
return true;
}
public $two = "second";
}
@@ -0,0 +1,7 @@
{{#t}}
* first
{{/t}}
* {{two}}
{{#t}}
* third
{{/t}}
@@ -0,0 +1,3 @@
* first
* second
* third
@@ -0,0 +1,5 @@
<?php
class Escaped extends Mustache {
public $title = "Bear > Shark";
}
@@ -0,0 +1 @@
<h1>{{title}}</h1>
@@ -0,0 +1 @@
<h1>Bear &gt; Shark</h1>
@@ -0,0 +1,5 @@
<?php
class InvertedSection extends Mustache {
public $repo = array();
}
@@ -0,0 +1,2 @@
{{#repo}}<b>{{name}}</b>{{/repo}}
{{^repo}}No repos :({{/repo}}
@@ -0,0 +1 @@
No repos :(
@@ -0,0 +1,14 @@
<?php
class Sections extends Mustache {
public $start = "It worked the first time.";
public function middle() {
return array(
array('item' => "And it worked the second time."),
array('item' => "As well as the third."),
);
}
public $final = "Then, surprisingly, it worked the final time.";
}
@@ -0,0 +1,5 @@
* {{ start }}
{{# middle }}
* {{ item }}
{{/ middle }}
* {{ final }}
@@ -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.
+12
View File
@@ -0,0 +1,12 @@
<?php
class Simple extends Mustache {
public $name = "Chris";
public $value = 10000;
public function taxed_value() {
return $this->value - ($this->value * 0.4);
}
public $in_ca = true;
};
@@ -0,0 +1,5 @@
Hello {{name}}
You have just won ${{value}}!
{{#in_ca}}
Well, ${{ taxed_value }}, after taxes.
{{/in_ca}}
+3
View File
@@ -0,0 +1,3 @@
Hello Chris
You have just won $10000!
Well, $6000, after taxes.
@@ -0,0 +1,5 @@
<?php
class Unescaped extends Mustache {
public $title = "Bear > Shark";
}
@@ -0,0 +1 @@
<h1>{{{title}}}</h1>
@@ -0,0 +1 @@
<h1>Bear > Shark</h1>
+5
View File
@@ -0,0 +1,5 @@
<?php
class UTF8Unescaped extends Mustache {
public $test = '中文又来啦';
}
+1
View File
@@ -0,0 +1 @@
<h1>中文 {{test}}</h1>
+1
View File
@@ -0,0 +1 @@
<h1>中文 中文又来啦</h1>
@@ -0,0 +1,5 @@
<?php
class UTF8 extends Mustache {
public $test = '中文又来啦';
}
@@ -0,0 +1 @@
<h1>中文 {{{test}}}</h1>
@@ -0,0 +1 @@
<h1>中文 中文又来啦</h1>
+110
View File
@@ -0,0 +1,110 @@
<?php
require_once '../Mustache.php';
require_once 'PHPUnit/Framework.php';
/**
* A PHPUnit test case for Mustache.php.
*
* This is a very basic, very rudimentary unit test case. It's probably more important to have tests
* than to have elegant tests, so let's bear with it for a bit.
*
* This class assumes an example directory exists at `../examples` with the following structure:
*
* @code
* examples
* foo
* Foo.php
* foo.mustache
* foo.txt
* bar
* Bar.php
* bar.mustache
* bar.txt
* @endcode
*
* To use this test:
*
* 1. {@link http://www.phpunit.de/manual/current/en/installation.html Install PHPUnit}
* 2. run phpunit from the `test` directory:
* `phpunit MustacheTest`
* 3. Fix bugs. Lather, rinse, repeat.
*
* @extends PHPUnit_Framework_TestCase
*/
class MustacheTest extends PHPUnit_Framework_TestCase {
/**
* Test everything in the `examples` directory.
*
* @dataProvider getExamples
* @access public
* @param mixed $class
* @param mixed $template
* @param mixed $output
* @return void
*/
public function testExamples($class, $template, $output) {
$m = new $class;
$this->assertEquals($output, $m->render($template));
}
/**
* 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;
}
}