Merge branch 'feature/pragma-blocks' into dev

This commit is contained in:
Justin Hileman
2014-08-15 23:18:10 -07:00
11 changed files with 1047 additions and 22 deletions
+184 -9
View File
@@ -16,6 +16,9 @@
*/ */
class Mustache_Compiler class Mustache_Compiler
{ {
private $pragmas;
private $defaultPragmas = array();
private $sections; private $sections;
private $source; private $source;
private $indentNextLine; private $indentNextLine;
@@ -23,7 +26,6 @@ class Mustache_Compiler
private $entityFlags; private $entityFlags;
private $charset; private $charset;
private $strictCallables; private $strictCallables;
private $pragmas;
/** /**
* Compile a Mustache token parse tree into PHP source code. * Compile a Mustache token parse tree into PHP source code.
@@ -40,7 +42,7 @@ class Mustache_Compiler
*/ */
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT) public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
{ {
$this->pragmas = array(); $this->pragmas = $this->defaultPragmas;
$this->sections = array(); $this->sections = array();
$this->source = $source; $this->source = $source;
$this->indentNextLine = true; $this->indentNextLine = true;
@@ -52,6 +54,23 @@ class Mustache_Compiler
return $this->writeCode($tree, $name); return $this->writeCode($tree, $name);
} }
/**
* Enable pragmas across all templates, regardless of the presence of pragma
* tags in the individual templates.
*
* @internal Users should set global pragmas in Mustache_Engine, not here :)
*
* @param array $pragmas
*/
public function setPragmas(array $pragmas)
{
$this->pragmas = array();
foreach ($pragmas as $pragma) {
$this->pragmas[$pragma] = true;
}
$this->defaultPragmas = $this->pragmas;
}
/** /**
* Helper function for walking the Mustache token parse tree. * Helper function for walking the Mustache token parse tree.
* *
@@ -93,7 +112,6 @@ class Mustache_Compiler
break; break;
case Mustache_Tokenizer::T_PARTIAL: case Mustache_Tokenizer::T_PARTIAL:
case Mustache_Tokenizer::T_PARTIAL_2:
$code .= $this->partial( $code .= $this->partial(
$node[Mustache_Tokenizer::NAME], $node[Mustache_Tokenizer::NAME],
isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '', isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
@@ -101,6 +119,39 @@ class Mustache_Compiler
); );
break; break;
case Mustache_Tokenizer::T_PARENT:
$code .= $this->parent(
$node[Mustache_Tokenizer::NAME],
isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
$node[Mustache_Tokenizer::NODES],
$level
);
break;
case Mustache_Tokenizer::T_BLOCK_ARG:
$code .= $this->blockArg(
$node[Mustache_Tokenizer::NODES],
$node[Mustache_Tokenizer::NAME],
$node[Mustache_Tokenizer::INDEX],
$node[Mustache_Tokenizer::END],
$node[Mustache_Tokenizer::OTAG],
$node[Mustache_Tokenizer::CTAG],
$level
);
break;
case Mustache_Tokenizer::T_BLOCK_VAR:
$code .= $this->blockVar(
$node[Mustache_Tokenizer::NODES],
$node[Mustache_Tokenizer::NAME],
$node[Mustache_Tokenizer::INDEX],
$node[Mustache_Tokenizer::END],
$node[Mustache_Tokenizer::OTAG],
$node[Mustache_Tokenizer::CTAG],
$level
);
break;
case Mustache_Tokenizer::T_UNESCAPED: case Mustache_Tokenizer::T_UNESCAPED:
case Mustache_Tokenizer::T_UNESCAPED_2: case Mustache_Tokenizer::T_UNESCAPED_2:
$code .= $this->variable($node[Mustache_Tokenizer::NAME], false, $level); $code .= $this->variable($node[Mustache_Tokenizer::NAME], false, $level);
@@ -135,6 +186,7 @@ class Mustache_Compiler
{ {
$this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context); $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
$buffer = \'\'; $buffer = \'\';
$newContext = array();
%s %s
return $buffer; return $buffer;
@@ -149,6 +201,7 @@ class Mustache_Compiler
public function renderInternal(Mustache_Context $context, $indent = \'\') public function renderInternal(Mustache_Context $context, $indent = \'\')
{ {
$buffer = \'\'; $buffer = \'\';
$newContext = array();
%s %s
return $buffer; return $buffer;
@@ -170,11 +223,75 @@ class Mustache_Compiler
$code = $this->walk($tree); $code = $this->walk($tree);
$sections = implode("\n", $this->sections); $sections = implode("\n", $this->sections);
$klass = empty($this->sections) ? self::KLASS_NO_LAMBDAS : self::KLASS; $klass = empty($this->sections) ? self::KLASS_NO_LAMBDAS : self::KLASS;
$callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : ''; $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections); return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections);
} }
const BLOCK_VAR = '
$value = $this->resolveValue($context->findInBlock(%s), $context, $indent);
if ($value && !is_array($value) && !is_object($value)) {
$buffer .= $value;
} else {
%s
}
';
/**
* Generate Mustache Template inheritance block variable 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 PHP source code
*/
private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level)
{
$id_str = var_export($id, true);
return sprintf($this->prepare(self::BLOCK_VAR, $level), $id_str, $this->walk($nodes, 2));
}
const BLOCK_ARG = '
// %s block_arg
$value = $this->section%s($context, $indent, true);
$newContext[%s] = %s$value;
';
/**
* Generate Mustache Template inheritance block argument 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 PHP source code
*/
private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
{
$key = $this->section($nodes, $id, $start, $end, $otag, $ctag, $level, true);
$filters = '';
if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) {
list($id, $filters) = $this->getFilters($id, $level);
}
$method = $this->getFindMethod($id);
$id = var_export($id, true);
return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key, $id, $this->flushIndent());
}
const SECTION_CALL = ' const SECTION_CALL = '
// %s section // %s section
$value = $context->%s(%s);%s $value = $context->%s(%s);%s
@@ -198,7 +315,8 @@ class Mustache_Compiler
} elseif (!empty($value)) { } elseif (!empty($value)) {
$values = $this->isIterable($value) ? $value : array($value); $values = $this->isIterable($value) ? $value : array($value);
foreach ($values as $value) { foreach ($values as $value) {
$context->push($value);%s $context->push($value);
%s
$context->pop(); $context->pop();
} }
} }
@@ -216,10 +334,11 @@ class Mustache_Compiler
* @param string $otag Current Mustache opening tag * @param string $otag Current Mustache opening tag
* @param string $ctag Current Mustache closing tag * @param string $ctag Current Mustache closing tag
* @param int $level * @param int $level
* @param bool $arg (default: false)
* *
* @return string Generated section PHP source code * @return string Generated section PHP source code
*/ */
private function section($nodes, $id, $start, $end, $otag, $ctag, $level) private function section($nodes, $id, $start, $end, $otag, $ctag, $level, $arg = false)
{ {
$filters = ''; $filters = '';
@@ -227,8 +346,6 @@ class Mustache_Compiler
list($id, $filters) = $this->getFilters($id, $level); list($id, $filters) = $this->getFilters($id, $level);
} }
$method = $this->getFindMethod($id);
$id = var_export($id, true);
$source = var_export(substr($this->source, $start, $end - $start), true); $source = var_export(substr($this->source, $start, $end - $start), true);
$callable = $this->getCallable(); $callable = $this->getCallable();
@@ -238,13 +355,20 @@ class Mustache_Compiler
$delims = ''; $delims = '';
} }
$key = ucfirst(md5($delims."\n".$source)); $key = ucfirst(md5($delims."\n".$source));
if (!isset($this->sections[$key])) { if (!isset($this->sections[$key])) {
$this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $delims, $this->walk($nodes, 2)); $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $delims, $this->walk($nodes, 2));
} }
return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key); if ($arg === true) {
return $key;
} else {
$method = $this->getFindMethod($id);
$id = var_export($id, true);
return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key);
}
} }
const INVERTED_SECTION = ' const INVERTED_SECTION = '
@@ -301,6 +425,50 @@ class Mustache_Compiler
); );
} }
const PARENT = '
%s
if ($parent = $this->mustache->LoadPartial(%s)) {
$context->pushBlockContext($newContext);
$buffer .= $parent->renderInternal($context, $indent);
$context->popBlockContext();
}
';
/**
* Generate Mustache Template inheritance parent call PHP source.
*
* @param string $id Parent tag name
* @param string $indent Whitespace indent to apply to parent
* @param array $children Child nodes
* @param int $level
*
* @return string Generated PHP source code
*/
private function parent($id, $indent, array $children, $level)
{
$realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs'));
return sprintf(
$this->prepare(self::PARENT, $level),
$this->walk($realChildren, $level),
var_export($id, true),
var_export($indent, true)
);
}
/**
* Helper method for filtering out non-block-arg tokens.
*
* @param array $node
*
* @return boolean True if $node is a block arg token.
*/
private static function onlyBlockArgs(array $node)
{
return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG;
}
const VARIABLE = ' const VARIABLE = '
$value = $this->resolveValue($context->%s(%s), $context, $indent);%s $value = $this->resolveValue($context->%s(%s), $context, $indent);%s
$buffer .= %s%s; $buffer .= %s%s;
@@ -466,6 +634,13 @@ class Mustache_Compiler
const IS_CALLABLE = '!is_string(%s) && is_callable(%s)'; const IS_CALLABLE = '!is_string(%s) && is_callable(%s)';
const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)'; const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';
/**
* Helper function to compile strict vs lax "is callable" logic.
*
* @param string $variable (default: '$value')
*
* @return string "is callable" logic
*/
private function getCallable($variable = '$value') private function getCallable($variable = '$value')
{ {
$tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE; $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE;
+40 -1
View File
@@ -14,7 +14,8 @@
*/ */
class Mustache_Context class Mustache_Context
{ {
private $stack = array(); private $stack = array();
private $blockStack = array();
/** /**
* Mustache rendering Context constructor. * Mustache rendering Context constructor.
@@ -38,6 +39,16 @@ class Mustache_Context
array_push($this->stack, $value); array_push($this->stack, $value);
} }
/**
* Push a new Context frame onto the block context stack.
*
* @param mixed $value Object or array to use for block context
*/
public function pushBlockContext($value)
{
array_push($this->blockStack, $value);
}
/** /**
* Pop the last Context frame from the stack. * Pop the last Context frame from the stack.
* *
@@ -48,6 +59,16 @@ class Mustache_Context
return array_pop($this->stack); return array_pop($this->stack);
} }
/**
* Pop the last block Context frame from the stack.
*
* @return mixed Last block Context frame (object or array)
*/
public function popBlockContext()
{
return array_pop($this->blockStack);
}
/** /**
* Get the last Context frame. * Get the last Context frame.
* *
@@ -120,6 +141,24 @@ class Mustache_Context
return $value; return $value;
} }
/**
* Find an argument in the block context stack.
*
* @param string $id
*
* @return mixed Variable value, or '' if not found.
*/
public function findInBlock($id)
{
foreach ($this->blockStack as $context) {
if (array_key_exists($id, $context)) {
return $context[$id];
}
}
return '';
}
/** /**
* Helper function to find a variable in the Context stack. * Helper function to find a variable in the Context stack.
* *
+41 -3
View File
@@ -27,6 +27,13 @@ class Mustache_Engine
const SPEC_VERSION = '1.1.2'; const SPEC_VERSION = '1.1.2';
const PRAGMA_FILTERS = 'FILTERS'; const PRAGMA_FILTERS = 'FILTERS';
const PRAGMA_BLOCKS = 'BLOCKS';
// Known pragmas
private static $knownPragmas = array(
self::PRAGMA_FILTERS => true,
self::PRAGMA_BLOCKS => true,
);
// Template cache // Template cache
private $templates = array(); private $templates = array();
@@ -44,6 +51,7 @@ class Mustache_Engine
private $charset = 'UTF-8'; private $charset = 'UTF-8';
private $logger; private $logger;
private $strictCallables = false; private $strictCallables = false;
private $pragmas = array();
// Services // Services
private $tokenizer; private $tokenizer;
@@ -110,6 +118,10 @@ class Mustache_Engine
* // helps protect against arbitrary code execution when user input is passed directly into the template. * // helps protect against arbitrary code execution when user input is passed directly into the template.
* // This currently defaults to false, but will default to true in v3.0. * // This currently defaults to false, but will default to true in v3.0.
* 'strict_callables' => true, * 'strict_callables' => true,
*
* // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
* // templates.
* 'pragmas' => [Mustache_Engine::PRAGMA_FILTERS],
* ); * );
* *
* @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable. * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable.
@@ -176,6 +188,15 @@ class Mustache_Engine
if (isset($options['strict_callables'])) { if (isset($options['strict_callables'])) {
$this->strictCallables = $options['strict_callables']; $this->strictCallables = $options['strict_callables'];
} }
if (isset($options['pragmas'])) {
foreach ($options['pragmas'] as $pragma) {
if (!isset(self::$knownPragmas[$pragma])) {
throw new Mustache_Exception_InvalidArgumentException(sprintf('Unknown pragma: "%s".', $pragma));
}
$this->pragmas[$pragma] = true;
}
}
} }
/** /**
@@ -226,6 +247,16 @@ class Mustache_Engine
return $this->charset; return $this->charset;
} }
/**
* Get the current globally enabled pragmas.
*
* @return array
*/
public function getPragmas()
{
return array_keys($this->pragmas);
}
/** /**
* Set the Mustache template Loader instance. * Set the Mustache template Loader instance.
* *
@@ -563,12 +594,13 @@ class Mustache_Engine
public function getTemplateClassName($source) public function getTemplateClassName($source)
{ {
return $this->templateClassPrefix . md5(sprintf( return $this->templateClassPrefix . md5(sprintf(
'version:%s,escape:%s,entity_flags:%i,charset:%s,strict_callables:%s,source:%s', 'version:%s,escape:%s,entity_flags:%i,charset:%s,strict_callables:%s,pragmas:%s,source:%s',
self::VERSION, self::VERSION,
isset($this->escape) ? 'custom' : 'default', isset($this->escape) ? 'custom' : 'default',
$this->entityFlags, $this->entityFlags,
$this->charset, $this->charset,
$this->strictCallables ? 'true' : 'false', $this->strictCallables ? 'true' : 'false',
implode(' ', array_keys($this->pragmas)),
$source $source
)); ));
} }
@@ -705,7 +737,10 @@ class Mustache_Engine
*/ */
private function parse($source) private function parse($source)
{ {
return $this->getParser()->parse($this->tokenize($source)); $parser = $this->getParser();
$parser->setPragmas($this->getPragmas());
return $parser->parse($this->tokenize($source));
} }
/** /**
@@ -728,7 +763,10 @@ class Mustache_Engine
array('className' => $name) array('className' => $name)
); );
return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags); $compiler = $this->getCompiler();
$compiler->setPragmas($this->getPragmas());
return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
} }
/** /**
+65 -3
View File
@@ -18,6 +18,8 @@ class Mustache_Parser
{ {
private $lineNum; private $lineNum;
private $lineTokens; private $lineTokens;
private $pragmas;
private $defaultPragmas = array();
/** /**
* Process an array of Mustache tokens and convert them into a parse tree. * Process an array of Mustache tokens and convert them into a parse tree.
@@ -30,10 +32,28 @@ class Mustache_Parser
{ {
$this->lineNum = -1; $this->lineNum = -1;
$this->lineTokens = 0; $this->lineTokens = 0;
$this->pragmas = $this->defaultPragmas;
return $this->buildTree($tokens); return $this->buildTree($tokens);
} }
/**
* Enable pragmas across all templates, regardless of the presence of pragma
* tags in the individual templates.
*
* @internal Users should set global pragmas in Mustache_Engine, not here :)
*
* @param array $pragmas
*/
public function setPragmas(array $pragmas)
{
$this->pragmas = array();
foreach ($pragmas as $pragma) {
$this->pragmas[$pragma] = true;
}
$this->defaultPragmas = $this->pragmas;
}
/** /**
* Helper method for recursively building a parse tree. * Helper method for recursively building a parse tree.
* *
@@ -60,11 +80,13 @@ class Mustache_Parser
switch ($token[Mustache_Tokenizer::TYPE]) { switch ($token[Mustache_Tokenizer::TYPE]) {
case Mustache_Tokenizer::T_DELIM_CHANGE: case Mustache_Tokenizer::T_DELIM_CHANGE:
$this->checkIfTokenIsAllowedInParent($parent, $token);
$this->clearStandaloneLines($nodes, $tokens); $this->clearStandaloneLines($nodes, $tokens);
break; break;
case Mustache_Tokenizer::T_SECTION: case Mustache_Tokenizer::T_SECTION:
case Mustache_Tokenizer::T_INVERTED: case Mustache_Tokenizer::T_INVERTED:
$this->checkIfTokenIsAllowedInParent($parent, $token);
$this->clearStandaloneLines($nodes, $tokens); $this->clearStandaloneLines($nodes, $tokens);
$nodes[] = $this->buildTree($tokens, $token); $nodes[] = $this->buildTree($tokens, $token);
break; break;
@@ -97,15 +119,40 @@ class Mustache_Parser
return $parent; return $parent;
case Mustache_Tokenizer::T_PARTIAL: case Mustache_Tokenizer::T_PARTIAL:
case Mustache_Tokenizer::T_PARTIAL_2: $this->checkIfTokenIsAllowedInParent($parent, $token);
// store the whitespace prefix for laters! //store the whitespace prefix for laters!
if ($indent = $this->clearStandaloneLines($nodes, $tokens)) { if ($indent = $this->clearStandaloneLines($nodes, $tokens)) {
$token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE]; $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE];
} }
$nodes[] = $token; $nodes[] = $token;
break; break;
case Mustache_Tokenizer::T_PARENT:
$this->checkIfTokenIsAllowedInParent($parent, $token);
$nodes[] = $this->buildTree($tokens, $token);
break;
case Mustache_Tokenizer::T_BLOCK_VAR:
if (isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS])) {
// BLOCKS pragma is enabled, let's do this!
if ($parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
$token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG;
}
$this->clearStandaloneLines($nodes, $tokens);
$nodes[] = $this->buildTree($tokens, $token);
} else {
// pretend this was just a normal "escaped" token...
$token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_ESCAPED;
// TODO: figure out how to figure out if there was a space after this dollar:
$token[Mustache_Tokenizer::NAME] = '$' . $token[Mustache_Tokenizer::NAME];
$nodes[] = $token;
}
break;
case Mustache_Tokenizer::T_PRAGMA: case Mustache_Tokenizer::T_PRAGMA:
$this->pragmas[$token[Mustache_Tokenizer::NAME]] = true;
// no break
case Mustache_Tokenizer::T_COMMENT: case Mustache_Tokenizer::T_COMMENT:
$this->clearStandaloneLines($nodes, $tokens); $this->clearStandaloneLines($nodes, $tokens);
$nodes[] = $token; $nodes[] = $token;
@@ -197,10 +244,25 @@ class Mustache_Parser
*/ */
private function tokenIsWhitespace(array $token) private function tokenIsWhitespace(array $token)
{ {
if ($token[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_TEXT) { if ($token[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_TEXT) {
return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]); return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]);
} }
return false; return false;
} }
/**
* Check whether a token is allowed inside a parent tag.
*
* @throws Mustache_Exception_SyntaxException if an invalid token is found inside a parent tag.
*
* @param array|null $parent
* @param array $token
*/
private function checkIfTokenIsAllowedInParent($parent, array $token)
{
if ($parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token);
}
}
} }
+3 -1
View File
@@ -63,7 +63,9 @@ abstract class Mustache_Template
*/ */
public function render($context = array()) public function render($context = array())
{ {
return $this->renderInternal($this->prepareContextStack($context)); return $this->renderInternal(
$this->prepareContextStack($context)
);
} }
/** /**
+8 -5
View File
@@ -27,13 +27,15 @@ class Mustache_Tokenizer
const T_END_SECTION = '/'; const T_END_SECTION = '/';
const T_COMMENT = '!'; const T_COMMENT = '!';
const T_PARTIAL = '>'; const T_PARTIAL = '>';
const T_PARTIAL_2 = '<'; const T_PARENT = '<';
const T_DELIM_CHANGE = '='; const T_DELIM_CHANGE = '=';
const T_ESCAPED = '_v'; const T_ESCAPED = '_v';
const T_UNESCAPED = '{'; const T_UNESCAPED = '{';
const T_UNESCAPED_2 = '&'; const T_UNESCAPED_2 = '&';
const T_TEXT = '_t'; const T_TEXT = '_t';
const T_PRAGMA = '%'; const T_PRAGMA = '%';
const T_BLOCK_VAR = '$';
const T_BLOCK_ARG = '$arg';
// Valid token types // Valid token types
private static $tagTypes = array( private static $tagTypes = array(
@@ -42,19 +44,20 @@ class Mustache_Tokenizer
self::T_END_SECTION => true, self::T_END_SECTION => true,
self::T_COMMENT => true, self::T_COMMENT => true,
self::T_PARTIAL => true, self::T_PARTIAL => true,
self::T_PARTIAL_2 => true, self::T_PARENT => true,
self::T_DELIM_CHANGE => true, self::T_DELIM_CHANGE => true,
self::T_ESCAPED => true, self::T_ESCAPED => true,
self::T_UNESCAPED => true, self::T_UNESCAPED => true,
self::T_UNESCAPED_2 => true, self::T_UNESCAPED_2 => true,
self::T_PRAGMA => true, self::T_PRAGMA => true,
self::T_BLOCK_VAR => true,
); );
// Interpolated tags // Interpolated tags
private static $interpolatedTags = array( private static $interpolatedTags = array(
self::T_ESCAPED => true, self::T_ESCAPED => true,
self::T_UNESCAPED => true, self::T_UNESCAPED => true,
self::T_UNESCAPED_2 => true, self::T_UNESCAPED_2 => true,
); );
// Token properties // Token properties
+2
View File
@@ -36,6 +36,7 @@ class Mustache_Test_EngineTest extends Mustache_Test_FunctionalTestCase
'escape' => 'strtoupper', 'escape' => 'strtoupper',
'entity_flags' => ENT_QUOTES, 'entity_flags' => ENT_QUOTES,
'charset' => 'ISO-8859-1', 'charset' => 'ISO-8859-1',
'pragmas' => array(Mustache_Engine::PRAGMA_FILTERS),
)); ));
$this->assertSame($logger, $mustache->getLogger()); $this->assertSame($logger, $mustache->getLogger());
@@ -50,6 +51,7 @@ class Mustache_Test_EngineTest extends Mustache_Test_FunctionalTestCase
$this->assertTrue($mustache->hasHelper('bar')); $this->assertTrue($mustache->hasHelper('bar'));
$this->assertFalse($mustache->hasHelper('baz')); $this->assertFalse($mustache->hasHelper('baz'));
$this->assertInstanceOf('Mustache_Cache_FilesystemCache', $mustache->getCache()); $this->assertInstanceOf('Mustache_Cache_FilesystemCache', $mustache->getCache());
$this->assertEquals(array(Mustache_Engine::PRAGMA_FILTERS), $mustache->getPragmas());
} }
public static function getFoo() public static function getFoo()
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2014 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* @group pragmas
* @group functional
*/
class Mustache_Test_FiveThree_Functional_EngineTest extends PHPUnit_Framework_TestCase
{
/**
* @dataProvider pragmaData
*/
public function testPragmasConstructorOption($pragmas, $helpers, $data, $tpl, $expect)
{
$mustache = new Mustache_Engine(array(
'pragmas' => $pragmas,
'helpers' => $helpers,
));
$this->assertEquals($expect, $mustache->render($tpl, $data));
}
public function pragmaData()
{
$helpers = array(
'longdate' => function (\DateTime $value) {
return $value->format('Y-m-d h:m:s');
}
);
$data = array(
'date' => new DateTime('1/1/2000', new DateTimeZone('UTC')),
);
$tpl = '{{ date | longdate }}';
return array(
array(array(Mustache_Engine::PRAGMA_FILTERS), $helpers, $data, $tpl, '2000-01-01 12:01:00'),
array(array(), $helpers, $data, $tpl, '' ),
);
}
}
@@ -0,0 +1,437 @@
<?php
/**
* @group inheritance
* @group functional
*/
class Mustache_Test_Functional_InheritanceTest extends PHPUnit_Framework_TestCase
{
private $mustache;
public function setUp()
{
$this->mustache = new Mustache_Engine(array(
'pragmas' => array(Mustache_Engine::PRAGMA_BLOCKS),
));
}
public function getIllegalInheritanceExamples()
{
return array(
array(
array(
'foo' => '{{$baz}}default content{{/baz}}',
),
array(
'bar' => 'set by user'
),
'{{< foo }}{{# bar }}{{$ baz }}{{/ baz }}{{/ bar }}{{/ foo }}',
),
array(
array(
'foo' => '{{$baz}}default content{{/baz}}'
),
array(
),
'{{<foo}}{{^bar}}{{$baz}}set by template{{/baz}}{{/bar}}{{/foo}}',
),
array(
array(
'foo' => '{{$baz}}default content{{/baz}}',
'qux' => 'I am a partial'
),
array(
),
'{{<foo}}{{>qux}}{{$baz}}set by template{{/baz}}{{/foo}}'
),
array(
array(
'foo' => '{{$baz}}default content{{/baz}}'
),
array(),
'{{<foo}}{{=<% %>=}}<%={{ }}=%>{{/foo}}'
)
);
}
public function getLegalInheritanceExamples()
{
return array(
array(
array(
'foo' => '{{$baz}}default content{{/baz}}',
),
array(
'bar' => 'set by user'
),
'{{<foo}}{{bar}}{{$baz}}override{{/baz}}{{/foo}}',
'override'
),
array(
array(
'foo' => '{{$baz}}default content{{/baz}}'
),
array(
),
'{{<foo}}{{! ignore me }}{{$baz}}set by template{{/baz}}{{/foo}}',
'set by template'
),
array(
array(
'foo' => '{{$baz}}defualt content{{/baz}}'
),
array(),
'{{<foo}}set by template{{$baz}}also set by template{{/baz}}{{/foo}}',
'also set by template'
)
);
}
public function testDefaultContent()
{
$tpl = $this->mustache->loadTemplate('{{$title}}Default title{{/title}}');
$data = array();
$this->assertEquals('Default title', $tpl->render($data));
}
public function testDefaultContentRendersVariables()
{
$tpl = $this->mustache->loadTemplate('{{$foo}}default {{bar}} content{{/foo}}');
$data = array(
'bar' => 'baz'
);
$this->assertEquals('default baz content', $tpl->render($data));
}
public function testDefaultContentRendersTripleMustacheVariables()
{
$tpl = $this->mustache->loadTemplate('{{$foo}}default {{{bar}}} content{{/foo}}');
$data = array(
'bar' => '<baz>'
);
$this->assertEquals('default <baz> content', $tpl->render($data));
}
public function testDefaultContentRendersSections()
{
$tpl = $this->mustache->loadTemplate(
'{{$foo}}default {{#bar}}{{baz}}{{/bar}} content{{/foo}}'
);
$data = array(
'bar' => array('baz' => 'qux')
);
$this->assertEquals('default qux content', $tpl->render($data));
}
public function testDefaultContentRendersNegativeSections()
{
$tpl = $this->mustache->loadTemplate(
'{{$foo}}default {{^bar}}{{baz}}{{/bar}} content{{/foo}}'
);
$data = array(
'foo' => array('bar' => 'qux'),
'baz' => 'three'
);
$this->assertEquals('default three content', $tpl->render($data));
}
public function testMustacheInjectionInDefaultContent()
{
$tpl = $this->mustache->loadTemplate(
'{{$foo}}default {{#bar}}{{baz}}{{/bar}} content{{/foo}}'
);
$data = array(
'bar' => array('baz' => '{{qux}}')
);
$this->assertEquals('default {{qux}} content', $tpl->render($data));
}
public function testDefaultContentRenderedInsideIncludedTemplates()
{
$partials = array(
'include' => '{{$foo}}default content{{/foo}}'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'{{<include}}{{/include}}'
);
$data = array();
$this->assertEquals('default content', $tpl->render($data));
}
public function testOverriddenContent()
{
$partials = array(
'super' => '...{{$title}}Default title{{/title}}...'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'{{<super}}{{$title}}sub template title{{/title}}{{/super}}'
);
$data = array();
$this->assertEquals('...sub template title...', $tpl->render($data));
}
public function testOverriddenPartial()
{
$partials = array(
'partial' => '|{{$stuff}}...{{/stuff}}{{$default}} default{{/default}}|'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'test {{<partial}}{{$stuff}}override1{{/stuff}}{{/partial}} {{<partial}}{{$stuff}}override2{{/stuff}}{{/partial}}'
);
$data = array();
$this->assertEquals('test |override1 default| |override2 default|', $tpl->render($data));
}
public function testDataDoesNotOverrideBlock()
{
$partials = array(
'include' => '{{$var}}var in include{{/var}}'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'{{<include}}{{$var}}var in template{{/var}}{{/include}}'
);
$data = array(
'var' => 'var in data'
);
$this->assertEquals('var in template', $tpl->render($data));
}
public function testDataDoesNotOverrideDefaultBlockValue()
{
$partials = array(
'include' => '{{$var}}var in include{{/var}}'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'{{<include}}{{/include}}'
);
$data = array(
'var' => 'var in data'
);
$this->assertEquals('var in include', $tpl->render($data));
}
public function testOverridePartialWithNewlines()
{
$partials = array(
'partial' => '{{$ballmer}}peaking{{/ballmer}}'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
"{{<partial}}{{\$ballmer}}\npeaked\n\n:(\n{{/ballmer}}{{/partial}}"
);
$data = array();
$this->assertEquals("peaked\n\n:(\n", $tpl->render($data));
}
public function testInheritIndentationWhenOverridingAPartial()
{
$partials = array(
'partial' =>
'stop:
{{$nineties}}collaborate and listen{{/nineties}}'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'{{<partial}}{{$nineties}}hammer time{{/nineties}}{{/partial}}'
);
$data = array();
$this->assertEquals(
'stop:
hammer time',
$tpl->render($data)
);
}
public function testOverrideOneSubstitutionButNotTheOther()
{
$partials = array(
'partial' => '{{$stuff}}default one{{/stuff}}, {{$stuff2}}default two{{/stuff2}}'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'{{<partial}}{{$stuff2}}override two{{/stuff2}}{{/partial}}'
);
$data = array();
$this->assertEquals('default one, override two', $tpl->render($data));
}
public function testSuperTemplatesWithNoParameters()
{
$partials = array(
'include' => '{{$foo}}default content{{/foo}}'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'{{>include}}|{{<include}}{{/include}}'
);
$data = array();
$this->assertEquals('default content|default content', $tpl->render($data));
}
public function testRecursionInInheritedTemplates()
{
$partials = array(
'include' => '{{$foo}}default content{{/foo}} {{$bar}}{{<include2}}{{/include2}}{{/bar}}',
'include2' => '{{$foo}}include2 default content{{/foo}} {{<include}}{{$bar}}don\'t recurse{{/bar}}{{/include}}'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'{{<include}}{{$foo}}override{{/foo}}{{/include}}'
);
$data = array();
$this->assertEquals('override override override don\'t recurse', $tpl->render($data));
}
public function testTopLevelSubstitutionsTakePrecedenceInMultilevelInheritance()
{
$partials = array(
'parent' => '{{<older}}{{$a}}p{{/a}}{{/older}}',
'older' => '{{<grandParent}}{{$a}}o{{/a}}{{/grandParent}}',
'grandParent' => '{{$a}}g{{/a}}'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'{{<parent}}{{$a}}c{{/a}}{{/parent}}'
);
$data = array();
$this->assertEquals('c', $tpl->render($data));
}
public function testMultiLevelInheritanceNoSubChild()
{
$partials = array(
'parent' => '{{<older}}{{$a}}p{{/a}}{{/older}}',
'older' => '{{<grandParent}}{{$a}}o{{/a}}{{/grandParent}}',
'grandParent' => '{{$a}}g{{/a}}'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'{{<parent}}{{/parent}}'
);
$data = array();
$this->assertEquals('p', $tpl->render($data));
}
public function testIgnoreTextInsideSuperTemplatesButParseArgs()
{
$partials = array(
'include' => '{{$foo}}default content{{/foo}}'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'{{<include}} asdfasd {{$foo}}hmm{{/foo}} asdfasdfasdf {{/include}}'
);
$data = array();
$this->assertEquals('hmm', $tpl->render($data));
}
public function testIgnoreTextInsideSuperTemplates()
{
$partials = array(
'include' => '{{$foo}}default content{{/foo}}'
);
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate(
'{{<include}} asdfasd asdfasdfasdf {{/include}}'
);
$data = array();
$this->assertEquals('default content', $tpl->render($data));
}
/**
* @dataProvider getIllegalInheritanceExamples
* @expectedException Mustache_Exception_SyntaxException
* @expectedExceptionMessage Illegal content in < parent tag
*/
public function testIllegalInheritanceExamples($partials, $data, $template)
{
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate($template);
$tpl->render($data);
}
/**
* @dataProvider getLegalInheritanceExamples
*/
public function testLegalInheritanceExamples($partials, $data, $template, $expect)
{
$this->mustache->setPartials($partials);
$tpl = $this->mustache->loadTemplate($template);
$this->assertSame($expect, $tpl->render($data));
}
}
+188
View File
@@ -88,6 +88,7 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
Mustache_Tokenizer::VALUE => 'bar' Mustache_Tokenizer::VALUE => 'bar'
), ),
), ),
array( array(
array( array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT, Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
@@ -116,6 +117,166 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
), ),
), ),
// This *would* be an invalid inheritance parse tree, but that pragma
// isn't enabled so it'll thunk it back into an "escaped" token:
array(
array(
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_VAR,
Mustache_Tokenizer::NAME => 'foo',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
),
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::VALUE => 'bar'
),
),
array(
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
Mustache_Tokenizer::NAME => '$foo',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
),
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::VALUE => 'bar'
),
),
),
);
}
/**
* @dataProvider getInheritanceTokenSets
*/
public function testParseWithInheritance($tokens, $expected)
{
$parser = new Mustache_Parser;
$parser->setPragmas(array(Mustache_Engine::PRAGMA_BLOCKS));
$this->assertEquals($expected, $parser->parse($tokens));
}
public function getInheritanceTokenSets()
{
return array(
array(
array(
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT,
Mustache_Tokenizer::NAME => 'foo',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::INDEX => 8
),
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_VAR,
Mustache_Tokenizer::NAME => 'bar',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::INDEX => 16
),
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::VALUE => 'baz'
),
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_END_SECTION,
Mustache_Tokenizer::NAME => 'bar',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::INDEX => 19
),
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_END_SECTION,
Mustache_Tokenizer::NAME => 'foo',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::INDEX => 27
)
),
array(
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT,
Mustache_Tokenizer::NAME => 'foo',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::INDEX => 8,
Mustache_Tokenizer::END => 27,
Mustache_Tokenizer::NODES => array(
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_ARG,
Mustache_Tokenizer::NAME => 'bar',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::INDEX => 16,
Mustache_Tokenizer::END => 19,
Mustache_Tokenizer::NODES => array(
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::VALUE => 'baz'
)
)
)
)
)
)
),
array(
array(
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_VAR,
Mustache_Tokenizer::NAME => 'foo',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
),
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::VALUE => 'bar'
),
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_END_SECTION,
Mustache_Tokenizer::NAME => 'foo',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::INDEX => 11,
),
),
array(
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_VAR,
Mustache_Tokenizer::NAME => 'foo',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::END => 11,
Mustache_Tokenizer::NODES => array(
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::VALUE => 'bar'
)
)
)
)
),
); );
} }
@@ -197,6 +358,33 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
), ),
), ),
), ),
// This *would* be a valid inheritance parse tree, but that pragma
// isn't enabled here so it's going to fail :)
array(
array(
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_VAR,
Mustache_Tokenizer::NAME => 'foo',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
),
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::VALUE => 'bar'
),
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_END_SECTION,
Mustache_Tokenizer::NAME => 'foo',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::INDEX => 11,
),
),
),
); );
} }
} }
+29
View File
@@ -273,6 +273,35 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase
) )
) )
), ),
// Ensure that $arg token is not picked up during tokenization
array(
'{{$arg}}default{{/arg}}',
null,
array(
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_VAR,
Mustache_Tokenizer::NAME => 'arg',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::INDEX => 8
),
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::VALUE => "default",
),
array(
Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_END_SECTION,
Mustache_Tokenizer::NAME => 'arg',
Mustache_Tokenizer::OTAG => '{{',
Mustache_Tokenizer::CTAG => '}}',
Mustache_Tokenizer::LINE => 0,
Mustache_Tokenizer::INDEX => 15,
)
)
),
); );
} }
} }