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