From f73df2af68b7ffda63efa2fa653d307bbfd6f1e9 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 27 Jul 2012 10:17:39 -0700 Subject: [PATCH 1/7] Add pragmas to compiler, tokenizer. --- src/Mustache/Compiler.php | 6 ++++++ src/Mustache/Tokenizer.php | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index aff8e88..d7ac74a 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -22,6 +22,7 @@ class Mustache_Compiler private $indentNextLine; private $customEscape; private $charset; + private $pragmas; /** * Compile a Mustache token parse tree into PHP source code. @@ -36,6 +37,7 @@ class Mustache_Compiler */ public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8') { + $this->pragmas = array(); $this->sections = array(); $this->source = $source; $this->indentNextLine = true; @@ -61,6 +63,10 @@ class Mustache_Compiler $level++; foreach ($tree as $node) { switch ($node[Mustache_Tokenizer::TYPE]) { + case Mustache_Tokenizer::T_PRAGMA: + $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true; + break; + case Mustache_Tokenizer::T_SECTION: $code .= $this->section( $node[Mustache_Tokenizer::NODES], diff --git a/src/Mustache/Tokenizer.php b/src/Mustache/Tokenizer.php index 1dd3ef8..3b9e69a 100644 --- a/src/Mustache/Tokenizer.php +++ b/src/Mustache/Tokenizer.php @@ -34,6 +34,7 @@ class Mustache_Tokenizer const T_UNESCAPED = '{'; const T_UNESCAPED_2 = '&'; const T_TEXT = '_t'; + const T_PRAGMA = '%'; // Valid token types private static $tagTypes = array( @@ -47,6 +48,7 @@ class Mustache_Tokenizer self::T_ESCAPED => true, self::T_UNESCAPED => true, self::T_UNESCAPED_2 => true, + self::T_PRAGMA => true, ); // Interpolated tags @@ -67,6 +69,7 @@ class Mustache_Tokenizer const NODES = 'nodes'; const VALUE = 'value'; + private $pragmas; private $state; private $tagType; private $tag; @@ -126,6 +129,9 @@ class Mustache_Tokenizer if ($this->tagType === self::T_DELIM_CHANGE) { $i = $this->changeDelimiters($text, $i); $this->state = self::IN_TEXT; + } elseif ($this->tagType === self::T_PRAGMA) { + $i = $this->addPragma($text, $i); + $this->state = self::IN_TEXT; } else { if ($tag !== null) { $i++; @@ -168,6 +174,13 @@ class Mustache_Tokenizer $this->filterLine(true); + foreach ($this->pragmas as $pragma) { + array_unshift($this->tokens, array( + self::TYPE => self::T_PRAGMA, + self::NAME => $pragma, + )); + } + return $this->tokens; } @@ -185,6 +198,7 @@ class Mustache_Tokenizer $this->lineStart = 0; $this->otag = '{{'; $this->ctag = '}}'; + $this->pragmas = array(); } /** @@ -270,6 +284,14 @@ class Mustache_Tokenizer return $closeIndex + strlen($close) - 1; } + private function addPragma($text, $index) + { + $end = strpos($text, $this->ctag, $index); + $this->pragmas[] = trim(substr($text, $index + 2, $end - $index - 2)); + + return $end + strlen($this->ctag) - 1; + } + /** * Test whether it's time to change tags. * From d209d1aea91bc512237841a79464ad3f7030914c Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 27 Jul 2012 10:18:16 -0700 Subject: [PATCH 2/7] Implement {{% FILTERS }} pragma. --- src/Mustache/Compiler.php | 38 ++++++++++++- src/Mustache/Engine.php | 6 ++- .../Test/FiveThree/Functional/FiltersTest.php | 53 +++++++++++++++++++ 3 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 test/Mustache/Test/FiveThree/Functional/FiltersTest.php diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index d7ac74a..7bc10b1 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -264,7 +264,7 @@ class Mustache_Compiler } const VARIABLE = ' - $value = $context->%s(%s); + $value = $context->%s(%s);%s if (!is_string($value) && is_callable($value)) { $value = $this->mustache ->loadLambda((string) call_user_func($value)) @@ -284,11 +284,45 @@ class Mustache_Compiler */ private function variable($id, $escape, $level) { + $filters = ''; + + if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) { + list($id, $filters) = $this->getFilters($id, $level); + } + $method = $this->getFindMethod($id); $id = ($method !== 'last') ? var_export($id, true) : ''; $value = $escape ? $this->getEscape() : '$value'; - return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $this->flushIndent(), $value); + return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value); + } + + const FILTER = ' + $filter = $context->%s(%s); + $value = (is_string($filter) || !is_callable($filter)) ? "" : call_user_func($filter, $value); + '; + + /** + * Generate Mustache Template variable filtering PHP source. + * + * @param string $id Variable name + * @param int $level + * + * @return string Generated variable filtering PHP source + */ + private function getFilters($id, $level) + { + $chunks = array_map('trim', explode('|', $id)); + $id = array_shift($chunks); + $filters = ''; + + foreach ($chunks as $filter) { + $method = $this->getFindMethod($filter); + $filter = ($method !== 'last') ? var_export($filter, true) : ''; + $filters .= sprintf($this->prepare(self::FILTER, $level), $method, $filter); + } + + return array($id, $filters); } const LINE = '$buffer .= "\n";'; diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 97cc57d..6e99943 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -23,8 +23,10 @@ */ class Mustache_Engine { - const VERSION = '2.0.2'; - const SPEC_VERSION = '1.1.2'; + const VERSION = '2.0.2'; + const SPEC_VERSION = '1.1.2'; + + const PRAGMA_FILTERS = 'FILTERS'; // Template cache private $templates = array(); diff --git a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php new file mode 100644 index 0000000..bc5b4de --- /dev/null +++ b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php @@ -0,0 +1,53 @@ +mustache = new Mustache_Engine; + } + + public function testSingleFilter() { + $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ date | longdate }}'); + + $this->mustache->addHelper('longdate', function(\DateTime $value) { + return $value->format('Y-m-d h:m:s'); + }); + + $foo = new \StdClass; + $foo->date = new DateTime('1/1/2000'); + + $this->assertEquals('2000-01-01 12:01:00', $tpl->render($foo)); + } + + public function testChainedFilters() { + $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ date | longdate | withbrackets }}'); + + $this->mustache->addHelper('longdate', function(\DateTime $value) { + return $value->format('Y-m-d h:m:s'); + }); + + $this->mustache->addHelper('withbrackets', function($value) { + return sprintf('[[%s]]', $value); + }); + + $foo = new \StdClass; + $foo->date = new DateTime('1/1/2000'); + + $this->assertEquals('[[2000-01-01 12:01:00]]', $tpl->render($foo)); + } +} From a64a6f82eeec8650f5a536c108b682b7e6a81be2 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 27 Jul 2012 10:56:56 -0700 Subject: [PATCH 3/7] Handle broken pipes --- src/Mustache/Compiler.php | 6 +++-- .../Test/FiveThree/Functional/FiltersTest.php | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index 7bc10b1..48d06ee 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -298,8 +298,10 @@ class Mustache_Compiler } const FILTER = ' - $filter = $context->%s(%s); - $value = (is_string($filter) || !is_callable($filter)) ? "" : call_user_func($filter, $value); + if (!empty($value)) { + $filter = $context->%s(%s); + $value = (is_string($filter) || !is_callable($filter)) ? "" : call_user_func($filter, $value); + } '; /** diff --git a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php index bc5b4de..ff2c7dd 100644 --- a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php +++ b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php @@ -50,4 +50,26 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T $this->assertEquals('[[2000-01-01 12:01:00]]', $tpl->render($foo)); } + + public function testBrokenPipe() { + $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ foo | bar | baz }}'); + $this->assertEquals('', $tpl->render(array( + 'foo' => 'FOO', + ))); + + $this->assertEquals('', $tpl->render(array( + 'foo' => 'FOO', + 'bar' => function($value) { return 'BAR'; }, + ))); + + $this->assertEquals('', $tpl->render(array( + 'foo' => 'FOO', + 'baz' => function($value) { return 'BAZ'; }, + ))); + + $this->assertEquals('', $tpl->render(array( + 'bar' => function($value) { return 'BAR'; }, + 'baz' => function($value) { return 'BAZ'; }, + ))); + } } From 1cf44d5de9dfca171013e847103468570d4059c0 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 27 Jul 2012 11:05:29 -0700 Subject: [PATCH 4/7] First value in the pipe should be interpolated first. --- src/Mustache/Compiler.php | 4 ++-- .../Mustache/Test/FiveThree/Functional/FiltersTest.php | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index 48d06ee..293f9e0 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -264,12 +264,12 @@ class Mustache_Compiler } const VARIABLE = ' - $value = $context->%s(%s);%s + $value = $context->%s(%s); if (!is_string($value) && is_callable($value)) { $value = $this->mustache ->loadLambda((string) call_user_func($value)) ->renderInternal($context, $indent); - } + }%s $buffer .= %s%s; '; diff --git a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php index ff2c7dd..1d1af85 100644 --- a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php +++ b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php @@ -72,4 +72,14 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T 'baz' => function($value) { return 'BAZ'; }, ))); } + + public function testInterpolateFirst() { + $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ foo | bar }}'); + $this->assertEquals('win!', $tpl->render(array( + 'foo' => 'FOO', + 'bar' => function($value) { + return ($value === 'FOO') ? 'win!' : 'fail :('; + }, + ))); + } } From fc453b5d0100003a734d47fd3584c7fe6f43a40f Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 1 Aug 2012 23:00:42 -0700 Subject: [PATCH 5/7] Better filters implementation: * Throw UnexpectedValueException when unknown filter is found. * More optimized compiler code. * Falsey initial values will still be fed through the pipe. --- src/Mustache/Compiler.php | 47 +++++++++----- .../Test/FiveThree/Functional/FiltersTest.php | 62 +++++++++++-------- 2 files changed, 67 insertions(+), 42 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index 293f9e0..1c3bcc4 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -297,13 +297,6 @@ class Mustache_Compiler return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value); } - const FILTER = ' - if (!empty($value)) { - $filter = $context->%s(%s); - $value = (is_string($filter) || !is_callable($filter)) ? "" : call_user_func($filter, $value); - } - '; - /** * Generate Mustache Template variable filtering PHP source. * @@ -314,17 +307,41 @@ class Mustache_Compiler */ private function getFilters($id, $level) { - $chunks = array_map('trim', explode('|', $id)); - $id = array_shift($chunks); - $filters = ''; + $filters = array_map('trim', explode('|', $id)); + $id = array_shift($filters); - foreach ($chunks as $filter) { - $method = $this->getFindMethod($filter); - $filter = ($method !== 'last') ? var_export($filter, true) : ''; - $filters .= sprintf($this->prepare(self::FILTER, $level), $method, $filter); + return array($id, $this->getFilter($filters, $level)); + } + + const FILTER = ' + $filter = $context->%s(%s); + if (!is_string($filter) && is_callable($filter)) { + $value = call_user_func($filter, $value);%s + } else { + throw new UnexpectedValueException(%s); + } + '; + + /** + * Generate PHP source for a single filter. + * + * @param array $filters + * @param int $level + * + * @return string Generated filter PHP source + */ + private function getFilter(array $filters, $level) + { + if (empty($filters)) { + return ''; } - return array($id, $filters); + $name = array_shift($filters); + $method = $this->getFindMethod($name); + $filter = ($method !== 'last') ? var_export($name, true) : ''; + $msg = var_export(sprintf('Filter not found: %s', $name), true); + + return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $this->getFilter($filters, $level + 1), $msg); } const LINE = '$buffer .= "\n";'; diff --git a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php index 1d1af85..b7d54d3 100644 --- a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php +++ b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php @@ -13,15 +13,18 @@ * @group filters * @group functional */ -class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_TestCase { +class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_TestCase +{ private $mustache; - public function setUp() { + public function setUp() + { $this->mustache = new Mustache_Engine; } - public function testSingleFilter() { + public function testSingleFilter() + { $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ date | longdate }}'); $this->mustache->addHelper('longdate', function(\DateTime $value) { @@ -34,7 +37,8 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T $this->assertEquals('2000-01-01 12:01:00', $tpl->render($foo)); } - public function testChainedFilters() { + public function testChainedFilters() + { $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ date | longdate | withbrackets }}'); $this->mustache->addHelper('longdate', function(\DateTime $value) { @@ -51,29 +55,8 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T $this->assertEquals('[[2000-01-01 12:01:00]]', $tpl->render($foo)); } - public function testBrokenPipe() { - $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ foo | bar | baz }}'); - $this->assertEquals('', $tpl->render(array( - 'foo' => 'FOO', - ))); - - $this->assertEquals('', $tpl->render(array( - 'foo' => 'FOO', - 'bar' => function($value) { return 'BAR'; }, - ))); - - $this->assertEquals('', $tpl->render(array( - 'foo' => 'FOO', - 'baz' => function($value) { return 'BAZ'; }, - ))); - - $this->assertEquals('', $tpl->render(array( - 'bar' => function($value) { return 'BAR'; }, - 'baz' => function($value) { return 'BAZ'; }, - ))); - } - - public function testInterpolateFirst() { + public function testInterpolateFirst() + { $tpl = $this->mustache->loadTemplate('{{% FILTERS }}{{ foo | bar }}'); $this->assertEquals('win!', $tpl->render(array( 'foo' => 'FOO', @@ -82,4 +65,29 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T }, ))); } + + /** + * @expectedException UnexpectedValueException + * @dataProvider getBrokenPipes + */ + public function testThrowsExceptionForBrokenPipes($tpl, $data) + { + $this->mustache + ->loadTemplate(sprintf('{{%% FILTERS }}{{ %s }}', $tpl)) + ->render($data); + } + + public function getBrokenPipes() + { + return array( + array('foo | bar', array()), + array('foo | bar', array('foo' => 'FOO')), + array('foo | bar', array('foo' => 'FOO', 'bar' => 'BAR')), + array('foo | bar | baz', array('foo' => 'FOO', 'bar' => function() { return 'BAR'; })), + array('foo | bar | baz', array('foo' => 'FOO', 'baz' => function() { return 'BAZ'; })), + array('foo | bar | baz', array('bar' => function() { return 'BAR'; })), + array('foo | bar | baz', array('baz' => function() { return 'BAZ'; })), + array('foo | bar.baz', array('foo' => 'FOO', 'bar' => function() { return 'BAR'; }, 'baz' => function() { return 'BAZ'; })), + ); + } } From 2872dd5048eee1fdcba62eee97a4c0cbf348cef2 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 1 Aug 2012 23:28:56 -0700 Subject: [PATCH 6/7] Another non-callable test case for good measure --- test/Mustache/Test/FiveThree/Functional/FiltersTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php index b7d54d3..8c51e37 100644 --- a/test/Mustache/Test/FiveThree/Functional/FiltersTest.php +++ b/test/Mustache/Test/FiveThree/Functional/FiltersTest.php @@ -83,6 +83,7 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T array('foo | bar', array()), array('foo | bar', array('foo' => 'FOO')), array('foo | bar', array('foo' => 'FOO', 'bar' => 'BAR')), + array('foo | bar', array('foo' => 'FOO', 'bar' => array(1, 2))), array('foo | bar | baz', array('foo' => 'FOO', 'bar' => function() { return 'BAR'; })), array('foo | bar | baz', array('foo' => 'FOO', 'baz' => function() { return 'BAZ'; })), array('foo | bar | baz', array('bar' => function() { return 'BAR'; })), From 242e94c91b4853812422288e8622d1295788f837 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Wed, 1 Aug 2012 23:29:20 -0700 Subject: [PATCH 7/7] Avoid indentomatic with long filter chains. --- src/Mustache/Compiler.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index 1c3bcc4..fe02419 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -315,11 +315,10 @@ class Mustache_Compiler const FILTER = ' $filter = $context->%s(%s); - if (!is_string($filter) && is_callable($filter)) { - $value = call_user_func($filter, $value);%s - } else { + if (is_string($filter) || !is_callable($filter)) { throw new UnexpectedValueException(%s); } + $value = call_user_func($filter, $value);%s '; /** @@ -341,7 +340,7 @@ class Mustache_Compiler $filter = ($method !== 'last') ? var_export($name, true) : ''; $msg = var_export(sprintf('Filter not found: %s', $name), true); - return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $this->getFilter($filters, $level + 1), $msg); + return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $msg, $this->getFilter($filters, $level)); } const LINE = '$buffer .= "\n";';