X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;ds=sidebyside;f=template.php;h=32c3783aef6c5b1077ddb789654066bd1298d4ed;hb=5c6801b4d9a3c68f7fd2601b6446ce1f1160bd2d;hp=1187a5445bafac746d79156e4afa2c6aaa54f1c3;hpb=22d5fb7ab7d4ee86bd59e194387dca268bd577a1;p=wfpl.git
diff --git a/template.php b/template.php
index 1187a54..32c3783 100644
--- a/template.php
+++ b/template.php
@@ -1,204 +1,594 @@
keyval = array('' => '~'); # so that ~~ in the template creates a single ~
- $this->sub_templates = array();
+# We parse the template string into a tree of strings and sub-templates.
+# A template is a hash with a name string, a pieces array, and possibly
+# an args array.
+
+function &parse_template($string) {
+ $tem =& tem_push();
+ $tem['pieces'] = array();
+ $matches = preg_split('/()/', preg_replace('//', '$1', $string), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+ foreach($matches as $match) {
+ if($match == '~~') $match = '~';
+ if(substr($match,0,1) == '~' and strlen($match) > 2) {
+ $args = explode(' ', substr($match,1,-1));
+
+ if(count($args) == 1 and $args[0] == '}') $name = '';
+ else $name = array_shift($args);
+
+ if(count($args) && $args[count($args)-1] == '{') { # open block
+ array_pop($args); # drop '{'
+ $tem =& tem_push($tem); # create a new sub-template
+ $tem['parent']['pieces'][] =& $tem; # as a piece of the parent
+ $tem['name'] = $name;
+ $tem['pieces'] = array();
+ $tem['args'] = $args;
+ } elseif(count($args) && $args[count($args)-1] == '}') { # close block
+ array_pop($args); # drop '}'
+ $cur = $tem['name'];
+ if($name && $name != $cur) {
+ die("Invalid template: tried to close '$name', but '$cur' is current.");
+ }
+ $tem =& $tem['parent'];
+ } else { # value slot
+ $tem['pieces'][] = array('name' => $name, 'args' => $args);
+ }
+ } else { # static string
+ $tem['pieces'][] = $match;
+ }
}
+ return $tem;
+}
- # set a key/value pair. if a ~tag~ in the template matches key it will be replaced by value
- function set($key, $value) {
- $this->keyval[$key] = $value;
+function fill_template($template, &$data, &$context = NULL) {
+ $context =& tem_push($context);
+ $context['data'] =& $data;
+ $output = '';
+
+ foreach($template['pieces'] as $tem) {
+ if(is_string($tem)) $output .= $tem;
+ elseif(isset($tem['pieces'])) { # sub-template
+ $rows =& tem_row_data($tem, $context);
+ $context['rows'] =& $rows;
+ foreach($rows as $key => &$row) {
+ $context['cur'] = $key;
+ $output .= fill_template($tem, $row, $context);
+ }
+ } else { # variable
+ $output .= tem_encoded_data($tem, $context);
+ }
}
+ $context =& $context['parent'];
+ return $output;
+}
- # clear a value. Functionally equivalent to set($key, '') but cleaner and more efficient
- function clear($key) {
- unset($this->keyval[$key]);
+
+# Implementation
+# --------------
+
+
+# To track our position in the template and in the data, we use a linked
+# stack structure. Each node is a hash with a reference to the parent
+# node along with whatever other data you want to add. For each stack,
+# you simply keep a variable with a reference to the top element. Then
+# the push and pop operations are:
+
+# $top =& tem_push($top);
+# $top =& $top['parent'];
+
+function &tem_push(&$stack = NULL) {
+ static $refs = array();
+
+ # Since a PHP reference is *not* a pointer to data, but a pointer to
+ # a variable (or array slot), we *have* to first put the new node in
+ # $refs, and then reference it from $new.
+
+ $refs[] = array();
+ $new =& $refs[count($refs)-1];
+ if($stack) $new['parent'] =& $stack;
+ return $new;
+}
+
+# To fill out a template, we do a depth-first traversal of the template
+# tree, replacing all tags with the data values.
+
+# The data starts out as a nested set of key/value pairs, where the
+# values can be:
+
+ # a string to fill a value slot
+ # a hash to fill one instance of a sub-template
+ # an array of hashes to fill multiple instances of a sub-template
+
+# The middle form will be converted to the last form as we use it.
+
+function tem_data_as_rows($value, $key) {
+ if(is_array($value)) {
+ # numeric keys
+ if(array_key_exists(0, $value)) {
+ if(is_array($value[0])) return $value; # already array of hashes.
+ else return columnize($value, $key);
+ # key/value pairs -- expand sub-template once.
+ } else return array($value);
+ } elseif($value || $value === 0 || $value === '0' || $value === '') {
+ # value -- expand sub-template once using only parent values
+ return array(array());
+ } else {
+ # empty value -- don't expand sub-template
+ return array();
}
+}
- # grab a value you stuck in earlier with set()
- function get($key) {
- return $this->keyval[$key];
+# To look up a key, we check each namespace (starting with the
+# innermost one) until a value is found.
+
+function tem_data_scope($key, $context) {
+ static $refs = array();
+
+ $scope = $context;
+ do{
+ if(array_key_exists($key, $scope['data'])) {
+ return $scope;
+ }
+ } while($scope = isset($scope['parent']) ? $scope['parent'] : null);
+
+ # not found; return empty scope.
+ $refs[] = array();
+ $ret = array();
+ $ret['parent'] =& $context;
+ $ret['data'] =& $refs[count($refs) - 1];
+ return $ret;
+}
+
+function tem_get_data($key, $context) {
+ $scope = tem_data_scope($key, $context);
+ if($scope) return isset($scope['data'][$key]) ? $scope['data'][$key] : null;
+}
+
+# Return the value for a tag as a set of rows to fill a sub-template.
+# If $tag has an arg, call the tem_auto function to munge the data.
+function &tem_row_data($tem, $context)
+{
+ $key = $tem['name'];
+ $scope = tem_data_scope($key, $context);
+ $auto_func = false;
+
+ if(count($tem['args'])) {
+ $auto_func = "tem_auto_" . $tem['args'][0];
+ if (!function_exists($auto_func)) {
+ die("ERROR: template auto function '$auto_func' not found.
\n");
+ }
+ # NAMESPACIFY $auto_func
+ }
+ $value = isset($scope['data'][$key]) ? $scope['data'][$key] : null;
+ if ($auto_func) {
+ $value = $auto_func($value, $key, $scope, $tem['args']);
}
- # run the template engine on one of the sub-templates and append the result
- # to the keyval in the main array. See tem_test.php for an example of how
- # this can be used.
- function sub($sub_template_name) {
- $this->keyval[$sub_template_name] .= template_run($this->sub_templates[$sub_template_name], $this->keyval);
+ $rows = tem_data_as_rows($value, $key);
+ if(is_array($value)) {
+ $scope['data'][$key] = $rows;
+ }
- # after running a sub-template, clear its sub-templates
- if(isset($this->sub_subs[$sub_template_name])) {
- foreach($this->sub_subs[$sub_template_name] as $sub_sub) {
- $this->clear($sub_sub);
- }
+ return $rows;
+}
+
+# Return the value for a tag as an encoded string.
+function tem_encoded_data($tag, $context)
+{
+ $key = $tag['name'];
+ $value = tem_get_data($key, $context);
+ foreach($tag['args'] as $encoding) {
+ $func = "enc_$encoding";
+ if (function_exists($func)) {
+ # NAMESPACIFY $func
+ $value = $func($value, $key);
+ } else {
+ die("ERROR: encoder function '$func' not found.
\n");
}
}
+ return $value;
+}
- # this is used by tem::load() and should be otherwise useless
- function _load(&$in, &$out, &$parents, &$parent) {
- while($in) {
- # scan for one of: 1) the begining of a sub-template 2) the end of this one 3) the end of the file
- $n = strpos($in, '', substr($in, 0, 12)) == 0) {
- $in = substr($in, 12);
- $parent = array_pop($parents);
- return;
- }
+function is_value_slot(&$piece) {
+ return is_array($piece) && !isset($piece['pieces']);
+}
- $matches = array();
- # this limits sub_template names to 50 chars
- if(ereg('^', substr($in, 0, 65), $matches)) {
- list($start_tag, $tag_name) = $matches;
+# Return a hash containing the top-level sub-templates of tem.
+function top_sub_templates($tem, $is_sub = 'is_sub_template') {
+ function_exists($is_sub) or die("no such function '$is_sub'.");
+ $subs = array();
+ foreach($tem['pieces'] as $piece) {
+ if($is_sub($piece)) {
+ $subs[$piece['name']] = $piece;
+ }
+ }
+ return $subs;
+}
- # keep track of the tree
- if(!isset($this->sub_subs[$parent])) {
- $this->sub_subs[$parent] = array();
- }
- array_push($this->sub_subs[$parent], $tag_name);
- array_push($parents, $parent);
- $parent = $tag_name;
-
- $out .= '~' . $tag_name . '~';
- $in = substr($in, strlen($start_tag));
- $this->sub_templates[$tag_name] = '';
- $this->_load($in, $this->sub_templates[$tag_name], $parents, $parent);
+# merge $subs (sub_templates) into variables in $main (template)
+function merge_sub_templates(&$main, &$subs) {
+ foreach($main['pieces'] as &$piece) {
+ if(is_array($piece)) { # not just text
+ if(isset($piece['pieces']) && $piece['pieces']) {
+ # a sub-template in main
+ merge_sub_templates($piece, $subs);
} else {
- # it's not a start tag or end tag, so let's pass it through:
- $out .= substr($in, 0, 5);
- $in = substr($in, 5);
+ # a value-slot in main
+ $sub = isset($subs[$piece['name']]) ? $subs[$piece['name']] : null;
+ $arg0 = isset($sub['args'][0]) ? $sub['args'][0] : null;
+ if($sub && $arg0 != 'hide') {
+ $piece = $subs[$piece['name']];
+ $piece['parent'] =& $main;
+ }
}
- } #repeat
+ }
}
+}
- # This is useful when you have sub-templates that you want to mess with
- # before the main template is run. But can also be used to simply specify
- # the filename ahead of time.
- function load($filename) {
- $this->filename = $filename;
- $tmp = read_whole_file($filename);
- $this->template = '';
- $parents = array('top_level_subs' => array());
- $parent = 'top_level_subs';
- $this->_load($tmp, $this->template, $parents, $parent);
- }
-
- # Run the template. Pass a filename, or a string, unless you've already
- # specified a template with load()
- function run($templ = false) {
- if($templ !== false) {
- if(strlen($templ) < 150 && file_exists($templ)) {
- $this->filename = $templ;
- unset($this->template);
- } else {
- $this->template = $templ;
+# Replace values in $main with top-level templates from $tem.
+function merge_templates(&$main, &$tem) {
+ $subs = top_sub_templates($tem);
+
+ merge_sub_templates($main, $subs);
+}
+
+
+
+# tem_auto functions
+# ------------------
+#
+# If a { tag has an argument, the corresponding tem_auto function is called.
+# This allows it to mangle the data to automate some common cases.
+
+# 'sep' (separator) sections will be shown for all but the last parent row.
+# Sample usage:
+#
+#
+# row content...
+#
([^<]*)
|'), 'template_filler', $template); +function tem_load_new($filename) { + $old = isset($GLOBALS['wfpl_template']) ? $GLOBALS['wfpl_template'] : null; + $GLOBALS['wfpl_template'] = new tem(); + $GLOBALS['wfpl_template']->load($filename); + return $old; } +# deprecated (old name for show) +function tem_sub($name) { + tem_show($name); +} ?>