This is free software. Go insane.
*/
// this will probably be added to PHP eventually, let's provide it
// ourselves while they don't
if (!function_exists("mb_ucfirst") && function_exists("mb_substr"))
{
function mb_ucfirst($string)
{
$string = mb_strtoupper(mb_substr($string, 0, 1)).mb_substr($string, 1);
return $string;
}
}
class jtpl
{
// debugging disabled by default
public $debug = false;
// where to read e.g. included template files from
public $templateroot = "./";
// Normally we emulate trigger_error to obtain the correct line numbers etc.,
// but in projects where custom error handlers are used, this is a pain
// so this feature can be disabled at runtime
public $honor_errorhandler = false;
// character set of the template
public $charset = "UTF-8";
// default variable escaper, empty by default (no escaping)
public $defaultvarescape = "";
// ignore preserves?
public $ignorepreserves = false;
// by default {file.tpl:INCLUDE} tokens cannot include files from an upper
// directory (e.g. paths containing two consecutive dots "../")
public $allowupperdirectoryincludes = false;
// by default {file.tpl:INCLUDE} tokens cannot include files using an
// absolute path (e.g. /etc/passwd)
public $allowabsolutepathincludes = false;
// main template
private $_template = "";
// current tree depth
private $_depth = "/";
// current iteration
private $_currentiteration = 0;
// storage for variables, case blocks, if blocks
private $_vars = array();
private $_ifblocks = array();
private $_caseblocks = array();
// cache for variable validation (only once per occasion)
private $_vars_namecheckcache = array();
// timer for performance tracking
private $_timer = false;
// template variable escapers
private $_escapers = array("HTML" => "_escape_html",
"XML" => "_escape_xml",
"JS" => "_escape_js",
"NL2BR" => "_escape_nl2br",
"OOO" => "_escape_ooo",
"URL" => "_escape_url",
"TPL" => "_escape_tpl",
"UCASE" => "_escape_ucase",
"LCASE" => "_escape_lcase",
"UCFIRST" => "_escape_ucfirst",
"INT" => "_escape_int",
"MONEY" => "_escape_money",
"PERCENTAGE" => "_escape_percentage",
"NOESCAPE" => "_escape_void");
// read a template from a file and use as root template
// expects a template file name on the local filesystem
public function readtemplate($file)
{
$this->_settemplateroot($file);
$data = $this->_readfile($file);
$this->_template = $data;
}
// read template data from string and use as root template
public function assigntemplate($data)
{
$this->_template = $data;
}
// include a template as a variable
// expects a template file name on the local filesystem
public function includetemplate($file, $variable)
{
$data = $this->_readfile($file);
$this->assignvar($variable, $data);
}
// read a file from disk into a string
private function _readfile($file)
{
if (!is_file($file))
{
$this->_trigger_jtplerror("Unable to open ".$file." as a template: file not found", E_USER_ERROR);
}
if (!is_readable($file))
{
$this->_trigger_jtplerror("Unable to open ".$file." as a template: permission denied", E_USER_ERROR);
}
$data = file_get_contents($file);
return $data;
}
// derive template path from assigned template file
// used in dynamic includes to determine the right template directory, no matter
// where the script itself is
private function _settemplateroot($filename)
{
if (preg_match("/(.*\/).*?/", $filename, $matches))
{
$this->templateroot = $matches[1];
} else
{
$this->templateroot = "./";
}
}
// this function is used to display/handle errors
// it will default to emulating trigger_error so correct line numbers
// will be stated.
private function _trigger_jtplerror($errormessage, $errortype = E_USER_WARNING)
{
// let's honour the local PHP configuration when we 'emulate'
// trigger_error()
$htmlerrors = ini_get("html_errors");
if ($htmlerrors)
{
$boldprepend = "";
$boldappend = "";
$linebreak = "
\n";
} else
{
$boldprepend = "";
$boldappend = "";
$linebreak = "\n";
}
switch ($errortype)
{
case E_USER_WARNING:
$fatal = false;
$messagetext = "Warning";
break;
case E_USER_ERROR:
$fatal = true;
$messagetext = "Fatal error";
break;
case E_USER_NOTICE:
$fatal = false;
$messagetext = "Notice";
break;
default:
// we should never arrive here, but let's put it in anyway...
$fatal = true;
$messagetext = "Unknown error";
break;
}
// get a backtrace
$backtrace = debug_backtrace();
// reverse the array so the first item becomes the last, which
// holds the entry important to us
$backtrace = array_reverse($backtrace);
$actualerror = $backtrace[0];
// these will not be too helpful when complaining about dynamic includes
// or variable modifiers, but this way at least we're not blaming ourselves
$file = $actualerror['file'];
$linenumber = $actualerror['line'];
if ($this->honor_errorhandler)
{
trigger_error($errormessage, $errortype);
} else
{
echo $linebreak.$boldprepend.$messagetext.$boldappend.": ".$errormessage." in ".$boldprepend.$file.$boldappend." on line ".$boldprepend.$linenumber.$boldappend.$linebreak;
}
// if the error is fatal, we should consider our trek ended
if ($fatal)
{
die;
}
}
// this will parse the whole template on the fly
private function _buildtree($processdata, $depth = "/", $scope = array())
{
$processeddata = "";
$text = "";
// iterate until there is nothing left
while ($processdata != "")
{
$token = "";
// find the first occurence of {
$firstcurl = strpos($processdata, "{");
if ($firstcurl > 0)
{
// first we have some text, then something
// that may very well be a token
$text .= substr($processdata, 0, $firstcurl);
$processdata = substr($processdata, $firstcurl);
} elseif ($firstcurl === false)
{
// only text left
$text .= $processdata;
$processdata = "";
} else
{
if (preg_match("/^{([a-z0-9_\-\.:;\|&\/]+)}([\n\r]{0,2})/is", $processdata, $match))
{
// possible real token
$token = $match[1];
// let's not catch these last newline characters,
// otherwise output will contain lots of them.
$tmp = $match[2];
// unless it is a variable, then we keep them intact (otherwise lines
// ending with a variable suddenly glue to the next line)
if (strpos($token, ":") === false)
{
$tmp = "";
}
$processdata = substr($processdata, strlen($token) + strlen($tmp) + 2);
} elseif (preg_match("/^{!--.*?--}/s", $processdata, $match))
{
// template comment, ignore the whole thing
$processdata = substr($processdata, strlen($match[0]));
} else
{
// probably something else... treat as text
$text .= substr($processdata, 0, 1);
$processdata = substr($processdata, 1);
}
}
// let's see what kind of token we have here
$tokentype = $this->_get_tokentype($token);
// handle tokens appropriately
if ($tokentype['type'] == "REPEAT" && $tokentype['starter'])
{
// we have a repeater! please iterate...
$this->enterblock($tokentype['name']);
$olddepth = $depth;
if ($depth == "/")
{
$depth = $tokentype['name'];
} else
{
$depth = $depth.":".$tokentype['name'];
}
if (!isset($this->_iterations[$depth]))
{
if ($this->debug)
{
$this->_trigger_jtplerror("REPEAT block '".$tokentype['name']."' never iterates at level ".$olddepth, E_USER_NOTICE);
}
$iterations = 0;
} else
{
$iterations = $this->_iterations[$depth];
}
// find out where this block ends
if (!$tempdata = $this->_find_block_end($processdata, $tokentype['name'].":END"))
{
$this->_trigger_jtplerror("Encountered REPEAT block '".$tokentype['name']."' START token without accompanying END", E_USER_ERROR);
}
// we have to treat this repeat block as a 'separate' template in this subroutine
$processdata = $tempdata['processdata'];
$blockdata = $tempdata['blockdata'];
for ($i = 1; $i <= $iterations; $i++)
{
// we need a different variable/if/case/etc. scope to parse this part
// of the document. we pass on our current scope too, as 'parent'
// variables etc., when not set in the repeat block itself, should be
// inherited.
$tempscope = $this->_createscope($depth."|".$i, $scope);
$text .= $this->_buildtree($blockdata, $depth."|".$i, $tempscope);
}
$this->leaveblock();
$depth = $olddepth;
} elseif ($tokentype['type'] == "IF" && $tokentype['starter'])
{
// IF block found
$blockname = $tokentype['name'];
if (!$tempdata = $this->_find_block_end($processdata, $blockname.":ENDIF"))
{
$this->_trigger_jtplerror("Encountered IF block '".$blockname."' IF token without accompanying ENDIF", E_USER_ERROR);
}
$processdata = $tempdata['processdata'];
$ifdata = $this->_split_ifblock($tempdata['blockdata'], $blockname);
// do we have AND/OR inside the IF block?
if (strpos($blockname, "|") > 0 || strpos($blockname, "&") > 0)
{
// grab IF block names and & | AND/OR separators
$matches = preg_split("/(&|\|)/", $blockname, -1, PREG_SPLIT_DELIM_CAPTURE);
$operator = "";
$parttrue = false;
foreach ($matches as $match)
{
if ($match == "|" || $match == "&")
{
$operator = $match;
} else
{
$previousparttrue = $parttrue;
// is this part supposed to be true?
if (isset($scope['ifblocks'][$match]))
{
$parttrue = $scope['ifblocks'][$match];
} else
{
if ($this->debug)
{
$this->_trigger_jtplerror("IF block portion '".$match."' of IF block '".$blockname."' not set, reverting to FALSE", E_USER_NOTICE);
}
$parttrue = false;
}
if (($previousparttrue || $parttrue) && $operator == "|")
{
// we've found an OR that is true, no need
// to continue
$iftrue = true;
break;
} elseif (!$parttrue && $operator == "&")
{
// we've found an AND that is false,
// no need to continue
$iftrue = false;
break;
}
}
if ($operator == "&")
{
// if we survived our last AND operator without stumbling
// upon something false, it must be true
$iftrue = true;
} else
{
// if we survived our last OR operator without stumbling
// upon something true, it must be false
$iftrue = false;
}
}
} elseif (isset($scope['ifblocks'][$blockname]))
{
$iftrue = $scope['ifblocks'][$blockname];
} else
{
if ($this->debug)
{
$this->_trigger_jtplerror("IF block '".$blockname."' not set, reverting to FALSE", E_USER_NOTICE);
}
$iftrue = false;
}
if ($iftrue)
{
$blockdata = $ifdata['if'];
} else
{
$blockdata = $ifdata['else'];
}
$text .= $this->_buildtree($blockdata, $depth, $scope);
} elseif ($tokentype['type'] == "PRESERVE" && $tokentype['starter'])
{
// PRESERVE block found, don't parse anything inside it
if (!$tempdata = $this->_find_block_end($processdata, "PRESERVE-END"))
{
$this->_trigger_jtplerror("Encountered PRESERVE-START token without accompanying PRESERVE-END", E_USER_ERROR);
}
if ($this->ignorepreserves)
{
$processdata = $tempdata['blockdata'].$tempdata['processdata'];
} else
{
$processdata = $tempdata['processdata'];
$text .= $tempdata['blockdata'];
}
} elseif ($tokentype['type'] == "CASE" && $tokentype['starter'])
{
// CASE block found
$blockname = $tokentype['name'];
// see where this block ends
if (!$tempdata = $this->_find_block_end($processdata, $blockname.":CASE-END"))
{
$this->_trigger_jtplerror("Encountered CASE token without accompanying CASE-END", E_USER_ERROR);
}
$processdata = $tempdata['processdata'];
$casedata = $this->_split_caseblock("{".$blockname.":".$tokentype['params'].":CASE}".$tempdata['blockdata'], $blockname);
if (isset($scope['caseblocks'][$blockname]))
{
$case = $scope['caseblocks'][$blockname];
} else
{
if ($this->debug)
{
$this->_trigger_jtplerror("CASE block '".$blockname."' not set, reverting to DEFAULT", E_USER_NOTICE);
}
$case = "DEFAULT";
}
if (!isset($casedata[$case]))
{
if (!isset($casedata['DEFAULT']))
{
if ($this->debug)
{
$this->_trigger_jtplerror("CASE block '".$blockname."' has no CASE called '".$case."', leaving empty (no DEFAULT to fall back to)", E_USER_NOTICE);
}
$blockdata = "";
} else
{
if ($this->debug)
{
$this->_trigger_jtplerror("CASE block '".$blockname."' has no CASE called '".$case."', reverting to DEFAULT", E_USER_NOTICE);
}
$blockdata = $casedata['DEFAULT'];
}
} else
{
$blockdata = $casedata[$case];
}
$text .= $this->_buildtree($blockdata, $depth, $scope);
} elseif ($tokentype['type'] == "INCLUDE")
{
// this is an INCLUDE token, let's include the file specified
// if it is allowed (presumably safe) to do so
if (!$this->allowupperdirectoryincludes && strpos($tokentype['name'], "..") !== false)
{
$this->_trigger_jtplerror("Dynamically including ".$tokentype['name']." not allowed (parent directory restriction in effect)", E_USER_ERROR);
}
if (!$this->allowabsolutepathincludes && $tokentype['name'][0] == "/")
{
$this->_trigger_jtplerror("Dynamically including ".$tokentype['name']." not allowed (absolute path restriction in effect)", E_USER_ERROR);
}
// is the include file included using an absolute path?
if ($tokentype['name'][0] == "/")
{
// yes, don't use current template dir when including
$blockdata = $this->_readfile($tokentype['name']);
} else
{
// no, relative... let's use the current template dir as a base path
$blockdata = $this->_readfile($this->templateroot.$tokentype['name']);
}
if ($tokentype['params'] != "")
{
$processdata = $this->modify_content($blockdata, $tokentype['params']).$processdata;
} else
{
$processdata = $blockdata.$processdata;
}
} elseif ($tokentype['type'] == "ESCAPE" && $tokentype['starter'])
{
// ESCAPE block found, escape using the correct escaper(s)
if (!$tempdata = $this->_find_block_end($processdata, $tokentype['params'].":ESCAPE-END"))
{
$this->_trigger_jtplerror("Encountered ESCAPE-START token without accompanying ESCAPE-END", E_USER_ERROR);
}
$processdata = $this->modify_content($tempdata['blockdata'], $tokentype['params']).$tempdata['processdata'];
} elseif ($tokentype['ender'])
{
// we're never supposed to find these...
switch ($tokentype['type'])
{
case "REPEAT":
$this->_trigger_jtplerror("Encountered REPEAT block '".$tokentype['name']."' closing token without accompanying '{".$tokentype['name'].":START}'", E_USER_ERROR);
break;
case "IF":
$this->_trigger_jtplerror("Encountered IF block '".$tokentype['name']."' closing token without accompanying '{".$tokentype['name'].":IF}", E_USER_ERROR);
break;
case "CASE":
$this->_trigger_jtplerror("Encountered CASE block '".$tokentype['name']."' closing token without accompanying '{".$tokentype['name'].":...:CASE}'", E_USER_ERROR);
break;
case "PRESERVE":
$this->_trigger_jtplerror("Encountered PRESERVE block closing token without accompanying '{PRESERVE-START}'", E_USER_ERROR);
break;
case "ESCAPE":
$this->_trigger_jtplerror("Encountered ESCAPE block closing token without accompanying {...;ESCAPE-END}", E_USER_ERROR);
break;
}
$this->_trigger_jtplerror("Encountered ".$tokentype['type']." block '".$tokentype['name']."' closing token without accompanying starting token", E_USER_ERROR);
}
// variables are a special case... we don't delegate these to ourselves since
// variables aren't "blocks".
if ($tokentype['type'] == "VARIABLE")
{
$var = $tokentype['name'];
if (isset($scope['vars'][$var]))
{
// if no modifiers are present in template, use the default one,
// if set
if ($this->defaultvarescape != "" && $tokentype['params'] == "")
{
$tokentype['params'] = $this->defaultvarescape;
}
if ($tokentype['params'] != "")
{
$processdata = $this->modify_content($scope['vars'][$var], $tokentype['params']).$processdata;
} else
{
$processdata = $scope['vars'][$var].$processdata;
}
} else
{
if ($this->debug)
{
$this->_trigger_jtplerror("VARIABLE '".$var."' not set", E_USER_NOTICE);
}
}
}
}
return $text;
}
private function _get_tokentype($token)
{
$tokentype = array("name" => "",
"type" => "",
"starter" => false,
"ender" => false,
"params" => "");
// what kind of token do we have...
if ($token == "")
{
// not a token at all
} elseif ($token == "PRESERVE-START")
{
// PRESERVE start token
$tokentype['type'] = "PRESERVE";
$tokentype['starter'] = true;
} elseif ($token == "PRESERVE-END")
{
// PRESERVE end token (shouldn't find these, but oh well...)
$tokentype['type'] = "PRESERVE";
$tokentype['ender'] = true;
} elseif (strpos($token, ":") === false)
{
// a plain old variable... 9 out of 10 tokens are plain old variables, match first
$tokentype['type'] = "VARIABLE";
$tokentype['name'] = $token;
// are there any modifiers?
if (strpos($token, ";") !== false)
{
// apparently...
// check which modifiers are there...
$matches = explode(";", $token, 2);
$tokentype['name'] = $matches[0];
if (isset($matches[1]))
{
$tokentype['params'] = $matches[1];
}
}
} else
{
// alright... some more difficult tag
preg_match("/^(.*):(.*?)$/", $token, $matches);
$data = $matches[1];
$type = $matches[2];
if ($type == "START")
{
$tokentype['type'] = "REPEAT";
$tokentype['starter'] = true;
$tokentype['name'] = $data;
} elseif ($type == "END")
{
$tokentype['type'] = "REPEAT";
$tokentype['ender'] = true;
$tokentype['name'] = $data;
} elseif ($type == "IF")
{
$tokentype['type'] = "IF";
$tokentype['starter'] = true;
$tokentype['name'] = $data;
} elseif ($type == "ENDIF")
{
$tokentype['type'] = "IF";
$tokentype['ender'] = true;
$tokentype['name'] = $data;
} elseif ($type == "CASE")
{
// we need to find the CASE name and options...
if (!preg_match("/^(.*?):(.*)$/", $data, $match))
{
$this->_trigger_jtplerror("Malformed CASE token: {".$matches[0]."}", E_USER_ERROR);
}
$tokentype['type'] = "CASE";
$tokentype['starter'] = true;
$tokentype['name'] = $match[1];
$tokentype['params'] = $match[2];
} elseif ($type == "CASE-END")
{
$tokentype['type'] = "CASE";
$tokentype['ender'] = true;
$tokentype['name'] = $data;
} elseif ($type == "ESCAPE-START")
{
$tokentype['type'] = "ESCAPE";
$tokentype['starter'] = true;
$tokentype['params'] = $data;
} elseif ($type == "ESCAPE-END")
{
$tokentype['type'] = "ESCAPE";
$tokentype['ender'] = true;
$tokentype['params'] = $data;
} elseif (substr($type, 0, 7) == "INCLUDE")
{
$tokentype['type'] = "INCLUDE";
$tokentype['name'] = $data;
// see if we have any escapers for this dynamic INCLUDE
if (strlen($type) > 7 && preg_match("/^INCLUDE;([\w;]+)$/", $type, $matches))
{
$tokentype['params'] = $matches[1];
}
} else
{
$didyoumean = "";
// something else after the colon... may be a misspelling
if (isset($this->_escapers[$type]))
{
$didyoumean = " (did you mean {".$data.";".$type."} ?)";
}
$this->_trigger_jtplerror("Unknown token type: {".$data.":".$type."}".$didyoumean, E_USER_ERROR);
}
}
return $tokentype;
}
// this function will search for the end of some block
private function _find_block_end($processdata, $blockend)
{
if (!preg_match("/(.*?){".preg_quote($blockend, "/")."}(.*)$/s", $processdata, $matches))
{
return false;
}
$data = array("processdata" => $matches[2],
"blockdata" => $matches[1]);
return $data;
}
// determine the IF and ELSE (if any) portions of the IF block
private function _split_ifblock($blockdata, $blockname)
{
if (!preg_match("/^(.*?){".preg_quote($blockname, "/").":ELSE}(.*?)$/s", $blockdata, $matches))
{
// there is no ELSE...
$if = $blockdata;
$else = "";
} else
{
$if = $matches[1];
$else = $matches[2];
}
$ifdata = array("if" => $if,
"else" => $else);
return $ifdata;
}
// determine the specific cases for this CASE block
private function _split_caseblock($blockdata, $blockname)
{
$blockname_preg = preg_quote($blockname, "/");
preg_match_all("/{".$blockname_preg.":(.*?):CASE}(.*?)(?={".$blockname_preg.":|$)/s", $blockdata, $matches);
$cases = array();
foreach ($matches[1] as $key => $value)
{
$casedata = explode(":", $value);
foreach ($casedata as $casetarget)
{
$cases[$casetarget] = $matches[2][$key];
}
}
return $cases;
}
// will modify content based on used escapers
public function modify_content($content, $modifiers)
{
$modifiers = preg_split("/;/", $modifiers);
foreach ($modifiers as $modifier)
{
if (!isset($this->_escapers[$modifier]))
{
$this->_trigger_jtplerror("Unsupported variable modifier '".$modifier."' detected in template", E_USER_WARNING);
continue;
}
$function = $this->_escapers[$modifier];
if (method_exists($this, $function))
{
$content = $this->$function($content);
} elseif (function_exists($function))
{
$content = $function($content);
} else
{
$this->_trigger_jtplerror("Unknown function '".$function."' for escaper '".$modifier."' ", E_USER_ERROR);
}
}
return $content;
}
// this way you can add custom template escapers
public function add_custom_escaper($escapetag, $function)
{
$this->_escapers[$escapetag] = $function;
}
private function _escape_html($content)
{
return htmlentities($content, ENT_QUOTES, $this->charset);
}
private function _escape_js($content)
{
$content = preg_replace("/('|\"|\\\)/s", "\\\\$1", $content);
$content = preg_replace("/(\r\n|\r|\n)/s", "\\n", $content);
return $content;
}
private function _escape_nl2br($content)
{
return nl2br($content);
}
private function _escape_xml($content)
{
static $trans;
$content = str_replace("&", "&" , $content);
if(!isset($trans))
{
$trans = get_html_translation_table(HTML_ENTITIES, ENT_QUOTES);
foreach($trans as $key => $value)
{
$ord = ord($key);
$trans[$key] = "".$ord.";";
}
// dont translate the '&' in case it is part of &xxx;
$trans[chr(38)] = '&';
}
return strtr($content, $trans);
}
// "escapes" a string to OpenOffice.org encoding
private function _escape_ooo($string)
{
return iconv("WINDOWS-1252", "UTF-8", $this->_escape_xml($string));
}
private function _escape_url($content)
{
return rawurlencode($content);
}
function _escape_percentage($content)
{
return sprintf("%.2f%%", $content * 100);
}
private function _escape_tpl($content)
{
$content = str_replace("{", "{", $content);
$content = str_replace("}", "}", $content);
return $content;
}
private function _escape_ucase($content)
{
if (function_exists("mb_strtoupper"))
{
return mb_strtoupper($content);
} else
{
return strtoupper($content);
}
}
private function _escape_lcase($content)
{
if (function_exists("mb_strtolower"))
{
return mb_strtolower($content);
} else
{
return strtolower($content);
}
}
private function _escape_ucfirst($content)
{
if (function_exists("mb_ucfirst"))
{
return mb_ucfirst($content);
} else
{
return ucfirst($content);
}
}
private function _escape_int($content)
{
return intval($content);
}
private function _escape_money($content)
{
return sprintf("%.2f", $content);
}
private function _escape_void($content)
{
return $content;
}
// assign a variable for use in a template (block)
public function assignvar($var, $content)
{
$depth = $this->_depth;
$iteration = $this->_currentiteration;
// iteration cannot be 0 when assigning variables
if ($iteration == 0 && $depth != "/")
{
$this->_trigger_jtplerror("Called assignvar() without prior startiteration()", E_USER_ERROR);
}
// no funky variable names please
if (!isset($this->_vars_namecheckcache[$var]))
{
if (!preg_match("/^[a-z0-9_\.\-]+$/i", $var))
{
$this->_trigger_jtplerror("' ".$var."' is not a valid variable name", E_USER_WARNING);
} else
{
// only validate a variable name once
$this->_vars_namecheckcache[$var] = true;
}
}
$this->_vars[$depth][$var] = $content;
}
// function to assign an entire array as vars (key => value)
// by default array keys (vars) are capitalised
public function assignvarsfromarray($vars, $touppercase = true)
{
if (is_array($vars))
{
foreach ($vars as $var => $val)
{
if ($touppercase)
{
$var = strtoupper($var);
}
$this->assignvar($var, $val);
}
} else
{
$this->_trigger_jtplerror("First argument supplied to assignvarsfromarray() is not an array", E_USER_WARNING);
}
}
// assign an if block to the current iteration depth
public function ifblock($blockname, $condition)
{
$depth = $this->_depth;
$iteration = $this->_currentiteration;
if ($iteration == 0 && $depth != "/")
{
$this->_trigger_jtplerror("Called ifblock() without prior startiteration()", E_USER_ERROR);
}
if (isset($this->_ifblocks[$depth]) && is_array($this->_ifblocks[$depth])&& array_key_exists($blockname, $this->_ifblocks[$depth]) && $this->debug)
{
$this->_trigger_jtplerror("IF block '".$blockname."' already declared", E_USER_WARNING);
}
$this->_ifblocks[$depth][strtoupper($blockname)] = $condition;
}
// assign a case block to the current iteration depth
public function caseblock($blockname, $condition)
{
$depth = $this->_depth;
$iteration = $this->_currentiteration;
if ($iteration == 0 && $depth != "/")
{
$this->_trigger_jtplerror("Called caseblock() without prior startiteration()", E_USER_ERROR);
}
if (isset($this->_caseblocks[$depth]) && is_array($this->_caseblocks[$depth]) && array_key_exists($blockname, $this->_caseblocks[$depth]) && $this->debug)
{
$this->_trigger_jtplerror("CASE block '".$blockname."' already declared", E_USER_WARNING);
}
$this->_caseblocks[$depth][$blockname] = $condition;
}
// enter a template block "level"
public function enterblock($depth)
{
$currentdepth = $this->_depth;
if ($currentdepth == "/")
{
if (isset($this->_iterations[$depth]))
{
$newdepth = $depth."|".intval($this->_iterations[$depth]);
} else
{
$newdepth = $depth."|0";
}
} else
{
if (isset($this->_iterations[$currentdepth.":".$depth]))
{
$newdepth = $currentdepth.":".$depth."|".intval($this->_iterations[$currentdepth.":".$depth]);
} else
{
$newdepth = $currentdepth.":".$depth."|0";
}
}
$this->_depth = $newdepth;
$this->_currentiteration = $this->_getcurrentiteration();
return true;
}
// leave a block "level"
public function leaveblock()
{
$currentdepth = $this->_depth;
// we cannot go higher than the root level!
if ($currentdepth == "/")
{
$this->_trigger_jtplerror("leaveblock(): already at root level", E_USER_ERROR);
}
$newdepth = preg_replace("/^(.*):.*?$/", "$1", $currentdepth);
// the only time this regex will not replace anything, is when
// we're about to ascend to the root level
if ($newdepth == $currentdepth)
{
$newdepth = "/";
}
$this->_depth = $newdepth;
$this->_currentiteration = $this->_getcurrentiteration();
}
// go back to root template (escape all repeat blocks)
public function torootblock()
{
$this->_depth = "/";
$this->_currentiteration = $this->_getcurrentiteration();
}
// start an iteration for a repeat block
public function startiteration()
{
$depth = $this->_depth;
// iterations on the root template do not make sense...
if ($depth == "/")
{
$this->_trigger_jtplerror("Attempted to start an iteration for the root template", E_USER_ERROR);
}
$currentdepth = $this->_getcurrentdepth();
if (!isset($this->_iterations[$currentdepth]))
{
$this->_iterations[$currentdepth] = 0;
}
$this->_iterations[$currentdepth]++;
$this->_increasedepthiteration();
$this->_currentiteration++;
}
private function _getcurrentiteration()
{
return $this->_getiteration($this->_depth);
}
private function _getiteration($depth)
{
if (preg_match("/^.*\|(\d+)$/", $depth, $matches))
{
return $matches[1];
} else
{
return 0;
}
}
private function _getcurrentdepth()
{
return $this->_getdepth($this->_depth);
}
private function _getdepth($depth)
{
if (preg_match("/^(.*)\|\d+$/", $depth, $matches))
{
return $matches[1];
} else
{
return "/";
}
}
private function _increasedepthiteration()
{
$depth = $this->_getcurrentdepth();
if ($depth == "/")
{
return false;
}
$iteration = $this->_currentiteration;
$iteration++;
$newdepth = $depth."|".$iteration;
$this->_depth = $newdepth;
}
// create a var/if/case scope for parsing content
private function _createscope($depth, $parentscope = false)
{
if (!$parentscope)
{
$parentscope = array("vars" => array(),
"ifblocks" => array(),
"caseblocks" => array());
}
if (isset($this->_vars[$depth]))
{
$vars = array_merge($parentscope['vars'], $this->_vars[$depth]);
} else
{
$vars = $parentscope['vars'];
}
if (isset($this->_ifblocks[$depth]))
{
$ifblocks = array_merge($parentscope['ifblocks'], $this->_ifblocks[$depth]);
} else
{
$ifblocks = $parentscope['ifblocks'];
}
if (isset($this->_caseblocks[$depth]))
{
$caseblocks = array_merge($parentscope['caseblocks'], $this->_caseblocks[$depth]);
} else
{
$caseblocks = $parentscope['caseblocks'];
}
$scope = array("vars" => $vars,
"ifblocks" => $ifblocks,
"caseblocks" => $caseblocks);
return $scope;
}
// simple function to get current time
private function _gettime()
{
return microtime(1);
}
// this will render the template
private function _process()
{
$timer = $this->_gettime();
// reset template iterator before processing
$this->_depth = "";
$data = $this->_template;
// we need a variable/if/case/etc. scope to parse the document
$scope = $this->_createscope("/");
$document = $this->_buildtree($data, "/", $scope);
$timer = $this->_gettime() - $timer;
$this->_timer = $timer;
return $document;
}
// return the time taken to parse the document (returns false is no
// processing has been done yet)
public function getparsetime()
{
return $this->_timer;
}
// return the generated document
public function getdocument()
{
$document = $this->_process($this->_template);
return $document;
}
// display the generated document
public function display()
{
$document = $this->_process($this->_template);
echo $document;
}
}
?>