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; } } ?>