Write the initial builtin Template macro functions implementation.
[hoplite.git] / views / template.php
1 <?php
2 // Hoplite
3 // Copyright (c) 2011 Blue Static
4 //
5 // This program is free software: you can redistribute it and/or modify it
6 // under the terms of the GNU General Public License as published by the Free
7 // Software Foundation, either version 3 of the License, or any later version.
8 //
9 // This program is distributed in the hope that it will be useful, but WITHOUT
10 // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12 // more details.
13 //
14 // You should have received a copy of the GNU General Public License along with
15 // this program. If not, see <http://www.gnu.org/licenses/>.
16
17 namespace hoplite\views;
18
19 use \hoplite\base\Profiling;
20
21 require_once HOPLITE_ROOT . '/base/filter.php';
22 require_once HOPLITE_ROOT . '/base/profiling.php';
23
24 /*!
25 Renders a template with additional vars.
26 @param string The template name to render
27 @param array Variables with which to render.
28 */
29 function Inject($name, $vars = array())
30 {
31 echo TemplateLoader::Fetch($name)->Render($vars);
32 }
33
34 /*! @brief Creates a URL via RootController::MakeURL().
35 This requires the root controller be set in the $GLOBALS as
36 hoplite\http\RootController.
37 @param string Path.
38 */
39 function MakeURL($path)
40 {
41 return $GLOBALS['hoplite\http\RootController']->MakeURL($path, FALSE);
42 }
43
44 /*!
45 Template parses a a text file (typically HTML) by expanding a small macro
46 language into "compiled" PHP.
47
48 The opening and close tags are user-customizable but the default is {% %}.
49
50 The open and close tags are translated to '<?php' and '?>', respectively.
51
52 Modifiers may be placed after the open and close tags as shorthand for
53 further shorthand.
54
55 {% expression %}
56 Evaluates a non-printing expression, treating it as pure PHP. This can
57 be used to evaluate conditions:
58 {% if (!$user->user_id): %}<p>Hello, Guest!</p>{% endif %}
59
60 {%= $value %}
61 Prints the value and automatically HTML-escapes it.
62
63 {%= $value | int }
64 Prints $value by coerceing it to an integer. Other coercion types
65 are str (the default if no pipe symbol, above), int, float, and
66 raw (no escaping).
67
68 {%# macro_function arg1, arg2, ... argN %}
69 Runs the built-in macro_function with the specified arguments.
70 The built-ins are:
71 * import(template_name:string, new_variables:array)
72 Imports a subtemplate into the current scope, giving it access
73 to all the current variables, while overriding and defining
74 new ones via new_variables.
75 * url(path:string)
76 Constructs an absolute URL to the given path from the script
77 root.
78 */
79 class Template
80 {
81 /*! @var string The macro opening delimiter. */
82 static protected $open_tag = '{%';
83
84 /*! @var string The macro closing delimiter. */
85 static protected $close_tag = '%}';
86
87 /*! @var string The name of the template. */
88 protected $template_name = '';
89
90 /*! @var string The compiled template. */
91 protected $data = NULL;
92
93 /*! @var array Variables to provide to the template. */
94 protected $vars = array();
95
96 /*!
97 Creates a new Template with a given name.
98 @param string
99 */
100 private function __construct($name)
101 {
102 $this->template_name = $name;
103 }
104
105 static public function NewWithData($name, $data)
106 {
107 $template = new Template($name);
108 $template->data = $template->_ProcessTemplate($data);
109 return $template;
110 }
111
112 static public function NewWithCompiledData($name, $data)
113 {
114 $template = new Template($name);
115 $template->data = $data;
116 return $template;
117 }
118
119 /*!
120 Sets the open and closing delimiters. The caller is responsible for choosing
121 values that will not cause the parser to barf.
122 */
123 static public function set_open_close_tags($open_tag, $close_tag)
124 {
125 self::$open_tag = $open_tag;
126 self::$close_tag = $close_tag;
127 }
128
129 /*! Gets the name of the template. */
130 public function template_name() { return $this->template_name; }
131
132 /*! Gets the parsed data of the template. */
133 public function template() { return $this->data; }
134
135 /*! Overload property accessors to set view variables. */
136 public function __get($key)
137 {
138 return $this->vars[$key];
139 }
140 public function __set($key, $value)
141 {
142 $this->vars[$key] = $value;
143 }
144
145 /*! This includes the template and renders it out. */
146 public function Render($vars = array())
147 {
148 $__template_data = $this->data;
149 $__template_vars = array_merge($this->vars, $vars);
150 $render = function () use ($__template_data, $__template_vars) {
151 extract($__template_vars);
152 eval('?>' . $__template_data . '<' . '?');
153 };
154
155 ob_start();
156
157 $error = error_reporting();
158 error_reporting($error & ~E_NOTICE);
159
160 $render();
161
162 error_reporting($error);
163
164 if (Profiling::IsProfilingEnabled())
165 TemplateLoader::GetInstance()->MarkTemplateRendered($this->name);
166
167 $data = ob_get_contents();
168 ob_end_clean();
169 return $data;
170 }
171
172 /*! @brief Does any pre-processing on the template.
173 This performs the macro expansion. The language is very simple and is merely
174 shorthand for the PHP tags.
175
176 @param string Raw template data
177 @return string Executable PHP
178 */
179 protected function _ProcessTemplate($data)
180 {
181 // The parsed output as compiled PHP.
182 $processed = '';
183
184 // If processing a macro, this contains the contents of the macro while
185 // it is being extracted from the template.
186 $macro = '';
187 $length = strlen($data);
188 $i = 0; // The current position of the iterator.
189 $looking_for_end = FALSE; // Whehter or not an end tag is expected.
190 $line_number = 1; // The current line number.
191 $i_last_line = 0; // The value of |i| at the previous new line, used for column numbering.
192
193 $open_tag_len = strlen(self::$open_tag);
194 $close_tag_len = strlen(self::$close_tag);
195
196 while ($i < $length) {
197 // See how far the current position is from the end of the string.
198 $delta = $length - $i;
199
200 // When a new line is reached, update the counters.
201 if ($data[$i] == "\n") {
202 ++$line_number;
203 $i_last_line = $i;
204 }
205
206 // Check for the open tag.
207 if ($delta >= $open_tag_len &&
208 substr($data, $i, $open_tag_len) == self::$open_tag) {
209 // If an expansion has already been opened, then it's an error to nest.
210 if ($looking_for_end) {
211 $column = $i - $i_last_line;
212 throw new TemplateException("Unexpected start of expansion at line $line_number:$column");
213 }
214
215 $looking_for_end = TRUE;
216 $macro = '';
217 $i += $open_tag_len;
218 continue;
219 }
220 // Check for the close tag.
221 else if ($delta >= $close_tag_len &&
222 substr($data, $i, $close_tag_len) == self::$close_tag) {
223 // If an end tag was encountered without an open tag, that's an error.
224 if (!$looking_for_end) {
225 $column = $i - $i_last_line;
226 throw new TemplateException("Unexpected end of expansion at line $line_number:$column");
227 }
228
229 $expanded_macro = $this->_ProcessMacro($macro);
230 $processed .= "<?php $expanded_macro ?>";
231 $looking_for_end = FALSE;
232 $i += $close_tag_len;
233 continue;
234 }
235
236 // All other characters go into a storage bin. If currently in a macro,
237 // save off the data separately for parsing.
238 if ($looking_for_end)
239 $macro .= $data[$i];
240 else
241 $processed .= $data[$i];
242 ++$i;
243 }
244
245 return $processed;
246 }
247
248 /*!
249 Takes the contents of a macro, i.e. the string between the open and close
250 tags, and transforms it into a PHP statement.
251 */
252 protected function _ProcessMacro($macro)
253 {
254 if (strlen($macro) < 1)
255 return $macro;
256
257 // If the macro has a modifier character, as described in the class comment,
258 // futher thransform the statement.
259 switch ($macro[0]) {
260 case '=': return $this->_ProcessInterpolation(substr($macro, 1));
261 case '#': return $this->_ProcessBuiltin(substr($macro, 1));
262 default: return $macro;
263 }
264 }
265
266 /*!
267 Creates a printing expression for a value, optionally coercing and escaping
268 it to a specific type.
269 */
270 protected function _ProcessInterpolation($macro)
271 {
272 // The pipe operator specifies how to sanitize the output.
273 $formatter_pos = strrpos($macro, '|');
274
275 // No specifier defaults to escaped string.
276 if ($formatter_pos === FALSE)
277 return 'echo hoplite\\base\\filter\\String(' . $macro . ')';
278
279 // Otherwise, apply the right filter.
280 $formatter = trim(substr($macro, $formatter_pos + 1));
281 $function = '';
282 switch (strtolower($formatter)) {
283 case 'int': $function = 'Int'; break;
284 case 'float': $function = 'Float'; break;
285 case 'str': $function = 'String'; break;
286 case 'raw': $function = 'RawString'; break;
287 default:
288 throw new TemplateException('Invalid macro formatter "' . $formatter . '"');
289 }
290
291 // Now get the expression and return a PHP statement.
292 $expression = trim(substr($macro, 0, $formatter_pos));
293 return 'echo hoplite\\base\\filter\\' . $function . '(' . $expression . ')';
294 }
295
296 protected function _ProcessBuiltin($macro)
297 {
298 $macro = trim($macro);
299 $function_pos = strpos($macro, ' ');
300 if ($function_pos === FALSE)
301 throw new TemplateException('No macro function specified');
302
303 $function = substr($macro, 0, $function_pos);
304 $args = substr($macro, $function_pos);
305 switch ($function) {
306 case 'url': return $this->_Builtin_url($args);
307 case 'import': return $this->_Builtin_import($args);
308 default:
309 throw new TemplateException("Invalid macro function '$function'");
310 }
311 }
312
313 protected function _Builtin_url($args)
314 {
315 return "echo hoplite\\views\\MakeURL($args)";
316 }
317
318 protected function _Builtin_import($args)
319 {
320 $template = $args;
321 $vars = '';
322
323 $template_pos = strpos($args, ',');
324 if ($template_pos !== FALSE) {
325 $template = substr($args, 0, $template_pos);
326 $vars = substr($args, $template_pos + 1);
327 }
328
329 if ($vars)
330 $vars = "array_merge(\$__template_vars, $vars)";
331 else
332 $vars = '$__template_vars';
333
334 return "hoplite\\views\\Inject($template, $vars)";
335 }
336 }
337
338 class TemplateException extends \Exception {}