Sfoglia il codice sorgente

Merge pull request #102 from bobthecow/feature/filters

Filters implementation.
Justin Hileman 13 anni fa
parent
commit
010e360c5a

+ 60 - 2
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],
@@ -263,7 +269,7 @@ class Mustache_Compiler
             $value = $this->mustache
                 ->loadLambda((string) call_user_func($value))
                 ->renderInternal($context, $indent);
-        }
+        }%s
         $buffer .= %s%s;
     ';
 
@@ -278,11 +284,63 @@ 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);
+    }
+
+    /**
+     * 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)
+    {
+        $filters = array_map('trim', explode('|', $id));
+        $id      = array_shift($filters);
+
+        return array($id, $this->getFilter($filters, $level));
+    }
+
+    const FILTER = '
+        $filter = $context->%s(%s);
+        if (is_string($filter) || !is_callable($filter)) {
+            throw new UnexpectedValueException(%s);
+        }
+        $value = call_user_func($filter, $value);%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 '';
+        }
+
+        $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, $msg, $this->getFilter($filters, $level));
     }
 
     const LINE = '$buffer .= "\n";';

+ 4 - 2
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();

+ 22 - 0
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.
      *

+ 94 - 0
test/Mustache/Test/FiveThree/Functional/FiltersTest.php

@@ -0,0 +1,94 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2012 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group filters
+ * @group functional
+ */
+class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_TestCase
+{
+
+    private $mustache;
+
+    public function setUp()
+    {
+        $this->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));
+    }
+
+    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 :(';
+            },
+        )));
+    }
+
+    /**
+     * @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', 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'; })),
+            array('foo | bar | baz', array('baz' => function() { return 'BAZ'; })),
+            array('foo | bar.baz', array('foo' => 'FOO', 'bar' => function() { return 'BAR'; }, 'baz' => function() { return 'BAZ'; })),
+        );
+    }
+}