Rewrite the template system to have user-customizable open and close tags.
authorRobert Sesek <rsesek@bluestatic.org>
Sat, 1 Jun 2013 18:03:19 +0000 (14:03 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Sat, 1 Jun 2013 18:03:19 +0000 (14:03 -0400)
This requires a change in the grammar to normalize it. Rather than having {% %}
print an expression, it now evaluates a statement. To print, use {%= %} instead.
{!% %} has been removed.

CHANGES
testing/tests/views/template_test.php
views/template.php

diff --git a/CHANGES b/CHANGES
index 1322407202beaf97663e0ca3e2cffd6b5ee38f81..8217688c0fb1236c73f07e027f2c2a7b861b327d 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,5 +1,9 @@
 Version 2.0
 ================================================================================
 Version 2.0
 ================================================================================
+- Rewrote the template parser to allow customizable open and close tags, which
+  changed the grammar a little.
+    {!% %} has been switched to {% %}
+    {% %} has been switched to {%= %}
 - Split caching logic out of views\TemplateLoader into an interface
   views\CacheBackend and views\FileCacheBackend.
 - Add template usage profiling to views\.
 - Split caching logic out of views\TemplateLoader into an interface
   views\CacheBackend and views\FileCacheBackend.
 - Add template usage profiling to views\.
index 1c78c85a1f3f59c3b8e2637ad0c5dd8cc5180a81..eb0d0dc3fcf0721c187316365ea4b9c467e9975f 100644 (file)
@@ -34,14 +34,14 @@ class TemplateTest extends \PHPUnit_Framework_TestCase
 
   public function testRender1Var()
   {
 
   public function testRender1Var()
   {
-    $template = Template::NewWithData('test', 'Hello, {% $name | str %}');
+    $template = Template::NewWithData('test', 'Hello, {%= $name | str %}');
     $template->name = 'Robert';
     $this->assertEquals('Hello, Robert', $this->_Render($template));
   }
 
   public function testRender2Vars()
   {
     $template->name = 'Robert';
     $this->assertEquals('Hello, Robert', $this->_Render($template));
   }
 
   public function testRender2Vars()
   {
-    $template = Template::NewWithData('test', 'Hello, {% $name %}. Today is the {% $date->day | int %} of July.');
+    $template = Template::NewWithData('test', 'Hello, {%= $name %}. Today is the {%= $date->day | int %} of July.');
     $date = new \stdClass();
     $date->day = 26;
     $template->name = 'Robert';
     $date = new \stdClass();
     $date->day = 26;
     $template->name = 'Robert';
@@ -52,7 +52,7 @@ class TemplateTest extends \PHPUnit_Framework_TestCase
   public function testRenderIf()
   {
     $template = Template::NewWithData('test',
   public function testRenderIf()
   {
     $template = Template::NewWithData('test',
-        'You are {!% if (!$user->logged_in): %}not logged in{!% else: %}{% $user->name %}{!% endif %}');
+        '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';
     $template->user = new \stdClass();
     $template->user->logged_in = TRUE;
     $template->user->name = 'Robert';
@@ -87,10 +87,10 @@ class TemplateTest extends \PHPUnit_Framework_TestCase
 
     try {
       $catch = FALSE;
 
     try {
       $catch = FALSE;
-      $template = Template::NewWithData('test', "Salve\n\n{% \$name {!%");
+      $template = Template::NewWithData('test', "Salve\n\n{%= \$name {%");
     } catch (\hoplite\views\TemplateException $e) {
       $message = $e->GetMessage();
     } catch (\hoplite\views\TemplateException $e) {
       $message = $e->GetMessage();
-      $this->assertTrue(strpos($message, '3:10') !== FALSE);
+      $this->assertTrue(strpos($message, '3:11') !== FALSE);
       $catch = TRUE;
     }
     $this->assertTrue($catch);
       $catch = TRUE;
     }
     $this->assertTrue($catch);
@@ -98,7 +98,7 @@ class TemplateTest extends \PHPUnit_Framework_TestCase
 
   public function testRenderVars()
   {
 
   public function testRenderVars()
   {
-    $template = Template::NewWithData('test', 'Some {% $v %}');
+    $template = Template::NewWithData('test', 'Some {%= $v %}');
     $this->assertEquals('Some value', $template->Render(array('v' => 'value')));
 
     $template->v = 'other';
     $this->assertEquals('Some value', $template->Render(array('v' => 'value')));
 
     $template->v = 'other';
index 53598efe60c1584b150630d4848e0153854337c8..71cc98cc48ee915c7e4d80128ce012796b776148 100644 (file)
@@ -42,13 +42,37 @@ function MakeURL($path)
 }
 
 /*!
 }
 
 /*!
-  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.
+  Template parses a a text file (typically HTML) by expanding a small macro
+  language into "compiled" PHP.
+
+  The opening and close tags are user-customizable but the default is {% %}.
+
+  The open and close tags are translated to '<?php' and '?>', respectively.
+
+  Modifiers may be placed after the open and close tags as shorthand for
+  further shorthand.
+
+    {% expression %}
+        Evaluates a non-printing expression, treating it as pure PHP. This can
+        be used to evaluate conditions:
+            {% if (!$user->user_id): %}<p>Hello, Guest!</p>{% endif %}
+
+    {%= $value %}
+        Prints the value and automatically HTML-escapes it.
+
+    {%= $value | int }
+        Prints $value by coerceing it to an integer. Other coercion types
+        are str (the default if no pipe symbol, above), int, float, and
+        raw (no escaping).
 */
 class Template
 {
 */
 class Template
 {
+  /*! @var string The macro opening delimiter. */
+  static protected $open_tag = '{%';
+
+  /*! @var string The macro closing delimiter. */
+  static protected $close_tag = '%}';
+
   /*! @var string The name of the template. */
   protected $template_name = '';
 
   /*! @var string The name of the template. */
   protected $template_name = '';
 
@@ -81,6 +105,16 @@ class Template
     return $template;
   }
 
     return $template;
   }
 
+  /*!
+    Sets the open and closing delimiters. The caller is responsible for choosing
+    values that will not cause the parser to barf.
+  */
+  static public function set_open_close_tags($open_tag, $close_tag)
+  {
+    self::$open_tag = $open_tag;
+    self::$close_tag = $close_tag;
+  }
+
   /*! Gets the name of the template. */
   public function template_name() { return $this->template_name; }
 
   /*! Gets the name of the template. */
   public function template_name() { return $this->template_name; }
 
@@ -128,18 +162,6 @@ class Template
     This performs the macro expansion. The language is very simple and is merely
     shorthand for the PHP tags.
 
     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
   */
     @param string Raw template data
     @return string Executable PHP
   */
@@ -151,14 +173,15 @@ class Template
     // If processing a macro, this contains the contents of the macro while
     // it is being extracted from the template.
     $macro = '';
     // 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.
     $i_last_line = 0;  // The value of |i| at the previous new line, used for column numbering.
 
     $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.
     $i_last_line = 0;  // The value of |i| at the previous new line, used for column numbering.
 
+    $open_tag_len = strlen(self::$open_tag);
+    $close_tag_len = strlen(self::$close_tag);
+
     while ($i < $length) {
       // See how far the current position is from the end of the string.
       $delta = $length - $i;
     while ($i < $length) {
       // See how far the current position is from the end of the string.
       $delta = $length - $i;
@@ -169,8 +192,9 @@ class Template
         $i_last_line = $i;
       }
 
         $i_last_line = $i;
       }
 
-      // Check for simple PHP short-tag expansion.
-      if ($delta >= 3 && substr($data, $i, 3) == '{!%') {
+      // Check for the open tag.
+      if ($delta >= $open_tag_len &&
+          substr($data, $i, $open_tag_len) == self::$open_tag) {
         // If an expansion has already been opened, then it's an error to nest.
         if ($looking_for_end) {
           $column = $i - $i_last_line;
         // If an expansion has already been opened, then it's an error to nest.
         if ($looking_for_end) {
           $column = $i - $i_last_line;
@@ -178,51 +202,29 @@ class Template
         }
 
         $looking_for_end = TRUE;
         }
 
         $looking_for_end = TRUE;
-        $processed .= '<' . '?php';
-        $i += 3;
+        $macro = '';
+        $i += $open_tag_len;
         continue;
       }
         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) {
-            $column = $i - $i_last_line;
-            throw new TemplateException("Unexpected end of expansion at line $line_number:$column");
-          }
-
-          // 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 == '{%') {
-          // If an expansion has already been opened, then it's an error to nest.
-          if ($looking_for_end) {
-            $column = $i - $i_last_line;
-            throw new TemplateException("Unexpected start of expansion at line $line_number:$column");
-          }
-
-          $processed .= '<' . '?php echo ';
-          $macro = '';
-          $in_macro = TRUE;
-          $looking_for_end = TRUE;
-          $i += 2;
-          continue;
+      // Check for the close tag.
+      else if ($delta >= $close_tag_len &&
+               substr($data, $i, $close_tag_len) == self::$close_tag) {
+        // If an end tag was encountered without an open tag, that's an error.
+        if (!$looking_for_end) {
+          $column = $i - $i_last_line;
+          throw new TemplateException("Unexpected end of expansion at line $line_number:$column");
         }
         }
+
+        $expanded_macro = $this->_ProcessMacro($macro);
+        $processed .= "<?php $expanded_macro ?>";
+        $looking_for_end = FALSE;
+        $i += $close_tag_len;
+        continue;
       }
 
       // All other characters go into a storage bin. If currently in a macro,
       // save off the data separately for parsing.
       }
 
       // All other characters go into a storage bin. If currently in a macro,
       // save off the data separately for parsing.
-      if ($in_macro)
+      if ($looking_for_end)
         $macro .= $data[$i];
       else
         $processed .= $data[$i];
         $macro .= $data[$i];
       else
         $processed .= $data[$i];
@@ -233,18 +235,34 @@ class Template
   }
 
   /*!
   }
 
   /*!
-    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.
+    Takes the contents of a macro, i.e. the string between the open and close
+    tags, and transforms it into a PHP statement.
   */
   protected function _ProcessMacro($macro)
   */
   protected function _ProcessMacro($macro)
+  {
+    if (strlen($macro) < 1)
+      return $macro;
+
+    // If the macro has a modifier character, as described in the class comment,
+    // futher thransform the statement.
+    switch ($macro[0]) {
+      case '=': return $this->_ProcessInterpolation(substr($macro, 1));
+      default:  return $macro;
+    }
+  }
+
+  /*!
+    Creates a printing expression for a value, optionally coercing and escaping
+    it to a specific type.
+  */
+  protected function _ProcessInterpolation($macro)
   {
     // The pipe operator specifies how to sanitize the output.
     $formatter_pos = strrpos($macro, '|');
 
     // No specifier defaults to escaped string.
     if ($formatter_pos === FALSE)
   {
     // 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 . ')';
+      return 'echo hoplite\\base\\filter\\String(' . $macro . ')';
 
     // Otherwise, apply the right filter.
     $formatter = trim(substr($macro, $formatter_pos + 1));
 
     // Otherwise, apply the right filter.
     $formatter = trim(substr($macro, $formatter_pos + 1));
@@ -260,7 +278,7 @@ class Template
 
     // Now get the expression and return a PHP statement.
     $expression = trim(substr($macro, 0, $formatter_pos));
 
     // Now get the expression and return a PHP statement.
     $expression = trim(substr($macro, 0, $formatter_pos));
-    return 'hoplite\\base\\filter\\' . $function . '(' . $expression . ')';
+    return 'echo hoplite\\base\\filter\\' . $function . '(' . $expression . ')';
   }
 }
 
   }
 }