Initial Mustache.php 2.0 commit.

This commit is contained in:
Justin Hileman
2012-02-29 18:01:57 -08:00
parent 015a4bf4ba
commit d2136d91b3
18 changed files with 1604 additions and 892 deletions
+129
View File
@@ -0,0 +1,129 @@
<?php
namespace Mustache;
/**
* Mustache output Buffer class.
*
* Buffer instances are used by Mustache Templates for collecting output during rendering
*/
class Buffer {
private $buffer = '';
private $indent = '';
private $charset = 'UTF-8';
/**
* Mustache Buffer constructor.
*
* @param string $indent Initial indent level for all lines of this buffer (default: '')
* @param string $charset Override the character set used by `htmlspecialchars()` (default: 'UTF-8')
*/
public function __construct($indent = null, $charset = null) {
if ($indent !== null) {
$this->setIndent($indent);
}
if ($charset !== null) {
$this->charset = $charset;
}
}
/**
* Get the current indent level.
*
* @return string
*/
public function getIndent() {
return $this->indent;
}
/**
* Set the buffer indent level.
*
* Each line output by this buffer will be prefixed by this whitespace. This is used when rendering
* partials and Lambda sections.
*
* @param string $indent
*/
public function setIndent($indent) {
$this->indent = $indent;
}
/**
* Get the character set used when escaping values.
*
* @return string
*/
public function getCharset() {
return $this->charset;
}
/**
* Write a newline to the Buffer.
*/
public function writeLine() {
$this->buffer .= "\n";
}
/**
* Output text to the Buffer.
*
* @see \Mustache\Buffer::write
*
* @param string $text
* @param bool $escape Escape this text with `htmlspecialchars()`? (default: false)
*/
public function writeText($text, $escape = false) {
$this->write($text, true, $escape);
}
/**
* Add output to the Buffer.
*
* @param string $text
* @param bool $indent Indent this line? (default: false)
* @param bool $escape Escape this text with `htmlspecialchars()`? (default: false)
*/
public function write($text, $indent = false, $escape = false) {
$text = (string) $text;
if ($escape) {
$text = $this->escape($text);
}
if ($indent) {
$this->buffer .= $this->indent . $text;
} else {
$this->buffer .= $text;
}
}
/**
* Flush the contents of the Buffer.
*
* Resets the buffer and returns the current contents.
*
* @return string
*/
public function flush() {
$buffer = $this->buffer;
$this->buffer = '';
return $buffer;
}
/**
* Helper function to escape text.
*
* Uses the Buffer's character set (default 'UTF-8', passed as the second argument to `__construct`).
*
* @see htmlspecialchars
*
* @param string $text
*
* @return string Escaped text
*/
private function escape($text) {
return htmlspecialchars($text, ENT_COMPAT, $this->charset);
}
}
+292
View File
@@ -0,0 +1,292 @@
<?php
namespace Mustache;
/**
* Mustache Compiler class.
*
* This class is responsible for turning a Mustache token parse tree into normal PHP source code.
*/
class Compiler {
/**
* Compile a Mustache token parse tree into PHP source code.
*
* @param string $source Mustache Template source code
* @param string $tree Parse tree of Mustache tokens
* @param string $name Mustache Template class name
*
* @return string Generated PHP source code
*/
public function compile($source, array $tree, $name) {
$this->source = $source;
return $this->writeCode($tree, $name);
}
/**
* Helper function for walking the Mustache token parse tree.
*
* @throws \InvalidArgumentException upon encountering unknown token types.
*
* @param array $tree Parse tree of Mustache tokens
* @param int $level (default: 0)
*
* @return string Generated PHP source code;
*/
private function walk(array $tree, $level = 0) {
$code = '';
$level++;
foreach ($tree as $node) {
switch (is_string($node) ? 'text' : $node[Tokenizer::TAG]) {
case '#':
$code .= $this->section(
$node[Tokenizer::NODES],
$node[Tokenizer::NAME],
$node[Tokenizer::INDEX],
$node[Tokenizer::END],
$node[Tokenizer::OTAG],
$node[Tokenizer::CTAG],
$level
);
break;
case '^':
$code .= $this->invertedSection(
$node[Tokenizer::NODES],
$node[Tokenizer::NAME],
$level
);
break;
case '<':
case '>':
$code .= $this->partial(
$node[Tokenizer::NAME],
isset($node[Tokenizer::INDENT]) ? $node[Tokenizer::INDENT] : '',
$level
);
break;
case '{':
case '&':
$code .= $this->variable($node[Tokenizer::NAME], false, $level);
break;
case '!':
break;
case '_v':
$code .= $this->variable($node[Tokenizer::NAME], true, $level);
break;
case 'text':
$code .= $this->text($node, $level);
break;
default:
throw new \InvalidArgumentException('Unknown node type: '.json_encode($node));
}
}
return $code;
}
const KLASS = '<?php
class %s extends \Mustache\Template {
public function renderInternal(\Mustache\Context $context, $indent = \'\') {
$mustache = $this->mustache;
$buffer = new \Mustache\Buffer($indent, $mustache->getCharset());
%s
return $buffer->flush();
}
}';
/**
* Generate Mustache Template class PHP source.
*
* @param array $tree Parse tree of Mustache tokens
* @param string $name Mustache Template class name
*
* @return string Generated PHP source code
*/
private function writeCode($tree, $name) {
return sprintf($this->prepare(self::KLASS, 0, false), $name, $this->walk($tree), $name);
}
const SECTION = '
// %s section
$value = $context->%s(%s);
if ($context->isCallable($value)) {
$source = %s;
$buffer->write(
$mustache
->loadLambda((string) call_user_func($value, $source)%s)
->renderInternal($context, $buffer->getIndent())
);
} elseif ($context->isTruthy($value)) {
$values = $context->isIterable($value) ? $value : array($value);
foreach ($values as $value) {
$context->push($value);%s
$context->pop();
}
}';
/**
* Generate Mustache Template section PHP source.
*
* @param array $nodes Array of child tokens
* @param string $id Section name
* @param int $start Section start offset
* @param int $end Section end offset
* @param string $otag Current Mustache opening tag
* @param string $ctag Current Mustache closing tag
* @param int $level
*
* @return string Generated section PHP source code
*/
private function section($nodes, $id, $start, $end, $otag, $ctag, $level) {
$method = $this->getFindMethod($id);
$id = var_export($id, true);
$source = var_export(substr($this->source, $start, $end - $start), true);
if ($otag !== '{{' || $ctag !== '}}') {
$delims = ', '.var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
} else {
$delims = '';
}
return sprintf($this->prepare(self::SECTION, $level), $id, $method, $id, $source, $delims, $this->walk($nodes, $level + 1));
}
const INVERTED_SECTION = '
// %s inverted section
if (!$context->isTruthy($context->%s(%s))) {
%s
}';
/**
* Generate Mustache Template inverted section PHP source.
*
* @param array $nodes Array of child tokens
* @param string $id Section name
* @param int $level
*
* @return string Generated inverted section PHP source code
*/
private function invertedSection($nodes, $id, $level) {
$method = $this->getFindMethod($id);
$id = var_export($id, true);
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $this->walk($nodes, $level));
}
const PARTIAL = '$buffer->write($mustache->loadPartial(%s)->renderInternal($context, %s));';
/**
* Generate Mustache Template partial call PHP source.
*
* @param string $id Partial name
* @param string $indent Whitespace indent to apply to partial
* @param int $level
*
* @return string Generated partial call PHP source code
*/
private function partial($id, $indent, $level) {
return sprintf(
$this->prepare(self::PARTIAL, $level),
var_export($id, true),
var_export($indent, true)
);
}
const VARIABLE = '
$value = $context->%s(%s);
if ($context->isCallable($value)) {
$value = $mustache
->loadLambda((string) call_user_func($value))
->renderInternal($context, $buffer->getIndent());
}
$buffer->writeText($value, %s);
';
/**
* Generate Mustache Template variable interpolation PHP source.
*
* @param string $id Variable name
* @param boolean $escape Escape the variable value for output?
* @param int $level
*
* @return string Generated variable interpolation PHP source
*/
private function variable($id, $escape, $level) {
$method = $this->getFindMethod($id);
$id = ($method !== 'last') ? var_export($id, true) : '';
$escape = $escape ? 'true' : 'false';
return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $escape);
}
const LINE = '$buffer->writeLine();';
const TEXT = '$buffer->writeText(%s);';
/**
* Generate Mustache Template output Buffer call PHP source.
*
* @param string $text
* @param int $level
*
* @return string Generated output Buffer call PHP source
*/
private function text($text, $level) {
if ($text === "\n") {
return $this->prepare(self::LINE, $level);
} else {
return sprintf($this->prepare(self::TEXT, $level), var_export($text, true));
}
}
/**
* Prepare PHP source code snippet for output.
*
* @param string $text
* @param int $bonus Additional indent level (default: 0)
* @param boolean $prependNewline Prepend a newline to the snippet? (default: true)
*
* @return string PHP source code snippet
*/
private function prepare($text, $bonus = 0, $prependNewline = true) {
$text = ($prependNewline ? "\n" : '').trim($text);
if ($prependNewline) {
$bonus++;
}
return preg_replace("/\n(\t\t)?/", "\n".str_repeat("\t", $bonus), $text);
}
/**
* Select the appropriate Context `find` method for a given $id.
*
* The return value will be one of `find`, `findDot` or `last`.
*
* @see \Mustache\Context::find
* @see \Mustache\Context::findDot
* @see \Mustache\Context::last
*
* @param string $id Variable name
*
* @return string `find` method name
*/
private function getFindMethod($id) {
if ($id === '.') {
return 'last';
} elseif (strpos($id, '.') === false) {
return 'find';
} else {
return 'findDot';
}
}
}
+203
View File
@@ -0,0 +1,203 @@
<?php
namespace Mustache;
/**
* Mustache Template rendering Context.
*/
class Context {
private $stack = array();
/**
* Mustache rendering Context constructor.
*
* @param mixed $context Default rendering context (default: null)
*/
public function __construct($context = null) {
if ($context !== null) {
$this->stack = array($context);
}
}
/**
* Helper function to test whether a value is 'truthy'.
*
* @param mixed $value
*
* @return boolean True if the value is 'truthy'
*/
public function isTruthy($value) {
return !empty($value);
}
/**
* Higher order sections helper: tests whether a value is a valid callback.
*
* In Mustache.php, a variable is considered 'callable' if the variable is:
*
* 1. An anonymous function.
* 2. An object and the name of a public function, e.g. `array($someObject, 'methodName')`
* 3. A class name and the name of a public static function, e.g. `array('SomeClass', 'methodName')`
*
* Note that this specifically excludes strings, which PHP would normally consider 'callable'.
*
* @param mixed $value
*
* @return boolean True if the value is 'callable'
*/
public function isCallable($value) {
return !is_string($value) && is_callable($value);
}
/**
* Tests whether a value should be iterated over (e.g. in a section context).
*
* In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists
* should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript,
* Java, Python, etc.
*
* PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish
* between between a list of things (numeric, normalized array) and a set of variables to be used as section context
* (associative array). In other words, this will be iterated over:
*
* $items = array(
* array('name' => 'foo'),
* array('name' => 'bar'),
* array('name' => 'baz'),
* );
*
* ... but this will be used as a section context block:
*
* $items = array(
* 1 => array('name' => 'foo'),
* 'banana' => array('name' => 'bar'),
* 42 => array('name' => 'baz'),
* );
*
* @param mixed $value
*
* @return boolean True if the value is 'iterable'
*/
public function isIterable($value) {
if (is_object($value)) {
return $value instanceof \Traversable;
} elseif (is_array($value)) {
return !array_diff_key($value, array_keys(array_keys($value)));
}
return false;
}
/**
* Push a new Context frame onto the stack.
*
* @param mixed $value Object or array to use for context
*/
public function push($value) {
array_push($this->stack, $value);
}
/**
* Pop the last Context frame from the stack.
*
* @return mixed Last Context frame (object or array)
*/
public function pop() {
return array_pop($this->stack);
}
/**
* Get the last Context frame.
*
* @return mixed Last Context frame (object or array)
*/
public function last() {
return end($this->stack);
}
/**
* Find a variable in the Context stack.
*
* Starting with the last Context frame (the context of the innermost section), and working back to the top-level
* rendering context, look for a variable with the given name:
*
* * If the Context frame is an associative array which contains the key $id, returns the value of that element.
* * If the Context frame is an object, this will check first for a public method, then a public property named
* $id. Failing both of these, it will try `__isset` and `__get` magic methods.
* * If a value named $id is not found in any Context frame, returns an empty string.
*
* @param string $id Variable name
*
* @return mixed Variable value, or '' if not found
*/
public function find($id) {
return $this->findVariableInStack($id, $this->stack);
}
/**
* Find a 'dot notation' variable in the Context stack.
*
* Note that dot notation traversal bubbles through scope differently than the regular find method. After finding
* the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous
* result. For example, given the following context stack:
*
* $data = array(
* 'name' => 'Fred',
* 'child' => array(
* 'name' => 'Bob'
* ),
* );
*
* ... and the Mustache following template:
*
* {{ child.name }}
*
* ... the `name` value is only searched for within the `child` value of the global Context, not within parent
* Context frames.
*
* @param string $id Dotted variable selector
*
* @return mixed Variable value, or '' if not found
*/
public function findDot($id) {
$chunks = explode('.', $id);
$first = array_shift($chunks);
$value = $this->findVariableInStack($first, $this->stack);
foreach ($chunks as $chunk) {
if ($value === '') {
return $value;
}
$value = $this->findVariableInStack($chunk, array($value));
}
return $value;
}
/**
* Helper function to find a variable in the Context stack.
*
* @see \Mustache\Context::find
*
* @param string $id Variable name
* @param array $stack Context stack
*
* @return mixed Variable value, or '' if not found
*/
private function findVariableInStack($id, array $stack) {
for ($i = count($stack) - 1; $i >= 0; $i--) {
if (is_object($stack[$i])) {
if (method_exists($stack[$i], $id)) {
return $stack[$i]->$id();
} elseif (isset($stack[$i]->$id)) {
return $stack[$i]->$id;
}
} elseif (is_array($stack[$i]) && array_key_exists($id, $stack[$i])) {
return $stack[$i][$id];
}
}
return '';
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace Mustache;
/**
* Mustache Template Loader interface.
*/
interface Loader {
/**
* Load a Template by name.
*
* @param string $name
*
* @return string Mustache Template source
*/
function load($name);
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace Mustache\Loader;
use Mustache\Loader;
use Mustache\Loader\MutableLoader;
/**
* Mustache Template array Loader implementation.
*
* An ArrayLoader instance loads Mustache Template source by name from an initial array:
*
* $loader = new ArrayLoader(
* 'foo' => '{{ bar }}',
* 'baz' => 'Hey {{ qux }}!'
* );
*
* $tpl = $loader->load('foo'); // '{{ bar }}'
*
* The ArrayLoader is used internally as a partials loader by \Mustache\Mustache instance when an array of partials
* is set. It can also be used as a quick-and-dirty Template loader.
*
* @implements Loader
* @implements MutableLoader
*/
class ArrayLoader implements Loader, MutableLoader {
/**
* ArrayLoader constructor.
*
* @param array $templates Associative array of Template source (default: array())
*/
public function __construct(array $templates = array()) {
$this->templates = $templates;
}
/**
* Load a Template.
*
* @param string $name
*
* @return string Mustache Template source
*/
public function load($name) {
if (!isset($this->templates[$name])) {
throw new \InvalidArgumentException('Template '.$name.' not found.');
}
return $this->templates[$name];
}
/**
* Set an associative array of Template sources for this loader.
*
* @param array $templates
*/
public function setTemplates(array $templates) {
$this->templates = $templates;
}
/**
* Set a Template source by name.
*
* @param string $name
* @param string $template Mustache Template source
*/
public function setTemplate($name, $template) {
$this->templates[$name] = $template;
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace Mustache\Loader;
use Mustache\Loader;
/**
* Mustache Template filesystem Loader implementation.
*
* An ArrayLoader instance loads Mustache Template source from the filesystem by name:
*
* $loader = new FilesystemLoader(__DIR__.'/views');
* $tpl = $loader->load('foo'); // equivalent to `file_get_contents(__DIR__.'/views/foo.mustache');
*
* This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates:
*
* $m = new Mustache(array(
* 'loader' => new FilesystemLoader(__DIR__.'/views'),
* 'partials_loader' => new FilesystemLoader(__DIR__.'/views/partials'),
* ));
*
* @implements Loader
*/
class FilesystemLoader implements Loader {
private $baseDir;
private $extension = '.mustache';
private $templates = array();
/**
* Mustache filesystem Loader constructor.
*
* Passing an $options array allows overriding certain Loader options during instantiation:
*
* $options = array(
* // The filename extension used for Mustache templates. Defaults to '.mustache'
* 'extension' => '.ms',
* );
*
* @throws \RuntimeException if $baseDir does not exist.
*
* @param string $baseDir Base directory containing Mustache template files.
* @param array $options Array of Loader options (default: array())
*/
public function __construct($baseDir, array $options = array()) {
$this->baseDir = rtrim(realpath($baseDir), '/');
if (!is_dir($this->baseDir)) {
throw new \RuntimeException('FilesystemLoader baseDir must be a directory: '.$baseDir);
}
if (isset($options['extension'])) {
$this->extension = '.' . ltrim($options['extension'], '.');
}
}
/**
* Load a Template by name.
*
* $loader = new FilesystemLoader(__DIR__.'/views');
* $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache";
*
* @param string $name
*
* @return string Mustache Template source
*/
public function load($name) {
if (!isset($this->templates[$name])) {
$this->templates[$name] = $this->loadFile($name);
}
return $this->templates[$name];
}
/**
* Helper function for loading a Mustache file by name.
*
* @throws \InvalidArgumentException if a template file is not found.
*
* @param string $name
*
* @return string Mustache Template source
*/
private function loadFile($name) {
$fileName = $this->getFileName($name);
if (!file_exists($fileName)) {
throw new \InvalidArgumentException('Template '.$name.' not found.');
}
return file_get_contents($fileName);
}
/**
* Helper function for getting a Mustache template file name.
*
* @param string $name
*
* @return string Template file name
*/
private function getFileName($name) {
$fileName = $this->baseDir . '/' . $name;
if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) {
$fileName .= $this->extension;
}
return $fileName;
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Mustache\Loader;
/**
* Mustache Template mutable Loader interface.
*/
interface MutableLoader {
/**
* Set an associative array of Template sources for this loader.
*
* @param array $templates
*/
function setTemplates(array $templates);
/**
* Set a Template source by name.
*
* @param string $name
* @param string $template Mustache Template source
*/
function setTemplate($name, $template);
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Mustache\Loader;
use Mustache\Loader;
/**
* Mustache Template string Loader implementation.
*
* A StringLoader instance is essentially a noop. It simply passes the 'name' argument straight through:
*
* $loader = new StringLoader;
* $tpl = $loader->load('{{ foo }}'); // '{{ foo }}'
*
* This is the default Template Loader instance used by Mustache:
*
* $m = new Mustache;
* $tpl = $m->loadTemplate('{{ foo }}');
* echo $tpl->render(array('foo' => 'bar')); // "bar"
*
* @implements Loader
*/
class StringLoader implements Loader {
/**
* Load a Template by source.
*
* @param string $name Mustache Template source
*
* @return string Mustache Template source
*/
public function load($name) {
return $name;
}
}
+318 -828
View File
File diff suppressed because it is too large Load Diff
-85
View File
@@ -1,85 +0,0 @@
<?php
/**
* A Mustache Partial filesystem loader.
*
* @author Justin Hileman {@link http://justinhileman.com}
*/
class MustacheLoader implements ArrayAccess {
protected $baseDir;
protected $partialsCache = array();
protected $extension;
/**
* MustacheLoader constructor.
*
* @access public
* @param string $baseDir Base template directory.
* @param string $extension File extension for Mustache files (default: 'mustache')
* @return void
*/
public function __construct($baseDir, $extension = 'mustache') {
if (!is_dir($baseDir)) {
throw new InvalidArgumentException('$baseDir must be a valid directory, ' . $baseDir . ' given.');
}
$this->baseDir = $baseDir;
$this->extension = $extension;
}
/**
* @param string $offset Name of partial
* @return boolean
*/
public function offsetExists($offset) {
return (isset($this->partialsCache[$offset]) || file_exists($this->pathName($offset)));
}
/**
* @throws InvalidArgumentException if the given partial doesn't exist
* @param string $offset Name of partial
* @return string Partial template contents
*/
public function offsetGet($offset) {
if (!$this->offsetExists($offset)) {
throw new InvalidArgumentException('Partial does not exist: ' . $offset);
}
if (!isset($this->partialsCache[$offset])) {
$this->partialsCache[$offset] = file_get_contents($this->pathName($offset));
}
return $this->partialsCache[$offset];
}
/**
* MustacheLoader is an immutable filesystem loader. offsetSet throws a LogicException if called.
*
* @throws LogicException
* @return void
*/
public function offsetSet($offset, $value) {
throw new LogicException('Unable to set offset: MustacheLoader is an immutable ArrayAccess object.');
}
/**
* MustacheLoader is an immutable filesystem loader. offsetUnset throws a LogicException if called.
*
* @throws LogicException
* @return void
*/
public function offsetUnset($offset) {
throw new LogicException('Unable to unset offset: MustacheLoader is an immutable ArrayAccess object.');
}
/**
* An internal helper for generating path names.
*
* @param string $file Partial name
* @return string File path
*/
protected function pathName($file) {
return $this->baseDir . '/' . $file . '.' . $this->extension;
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace Mustache;
/**
* Mustache Parser class.
*
* This class is responsible for turning a set of Mustache tokens into a parse tree.
*/
class Parser {
/**
* Process an array of Mustache tokens and convert them into a parse tree.
*
* @param array $tree Set of Mustache tokens
*
* @return array Mustache token parse tree
*/
public function parse(array $tokens = array()) {
return $this->buildTree(new \ArrayIterator($tokens));
}
/**
* Helper method for recursively building a parse tree.
*
* @throws \LogicException when nesting errors or mismatched section tags are encountered.
*
* @param \ArrayIterator $tokens Stream of Mustache tokens
* @param array $parent Parent token (default: null)
*
* @return array Mustache Token parse tree
*/
private function buildTree(\ArrayIterator $tokens, array $parent = null) {
$nodes = array();
do {
$token = $tokens->current();
$tokens->next();
if ($token === null) {
continue;
} elseif (is_array($token)) {
switch ($token[Tokenizer::TAG]) {
case '#':
case '^':
$nodes[] = $this->buildTree($tokens, $token);
break;
case '/':
if (!isset($parent)) {
throw new \LogicException('Unexpected closing tag: /'. $token[Tokenizer::NAME]);
}
if ($token[Tokenizer::NAME] !== $parent[Tokenizer::NAME]) {
throw new \LogicException('Nesting error: ' . $parent[Tokenizer::NAME] . ' vs. ' . $token[Tokenizer::NAME]);
}
$parent[Tokenizer::END] = $token[Tokenizer::INDEX];
$parent[Tokenizer::NODES] = $nodes;
return $parent;
break;
default:
$nodes[] = $token;
break;
}
} else {
$nodes[] = $token;
}
} while($tokens->valid());
if (isset($parent)) {
throw new \LogicException('Missing closing tag: ' . $parent[Tokenizer::NAME]);
}
return $nodes;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace Mustache;
/**
* Abstract Mustache Template class.
*
* @abstract
*/
abstract class Template {
/**
* @var \Mustache\Mustache
*/
protected $mustache;
/**
* Mustache Template constructor.
*
* @param \Mustache\Mustache $mustache
*/
public function __construct(Mustache $mustache) {
$this->mustache = $mustache;
}
/**
* Mustache Template instances can be treated as a function and rendered by simply calling them:
*
* $m = new Mustache;
* $tpl = $m->loadTemplate('Hello, {{ name }}!');
* echo $tpl(array('name' => 'World')); // "Hello, World!"
*
* @see \Mustache\Template::render
*
* @param mixed $context Array or object rendering context (default: array())
*
* @return string Rendered template
*/
public function __invoke($context = array()) {
return $this->render($context);
}
/**
* Render this template given the rendering context.
*
* @param mixed $context Array or object rendering context (default: array())
*
* @return string Rendered template
*/
public function render($context = array()) {
return $this->renderInternal(new Context($context));
}
/**
* Internal rendering method implemented by Mustache Template concrete subclasses.
*
* This is where the magic happens :)
*
* @abstract
*
* @param \Mustache\Context $context
*
* @return string Rendered template
*/
abstract public function renderInternal(Context $context);
}
+262
View File
@@ -0,0 +1,262 @@
<?php
namespace Mustache;
/**
* Mustache Tokenizer class.
*
* This class is responsible for turning raw template source into a set of Mustache tokens.
*/
class Tokenizer {
// Finite state machine states
const IN_TEXT = 0;
const IN_TAG_TYPE = 1;
const IN_TAG = 2;
// Token types
const T_SECTION = 1;
const T_INVERTED = 2;
const T_END_SECTION = 3;
const T_COMMENT = 4;
const T_PARTIAL = 5;
const T_PARTIAL_2 = 6;
const T_DELIM_CHANGE = 7;
const T_ESCAPED = 8;
const T_UNESCAPED = 9;
const T_UNESCAPED_2 = 10;
// Token types map
private static $tagTypes = array(
'#' => self::T_SECTION,
'^' => self::T_INVERTED,
'/' => self::T_END_SECTION,
'!' => self::T_COMMENT,
'>' => self::T_PARTIAL,
'<' => self::T_PARTIAL_2,
'=' => self::T_DELIM_CHANGE,
'_v' => self::T_ESCAPED,
'{' => self::T_UNESCAPED,
'&' => self::T_UNESCAPED_2,
);
// Token properties
const NODES = 'nodes';
const TAG = 'tag';
const NAME = 'name';
const OTAG = 'otag';
const CTAG = 'ctag';
const INDEX = 'index';
const END = 'end';
const INDENT = 'indent';
private $state;
private $tagType;
private $tag;
private $buf;
private $tokens;
private $seenTag;
private $lineStart;
private $otag;
private $ctag;
/**
* Scan and tokenize template source.
*
* @param string $text Mustache template source to tokenize
* @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null)
*
* @return array Set of Mustache tokens
*/
public function scan($text, $delimiters = null) {
$this->reset();
if ($delimiters = trim($delimiters)) {
list($otag, $ctag) = explode(' ', $delimiters);
$this->otag = $otag;
$this->ctag = $ctag;
}
$len = strlen($text);
for ($i = 0; $i < $len; $i++) {
switch ($this->state) {
case self::IN_TEXT:
if ($this->tagChange($this->otag, $text, $i)) {
$i--;
$this->flushBuffer();
$this->state = self::IN_TAG_TYPE;
} else {
if ($text[$i] == "\n") {
$this->filterLine();
} else {
$this->buffer .= $text[$i];
}
}
break;
case self::IN_TAG_TYPE:
$i += strlen($this->otag) - 1;
$tag = isset(self::$tagTypes[$text[$i + 1]]) ? self::$tagTypes[$text[$i + 1]] : null;
$this->tagType = $tag ? $text[$i + 1] : '_v';
if ($this->tagType === '=') {
$i = $this->changeDelimiters($text, $i);
$this->state = self::IN_TEXT;
} else {
if ($tag) {
$i++;
}
$this->state = self::IN_TAG;
}
$this->seenTag = $i;
break;
default:
if ($this->tagChange($this->ctag, $text, $i)) {
$this->tokens[] = array(
self::TAG => $this->tagType,
self::NAME => trim($this->buffer),
self::OTAG => $this->otag,
self::CTAG => $this->ctag,
self::INDEX => ($this->tagType == '/') ? $this->seenTag - strlen($this->otag) : $i + strlen($this->ctag)
);
$this->buffer = '';
$i += strlen($this->ctag) - 1;
$this->state = self::IN_TEXT;
if ($this->tagType == '{') {
if ($this->ctag == '}}') {
$i++;
} else {
$this->cleanTripleStache($this->tokens[count($this->tokens) - 1]);
}
}
} else {
$this->buffer .= $text[$i];
}
break;
}
}
$this->filterLine(true);
return $this->tokens;
}
/**
* Helper function to reset tokenizer internal state.
*/
private function reset() {
$this->state = self::IN_TEXT;
$this->tagType = null;
$this->tag = null;
$this->buffer = '';
$this->tokens = array();
$this->seenTag = false;
$this->lineStart = 0;
$this->otag = '{{';
$this->ctag = '}}';
}
/**
* Flush the current buffer to a token.
*/
private function flushBuffer() {
if (!empty($this->buffer)) {
$this->tokens[] = $this->buffer;
$this->buffer = '';
}
}
/**
* Test whether the current line is entirely made up of whitespace.
*
* @return boolean True if the current line is all whitespace
*/
private function lineIsWhitespace() {
$tokensCount = count($this->tokens);
for ($j = $this->lineStart; $j < $tokensCount; $j++) {
$token = $this->tokens[$j];
if (is_array($token) && isset(self::$tagTypes[$token[self::TAG]])) {
if (self::$tagTypes[$token[self::TAG]] >= self::T_ESCAPED) {
return false;
}
} elseif (is_string($token)) {
if (preg_match('/\S/', $token)) {
return false;
}
}
}
return true;
}
/**
* Filter out whitespace-only lines and store indent levels for partials.
*
* @param bool $noNewLine Suppress the newline? (default: false)
*/
private function filterLine($noNewLine = false) {
$this->flushBuffer();
if ($this->seenTag && $this->lineIsWhitespace()) {
$tokensCount = count($this->tokens);
for ($j = $this->lineStart; $j < $tokensCount; $j++) {
if (!is_array($this->tokens[$j])) {
if (isset($this->tokens[$j+1]) && is_array($this->tokens[$j+1]) && $this->tokens[$j+1][self::TAG] == '>') {
$this->tokens[$j+1][self::INDENT] = (string) $this->tokens[$j];
}
$this->tokens[$j] = null;
}
}
} elseif (!$noNewLine) {
$this->tokens[] = "\n";
}
$this->seenTag = false;
$this->lineStart = count($this->tokens);
}
/**
* Change the current Mustache delimiters. Set new `otag` and `ctag` values.
*
* @param string $text Mustache template source
* @param int $index Current tokenizer index
*
* @return int New index value
*/
private function changeDelimiters($text, $index) {
$startIndex = strpos($text, '=', $index) + 1;
$close = '='.$this->ctag;
$closeIndex = strpos($text, $close, $index);
list($otag, $ctag) = explode(' ', trim(substr($text, $startIndex, $closeIndex - $startIndex)));
$this->otag = $otag;
$this->ctag = $ctag;
return $closeIndex + strlen($close) - 1;
}
/**
* Clean up `{{{ tripleStache }}}` style tokens.
*
* @param array &$token
*/
private function cleanTripleStache(&$token) {
if (substr($token[self::NAME], -1) === '}') {
$token[self::NAME] = trim(substr($token[self::NAME], 0, -1));
}
}
/**
* Test whether it's time to change tags.
*
* @param string $tag Current tag name
* @param string $text Mustache template source
* @param int $index Current tokenizer index
*
* @return boolean True if this is a closing section tag
*/
private function tagChange($tag, $text, $index) {
return substr($text, $index, strlen($tag)) === $tag;
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
require __DIR__.'/../src/Mustache/Buffer.php';
require __DIR__.'/../src/Mustache/Compiler.php';
require __DIR__.'/../src/Mustache/Context.php';
require __DIR__.'/../src/Mustache/Loader.php';
require __DIR__.'/../src/Mustache/Loader/MutableLoader.php';
require __DIR__.'/../src/Mustache/Loader/ArrayLoader.php';
require __DIR__.'/../src/Mustache/Loader/StringLoader.php';
require __DIR__.'/../src/Mustache/Loader/FilesystemLoader.php';
require __DIR__.'/../src/Mustache/Mustache.php';
require __DIR__.'/../src/Mustache/Parser.php';
require __DIR__.'/../src/Mustache/Template.php';
require __DIR__.'/../src/Mustache/Tokenizer.php';
require __DIR__.'/lib/yaml/lib/sfYamlParser.php';
+1
View File
@@ -0,0 +1 @@
alpha contents
+1
View File
@@ -0,0 +1 @@
beta contents
+1
View File
@@ -0,0 +1 @@
one contents
+1
View File
@@ -0,0 +1 @@
two contents