JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
metaform: merge ckeditor settings from cms
[wfpl.git] / template.php
1 <?php
2
3 #  Copyright (C) 2008,2009 Joshua Grams <josh@qualdan.com>
4 #
5 #  This program is free software: you can redistribute it and/or modify
6 #  it under the terms of the GNU General Public License as published by
7 #  the Free Software Foundation, either version 3 of the License, or
8 #  (at your option) any later version.
9 #  
10 #  This program is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #  GNU General Public License for more details.
14 #  
15 #  You should have received a copy of the GNU General Public License
16 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18
19 # This is a simple template-handling system.  You pass it a big data 
20 # structure with key/value pairs, and a template string to fill out.
21 #
22 # Within a template, it recognizes tags of the form ~name [arg...]~, 
23 # optionally wrapped in HTML comments (which will be removed along with 
24 # the tag markers when the template is filled out).
25 #
26 # { and } as the final argument mark those tags as being the start and 
27 # end of a sub-template (for optional or repeated sections).  All other 
28 # tags represent slots to be directly filled by data values.  On a } 
29 # tag, the name is optional, but must match the corresponding { tag if 
30 # present.
31 #
32 # For a value tag, arguments represent encodings to be applied 
33 # successively.  For instance, ~foo html~ will encode it to be safe in 
34 # HTML ('&' to '&amp;', '<' to '&lt;', and so on).
35 #
36 # { tags can take one argument, which will call the corresponding 
37 # tem_auto_* function to munge the data, automating certain common use 
38 # cases.  See the comments on the tem_auto functions for more details.
39
40 require_once(__DIR__.'/'.'encode.php');
41 require_once(__DIR__.'/'.'file.php');
42 require_once(__DIR__.'/'.'misc.php');
43
44
45 # Top-Level Functions
46 # -------------------
47
48 function template($template, $data) {
49         return fill_template(parse_template($template), $data);
50 }
51
52 function template_file($filename, $data) {
53         return fill_template(parse_template_file($filename), $data);
54 }
55
56 function &parse_template_file($filename) {
57         return parse_template(file_get_contents($filename));
58 }
59
60 # We parse the template string into a tree of strings and sub-templates.
61 # A template is a hash with a name string, a pieces array, and possibly 
62 # an args array.
63
64 function &parse_template($string) {
65         $tem =& tem_push();
66         $tem['pieces'] = array();
67         $matches = preg_split('/(<!--)?(~[^~]*~)(?(1)-->)/', preg_replace('/<!--(~[^~]*~)-->/', '$1', $string), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
68         foreach($matches as $match) {
69                 if($match == '~~') $match = '~';
70                 if(substr($match,0,1) == '~' and strlen($match) > 2) {
71                         $args = explode(' ', substr($match,1,-1));
72
73                         if(count($args) == 1 and $args[0] == '}') $name = '';
74                         else $name = array_shift($args);
75
76                         if(count($args) && $args[count($args)-1] == '{') {  # open block
77                                 array_pop($args);  # drop '{'
78                                 $tem =& tem_push($tem);              # create a new sub-template
79                                 $tem['parent']['pieces'][] =& $tem;  # as a piece of the parent
80                                 $tem['name'] = $name;
81                                 $tem['pieces'] = array();
82                                 $tem['args'] = $args;
83                         } elseif(count($args) && $args[count($args)-1] == '}') {  # close block
84                                 array_pop($args);  # drop '}'
85                                 $cur = $tem['name'];
86                                 if($name && $name != $cur) {
87                                         die("Invalid template: tried to close '$name', but '$cur' is current.");
88                                 }
89                                 $tem =& $tem['parent'];
90                         } else {  # value slot
91                                 $tem['pieces'][] = array('name' => $name, 'args' => $args);
92                         }
93                 } else {  # static string
94                         $tem['pieces'][] = $match;
95                 }
96         }
97         return $tem;
98 }
99
100 function fill_template($template, &$data, &$context = NULL) {
101         $context =& tem_push($context);
102         $context['data'] =& $data;
103         $output = '';
104
105         foreach($template['pieces'] as $tem) {
106                 if(is_string($tem)) $output .= $tem;
107                 elseif(isset($tem['pieces'])) {  # sub-template
108                         $rows =& tem_row_data($tem, $context);
109                         $context['rows'] =& $rows;
110                         foreach($rows as $key => &$row) {
111                                 $context['cur'] = $key;
112                                 $output .= fill_template($tem, $row, $context);
113                         }
114                 } else {  # variable
115                         $output .= tem_encoded_data($tem, $context);
116                 }
117         }
118         $context =& $context['parent'];
119         return $output;
120 }
121
122
123 # Implementation
124 # --------------
125
126
127 # To track our position in the template and in the data, we use a linked 
128 # stack structure.  Each node is a hash with a reference to the parent 
129 # node along with whatever other data you want to add.  For each stack, 
130 # you simply keep a variable with a reference to the top element.  Then 
131 # the push and pop operations are:
132
133 # $top =& tem_push($top);
134 # $top =& $top['parent'];
135
136 function &tem_push(&$stack = NULL) {
137         static $refs = array();
138
139         # Since a PHP reference is *not* a pointer to data, but a pointer to 
140         # a variable (or array slot), we *have* to first put the new node in
141         # $refs, and then reference it from $new.
142
143         $refs[] = array();
144         $new =& $refs[count($refs)-1];
145         if($stack) $new['parent'] =& $stack;
146         return $new;
147 }
148
149 # To fill out a template, we do a depth-first traversal of the template 
150 # tree, replacing all tags with the data values.
151
152 # The data starts out as a nested set of key/value pairs, where the 
153 # values can be:
154
155         # a string to fill a value slot
156         # a hash to fill one instance of a sub-template
157         # an array of hashes to fill multiple instances of a sub-template
158
159 # The middle form will be converted to the last form as we use it.
160
161 function tem_data_as_rows($value, $key) {
162         if(is_array($value)) {
163                 # numeric keys
164                 if(array_key_exists(0, $value)) {
165                         if(is_array($value[0])) return $value;  # already array of hashes.
166                         else return columnize($value, $key);
167                 # key/value pairs -- expand sub-template once.
168                 } else return array($value);
169         } elseif($value || $value === 0 || $value === '0' || $value === '') {
170                 # value -- expand sub-template once using only parent values
171                 return array(array());
172         } else {
173                 # empty value -- don't expand sub-template
174                 return array();
175         }
176 }
177
178 # To look up a key, we check each namespace (starting with the
179 # innermost one) until a value is found.
180
181 function tem_data_scope($key, $context) {
182         static $refs = array();
183
184         $scope = $context;
185         do{
186                 if(array_key_exists($key, $scope['data'])) {
187                         return $scope;
188                 }
189         } while($scope = isset($scope['parent']) ? $scope['parent'] : null);
190
191         # not found; return empty scope.
192         $refs[] = array();
193         $ret = array();
194         $ret['parent'] =& $context;
195         $ret['data'] =& $refs[count($refs) - 1];
196         return $ret;
197 }
198
199 function tem_get_data($key, $context) {
200         $scope = tem_data_scope($key, $context);
201         if($scope) return isset($scope['data'][$key]) ? $scope['data'][$key] : null;
202 }
203
204 # Return the value for a tag as a set of rows to fill a sub-template.
205 # If $tag has an arg, call the tem_auto function to munge the data.
206 function &tem_row_data($tem, $context)
207 {
208         $key = $tem['name'];
209         $scope = tem_data_scope($key, $context);
210         $auto_func = false;
211
212         if(count($tem['args'])) {
213                 $auto_func = "tem_auto_" . $tem['args'][0];
214                 if (!function_exists($auto_func)) {
215                         die("ERROR: template auto function '$auto_func' not found.<br>\n");
216                 }
217                 # NAMESPACIFY $auto_func
218         }
219         $value = isset($scope['data'][$key]) ? $scope['data'][$key] : null;
220         if ($auto_func) {
221                 $value = $auto_func($value, $key, $scope, $tem['args']);
222         }
223
224         $rows = tem_data_as_rows($value, $key);
225         if(is_array($value)) {
226                 $scope['data'][$key] = $rows;
227         }
228
229         return $rows;
230 }
231
232 # Return the value for a tag as an encoded string.
233 function tem_encoded_data($tag, $context)
234 {
235         $key = $tag['name'];
236         $value = tem_get_data($key, $context);
237         foreach($tag['args'] as $encoding) {
238                 $func = "enc_$encoding";
239                 if (function_exists($func)) {
240                         # NAMESPACIFY $func
241                         $value = $func($value, $key);
242                 } else {
243                         die("ERROR: encoder function '$func' not found.<br>\n");
244                 }
245         }
246         return $value;
247 }
248
249
250 function is_sub_template(&$piece) {
251         return is_array($piece) && isset($piece['pieces']);
252 }
253
254 function is_value_slot(&$piece) {
255         return is_array($piece) && !isset($piece['pieces']);
256 }
257
258 # Return a hash containing the top-level sub-templates of tem.
259 function top_sub_templates($tem, $is_sub = 'is_sub_template') {
260         function_exists($is_sub) or die("no such function '$is_sub'.");
261         $subs = array();
262         foreach($tem['pieces'] as $piece) {
263                 if($is_sub($piece)) {
264                         $subs[$piece['name']] = $piece;
265                 }
266         }
267         return $subs;
268 }
269
270 # merge $subs (sub_templates) into variables in $main (template)
271 function merge_sub_templates(&$main, &$subs) {
272         foreach($main['pieces'] as &$piece) {
273                 if(is_array($piece)) { # not just text
274                         if(isset($piece['pieces']) && $piece['pieces']) {
275                                 # a sub-template in main
276                                 merge_sub_templates($piece, $subs);
277                         } else {
278                                 # a value-slot in main
279                                 $sub = isset($subs[$piece['name']]) ? $subs[$piece['name']] : null;
280                                 $arg0 = isset($sub['args'][0]) ? $sub['args'][0] : null;
281                                 if($sub && $arg0 != 'hide') {
282                                         $piece = $subs[$piece['name']];
283                                         $piece['parent'] =& $main;
284                                 }
285                         }
286                 }
287         }
288 }
289
290 # Replace values in $main with top-level templates from $tem.
291 function merge_templates(&$main, &$tem) {
292         $subs = top_sub_templates($tem);
293
294         merge_sub_templates($main, $subs);
295 }
296
297
298
299 # tem_auto functions
300 # ------------------
301 #
302 # If a { tag has an argument, the corresponding tem_auto function is called.
303 # This allows it to mangle the data to automate some common cases.
304
305 # 'sep' (separator) sections will be shown for all but the last parent row.
306 # Sample usage:
307 #       <!--~rows {~-->
308 #               <!--~row {~-->
309 #                       row content...
310 #                       <!--~separator sep {~--><hr><!--~}~-->
311 #               <!--~}~-->
312 #       <!--~}~-->
313 #
314 function tem_auto_sep(&$value, $key, $context) {
315         $rows =& $context['parent']['parent'];
316         if($rows['cur'] != count($rows['rows'])-1)  # last row?
317                 return true;  # show once
318 }
319
320 # auto-show once, only when this is the first row of the parent
321 function tem_auto_last(&$value, $key, $context) {
322         $rows =& $context['parent']['parent'];
323         if($rows['cur'] == count($rows['rows'])-1)  # last row?
324                 return true;  # show once
325 }
326
327 # auto-show once, only when this is the last row of the parent
328 function tem_auto_first(&$value, $key, $context) {
329         $rows =& $context['parent']['parent'];
330         if($rows['cur'] == 0)  # first row?
331                 return true;  # show once
332 }
333
334 # 'show' sections will be shown unless the corresponding data
335 # value === false
336 function tem_auto_show(&$value) {
337         if($value === null) return true;
338         return $value;
339 }
340
341 # 'empty' sections will be shown only if the corresponding data value is the
342 # empty string
343 function tem_auto_empty(&$value) {
344         if($value === '') return true;
345         return null;
346 }
347
348 # 'nonempty' sections will not be shown if the corresponding data
349 # value is the empty string
350 function tem_auto_nonempty(&$value) {
351         if($value === '') return null;
352         return $value;
353 }
354
355 # 'unset' sections will not be shown if the corresponding data
356 # value is not set (opposite of default)
357 function tem_auto_unset(&$value) {
358         if($value === null) {
359                 return '';
360         } else {
361                 return null;
362         }
363 }
364
365 # 'evenodd' sections are given an 'evenodd' attribute whose value
366 # alternates between 'even' and 'odd'.
367 function tem_auto_evenodd(&$values) {
368         $even = true;
369         if($values) foreach($values as &$value) {
370                 $value['evenodd'] = $even ? 'even' : 'odd';
371                 $even = !$even;
372         }
373         return $values;
374 }
375
376 # 'once' sections are shown exactly once if the value is set (and not at all
377 # otherwise.) This is useful when an array value would otherwise cause the
378 # section to be displayed multiple times.
379 function tem_auto_once(&$value) {
380         if($value === null) return null;
381         return true;
382 }
383
384 function tem_auto_once_if(&$value) {
385         if($value) return true;
386         return null;
387 }
388
389 # 'once' sections are shown exactly once if php evaluates the value to false
390 # (and not at all otherwise.) This is useful when an array value would
391 # otherwise cause the section to be displayed multiple times.
392 function tem_auto_once_else(&$value) {
393         if($value) return null;
394         return true;;
395 }
396
397
398
399
400
401 # Backward Compatibility
402 # ----------------------
403
404 function is_shown($piece) {
405         return isset($piece['args'][0]) && $piece['args'][0] == 'hide';
406 }
407
408 function is_shown_sub_template($piece) {
409         return is_sub_template($piece) and is_shown($piece);
410 }
411
412 # Old-style templates don't show unless explicitly requested.
413 function tem_auto_hide(&$value, $key, $context) {
414         unset($context['data'][$key]);
415         return false;
416 }
417
418 # The old API is being used with the named sub-template,
419 # so hide it and insert a value slot for its expansion(s).
420 function &tem_is_old_sub($name, &$template) {
421         foreach($template['pieces'] as $key => &$piece) {
422                 if(is_sub_template($piece)) {
423                         if($piece['name'] == $name) {
424                                 if(!is_shown($piece)) {
425                                         # hide template unless explicitly show()n.
426                                         $piece['args'] = array('hide');
427                                         # insert a value slot with the same name (for the expansion).
428                                         $var = array('name' => $name, 'args' => array());
429                                         array_splice($template['pieces'], $key, 0, array($var));
430                                 }
431                                 return $piece;
432                         }
433                         $tem = tem_is_old_sub($name, $piece);
434                         if($tem) return $tem;
435                 }
436         }
437         return false;
438 }
439
440 class tem {
441         var $template;
442         var $data; 
443
444         function tem() {
445                 $this->template = array('pieces' => array());
446                 $this->data = array();
447         }
448         
449         function set($key, $value = true) {
450                 $this->data[$key] = $value;
451         }
452
453         function sets($hash) {
454                 foreach($hash as $key => $value) {
455                         $this->set($key, $value);
456                 }
457         }
458
459         function append($key, $value) {
460                 if (!isset($this->data[$key])) {
461                         $this->data[$key] = '';
462                 }
463                 $this->data[$key] .= $value;
464         }
465
466         function prepend($key, $value) {
467                 $this->data[$key] = $value . $this->data[$key];
468         }
469
470         function clear($key) {
471                 unset($this->data[$key]);
472         }
473
474         function get($key) {
475                 return $this->data[$key];
476         }
477
478         function show($name) {
479                 $tem = tem_is_old_sub($name, $this->template);
480                 if($tem) {
481                         if (!isset($this->data[$name])) {
482                                 $this->data[$name] = '';
483                         }
484                         $this->data[$name] .= fill_template($tem, $this->data);
485                 }
486         }
487
488         function show_separated($name) {
489                 if($this->get($name)) {
490                         $this->show($name . '_sep');
491                 }
492                 $this->show($name);
493         }
494
495         function load_str($str) {
496                 $this->template =& parse_template($str);
497         }
498
499         function load($filename) {
500                 $this->template =& parse_template_file($filename);
501         }
502
503         function run($tem = false) {
504                 if($tem) {
505                         if(strlen($tem < 150 && file_exists($tem))) $this->load($tem);
506                         else $this->load_str($tem);
507                 }
508
509                 return fill_template($this->template, $this->data);
510         }
511
512         function output($tem = false) {
513                 print($this->run($tem));
514         }
515
516         # merge top-level sub-templates of $tem (object) into $this,
517         # supporting both new and old semantics.
518         function merge($tem) {
519                 # append expansions to $this->data (old style)
520
521                 $subs = $tem->top_subs('is_shown_sub_template');
522                 if($subs) foreach($subs as $name => $val) {
523                         $this->append($name, $val);
524                         unset($tem->data[$name]);  # so array_merge() won't overwrite things
525                 }
526
527                 # merge the data arrays and template trees (new style)
528                 $this->data = array_merge($this->data, $tem->data);
529                 merge_templates($this->template, $tem->template);
530         }
531
532         # see merge() above
533         function merge_file($filename) {
534                 $other_tem = new tem();
535                 $other_tem->load($filename);
536                 $this->merge($other_tem);
537         }
538
539         function top_sub_names($is_sub = 'is_sub_template') {
540                 return array_keys(top_sub_templates($this->template, $is_sub));
541         }
542
543         function top_subs($is_sub = 'is_sub_template') {
544                 $ret = array();
545                 $names = $this->top_sub_names($is_sub);
546                 foreach($names as $name) {
547                         $ret[$name] = $this->get($name);
548                 }
549                 return $ret;
550         }
551
552         # old name for show (deprecated)
553         function sub($name) {
554                 $this->show($name);
555         }
556 }
557
558 function tem_init() {
559         if(!$GLOBALS['wfpl_template']) {
560                 $GLOBALS['wfpl_template'] = new tem();
561         }
562 }
563
564 function tem_append($key, $value) {
565         tem_init();
566         $GLOBALS['wfpl_template']->append($key, $value);
567 }
568
569 function tem_prepend($key, $value) {
570         tem_init();
571         $GLOBALS['wfpl_template']->prepend($key, $value);
572 }
573
574 function tem_set($key, $value = true) {
575         tem_init();
576         $GLOBALS['wfpl_template']->set($key, $value);
577 }
578
579 function tem_sets($hash) {
580         tem_init();
581         $GLOBALS['wfpl_template']->sets($hash);
582 }
583
584 function tem_get($key) {
585         tem_init();
586         return $GLOBALS['wfpl_template']->get($key);
587 }
588
589 function tem_run($tem = false) {
590         tem_init();
591         return $GLOBALS['wfpl_template']->run($tem);
592 }
593
594 function tem_show($name) {
595         tem_init();
596         return $GLOBALS['wfpl_template']->show($name);
597 }
598
599 function tem_show_separated($name) {
600         tem_init();
601         $GLOBALS['wfpl_template']->show_separated($name);
602 }
603
604 function tem_load($filename) {
605         tem_init();
606         $GLOBALS['wfpl_template']->load($filename);
607 }
608
609 function tem_merge($tem) {
610         tem_init();
611         $GLOBALS['wfpl_template']->merge($tem);
612 }
613
614 function tem_merge_file($filename) {
615         tem_init();
616         $GLOBALS['wfpl_template']->merge_file($filename);
617 }
618
619 function tem_output($filename = false) {
620         tem_init();
621         $GLOBALS['wfpl_template']->output($filename);
622 }
623
624 function tem_top_subs() {
625         tem_init();
626         return $GLOBALS['wfpl_template']->top_subs();
627 }
628
629 function tem_top_sub_names() {
630         tem_init();
631         return $GLOBALS['wfpl_template']->top_sub_names();
632 }
633
634 function tem_load_new($filename) {
635         $old = isset($GLOBALS['wfpl_template']) ? $GLOBALS['wfpl_template'] : null;
636         $GLOBALS['wfpl_template'] = new tem();
637         $GLOBALS['wfpl_template']->load($filename);
638         return $old;
639 }
640
641 # deprecated (old name for show)
642 function tem_sub($name) {
643         tem_show($name);
644 }
645
646 ?>