From e734487fd1be3f07c4f0c4441e009cecd1c880fe Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sat, 1 Jun 2013 14:03:19 -0400 Subject: [PATCH] Rewrite the template system to have user-customizable open and close tags. 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 | 4 + testing/tests/views/template_test.php | 12 +-- views/template.php | 144 +++++++++++++++----------- 3 files changed, 91 insertions(+), 69 deletions(-) diff --git a/CHANGES b/CHANGES index 1322407..8217688 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,9 @@ 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\. diff --git a/testing/tests/views/template_test.php b/testing/tests/views/template_test.php index 1c78c85..eb0d0dc 100644 --- a/testing/tests/views/template_test.php +++ b/testing/tests/views/template_test.php @@ -34,14 +34,14 @@ class TemplateTest extends \PHPUnit_Framework_TestCase 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 = 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'; @@ -52,7 +52,7 @@ class TemplateTest extends \PHPUnit_Framework_TestCase 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'; @@ -87,10 +87,10 @@ class TemplateTest extends \PHPUnit_Framework_TestCase 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(); - $this->assertTrue(strpos($message, '3:10') !== FALSE); + $this->assertTrue(strpos($message, '3:11') !== FALSE); $catch = TRUE; } $this->assertTrue($catch); @@ -98,7 +98,7 @@ class TemplateTest extends \PHPUnit_Framework_TestCase 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'; diff --git a/views/template.php b/views/template.php index 53598ef..71cc98c 100644 --- a/views/template.php +++ b/views/template.php @@ -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 '', 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): %}

Hello, Guest!

{% 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 { + /*! @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 = ''; @@ -81,6 +105,16 @@ class 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; } @@ -128,18 +162,6 @@ class 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: -

Hello, {% $user->name %}!

- - To specify the type to format, you use the pipe symbol and then one of the - following types: str (default; above), int, float, raw. -

Hello, {% %user->name | str %}

-

Hello, user #{% $user->user_id | int %}

- - To evaluate a non-printing expression, simply add a '!' before the first '%': - {!% if (!$user->user_id): %}

Hello, Guest!

{!% endif %} - @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 = ''; - $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. + $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; @@ -169,8 +192,9 @@ class Template $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; @@ -178,51 +202,29 @@ class Template } $looking_for_end = TRUE; - $processed .= '<' . '?php'; - $i += 3; + $macro = ''; + $i += $open_tag_len; 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 .= ""; + $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. - if ($in_macro) + if ($looking_for_end) $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) + { + 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) - 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)); @@ -260,7 +278,7 @@ class Template // 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 . ')'; } } -- 2.22.5