Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hogan.js style template inheritance #198

Merged
merged 14 commits into from
Aug 16, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 113 additions & 8 deletions src/Mustache/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
class Mustache_Compiler
{

private $pragmas;
private $sections;
private $source;
private $indentNextLine;
private $customEscape;
private $entityFlags;
private $charset;
private $strictCallables;
private $pragmas;

/**
* Compile a Mustache token parse tree into PHP source code.
Expand Down Expand Up @@ -94,14 +94,46 @@ private function walk(array $tree, $level = 0)
break;

case Mustache_Tokenizer::T_PARTIAL:
case Mustache_Tokenizer::T_PARTIAL_2:
$code .= $this->partial(
$node[Mustache_Tokenizer::NAME],
isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
$level
);
break;

case Mustache_Tokenizer::T_PARENT:
$code .= $this->parent(
$node[Mustache_Tokenizer::NAME],
isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
$level,
$node[Mustache_Tokenizer::NODES]
);
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_2:
$code .= $this->variable($node[Mustache_Tokenizer::NAME], false, $level);
Expand All @@ -122,7 +154,6 @@ private function walk(array $tree, $level = 0)
throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node);
}
}

return $code;
}

Expand All @@ -136,6 +167,7 @@ public function renderInternal(Mustache_Context $context, $indent = \'\')
{
$this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
$buffer = \'\';
$newContext = array();
%s

return $buffer;
Expand All @@ -150,6 +182,7 @@ class %s extends Mustache_Template
public function renderInternal(Mustache_Context $context, $indent = \'\')
{
$buffer = \'\';
$newContext = array();
%s

return $buffer;
Expand All @@ -171,11 +204,49 @@ private function writeCode($tree, $name)
$code = $this->walk($tree);
$sections = implode("\n", $this->sections);
$klass = empty($this->sections) ? self::KLASS_NO_LAMBDAS : self::KLASS;

$callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';

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
}
';

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;
';

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 = '
// %s section
$value = $context->%s(%s);%s
Expand All @@ -199,7 +270,8 @@ private function section%s(Mustache_Context $context, $indent, $value)
} elseif (!empty($value)) {
$values = $this->isIterable($value) ? $value : array($value);
foreach ($values as $value) {
$context->push($value);%s
$context->push($value);
%s
$context->pop();
}
}
Expand All @@ -220,16 +292,14 @@ private function section%s(Mustache_Context $context, $indent, $value)
*
* @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 = '';

if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) {
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);
$callable = $this->getCallable();

Expand All @@ -245,7 +315,13 @@ private function section($nodes, $id, $start, $end, $otag, $ctag, $level)
$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 = '
Expand Down Expand Up @@ -302,6 +378,35 @@ private function partial($id, $indent, $level)
);
}

const PARENT = '

if ($parent = $this->mustache->LoadPartial(%s)) {
$context->pushBlockContext($newContext);
$buffer .= $parent->renderInternal($context, $indent);
$context->popBlockContext();
}
';

private function parent($id, $indent, $level, $children)
{
$block = '';

$real_children = array_filter($children, array(__CLASS__, 'return_only_block_args'));

$block = $this->walk($real_children, $level);

return $block. sprintf(
$this->prepare(self::PARENT, $level),
var_export($id, true),
var_export($indent, true)
);
}

private static function return_only_block_args($child)
{
return $child[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_BLOCK_ARG;
}

const VARIABLE = '
$value = $this->resolveValue($context->%s(%s), $context, $indent);%s
$buffer .= %s%s;
Expand Down
22 changes: 22 additions & 0 deletions src/Mustache/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
class Mustache_Context
{
private $stack = array();
private $block_stack = array();

/**
* Mustache rendering Context constructor.
Expand All @@ -38,6 +39,11 @@ public function push($value)
array_push($this->stack, $value);
}

public function pushBlockContext($value)
{
array_push($this->block_stack, $value);
}

/**
* Pop the last Context frame from the stack.
*
Expand All @@ -48,6 +54,11 @@ public function pop()
return array_pop($this->stack);
}

public function popBlockContext()
{
return array_pop($this->block_stack);
}

/**
* Get the last Context frame.
*
Expand Down Expand Up @@ -120,6 +131,17 @@ public function findDot($id)
return $value;
}

public function findInBlock($id)
{
foreach($this->block_stack as $context) {
if (array_key_exists($id, $context)) {
return $context[$id];
}
}

return '';
}

/**
* Helper function to find a variable in the Context stack.
*
Expand Down
27 changes: 24 additions & 3 deletions src/Mustache/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ private function buildTree(array &$tokens, array $parent = null)

switch ($token[Mustache_Tokenizer::TYPE]) {
case Mustache_Tokenizer::T_DELIM_CHANGE:
$this->checkIfTokenIsAllowedInParent($parent, $token);
$this->clearStandaloneLines($nodes, $tokens);
break;

case Mustache_Tokenizer::T_SECTION:
case Mustache_Tokenizer::T_INVERTED:
$this->checkIfTokenIsAllowedInParent($parent, $token);
$this->clearStandaloneLines($nodes, $tokens);
$nodes[] = $this->buildTree($tokens, $token);
break;
Expand Down Expand Up @@ -95,17 +97,29 @@ private function buildTree(array &$tokens, array $parent = null)
$parent[Mustache_Tokenizer::NODES] = $nodes;

return $parent;
break;

case Mustache_Tokenizer::T_PARTIAL:
case Mustache_Tokenizer::T_PARTIAL_2:
// store the whitespace prefix for laters!
$this->checkIfTokenIsAllowedInParent($parent, $token);
//store the whitespace prefix for laters!
if ($indent = $this->clearStandaloneLines($nodes, $tokens)) {
$token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE];
}
$nodes[] = $token;
break;

case Mustache_Tokenizer::T_PARENT:
$this->checkIfTokenIsAllowedInParent($parent, $token);
$nodes[] = $this->buildTree($tokens, $token);
break;

case Mustache_Tokenizer::T_BLOCK_VAR:
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);
break;

case Mustache_Tokenizer::T_PRAGMA:
case Mustache_Tokenizer::T_COMMENT:
$this->clearStandaloneLines($nodes, $tokens);
Expand Down Expand Up @@ -205,4 +219,11 @@ private function tokenIsWhitespace(array $token)

return false;
}

private function checkIfTokenIsAllowedInParent($parent, $token)
{
if ($parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token);
}
}
}
4 changes: 3 additions & 1 deletion src/Mustache/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ public function __invoke($context = array())
*/
public function render($context = array())
{
return $this->renderInternal($this->prepareContextStack($context));
return $this->renderInternal(
$this->prepareContextStack($context)
);
}

/**
Expand Down
7 changes: 5 additions & 2 deletions src/Mustache/Tokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ class Mustache_Tokenizer
const T_END_SECTION = '/';
const T_COMMENT = '!';
const T_PARTIAL = '>';
const T_PARTIAL_2 = '<';
const T_PARENT = '<';
const T_DELIM_CHANGE = '=';
const T_ESCAPED = '_v';
const T_UNESCAPED = '{';
const T_UNESCAPED_2 = '&';
const T_TEXT = '_t';
const T_PRAGMA = '%';
const T_BLOCK_VAR = '$';
const T_BLOCK_ARG = '$arg';

// Valid token types
private static $tagTypes = array(
Expand All @@ -43,12 +45,13 @@ class Mustache_Tokenizer
self::T_END_SECTION => true,
self::T_COMMENT => true,
self::T_PARTIAL => true,
self::T_PARTIAL_2 => true,
self::T_PARENT => true,
self::T_DELIM_CHANGE => true,
self::T_ESCAPED => true,
self::T_UNESCAPED => true,
self::T_UNESCAPED_2 => true,
self::T_PRAGMA => true,
self::T_BLOCK_VAR => true,
);

// Interpolated tags
Expand Down
Loading