Add hoplite\views\MakeURL() in a kind of hacky way.
[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 require_once HOPLITE_ROOT . '/base/filter.php';
20
21 /*!
22 Renders a template with additional vars.
23 @param string The template name to render
24 @param array Variables with which to render.
25 */
26 function Inject($name, $vars = array())
27 {
28 echo TemplateLoader::Fetch($name)->Render($vars);
29 }
30
31 /*! @brief Creates a URL via RootController::MakeURL().
32 This requires the root controller be set in the $GLOBALS as
33 hoplite\http\RootController.
34 @param string Path.
35 */
36 function MakeURL($path)
37 {
38 return $GLOBALS['hoplite\http\RootController']->MakeURL($path, FALSE);
39 }
40
41 /*!
42 A Template is initialized with a text file (typically HTML) and can render it
43 with data from some model. It has a short macro expansion system, equivalent
44 to PHP short open tags, but usable on all installations. Template caching of
45 the parsed state is available.
46 */
47 class Template
48 {
49 /*! @var string The name of the template. */
50 protected $template_name = '';
51
52 /*! @var string The compiled template. */
53 protected $data = NULL;
54
55 /*! @var array Variables to provide to the template. */
56 protected $vars = array();
57
58 /*!
59 Creates a new Template with a given name.
60 @param string
61 */
62 private function __construct($name)
63 {
64 $this->template_name = $name;
65 }
66
67 static public function NewWithData($data)
68 {
69 $template = new Template('');
70 $template->data = $template->_ProcessTemplate($data);
71 return $template;
72 }
73
74 static public function NewWithCompiledData($data)
75 {
76 $template = new Template('');
77 $template->data = $data;
78 return $template;
79 }
80
81 /*! Gets the name of the template. */
82 public function template_name() { return $this->template_name; }
83
84 /*! Gets the parsed data of the template. */
85 public function template() { return $this->data; }
86
87 /*! Overload property accessors to set view variables. */
88 public function __get($key)
89 {
90 return $this->vars[$key];
91 }
92 public function __set($key, $value)
93 {
94 $this->vars[$key] = $value;
95 }
96
97 /*! This includes the template and renders it out. */
98 public function Render($vars = array())
99 {
100 $_template = $this->data;
101 $_vars = array_merge($this->vars, $vars);
102 $render = function () use ($_template, $_vars) {
103 extract($_vars);
104 eval('?>' . $_template . '<' . '?');
105 };
106
107 ob_start();
108
109 $error = error_reporting();
110 error_reporting($error & ~E_NOTICE);
111
112 $render();
113
114 error_reporting($error);
115
116 $data = ob_get_contents();
117 ob_end_clean();
118 return $data;
119 }
120
121 /*! @brief Does any pre-processing on the template.
122 This performs the macro expansion. The language is very simple and is merely
123 shorthand for the PHP tags.
124
125 The most common thing needed in templates is string escaped output from an
126 expression. HTML entities are automatically escaped in this format:
127 <p>Hello, {% $user->name %}!</p>
128
129 To specify the type to format, you use the pipe symbol and then one of the
130 following types: str (default; above), int, float, raw.
131 <p>Hello, {% %user->name | str %}</p>
132 <p>Hello, user #{% $user->user_id | int %}</p>
133
134 To evaluate a non-printing expression, simply add a '!' before the first '%':
135 {!% if (!$user->user_id): %}<p>Hello, Guest!</p>{!% endif %}
136
137 @param string Raw template data
138 @return string Executable PHP
139 */
140 protected function _ProcessTemplate($data)
141 {
142 // The parsed output as compiled PHP.
143 $processed = '';
144
145 // If processing a macro, this contains the contents of the macro while
146 // it is being extracted from the template.
147 $macro = '';
148 $in_macro = FALSE;
149
150 $length = strlen($data);
151 $i = 0; // The current position of the iterator.
152 $looking_for_end = FALSE; // Whehter or not an end tag is expected.
153 $line_number = 1; // The current line number.
154 $i_last_line = 0; // The value of |i| at the previous new line, used for column numbering.
155
156 while ($i < $length) {
157 // See how far the current position is from the end of the string.
158 $delta = $length - $i;
159
160 // When a new line is reached, update the counters.
161 if ($data[$i] == "\n") {
162 ++$line_number;
163 $i_last_line = $i;
164 }
165
166 // Check for simple PHP short-tag expansion.
167 if ($delta >= 3 && substr($data, $i, 3) == '{!%') {
168 // If an expansion has already been opened, then it's an error to nest.
169 if ($looking_for_end) {
170 $column = $i - $i_last_line;
171 throw new TemplateException("Unexpected start of expansion at line $line_number:$column");
172 }
173
174 $looking_for_end = TRUE;
175 $processed .= '<' . '?php';
176 $i += 3;
177 continue;
178 }
179 // Check for either the end tag or the start of a macro expansion.
180 else if ($delta >= 2) {
181 $substr = substr($data, $i, 2);
182 // Check for an end tag.
183 if ($substr == '%}') {
184 // If an end tag was encountered without an open tag, that's an error.
185 if (!$looking_for_end) {
186 $column = $i - $i_last_line;
187 throw new TemplateException("Unexpected end of expansion at line $line_number:$column");
188 }
189
190 // If this is a macro, it's time to process it.
191 if ($in_macro)
192 $processed .= $this->_ProcessMacro($macro);
193
194 $looking_for_end = FALSE;
195 $in_macro = FALSE;
196 $processed .= ' ?>';
197 $i += 2;
198 continue;
199 }
200 // Check for the beginning of a macro.
201 else if ($substr == '{%') {
202 // If an expansion has already been opened, then it's an error to nest.
203 if ($looking_for_end) {
204 $column = $i - $i_last_line;
205 throw new TemplateException("Unexpected start of expansion at line $line_number:$column");
206 }
207
208 $processed .= '<' . '?php echo ';
209 $macro = '';
210 $in_macro = TRUE;
211 $looking_for_end = TRUE;
212 $i += 2;
213 continue;
214 }
215 }
216
217 // All other characters go into a storage bin. If currently in a macro,
218 // save off the data separately for parsing.
219 if ($in_macro)
220 $macro .= $data[$i];
221 else
222 $processed .= $data[$i];
223 ++$i;
224 }
225
226 return $processed;
227 }
228
229 /*!
230 Takes the contents of a macro |{% $some_var | int %}|, which is the part in
231 between the open and close brackets (excluding '%') and transforms it into a
232 PHP statement.
233 */
234 protected function _ProcessMacro($macro)
235 {
236 // The pipe operator specifies how to sanitize the output.
237 $formatter_pos = strrpos($macro, '|');
238
239 // No specifier defaults to escaped string.
240 if ($formatter_pos === FALSE)
241 return 'hoplite\\base\\filter\\String(' . $macro . ')';
242
243 // Otherwise, apply the right filter.
244 $formatter = trim(substr($macro, $formatter_pos + 1));
245 $function = '';
246 switch (strtolower($formatter)) {
247 case 'int': $function = 'Int'; break;
248 case 'float': $function = 'Float'; break;
249 case 'str': $function = 'String'; break;
250 case 'raw': $function = 'RawString'; break;
251 default:
252 throw new TemplateException('Invalid macro formatter "' . $formatter . '"');
253 }
254
255 // Now get the expression and return a PHP statement.
256 $expression = trim(substr($macro, 0, $formatter_pos));
257 return 'hoplite\\base\\filter\\' . $function . '(' . $expression . ')';
258 }
259 }
260
261 class TemplateException extends \Exception {}