Add basic template parser and test
authorRobert Sesek <rsesek@bluestatic.org>
Tue, 26 Jul 2011 12:54:38 +0000 (08:54 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Tue, 26 Jul 2011 12:54:38 +0000 (08:54 -0400)
base/filter.php [new file with mode: 0644]
testing/tests/views/template_test.php [new file with mode: 0644]
views/template.php [new file with mode: 0644]

diff --git a/base/filter.php b/base/filter.php
new file mode 100644 (file)
index 0000000..61a2bfd
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+// Hoplite
+// Copyright (c) 2011 Blue Static
+// 
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General License as published by the Free
+// Software Foundation, either version 3 of the License, or any later version.
+// 
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General License for
+// more details.
+//
+// You should have received a copy of the GNU General License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace hoplite\base\filter;
+
+function TrimmedString($str)
+{
+    return trim($str);
+}
+
+function String($str)
+{
+    $find = array(
+        '<',
+        '>',
+        '"'
+    );
+    $replace = array(
+        '&lt;',
+        '&gt;',
+        '&quot;'
+    );
+    return str_replace($find, $replace, $str);
+}
+
+function Int($int)
+{
+    return intval($int);
+}
+
+function Float($float)
+{
+    return floatval($float);
+}
+
+function Bool($bool)
+{
+    $str = strtolower(TrimmedString($bool));
+    if ($str == 'yes' || $str == 'true')
+        return TRUE;
+    else if ($str == 'no' || $str == 'false')
+        return FALSE;
+    return (bool)$bool;
+}
diff --git a/testing/tests/views/template_test.php b/testing/tests/views/template_test.php
new file mode 100644 (file)
index 0000000..0a64e18
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+// Hoplite
+// Copyright (c) 2011 Blue Static
+// 
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or any later version.
+// 
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace hoplite\test;
+use hoplite\views\Template;
+
+require_once HOPLITE_ROOT . '/views/template.php';
+
+class TemplateTest extends \PHPUnit_Framework_TestCase
+{
+  private function _Render($template)
+  {
+    ob_start();
+    $template->Render();
+    $data = ob_get_contents();
+    ob_end_clean();
+    return $data;
+  }
+
+  public function testRenderSimple()
+  {
+    $template = Template::NewWithData('Hello World');
+    $this->assertEquals('Hello World', $this->_Render($template));
+  }
+
+  public function testRender1Var()
+  {
+    $template = Template::NewWithData('Hello, {% $name | str %}');
+    $template->name = 'Robert';
+    $this->assertEquals('Hello, Robert', $this->_Render($template));
+  }
+
+  public function testRender2Vars()
+  {
+    $template = Template::NewWithData('Hello, {% $name %}. Today is the {% $date->day %} of July.');
+    $date = new \stdClass();
+    $date->day = 26;
+    $template->name = 'Robert';
+    $template->date = $date;
+    $this->assertEquals('Hello, Robert. Today is the 26 of July.', $this->_Render($template));
+  }
+
+  public function testRenderIf()
+  {
+    $template = Template::NewWithData(
+        'You are {!% if (!$user->logged_in): %}not logged in{!% else: %}{% $user->name %}{!% endif %}');
+    $template->user = new \stdClass();
+    $template->user->logged_in = TRUE;
+    $template->user->name = 'Robert';
+    $this->assertEquals('You are Robert', $this->_Render($template));
+
+    $template->user->logged_in = FALSE;
+    $this->assertEquals('You are not logged in', $this->_Render($template));
+  }
+}
diff --git a/views/template.php b/views/template.php
new file mode 100644 (file)
index 0000000..f9cb5e6
--- /dev/null
@@ -0,0 +1,219 @@
+<?php
+// Hoplite
+// Copyright (c) 2011 Blue Static
+// 
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or any later version.
+// 
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace hoplite\views;
+
+require_once HOPLITE_ROOT . '/base/filter.php';
+
+/*!
+ A Template is initialized with a text file (typically HTML) and can render it
+ with data from some model. It has a short macro expansion system, equivalent
+ to PHP short open tags, but usable on all installations. Template caching of
+ the parsed state is available.
+*/
+class Template
+{
+  /*! @var string The name of the template. */
+  protected $template_name = '';
+
+  /*! @var string The compiled template. */
+  protected $data = NULL;
+
+  /*! @var array Variables to provide to the template. */
+  protected $vars = array();
+
+  /*!
+    Creates a new Template with a given name.
+    @param string
+  */
+  private function __construct($name)
+  {
+    $this->template_name = $name;
+  }
+
+  static public function NewWithData($data)
+  {
+    $template = new Template('');
+    $template->data = $template->_ProcessTemplate($data);
+    return $template;
+  }
+
+  static public function NewFromCompiledData($data)
+  {
+    $template = new Template('');
+    $template->data = $data;
+    return $template;
+  }
+
+  /*! Gets the name of the template. */
+  public function template_name() { return $this->template_name; }
+
+  /*! Gets the parsed data of the template. */
+  public function template() { return $this->data; }
+
+  /*! Overload property accessors to set view variables. */
+  public function __get($key)
+  {
+    return $this->vars[$key];
+  }
+  public function __set($key, $value)
+  {
+    $this->vars[$key] = $value;
+  }
+
+  /*! This includes the template and renders it out. */
+  public function Render()
+  {
+    $_template = $this->data;
+    $_vars = $this->vars;
+    $render = function () use ($_template, $_vars) {
+      extract($_vars);
+      eval('?>' . $_template . '<' . '?');
+    };
+    $render();
+  }
+
+  /*! @brief Does any pre-processing on the template.
+    This performs the macro expansion. The language is very simple and is merely
+    shorthand for the PHP tags.
+
+    The most common thing needed in templates is string escaped output from an
+    expression. HTML entities are automatically escaped in this format:
+      <p>Hello, {% $user->name %}!</p>
+
+    To specify the type to format, you use the pipe symbol and then one of the
+    following types: str (default; above), int, float, raw.
+      <p>Hello, {% %user->name | str %}</p>
+      <p>Hello, user #{% $user->user_id | int %}</p>
+
+    To evaluate a non-printing expression, simply add a '!' before the first '%':
+      {!% if (!$user->user_id): %}<p>Hello, Guest!</p>{!% endif %}
+
+    @param string Raw template data
+    @return string Executable PHP
+  */
+  protected function _ProcessTemplate($data)
+  {
+    // The parsed output as compiled PHP.
+    $processed = '';
+
+    // If processing a macro, this contains the contents of the macro while
+    // it is being extracted from the template.
+    $macro = '';
+    $in_macro = FALSE;
+
+    $length = strlen($data);
+    $i = 0;  // The current position of the iterator.
+    $looking_for_end = FALSE;  // Whehter or not an end tag is expected.
+    $line_number = 1;  // The current line number.
+    $column_number = 0;  // The current column number.
+
+    while ($i < $length) {
+      // See how far the current position is from the end of the string.
+      $delta = $length - $i;
+
+      // When a new line is reached, update the counters.
+      if ($data[$i] == "\n") {
+        ++$line_number;
+        $column_number = 0;
+      }
+
+      // Check for simple PHP short-tag expansion.
+      if ($delta >= 3 && substr($data, $i, 3) == '{!%') {
+        // If an expansion has already been opened, then it's an error to nest.
+        if ($looking_for_end)
+          throw new TemplateException("Unexpected start of expansion at line $line_number:$column_number");
+
+        $looking_for_end = TRUE;
+        $processed .= '<' . '?php';
+        $i += 3;
+        continue;
+      }
+      // Check for either the end tag or the start of a macro expansion.
+      else if ($delta >= 2) {
+        $substr = substr($data, $i, 2);
+        // Check for an end tag.
+        if ($substr == '%}') {
+          // If an end tag was encountered without an open tag, that's an error.
+          if (!$looking_for_end)
+            throw new TemplateException("Unexpected end of expansion at line $line_number:$column_number");
+
+          // If this is a macro, it's time to process it.
+          if ($in_macro)
+            $processed .= $this->_ProcessMacro($macro);
+
+          $looking_for_end = FALSE;
+          $in_macro = FALSE;
+          $processed .= ' ?>';
+          $i += 2;
+          continue;
+        }
+        // Check for the beginning of a macro.
+        else if ($substr == '{%') {
+          $processed .= '<' . '?php echo ';
+          $macro = '';
+          $in_macro = TRUE;
+          $looking_for_end = TRUE;
+          $i += 2;
+          continue;
+        }
+      }
+
+      // All other characters go into a storage bin. If currently in a macro,
+      // save off the data separately for parsing.
+      if ($in_macro)
+        $macro .= $data[$i];
+      else
+        $processed .= $data[$i];
+      ++$i;
+    }
+
+    return $processed;
+  }
+
+  /*!
+    Takes the contents of a macro |{% $some_var | int %}|, which is the part in
+    between the open and close brackets (excluding '%') and transforms it into a
+    PHP statement.
+  */
+  protected function _ProcessMacro($macro)
+  {
+    // The pipe operator specifies how to sanitize the output.
+    $formatter_pos = strrpos($macro, '|');
+
+    // No specifier defaults to escaped string.
+    if ($formatter_pos === FALSE)
+      return 'hoplite\\base\\filter\\String(' . $macro . ')';
+
+    // Otherwise, apply the right filter.
+    $formatter = trim(substr($macro, $formatter_pos + 1));
+    $function = '';
+    switch (strtolower($formatter)) {
+      case 'int':   $function = 'Int'; break;
+      case 'float': $function = 'Float'; break;
+      case 'str':   $function = 'String'; break;
+      case 'raw':   $function = 'RawString'; break;
+      default:
+        throw new TemplateException('Invalid macro formatter "' . $formatter . '"');
+    }
+
+    // Now get the expression and return a PHP statement.
+    $expression = trim(substr($macro, 0, $formatter_pos));
+    return 'hoplite\\base\\filter\\' . $function . '(' . $expression . ')';
+  }
+}
+
+class TemplateException extends \Exception {}