This repository has been archived by the owner on May 1, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
jqTmpl.class.php
328 lines (266 loc) · 8.03 KB
/
jqTmpl.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
<?php
/**
* jquery-tmpl-php
* 2010 Adam Backstrom <[email protected]>
*
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* See project page on github for licenses:
*
* http://github.com/abackstrom/jquery-tmpl-php
*/
require_once 'HTML5/Parser.php';
/**
* jquery-tmpl implemented in PHP.
*
* $tpl = new jqTmpl;
*/
class jqTmpl {
/**
* Future home of cached templates?
*/
public $templates = array();
public $_templates = array();
/**
* Boolean for debug output.
*/
public $debug = false;
/**
* Load a new document into this template's phpQuery object so that we can
* specify templates by selectors.
*
* @param $html string the html fragment to load
* @return the created phpQueryObject
*/
public function load_document( $html ) {
$dom = HTML5_Parser::parse( $html );
// TODO: getElementById?
$nodes = $dom->getElementsByTagName('script');
// reset cached templates lists
$this->templates = $this->_templates = array();
foreach( $nodes as $node ) {
if( $id = $node->getAttribute('id') ) {
$this->_templates[$id] = $node->nodeValue;
}
}
}//end load_document
/**
* Fetch a template from the loaded DOM document by its id.
*
* @param $id string the id, with or without #
* @return string html string
*/
public function tmpl_by_id( $id ) {
// remove # if it's there
if( "#" === substr($id, 0, 1) ) {
$id = substr($id, 1);
}
if( isset($this->templates[$id]) ) {
return $this->templates[$id];
}
if( isset($this->_templates[$id]) ) {
return $this->templates[$id] = $this->preparse($this->_templates[$id]);
}
return null;
}//end getElementById
/**
* Render a template using some provided template variables.
*
* 1. $tpl->tmpl('<i>Hi, ${name}</i>', array('name' => 'Adam')); // "<i>Hi, Adam</i>"
*
* 2. $tpl->load_document('<div id="which">Mmm, {{= fruit}}</div>');
* $tpl->tmpl( $tpl->pq('#which'), array('fruit' => 'Donuts')); // "Mmm, Donuts"
*
* @param $tmpl string|phpQueryObject a template as a string or a phpQuery selection
* @param $data array array of data to populate into the template
* @return the rendered template string
*/
public function tmpl( $tmpl, $data = null, $options = array() ) {
if( preg_match('/^#-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/', $tmpl) ) {
$tmpl_string = $this->tmpl_by_id( $tmpl );
} else {
$tmpl_string = $tmpl;
$tmpl_string = $this->preparse( $tmpl_string );
}
// regex from jquery-tmpl, modified for named subpatterns
preg_match_all( '/\{\{(?<slash>\/?)(?<type>\w+|.)(?:\((?<fnargs>(?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(?<target>.*?)?)?(?<parens>\((?<args>(?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/', $tmpl_string, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE );
$state = array(
'eof' => strlen($tmpl_string), // length of the template string
'pos' => 0, // current position in template string
'if' => 0, // if statement depth
'skip' => false, // true if we are skipping output (ie. due to a false if condition)
'depth' => 0, // recursion depth
);
if( is_object($data) || ! isset($data) ) {
$html = $this->parse( $tmpl_string, $data, $matches, $state );
} elseif( is_array($data) ) {
$html = '';
foreach( $data as $row ) {
$state_this = $state; // reset this pass-by-reference var
$matches_this = $matches;
$html .= $this->parse( $tmpl_string, $row, $matches_this, $state_this );
}
} else {
throw new UnsupportedDataTypeException;
}
return $html;
}//end tmpl
/**
* Parse a template to build an HTML string.
*/
public function parse( $tmpl, $data, &$matches, &$state ) {
$html = '';
// each iteration of the function can track whether or not something
// has displayed. first pass will be from the parent {{if}}.
$if_did_output = ! $state['skip'];
extract( $state, EXTR_REFS );
$depth += 1;
while(true) {
$this->debug( "skip mode is [%s], depth [%d]", $skip ? 'on' : 'off', $depth );
if( empty($matches) ) {
if( $pos < $eof ) {
// just copy rest of string
$html .= substr($tmpl, $pos);
break;
}
break;
}
//
// we have another template tag
//
$match = array_shift($matches);
// we have text between template tags; get us up to the next tag,
// appending the intervening text if necessary (ie. not in skip mode)
if( $skip == false && $pos < $match[0][1] ) {
$this->debug('adding string [%s]', substr($tmpl, $pos, $match[0][1] - $pos));
$html .= substr($tmpl, $pos, $match[0][1] - $pos);
$pos = $match[0][1];
}
$type = $match['type'][0];
$target = isset($match['target']) ? $match['target'][0] : null;
$slash = $match['slash'][0] == '/';
$this->debug("type is [%s%s]; target is [%s]\n", $slash ? '/' : '', $type, $target);
// move string position after the template tag
$pos = $match[0][1] + strlen($match[0][0]);
//
// what is our template tag?
//
// {{= expression}}
if( $type == '=' && $skip == 0 ) {
$html .= htmlentities($data->$target);
}
// {{html expression}}
elseif( $type == 'html' && $skip == 0 ) {
$html .= $data->$target;
}
// {{if}}, {{/if}}
elseif( $type == 'if' ) {
if( ! $slash ) {
if( $skip ) {
// we're already skipping this block, just throw away the parsing
$this->parse( $tmpl, $data, $matches, $state );
$skip = true; // force this back to true, in case the above
// parse modified it.
continue;
}
$skip = (bool)$data->$target == false;
$html .= $this->parse( $tmpl, $data, $matches, $state );
}
// closing tag
else {
$depth -= 1;
return $html;
}
}
// {{else}}
elseif( $type == 'else' ) {
// this will happen in a recursed parse()
// loop has shown, so we can't.
if( $if_did_output ) {
$skip = true;
}
// loop hasn't shown yet; can we show?
else {
if( $target === null ) {
$if_did_output = true;
$skip = false;
} elseif( $target && $data->$target ) {
$if_did_output = true;
$skip = false;
}
}
// let processing continue as normal
}
// {{each foo}}, {{each(i, b) foo}}, {{/each}}
elseif( $type == 'each' ) {
if( $slash ) {
return $html;
}
if( ! $data->$target ) {
continue;
}
$reset_matches = $matches;
$reset_pos = $pos;
$data_copy = $data;
if( $match['fnargs'][0] ) {
list($index_name, $value_name) = explode(',', $match['fnargs'][0]);
$index_name = trim($index_name);
$value_name = trim($value_name);
} else {
$index_name = 'index';
$value_name = 'value';
}
// repeat the loop over this each block
foreach( $data->$target as $index => $value ) {
// reset first so last iteration ends in the correct place
$matches = $reset_matches;
$pos = $reset_pos;
$data_copy->$index_name = $index;
$data_copy->$value_name = $value;
$html .= $this->parse( $tmpl, $data_copy, $matches, $state);
}
}
// {{tmpl "#foo"}}
elseif( $type == 'tmpl' ) {
if( $skip ) {
continue;
}
$target = trim($target, '\'"');
$html .= $this->tmpl( $target, $data );
}
// {{! comment}}
elseif( $type == '!' ) {
// "comment tag, skipped by parser"
}
// unknown type
else {
throw new UnknownTagException("Template command not found: " . $type);
}
$this->debug( "remaining pattern: [%s]", substr($tmpl, $pos) );
}
$depth -= 1;
return $html;
}//end parse
/**
* Debug function.
*/
public function debug() {
if( $this->debug ) {
$args = func_get_args();
$s = array_shift($args);
vprintf( $s . "\n", $args );
}
}//end debug
/**
* Do some pre-rendering cleanup.
*
* @param $tmpl string the template string
* @return the cleaned string
*/
public function preparse( $tmpl ) {
$tmpl = preg_replace( '/\$\{\s*([^\}]*?)\s*\}/', "{{= $1}}", $tmpl );
return $tmpl;
}//end preparse
}//end class jqTmpl
class UnknownTagException extends RuntimeException { }
class UnsupportedDataTypeException extends RuntimeException { }