<?php // $Id: MTPL.php,v 1.1.1.1 2001/11/28 18:24:51 ramenboy Exp $
/*
* MTPL (MicroTemplate) class
*
* This class provides an extremely fast, lightweight templating system for
* HTML and other text-based documents.
*
* Like FastTemplate and its many clones, it supports nested blocks and
* looping. However, the entire implementation is done without a single
* regular expression. The PHP interpreter is leveraged for its built-in
* variable interpolation, and explode() is used to separate block markers
* from content using a simple delimiter. As a result, the overhead of
* reading and parsing template files is minimal. This should eliminate
* the need for a cache and improve the overall performance of template-
* driven sites.
*
* Since PHP's dollar sign notation (ie. "$var") is used to identify content
* placeholders, it is sometimes necessary to escape dollar signs in your
* template (ie. "\$$amount"). You can use arrays with the form
* $array[element]; just don't quote the index. $array['element'] or
* $array["element"] will generate PHP parse errors due to the way PHP
* evaluates hashes in a string context. For the same reason, multi-level
* arrays will not work ("$a[b][c]" becomes the value of $a[b] followed by
* the literal text "[c]").
*
* Global variables can be accessed using the usual $GLOBALS[] array method.
* For instance, to get the script URL, you can use $GLOBALS[PHP_SELF].
*
* The block markers look like "** begin: some_block **" as opposed to an
* HTML-comment or fake HTML tag style. This means that the markers will
* show up in a web browser if you view the template file. For a more
* HTML-friendly version with fake tags and form element utility functions,
* take a look at the HTPL class (HTPL.php).
*
* There are a few other differences. The assign() method found in most
* PHP template classes is gone. Instead, all assigned variables must be
* provided as a hash that is passed to the parse() method. Since in almost
* every single case a series of assign()'s is done followed by a single
* call to parse(), this eliminates some redundancy. It also provides a
* more strict scoping to template variables which should result in
* scripts that are easier to debug.
*
* Two variations on parse() exist: parseLoop() and parseOut(). parseLoop()
* takes a list of hashes instead of a single hash of variables to assign
* and calls parse() on the same block for each. This allows you to
* populate tables and drop-downs without the need to write loops into your
* code. parseOut() iteratively calls parse() for a given block and each of
* its parents in order. For instance, parseOut('main.section.sub') will
* parse 'main.section.sub', then 'main.section', then 'main'. Both of these
* methods should reduce the amount of code you need to write for typical
* operations.
*
* You will also notice that there is no out(), text(), or print() method.
* Rather than holding onto all output internally, the parse() and related
* methods return the resulting output. To output the result of a parse
* operation, simply: echo parse(...);
*
* PHP's native error handling is used for all errors and warnings.
* In addition, by adding "error_handling(E_ALL);" to your scripts, the
* PHP interpreter will warn you about any uninitialized template variables.
* Granted, the error messages are a bit odd (since interpolation is done
* inside of an eval()), but this can be a good way to track down typos.
* Hopefully PHP will provide stack traces on errors soon.
*
* If this script is opened directly, it will look for a sample template
* file called "test.mtpl" and run some brief test cases that should
* demonstrate how this class is to be used.
*
* Any comments, code suggestions, or bug-fixes would be appreciated.
* Don't expect any big feature additions, though; my goal was to keep this
* fast, simple, and effective. If you're looking for a more robust,
* feature-laden solution, I'd suggest you take a look at Smarty. On the
* other hand, if speed and transparency are your primary concerns, this
* may be just what you have been looking for.
*
* Author: Dave Benjamin <hide@address.com>
* Source: http://www.ovumdesign.com/
* Created: Fri Nov 23 21:18:28 PST 2001
* Version: $Revision: 1.1.1.1 $
*/
// Delimeter to separate block markers from content
define('MTPL_SEP', '**');
// Delemiter between block marker command and its name argument
define('MTPL_MARKER_SEP', ':');
// Block marker commands (case insensitive)
define('MTPL_MARKER_BEGIN', 'begin');
define('MTPL_MARKER_END', 'end');
// Error codes
define('E_MTPL_FILE', 1);
define('E_MTPL_END', 2);
define('E_MTPL_BLK', 3);
define('E_MTPL_UNT', 4);
// Error types (256 for error, 512 for warning) and messages for error codes
$MTPL_ERRORS = array(
E_MTPL_FILE => array(256, 'Unable to read file: <b>%s</b>'),
E_MTPL_END => array(512, 'Unexpected block end marker: <b>%s</b> '
. '(expected <b>%s</b>)'),
E_MTPL_BLK => array(256, 'Unknown block: <b>%s</b>'),
E_MTPL_UNT => array(512, 'Unterminated block: <b>%s</b>'),
);
class MTPL {
// public:
// Constructor - Processes a string of template content
// To read a file, call MTPL::readFile() instead.
function MTPL($str) {
$out = array();
$stack = array();
$alt = false;
foreach (array_slice($this->explodeTemplate($str), 1) as $in) {
$stack and $path = implode('.', $stack)
and (isset($out[$path]) or $out[$path] = '');
// The if-block gets executed for the first iteration,
// then then else-block for the second, then the if-block
// again and so forth until the template input is
// exhausted. The if-block processes commands, and the
// else-block processes content.
if ($alt = !$alt) {
// Separate the command from its argument.
list($cmd, $name) = $this->explodeMarker($in);
if (strtolower($cmd) == MTPL_MARKER_BEGIN) {
// Begin a block.
$stack and $this->subBlock[$path][$name] = true
and $out[$path] .= "\$in[$name]";
array_push($stack, $name);
} elseif (strtolower($cmd) == MTPL_MARKER_END) {
// End a block.
$name == end($stack)
or $this->error(E_MTPL_END, $name, end($stack));
array_pop($stack);
}
} else {
// Add content to the current block.
$stack and $out[$path] .= ltrim($in);
}
}
// If the stack isn't empty, one or more blocks were not terminated
// properly, so generate a warning.
$stack and $this->error(E_MTPL_UNT, end($stack));
$this->data = $out;
}
// Reads a template file and returns an MTPL object
function readFile($file) {
$text = @file($file) or MTPL::error(E_MTPL_FILE, $file);
return new MTPL(implode('', $text));
}
// Parses a block given its path and a hash of variables to assign
function parse($path, $vars = array()) {
$in = array();
// Make sure that the block exists.
isset($this->data[$path]) or $this->error(E_MTPL_BLK, $path);
// Insert the contents of all sub-blocks, then reset them.
if (isset($this->subBlock[$path])) {
foreach (array_keys($this->subBlock[$path]) as $sub) {
$in[$sub] = '';
$subPath = "$path.$sub";
if (isset($this->out[$subPath])) {
$in[$sub] = $this->out[$subPath];
unset($this->out[$subPath]);
}
}
}
// Interpolate variables within the block.
$out = $this->subst($this->data[$path], compact('in') + $vars);
// Hold onto output if it might be needed for a parent block.
if (strchr($path, '.')) {
isset($this->out[$path]) or $this->out[$path] = '';
$this->out[$path] .= $out;
}
return $out;
}
// Parses a block for each in a list of hashes of variables to assign
// An optional third parameter may contain additional variables to
// assign for every call to parse().
function parseLoop($path, $list, $vars = array()) {
$out = '';
foreach ($list as $lvars) $out .= $this->parse($path, $lvars + $vars);
return $out;
}
// Parses a block and each of its parents in order
function parseOut($path, $vars = array()) {
do $out = $this->parse($path, $vars);
while ($path = substr($path, 0, strrpos($path, '.')));
return $out;
}
// protected:
// Triggers an error given an error code and error message arguments
function error(/* $code, ... */) {
$args = func_get_args();
list($lvl, $msg) = $GLOBALS['MTPL_ERRORS'][array_shift($args)];
return trigger_error(MTPL::vsprintf($msg, $args), $lvl);
}
// Returns a command and argument for a block marker
function explodeMarker($str) {
$result = explode(MTPL_MARKER_SEP, $str) + array('', '');
return array(trim($result[0]), trim($result[1]));
}
// Returns an alternating list of block markers and content
function explodeTemplate($str) {
return explode(MTPL_SEP, $str);
}
// Performs variable substitution (interpolation) on a string
function subst($str, $vars) {
extract($vars);
return eval('return "' . str_replace('"', '\\"', $str) . '";');
}
// private:
// Calls sprintf() for an array of arguments
// This function is supposedly available in the CVS version of PHP.
// Here's an implementation for the rest of us. =)
function vsprintf($str, $args) {
$etc = '$args[' . implode('], $args[', array_keys($args)) . ']';
return $args ? eval("return sprintf(\$str, $etc);") : sprintf($str);
}
}
// Test cases
// ----------
// Evaluate the following code if this script is called directly.
if (realpath($SCRIPT_FILENAME) == __FILE__):
// Report all errors, warnings, and notices.
error_reporting(E_ALL);
// Start the timer.
list($a, $b) = explode(' ', microtime()); $time = $a + $b;
// Initialize a list of variable assignments for the parseLoop() example.
$vars = array(
array('b' => 5, 'c' => 6),
array('b' => 7, 'c' => 8),
);
// Read the template file.
$mtpl = MTPL::readFile('test.mtpl');
// Demonstrate parseLoop() and parseOut().
$mtpl->parseLoop('main.sub', $vars, array('user' => array('name' => 'joe')));
echo $mtpl->parseOut('main.sub2', array('a' => 4));
// Demonstrate basic parse().
$mtpl->parse('main.sub2');
$mtpl->parse('main.sub2');
echo $mtpl->parse('main', array('a' => 2));
// Stop the timer.
list($a, $b) = explode(' ', microtime());
$time = round($a + $b - $time, 4);
echo "<p>Time elapsed: $time seconds.</p>";
endif; // this script is called directly
?>