<?PHP
/*
Jotti's template engine version 4

JTPL is distributed under specific license,
see LICENSE for details.

If your copy of JTPL did not include a LICENSE
file, please visit:
http://jotti.org/openlibs/jtpl/LICENSE

Latest version is located at:
http://jotti.org/openlibs/jtpl

Some rudimentary documentation is available at:
http://jotti.org/openlibs/jtpl/USAGE

Copyright (C) 2002-2009 Jordi Bosveld <jotti@jotti.org>

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($string01)).mb_substr($string1);
        
        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 "<b>";
            
$boldappend "</b>";
            
$linebreak "<br />\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($processdata0$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($processdatastrlen($token) + strlen($tmp) + 2);
                } elseif (
preg_match("/^{!--.*?--}/s"$processdata$match))
                {
                    
// template comment, ignore the whole thing
                    
$processdata substr($processdatastrlen($match[0]));
                } else
                {
                    
// probably something else... treat as text
                    
$text .= substr($processdata01);
                    
$processdata substr($processdata1);
                }
            }
            
            
// 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 ".$olddepthE_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"|") > || strpos($blockname"&") > 0)
                {
                    
// grab IF block names and & | AND/OR separators
                    
$matches preg_split("/(&|\|)/"$blockname, -1PREG_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(";"$token2);
                
                
$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($type07) == "INCLUDE")
            {
                
$tokentype['type'] = "INCLUDE";
                
$tokentype['name'] = $data;
                
                
// see if we have any escapers for this dynamic INCLUDE
                
if (strlen($type) > && 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."}".$didyoumeanE_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($contentENT_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("&""&#38;" $content);

        if(!isset(
$trans))
        {
            
$trans get_html_translation_table(HTML_ENTITIESENT_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("{""&#0123;"$content);
        
$content str_replace("}""&#0125;"$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 == && $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 == && $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 == && $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;
    }
}

?>