# pass http://foo.com/bar.html to redirect to a full directory
function run_php($dest = false) {
if($dest) {
- # if it's got a : it must be a full URL, redirect
+ # if it has a : it must be a full URL, redirect
if(strpos($dest, ':')) {
redirect($dest);
exit();
}
}
- $GLOBALS['basename'] = $basename;
-
$html_file = "$basename.html";
$php_file = "$basename.php";
$html_exists = file_exists($html_file);
$php_exists = file_exists($php_file);
- if(file_exists('template.html')) {
- $GLOBALS['wfpl_main_template'] = new tem();
- $GLOBALS['wfpl_main_template']->load("template.html");
- $GLOBALS['wfpl_main_template']->set('basename', $basename);
-
- # This helps put in a stylesheet link if you have pages with custom css
- if(file_exists("$basename.css")) {
- $GLOBALS['wfpl_main_template']->set('css_link', "$basename.css");
- $GLOBALS['wfpl_main_template']->sub('css_links');
- }
- }
-
# cms_get can return one of:
# 1) false to indicate that there's no cms content for this basename
- # 2) a string to indicate a soft/full redirect just as foo_main()
- # 3) a hash of key/value pairs to be tem_set(key,value) on the template
- if(function_exists('cms_display')) {
- $cms_content = cms_display($basename, $GLOBALS['wfpl_main_template']);
+ # 2) a string to request a soft/full redirect just like foo_main()
+ # 3) a hash of key/value pairs to be added to the template
+ if(function_exists('cms_get')) {
+ $cms_content = cms_get($basename);
if(is_string($cms_content)) {
run_php($cms_content);
return;
}
- } else {
- $cms_content = false;
- }
-
- if(!$php_exists && !$html_exists && !$cms_content) {
- header('HTTP/1.0 404 File Not Found');
- if(file_exists('error_404.php') || file_exists('error_404.html')) {
- $GLOBALS['error_basename'] = $basename;
- run_php('error_404');
- return;
- } else {
- echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html><head><title>404</title></head><body><h1>404 File Not Found</h1></body></html>';
- exit();
- }
- }
-
- # If there's no template.html we don't want to parse $html_file.
- if($html_exists && !$php_exists && !file_exists('template.html')) {
- readfile($html_file);
- exit();
- }
-
- if($html_exists) {
- tem_load_new($html_file);
}
if($php_exists) {
run_php($other);
return;
}
- } else {
- $sub_names = tem_top_sub_names();
- foreach($sub_names as $sub_name) {
- tem_sub($sub_name);
+ } elseif($html_exists) {
+ readfile($html_file);
+ exit();
+ } elseif(!$cms_content) {
+ header('HTTP/1.0 404 File Not Found');
+ if(file_exists('404.php') || file_exists('404.html')) {
+ run_php('404');
+ return;
+ } else {
+ echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html><head><title>404</title></head><body><h1>404 File Not Found</h1></body></html>';
}
}
- # Check for $GLOBALS['wfpl_template'] because it might have been set (or unset) by the php script.
- if($GLOBALS['wfpl_template'] || $GLOBALS['wfpl_main_template']) {
- if($GLOBALS['wfpl_main_template']) {
- # if there was a template for that page, and one for the whole
- # site, copy all template sections that have been show()n to the
- # site-wide template
- if($GLOBALS['wfpl_template']) {
- $sections = tem_top_subs();
- if($sections) foreach($sections as $name => $val) {
- $GLOBALS['wfpl_main_template']->append($name, $val);
- }
- }
-
- $GLOBALS['wfpl_template'] = $GLOBALS['wfpl_main_template'];
- }
+ $data = &$GLOBALS['wfpl_template'];
+ $data['basename'] = $basename;
+ if(function_exists('display_messages')) {
+ display_messages();
+ }
+ if($cms_content) foreach($cms_content as $name => $value) {
+ $data[$name] .= $value;
+ }
+ if(file_exists("$basename.css")) {
+ $data['css_link'] = "$basename.css";
+ }
- # If you have a site-wide template (template.html) then messages will
- # be displayed there. If you instead want messages displayed on your
- # page, call display_messages() from your page_main().
- #
- # Either way, you'll need to require_once('code/wfpl/messages.php')
- # or require_once('code/wfpl/session_messages.php'). code/config.php
- # is a nice place to do this.
- if(function_exists('display_messages')) {
- display_messages();
+ if(file_exists("template.html")) {
+ $template = parse_template_file("template.html");
+ if($html_exists) {
+ $subs = parse_template_file($html_file);
+ $template = merge_templates($template, $subs);
}
-
- tem_output();
+ } elseif($html_exists) {
+ $template = parse_template_file("$html_file");
}
+ if($template) print fill_template($data, $template);
}
run_php();
<?php
-
-# This file contains generally useful template handling code. It is wrapped in
-# an object so that if you want/need to you can make more than one instance of
-# it and they won't step on each other's toes. Also there are a set of global
-# functions at the bottom so you don't have to mess around with objects if you
-# don't want to. The documentation will be on the object methods, but just know
-# that each has a straight function wrapper at the bottom with 'tem_' prepended
-# to the name.
-
-# This is designed to be as simple as it can be for your project. The simple
-# way to use it is to set some key/value pairs with tem_set() then call
-# tem_output('filename.html') to output the page. A more complex example
-# including the use of sub-templates can be found in tem_test.php
-
-# FIXME: sub-sub templates need to be cleared when the sub template containing
-# them is run
+# Copyright (C) 2008,2009 Joshua Grams <josh@qualdan.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+# This is a simple template-handling system. You pass it a big data
+# structure with key/value pairs, and a template string to fill out.
+#
+# Within a template, it recognizes tags delimited by tildes (~). When
+# the template is filled out, the tags will be replaced with the
+# corresponding data. Tags ending with '?' and '.' mark the start and
+# end of a sub-template (for optional or repeated text), and can be
+# wrapped in HTML comments (which will be removed along with the tags
+# when the template is filled out).
require_once('code/wfpl/encode.php');
-require_once('code/wfpl/misc.php');
require_once('code/wfpl/file.php');
+require_once('code/wfpl/misc.php');
-class tem {
- var $keyval; # an array containing key/value pairs
- var $filename; # template filename (sometimes not set)
- var $template; # contents of template
- var $sub_templates; # tag-name/template-string pairs
- var $sub_subs; # key: sub-template name value: array of names of the sub-templates of this one
-
- # initialize variables
- function tem() {
- $this->keyval = array('' => '~'); # so that ~~ in the template creates a single ~
- $this->sub_templates = array();
- }
- # 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;
- }
+# Public functions
+# ----------------
- # like set() but appends
- function append($key, $value) {
- $this->keyval[$key] .= $value;
- }
+function template($data, $template) {
+ return fill_template($data, parse_template($template));
+}
- # like set() but prepends
- function prepend($key, $value) {
- $this->keyval[$key] = $value . $this->keyval[$key];
- }
+function template_file($data, $filename) {
+ return fill_template($data, parse_template_file($filename));
+}
- # clear a value. Functionally equivalent to set($key, '') but cleaner and more efficient
- function clear($key) {
- unset($this->keyval[$key]);
- }
+function parse_template_file($filename) {
+ return parse_template(file_get_contents($filename));
+}
- # grab a value you stuck in earlier with set()
- function get($key) {
- return $this->keyval[$key];
- }
+# First we take the template string and break it up into an array
+# of strings and sub-arrays. The first item in a sub-array is the name
+# of the value or sub-template.
- # depricated (renamed show())
- function sub($sub_template_name) {
- $this->show($sub_template_name);
- }
+function parse_template($string) {
+ # Don't mess with the $stack/$tem assignments! Since
+ # PHP references point to the variable, not the data,
+ # it really does have to be written exactly like this.
+ $stack[] = array('name' => 'root', 'pieces' => array());
+ $tem = &last($stack);
+ # note: for some reason this captures '<!--' but not '-->'.
+ $matches = preg_split("/(<!--)?(~[^~]*~)(?(1)-->)/", $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+ foreach($matches as $match) {
+ if(substr($match,0,1) == '~') {
+ $args = explode(' ', substr($match,1,-1));
- # 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 show($sub_template_name) {
- $this->keyval[$sub_template_name] .= template_run($this->sub_templates[$sub_template_name], $this->keyval);
+ if(count($args) == 1 and $args[0] == '}') $name = '';
+ else $name = array_shift($args);
- # 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);
+ if(last($args) == '{') { # open block
+ array_pop($args);
+ $stack[] = array('name' => $name, 'pieces' => array(), 'args' => $args);
+ $tem['pieces'][] = &last($stack);
+ $tem = &last($stack);
+ } elseif(last($args) == '}') { # close block
+ array_pop($args);
+ $cur = $stack[count($stack)-1]['name'];
+ if($name && $name != $cur) {
+ die("Invalid template: tried to close '$name', but '$cur' is current.");
+ }
+ array_pop($stack); $tem = &last($stack);
+ } else { # value slot
+ $tem['pieces'][] = array('name' => $name, 'args' => $args);
}
+ } elseif($match and $match != '<!--') { # static string
+ $tem['pieces'][] = $match;
}
}
+ return $tem;
+}
- function show_separated($sub_template_name) {
- if($this->get($sub_template_name)) {
- $this->show($sub_template_name . '_sep');
- }
- $this->show($sub_template_name);
- }
-
- # 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, '<!--~');
- if($n === false) { # not found
- # we hit the end of the file
- $out .= $in;
- $in = '';
- return;
- }
-
- # move everything up to (but not including) <!-- to the output
- $out .= substr($in, 0, $n);
- $in = substr($in, $n);
-
- # we found something.
- # is it an end tag?
- if(strcmp('<!--~end~-->', substr($in, 0, 12)) == 0) {
- $in = substr($in, 12);
- $parent = array_pop($parents);
- return;
- }
-
- $matches = array();
- # this limits sub_template names to 50 chars
- if(ereg('^<!--~([^~]*) start~-->', substr($in, 0, 65), $matches)) {
- list($start_tag, $tag_name) = $matches;
+# Then we do a depth-first traversal of the template tree,
+# replacing all tags with the data values.
- # keep track of the tree
- if(!isset($this->sub_subs[$parent])) {
- $this->sub_subs[$parent] = array();
+function fill_template($data, $template, $context = NULL, $keychain = NULL) {
+ $context[] = $data;
+ foreach($template['pieces'] as $piece) {
+ if(is_string($piece)) $output .= $piece;
+ else {
+ if($piece['pieces']) { # sub-template
+ $keychain[] = $piece['name'];
+ $data = tem_get($piece, $context, $keychain);
+ foreach(template_rows($data) as $key => $row) {
+ $keychain[] = $key;
+ $output .= fill_template($row, $piece, $context, $keychain);
+ array_pop($keychain);
}
- 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);
- } 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);
- }
- } #repeat
- }
-
- # like load() except you pass a string instead of a filename
- function load_str($str) {
- $this->template = '';
- $parents = array('top_level_subs' => array());
- $parent = 'top_level_subs';
- $this->_load($str, $this->template, $parents, $parent);
- }
-
- # 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;
- $this->load_str(read_whole_file($filename));
- }
-
- # Run the template. Pass a filename, or a string, unless you've already
- # specified a template with load()
- function run($templ = false) {
- $template_string = $this->template;
- $template_file = $this->file;
- if($templ !== false) {
- if(strlen($templ) < 150 && file_exists($templ)) {
- $template_file = $templ;
- unset($template_string);
- } else {
- $template_string = $templ;
- }
+ array_pop($keychain);
+ } else $output .= tem_get_enc($piece, $context);
}
+ }
+ return $output;
+}
- if(!$template_string) {
- if(!$template_file) {
- print "sorry, no template to run\n";
- exit(1);
- }
- $template_string = read_whole_file($template_file);
- }
-
- return template_run($template_string, $this->keyval);
- }
+# Replace top-level values in $main with top-level templates from $tem.
+function merge_templates($main, $tem) {
+ $out = array('name' => $main['name'], 'pieces' => array());
- # same as run() except the output is print()ed
- function output($templ = false) {
- print($this->run($templ));
- }
+ $subs = top_sub_templates($tem);
- # return the names of the top level subs, or an empty array
- function top_sub_names() {
- if(isset($this->sub_subs['top_level_subs'])) {
- return $this->sub_subs['top_level_subs'];
- } else {
- return array();
+ foreach($main['pieces'] as $piece) {
+ if(is_array($piece) and !$piece['pieces'] and $subs[$piece['name']]) {
+ $piece = $subs[$piece['name']];
}
+ $out['pieces'][] = $piece;
}
-
- # return the contents of the top-level sub-templates
- #
- # this does not run the sub-templates, so if you've not called tem_show() on them, they will be blank.
- #
- # Return a hash.
- # keys: name of top level sub-template.
- # values: contents of said sub-template.
- function top_subs() {
- $ret = array();
- $names = $this->top_sub_names();
- foreach($names as $name) {
- $ret[$name] = $this->get($name);
- }
- return $ret;
- }
+ return $out;
}
-# Below are functions so you can use the above class without allocating or
-# keeping track of it.
-# get a reference to the current template object
-function tem_init() {
- if(!$GLOBALS['wfpl_template']) {
- $GLOBALS['wfpl_template'] = new tem();
- }
-}
-
-function tem_append($key, $value) {
- tem_init();
- $GLOBALS['wfpl_template']->append($key, $value);
-}
-
-function tem_prepend($key, $value) {
- tem_init();
- $GLOBALS['wfpl_template']->prepend($key, $value);
-}
-
-function tem_set($key, $value) {
- tem_init();
- $GLOBALS['wfpl_template']->set($key, $value);
-}
-
-function tem_get($key) {
- tem_init();
- return $GLOBALS['wfpl_template']->get($key);
-}
-function tem_run($templ = false) {
- tem_init();
- return $GLOBALS['wfpl_template']->run($templ);
-}
+# 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.
-# depricated (renamed tem_show())
-function tem_sub($sub_template_name) {
- tem_show($sub_template_name);
+# 'sep' (separator) sections will be shown for all but the last parent row.
+# Sample usage:
+# <!--~rows~-->
+# <!--~row~-->
+# row content...
+# <!--~separator sep {~--><hr><!--~separator }"-->
+# <!--~row~-->
+# <!--~rows~-->
+#
+function tem_auto_sep($piece, $context, $keychain) {
+ list($name, $index, $this_name) = array_slice($keychain, -3);
+ $array = _tem_get($name, $context);
+ if($index != count($array)-1) return true;
}
-function tem_show($sub_template_name) {
- tem_init();
- $GLOBALS['wfpl_template']->show($sub_template_name);
-}
+# 'once' sections will be shown once unless the corresponding data value
+# is false. We check only for false; 0 or '' will not work.
-function tem_show_separated($sub_template_name) {
- tem_init();
- $GLOBALS['wfpl_template']->show_separated($sub_template_name);
+function tem_auto_once($piece, $context, $keychain) {
+ $value = _tem_get(array_pop($keychain), $context);
+ if($value !== false) return true;
}
+# 'evenodd' sections are given an 'evenodd' attribute whose value
+# alternates between 'even' and 'odd'.
-function tem_load($filename) {
- tem_init();
- $GLOBALS['wfpl_template']->load($filename);
+function tem_auto_evenodd($piece, $context, $keychain) {
+ $rows = _tem_get(array_pop($keychain), $context);
+ $even = 0;
+ $text = array('even', 'odd');
+ foreach($rows as $key => $value) {
+ $rows[$key]['evenodd'] = $text[$even];
+ $even = 1 - $even;
+ }
+ return $rows;
}
-function tem_output($filename = false) {
- tem_init();
- $GLOBALS['wfpl_template']->output($filename);
-}
+# Internal functions
+# ------------------
+#
+# Of course, nothing stops you from using these, but I don't know
+# why you would want to...
-# this is used in template_run() and should be of no other use
-function template_filler($matches) {
- $match = array_pop($matches);
- list($tag, $enc) = explode('.', $match, 2);
- $value = $GLOBALS['wfpl_template_keyval'][$tag];
- if($enc) {
- $encs = explode('.', $enc);
- foreach($encs as $enc) {
- $enc = "enc_$enc";
- if(function_exists($enc)) {
- $value = $enc($value, $tag);
- } else {
- print "ERROR: encoder function '$enc' not found.<br>\n";
- exit(1);
- }
- }
+
+# Convert value to array of hashes for use in sub-template expansion.
+# This adds flexibility to how you represent your data.
+function template_rows($value) {
+ if(is_array($value)) {
+ # numeric keys, is already array of arrays -- expand sub-template for each.
+ if(array_key_exists(0, $value)) return $value;
+ # key/value pairs -- expand sub-template once.
+ else return array($value);
+ } elseif($value) {
+ # value -- expand sub-template once using only parent values
+ return array(array());
+ } else {
+ # empty value -- don't expand sub-template
+ return array();
}
- return $value;
}
-
-# pass a template string and an associative array of the key/values and it
-# returns the result.
-function template_run($template, &$keyval) {
- $GLOBALS['wfpl_template_keyval'] =& $keyval;
- return preg_replace_callback('`<!--~([^~]*)~-->|~([^~]*)~|<span class="template">([^<]*)</span>|<p class="template">([^<]*)</p>`', 'template_filler', $template);
+function _tem_get($key, $context) {
+ while($context) {
+ $data = array_pop($context);
+ if(array_key_exists($key, $data)) return $data[$key];
+ }
}
-function tem_top_sub_names() {
- tem_init();
- return $GLOBALS['wfpl_template']->top_sub_names();
+function tem_get($piece, $context, $keychain)
+{
+ if(count($piece['args'])) {
+ $func = "tem_auto_" . $piece['args'][0];
+ if(function_exists($func)) return $func($piece, $context, $keychain);
+ else die("ERROR: template auto function '$func' not found.<br>\n");
+ } else return _tem_get($piece['name'], $context);
}
-function tem_top_subs() {
- tem_init();
- return $GLOBALS['wfpl_template']->top_subs();
+# $tag is a hash with keys 'name' and 'args'.
+function tem_get_enc($tag, $context)
+{
+ $key = $tag['name'];
+ $value = _tem_get($key, $context);
+ foreach($tag['args'] as $encoding) {
+ $func = "enc_$encoding";
+ if(function_exists($func)) $value = $func($value, $key);
+ else die("ERROR: encoder function '$func' not found.<br>\n");
+ }
+ return $value;
}
-# replaces currently set template, and returns the old.
-function tem_load_new($file) {
- $old = $GLOBALS['wfpl_template'];
- $GLOBALS['wfpl_template'] = new tem();
- $GLOBALS['wfpl_template']->load($file);
- return $old;
+function top_sub_templates($tem) {
+ $subs = array();
+ foreach($tem['pieces'] as $piece) {
+ if(is_array($piece) and $piece['pieces']) {
+ $subs[$piece['name']] = $piece;
+ }
+ }
+ return $subs;
}
?>