<?php

/**
 * Reserved prefixes:
 *
 *  "L_" - lang var, {L_VAR} is eq to $lang['VAR']
 *  "$"  - php var,  {$VAR}  is eq to $VAR (in $this->execute() scope!)
 *  "#"  - constant, {#CON}  is eq to CON
 *
 */

if (!defined('BB_ROOT'))
{
	die(basename(__FILE__));
}

define('XS_SEPARATOR', '.');
define('XS_USE_ISSET', '1');

// cache filenames prefix
define('XS_TPL_PREFIX', 'tpl_');
define('XS_TPL_PREFIX2', 'tpl2_');

// internal xs mod definitions. do not edit.
define('XS_TAG_NONE', 0);
define('XS_TAG_PHP', 1);
define('XS_TAG_BEGIN', 2);
define('XS_TAG_END', 3);
define('XS_TAG_INCLUDE', 4);
define('XS_TAG_IF', 5);
define('XS_TAG_ELSE', 6);
define('XS_TAG_ELSEIF', 7);
define('XS_TAG_ENDIF', 8);
define('XS_TAG_BEGINELSE', 11);

class Template
{
	var $classname = "Template";

	// variable that holds all the data we'll be substituting into
	// the compiled templates.
	// ...
	// This will end up being a multi-dimensional array like this:
	// $this->_tpldata[block.][iteration#][child.][iteration#][child2.][iteration#][variablename] == value
	// if it's a root-level variable, it'll be like this:
	// $this->vars[varname] == value  or  $this->_tpldata['.'][0][varname] == value
	// array "vars" is added for easier access to data
	var $_tpldata = array('.' => array(0 => array()));
	var $vars;

	// Hash of filenames for each template handle.
	var $files = array();
	var $files_cache = array(); // array of cache files that exists
	var $files_cache2 = array(); // array of cache files (exists or not exists)

	// Root template directory.
	var $root = '';

	// Cache directory
	var $cachedir = CACHE_DIR;

	// Template root directory
	var $tpldir = '';

	// Default template directory.
	// If file for default template isn't found file from this template is used.
	var $tpldef = 'default';

	// this will hash handle names to the compiled code for that handle.
	var $compiled_code = array();

	// This will hold the uncompiled code for that handle.
	var $uncompiled_code = array();

	// Cache settings
	var $use_cache = 1;
	var $cache_writable = 1;

	// Auto-compile setting
	var $auto_compile = 1;

	// Current template name
	var $tpl = '';
	var $cur_tpl = '';

	// List of replacements. tpl files in this list will be replaced with other tpl files
	// according to configuration in xs.cfg
	var $replace = array();

	// counter for include
	var $include_count = 0;

	// extension tpl-cache files
	var $cached_tpl_ext = 'php';

	// eXtreme Styles variables
	var $xs_started = 0;
	var $xs_version = 8; // number version. internal. do not change.
	var $xs_versiontxt = '2.3.1'; // text version

	// These handles will be parsed if pparse() is executed.
	// Can be used to automatically include header/footer if there is any content.
	var $preparse = '';
	var $postparse = '';

	// subtemplates mod detection
	var $subtemplates = false;

	// style configuration
	var $style_config = array();

	var $lang = array();

	/**
	 * Constructor. Installs XS mod on first run or updates it and sets the root dir.
	 */
	function Template($root = '.')
	{
		global $bb_cfg, $lang;

		// setting pointer "vars"
		$this->vars = &$this->_tpldata['.'][0];
		// load configuration
		$this->tpldir = TEMPLATES_DIR;
		$this->root = $root;
		$this->tpl = basename($root);
		$this->lang =& $lang;
		$this->use_cache = $bb_cfg['xs_use_cache'];

		// Check template exists
		if (!file_exists($this->root) || !is_dir($this->root))
		{
			die("Theme ({$this->tpl}) directory not found");
		}
	}

	/**
	 * Destroys this template object. Should be called when you're done with it, in order
	 * to clear out the template data so you can load/parse a new template set.
	 */
	function destroy()
	{
		$this->_tpldata = array('.' => array(0 => array()));
		$this->vars = &$this->_tpldata['.'][0];
		$this->xs_started = 0;
	}

	/**
	 * Generates a full path+filename for the given filename, which can either
	 * be an absolute name, or a name relative to the rootdir for this Template
	 * object.
	 */
	function make_filename($filename, $xs_include = false)
	{
		// Check replacements list
		if(!$xs_include && isset($this->replace[$filename]))
		{
			$filename = $this->replace[$filename];
		}
		// Check if it's an absolute or relative path.
		if ((substr($filename, 0, 1) !== '/') && (substr($filename, 1, 1) !== ':'))
		{
			return $this->root . '/' . $filename;
		}
		else
		{
			return $filename;
		}
	}

	/**
	 * Converts template filename to cache filename.
	 * Returns empty string if non-cachable (for tpl files outside of root dir).
	 * $filename should be absolute filename
	 */
	function make_filename_cache ($filename)
	{
		$filename = clean_filename(str_replace(TEMPLATES_DIR, '', $filename));

		return $this->cachedir . XS_TPL_PREFIX . $filename .'.'. $this->cached_tpl_ext;
	}

	/**
	 * Sets the template filenames for handles. $filename_array
	 * should be a hash of handle => filename pairs.
	 */
	function set_filenames ($filenames)
	{
		foreach ($filenames as $handle => $filename)
		{
			$this->set_filename($handle, $filename);
		}
	}

	/**
	 * Assigns template filename for handle.
	 */
	function set_filename($handle, $filename, $xs_include = false, $quiet = false)
	{
		$can_cache = $this->use_cache;
		$this->files[$handle] = $this->make_filename($filename, $xs_include);
		$this->files_cache[$handle] = '';
		$this->files_cache2[$handle] = '';
		// checking if we have valid filename
		if(!$this->files[$handle])
		{
			if($xs_include || $quiet)
			{
				return false;
			}
			else
			{
				die("Template->make_filename(): Error - invalid template $filename");
			}
		}
		// creating cache filename
		if($can_cache)
		{
			$this->files_cache2[$handle] = $this->make_filename_cache($this->files[$handle]);
			if(@file_exists($this->files_cache2[$handle]))
			{
				$this->files_cache[$handle] = $this->files_cache2[$handle];
			}
		}
		// checking if tpl and/or php file exists
		if(empty($this->files_cache[$handle]) && !@file_exists($this->files[$handle]))
		{
			if($quiet)
			{
				return false;
			}
			die('Template->make_filename(): Error - template file not found: <br /><br />' . hide_bb_path($this->files[$handle]));
		}
		// checking if we should recompile cache
		if(!empty($this->files_cache[$handle]))
		{
			$cache_time = @filemtime($this->files_cache[$handle]);
			if(@filemtime($this->files[$handle]) > $cache_time)
			{
				// file was changed. don't use cache file (will be recompled if configuration allowes it)
				$this->files_cache[$handle] = '';
			}
		}
		return true;
	}

	/**
	 * includes file or executes code
	 */
	function execute($filename, $code, $handle)
	{
		$this->cur_tpl = $filename;

		global $lang, $source_lang, $bb_cfg, $user, $tr_cfg;

		$L =& $lang;
		$V =& $this->vars;
		$SL =& $source_lang;

		if ($filename)
		{
			include($filename);
		}
		else
		{
			eval($code);
		}
	}

	/**
	 * Load the file for the handle, compile the file,
	 * and run the compiled code. This will print out
	 * the results of executing the template.
	 */
	function pparse($handle)
	{
		// parsing header if there is one
		if($this->preparse || $this->postparse)
		{
			$preparse = $this->preparse;
			$postparse = $this->postparse;
			$this->preparse = '';
			$this->postparse = '';
			if($preparse)
			{
				$this->pparse($preparse);
			}
			if($postparse)
			{
				$str = $handle;
				$handle = $postparse;
				$this->pparse($str);
			}
		}
		// checking if handle exists
		if (empty($this->files[$handle]) && empty($this->files_cache[$handle]))
		{
			die("Template->loadfile(): No files found for handle $handle");
		}
		$this->xs_startup();
		$force_recompile = empty($this->uncompiled_code[$handle]) ? false : true;
		// checking if php file exists.
		if (!empty($this->files_cache[$handle]) && !$force_recompile)
		{
			// php file exists - running it instead of tpl
			$this->execute($this->files_cache[$handle], '', $handle);
			return true;
		}
		if (!$this->loadfile($handle))
		{
			die("Template->pparse(): Could not load template file for handle $handle");
		}
		// actually compile the template now.
		if (empty($this->compiled_code[$handle]))
		{
			// Actually compile the code now.
			if(!empty($this->files_cache2[$handle]) && empty($this->files_cache[$handle]) && !$force_recompile)
			{
				$this->compiled_code[$handle] = $this->compile2($this->uncompiled_code[$handle], $handle, $this->files_cache2[$handle]);
			}
			else
			{
				$this->compiled_code[$handle] = $this->compile2($this->uncompiled_code[$handle], '', '');
			}
		}
		// Run the compiled code.
		if (empty($this->files_cache[$handle]) || $force_recompile)
		{
			$this->execute('', $this->compiled_code[$handle], $handle);
		}
		else
		{
			$this->execute($this->files_cache[$handle], '', $handle);
		}
		return true;
	}

	/**
	 * Precompile file
	 */
	function precompile($template, $filename)
	{
		global $precompile_num;
		if(empty($precompile_num))
		{
			$precompile_num = 0;
		}
		$precompile_num ++;
		$handle = 'precompile_' . $precompile_num;
		// save old configuration
		$root = $this->root;
		$tpl_name = $this->tpl;
		$old_config = $this->use_cache;
		$old_autosave = $this->auto_compile;
		// set temporary configuration
		$this->root = $this->tpldir . $template;
		$this->tpl = $template;
		$this->use_cache = 1;
		$this->auto_compile = 1;
		// set filename
		$res = $this->set_filename($handle, $filename, true, true);
		if(!$res || !$this->files_cache2[$handle])
		{
			$this->root = $root;
			$this->tpl = $tpl_name;
			$this->use_cache = $old_config;
			$this->auto_compile = $old_autosave;
			return false;
		}
		$this->files_cache[$handle] = '';
		// load template
		$res = $this->loadfile($handle);
		if(!$res || empty($this->uncompiled_code[$handle]))
		{
			$this->root = $root;
			$this->tpl = $tpl_name;
			$this->use_cache = $old_config;
			$this->auto_compile = $old_autosave;
			return false;
		}
		// compile the code
		$this->compile2($this->uncompiled_code[$handle], $handle, $this->files_cache2[$handle]);
		// restore confirugation
		$this->root = $root;
		$this->tpl = $tpl_name;
		$this->use_cache = $old_config;
		$this->auto_compile = $old_autosave;
		return true;
	}

	/**
	 * Inserts the uncompiled code for $handle as the
	 * value of $varname in the root-level. This can be used
	 * to effectively include a template in the middle of another
	 * template.
	 * Note that all desired assignments to the variables in $handle should be done
	 * BEFORE calling this function.
	 */
	function assign_var_from_handle($varname, $handle)
	{
		ob_start();
		$res = $this->pparse($handle);
		$this->vars[$varname] = ob_get_contents();
		ob_end_clean();
		return $res;
	}

	/**
	 * Block-level variable assignment. Adds a new block iteration with the given
	 * variable assignments. Note that this should only be called once per block
	 * iteration.
	 */
	function assign_block_vars($blockname, $vararray)
	{
		if (strstr($blockname, '.'))
		{
			// Nested block.
			$blocks = explode('.', $blockname);
			$blockcount = sizeof($blocks) - 1;

			$str = &$this->_tpldata;
			for($i = 0; $i < $blockcount; $i++)
			{
				$str = &$str[$blocks[$i].'.'];
				$str = &$str[sizeof($str)-1];
			}
			// Now we add the block that we're actually assigning to.
			// We're adding a new iteration to this block with the given
			//	variable assignments.
			$str[$blocks[$blockcount].'.'][] = $vararray;
		}
		else
		{
			// Top-level block.
			// Add a new iteration to this block with the variable assignments
			// we were given.
			$this->_tpldata[$blockname.'.'][] = $vararray;
		}

		return true;
	}

	/**
	 * Root-level variable assignment. Adds to current assignments, overriding
	 * any existing variable assignment with the same name.
	 */
	function assign_vars ($vararray)
	{
		foreach ($vararray as $key => $val)
		{
			$this->vars[$key] = $val;
		}
	}

	/**
	 * Root-level variable assignment. Adds to current assignments, overriding
	 * any existing variable assignment with the same name.
	 */
	function assign_var ($varname, $varval = true)
	{
		$this->vars[$varname] = $varval;
	}

	/**
	 * TODO: Add type check [??]
	 * Root-level. Adds to current assignments, appends
	 * to any existing variable assignment with the same name.
	 */
	function append_vars ($vararray)
	{
		foreach ($vararray as $key => $val)
		{
			$this->vars[$key] = !isset($this->vars[$key]) ? $val : $this->vars[$key] . $val;
		}
	}

	/**
	 * If not already done, load the file for the given handle and populate
	 * the uncompiled_code[] hash with its code. Do not compile.
	 */
	function loadfile($handle)
	{
		// If cached file exists do nothing - it will be included via include()
		if(!empty($this->files_cache[$handle]))
		{
			return true;
		}

		// If the file for this handle is already loaded and compiled, do nothing.
		if (!empty($this->uncompiled_code[$handle]))
		{
			return true;
		}

		// If we don't have a file assigned to this handle, die.
		if (empty($this->files[$handle]))
		{
			die("Template->loadfile(): No file specified for handle $handle");
		}

		$filename = $this->files[$handle];

		if (($str = @file_get_contents($filename)) === false)
		{
			die("Template->loadfile(): File $filename for handle $handle is empty");
		}

		$this->uncompiled_code[$handle] = $str;

		return true;
	}

	/**
	 * Generates a reference to the given variable inside the given (possibly nested)
	 * block namespace. This is a string of the form:
	 * ' . $this->_tpldata['parent.'][$_parent_i]['$child1.'][$_child1_i]['$child2.'][$_child2_i]...['varname'] . '
	 * It's ready to be inserted into an "echo" line in one of the templates.
	 * NOTE: expects a trailing "." on the namespace.
	 */
	function generate_block_varref($namespace, $varname)
	{
		// Strip the trailing period.
		$namespace = substr($namespace, 0, strlen($namespace) - 1);

		// Get a reference to the data block for this namespace.
		$varref = $this->generate_block_data_ref($namespace, true);
		// Prepend the necessary code to stick this in an echo line.

		// Append the variable reference.
		$varref .= "['$varname']";

		$varref = "<?php echo isset($varref) ? $varref : ''; ?>";

		return $varref;
	}

	/**
	 * Generates a reference to the array of data values for the given
	 * (possibly nested) block namespace. This is a string of the form:
	 * $this->_tpldata['parent.'][$_parent_i]['$child1.'][$_child1_i]['$child2.'][$_child2_i]...['$childN.']
	 *
	 * If $include_last_iterator is true, then [$_childN_i] will be appended to the form shown above.
	 * NOTE: does not expect a trailing "." on the blockname.
	 */
	function generate_block_data_ref($blockname, $include_last_iterator)
	{
		// Get an array of the blocks involved.
		$blocks = explode('.', $blockname);
		$blockcount = sizeof($blocks) - 1;
		if($include_last_iterator)
		{
			return '$'. $blocks[$blockcount]. '_item';
		}
		else
		{
			return '$'. $blocks[$blockcount-1]. '_item[\''. $blocks[$blockcount]. '.\']';
		}
	}

	function compile_code($filename, $code)
	{
		//	$filename - file to load code from. used if $code is empty
		//	$code - tpl code

		// load code from file
		if (!$code && !empty($filename))
		{
			$code = file_get_contents($filename);
		}

		// Replace <!-- (END)PHP --> tags
		$search = array('<!-- PHP -->', '<!-- ENDPHP -->');
		$replace = array('<'.'?php ', ' ?'.'>');
		$code = str_replace($search, $replace, $code);

		// Break it up into lines and put " -->" back.
		$code_lines = explode(' -->', $code);
		$count = count($code_lines);
		for ($i = 0; $i < ($count - 1); $i++)
		{
			$code_lines[$i] .= ' -->';
		}

		$block_nesting_level = 0;
		$block_names = array();
		$block_names[0] = ".";
		$block_items = array();
		$count_if = 0;

		// prepare array for compiled code
		$compiled = array();

		// array of switches
		$sw = array();

		// replace all short php tags
		$new_code = array();
		$line_count = count($code_lines);
		for($i=0; $i<$line_count; $i++)
		{
			$line = $code_lines[$i];
			$pos = strpos($line, '<?');
			if($pos === false)
			{
				$new_code[] = $line;
				continue;
			}
			if(substr($line, $pos, 5) === '<?php')
			{
				// valid php tag. skip it
				$new_code[] = substr($line, 0, $pos + 5);
				$code_lines[$i] = substr($line, $pos + 5);
				$i --;
				continue;
			}
			// invalid php tag
			$new_code[] = substr($line, 0, $pos) . '<?php echo \'<?\'; ?>';
			$code_lines[$i] = substr($line, $pos + 2);
			$i --;
		}
		$code_lines = $new_code;

		// main loop
		$line_count = count($code_lines);
		for($i=0; $i<$line_count; $i++)
		{
			$line = $code_lines[$i];
			// reset keyword type
			$keyword_type = XS_TAG_NONE;
			// check if we have valid keyword in current line
			$pos1 = strpos($line, '<!-- ');
			if($pos1 === false)
			{
				// no keywords in this line
				$compiled[] = $this->_compile_text($line);
				continue;
			}
			// find end of html comment
			$pos2 = strpos($line, ' -->', $pos1);
			if($pos2 !== false)
			{
				// find end of keyword in comment
				$pos3 = strpos($line, ' ', $pos1 + 5);
				if($pos3 !== false && $pos3 <= $pos2)
				{
					$keyword = substr($line, $pos1 + 5, $pos3 - $pos1 - 5);
					// check keyword against list of supported keywords. case-sensitive
					if($keyword === 'BEGIN')
					{
						$keyword_type = XS_TAG_BEGIN;
					}
					elseif($keyword === 'END')
					{
						$keyword_type = XS_TAG_END;
					}
					elseif($keyword === 'INCLUDE')
					{
						$keyword_type = XS_TAG_INCLUDE;
					}
					elseif($keyword === 'IF')
					{
						$keyword_type = XS_TAG_IF;
					}
					elseif($keyword === 'ELSE')
					{
						$keyword_type = XS_TAG_ELSE;
					}
					elseif($keyword === 'ELSEIF')
					{
						$keyword_type = XS_TAG_ELSEIF;
					}
					elseif($keyword === 'ENDIF')
					{
						$keyword_type = XS_TAG_ENDIF;
					}
					elseif($keyword === 'BEGINELSE')
					{
						$keyword_type = XS_TAG_BEGINELSE;
					}
				}
			}
			if(!$keyword_type)
			{
				// not valid keyword. process the rest of line
				$compiled[] = $this->_compile_text(substr($line, 0, $pos1 + 4));
				$code_lines[$i] = substr($line, $pos1 + 4);
				$i --;
				continue;
			}
			// remove code before keyword
			if($pos1 > 0)
			{
				$compiled[] = $this->_compile_text(substr($line, 0, $pos1));
			}
			// remove keyword
			$keyword_str = substr($line, $pos1, $pos2 - $pos1 + 4);
			$params_str = $pos2 == $pos3 ? '' : substr($line, $pos3 + 1, $pos2 - $pos3 - 1);
			$code_lines[$i] = substr($line, $pos2 + 4);
			$i--;
			// Check keywords

			/*
			* <!-- BEGIN -->
			*/
			if($keyword_type == XS_TAG_BEGIN)
			{
				$params = explode(' ', $params_str);
				$num_params = count($params);
				// get variable name
				if($num_params == 1)
				{
					$var = $params[0];
				}
				elseif($num_params == 2)
				{
					if($params[0] === '')
					{
						$var = $params[1];
					}
					elseif($params[1] === '')
					{
						$var = $params[0];
					}
					else
					{
						// invalid tag
						$compiled[] = $keyword_str;
						continue;
					}
				}
				else
				{
					// invalid tag
					$compiled[] = $keyword_str;
					continue;
				}
				// adding code
				$block_nesting_level++;
				$block_names[$block_nesting_level] = $var;
				if(isset($block_items[$var]))
				{
					$block_items[$var] ++;
				}
				else
				{
					$block_items[$var] = 1;
				}
				if ($block_nesting_level < 2)
				{
					// Block is not nested.
					$line = '<'."?php\n\n";
					$line .= '$'. $var. '_count = ( isset($this->_tpldata[\''. $var. '.\']) ) ?  sizeof($this->_tpldata[\''. $var. '.\']) : 0;';
					$line .= "\n" . 'for ($'. $var. '_i = 0; $'. $var. '_i < $'. $var. '_count; $'. $var. '_i++)';
					$line .= "\n". '{'. "\n";
					$line .= ' $'. $var. '_item = &$this->_tpldata[\''. $var. '.\'][$'. $var. '_i];'."\n";
					$line .= " \${$var}_item['S_ROW_COUNT'] = \${$var}_i;\n";
					$line .= " \${$var}_item['S_NUM_ROWS'] = \${$var}_count;\n";
					$line .= "\n?".">";
				}
				else
				{
					// This block is nested.
					// Generate a namespace string for this block.
					$namespace = join('.', $block_names);
					// strip leading period from root level..
					$namespace = substr($namespace, 2);
					// Get a reference to the data array for this block that depends on the
					// current indices of all parent blocks.
					$varref = $this->generate_block_data_ref($namespace, false);
					// Create the for loop code to iterate over this block.
					$line = '<'."?php\n\n";
					$line .= '$'. $var. '_count = ( isset('. $varref. ') ) ? sizeof('. $varref. ') : 0;';
					$line .= "\n". 'for ($'. $var. '_i = 0; $'. $var. '_i < $'. $var. '_count; $'. $var. '_i++)';
					$line .= "\n". '{'. "\n";
					$line .= ' $'. $var. '_item = &'. $varref. '[$'. $var. '_i];'."\n";
					$line .= " \${$var}_item['S_ROW_COUNT'] = \${$var}_i;\n";
					$line .= " \${$var}_item['S_NUM_ROWS'] = \${$var}_count;\n";
					$line .= "\n?".">";
				}
				$compiled[] = $line;
				continue;
			}
			/*
			* <!-- END -->
			*/
			if($keyword_type == XS_TAG_END)
			{
				$params = explode(' ', $params_str);
				$num_params = count($params);
				if($num_params == 1)
				{
					$var = $params[0];
				}
				elseif($num_params == 2 && $params[0] === '')
				{
					$var = $params[1];
				}
				elseif($num_params == 2 && $params[1] === '')
				{
					$var = $params[0];
				}
				else
				{
					$compiled[] = $keyword_str;
					continue;
				}
				// We have the end of a block.
				$line = '<'."?php\n\n";
				$line .= '} // END ' . $var . "\n\n";
				$line .= 'if(isset($' . $var . '_item)) { unset($' . $var . '_item); } ';
				$line .= "\n\n?".">";
				if(isset($block_items[$var]))
				{
					$block_items[$var] --;
				}
				else
				{
					$block_items[$var] = -1;
				}
				unset($block_names[$block_nesting_level]);
				$block_nesting_level--;
				$compiled[] = $line;
				continue;
			}
			/*
			* <!-- BEGINELSE -->
			*/
			if($keyword_type == XS_TAG_BEGINELSE)
			{
				if($block_nesting_level)
				{
					$var = $block_names[$block_nesting_level];
					$compiled[] = '<' . '?php } if(!$' . $var . '_count) { ?' . '>';
				}
				else
				{
					$compiled[] = $keyword_str;
					continue;
				}
			}
			/*
			* <!-- INCLUDE -->
			*/
			if($keyword_type == XS_TAG_INCLUDE)
			{
				$params = explode(' ', $params_str);
				$num_params = count($params);
				if($num_params != 1)
				{
					$compiled[] = $keyword_str;
					continue;
				}
				$line = '<'.'?php ';
				$filehash = md5($params_str . $this->include_count . TIMENOW);
				$line .= ' $this->set_filename(\'xs_include_' . $filehash . '\', \'' . $params_str .'\', true); ';
				$line .= ' $this->pparse(\'xs_include_' . $filehash . '\'); ';
				$line .= ' ?'.'>';
				$this->include_count ++;
				$compiled[] = $line;
				continue;
			}
			/*
			* <!-- IF -->
			*/
			if($keyword_type == XS_TAG_IF || $keyword_type == XS_TAG_ELSEIF)
			{
				if(!$count_if)
				{
					$keyword_type = XS_TAG_IF;
				}
				$str = $this->compile_tag_if($params_str, $keyword_type == XS_TAG_IF ? false : true);
				if($str)
				{
					$compiled[] = '<?php ' . $str . ' ?>';
					if($keyword_type == XS_TAG_IF)
					{
						$count_if ++;
					}
				}
				else
				{
					$compiled[] = $keyword_str;
				}
				continue;
			}
			/*
			* <!-- ELSE -->
			*/
			if($keyword_type == XS_TAG_ELSE && $count_if > 0)
			{
				$compiled[] = '<?php } else { ?>';
				continue;
			}
			/*
			* <!-- ENDIF -->
			*/
			if($keyword_type == XS_TAG_ENDIF && $count_if > 0)
			{
				$compiled[] = '<?php } ?>';
				$count_if --;
				continue;
			}
		}

		// bring it back into a single string.
		$code_header = '';
		$code_footer = '';

		return $code_header . join('', $compiled) . $code_footer;
	}

	/*
	* Compile code between tags
	*/
	function _compile_text($code)
	{
		if(strlen($code) < 3)
		{
			return $code;
		}
		// change template varrefs into PHP varrefs
		// This one will handle varrefs WITH namespaces
		$varrefs = array();
		preg_match_all('#\{(([a-z0-9\-_]+?\.)+)([a-z0-9\-_]+?)\}#is', $code, $varrefs);
		$varcount = sizeof($varrefs[1]);
		$search = array();
		$replace = array();
		for ($i = 0; $i < $varcount; $i++)
		{
			$namespace = $varrefs[1][$i];
			$varname = $varrefs[3][$i];
			$new = $this->generate_block_varref($namespace, $varname);
			$search[] = $varrefs[0][$i];
			$replace[] = $new;
		}
		if(count($search) > 0)
		{
			$code = str_replace($search, $replace, $code);
		}
		// This will handle the remaining root-level varrefs
		$code = preg_replace('#\{(L_([a-z0-9\-_]+?))\}#i', '<?php echo isset($L[\'$2\']) ? $L[\'$2\'] : (isset($SL[\'$2\']) ? $SL[\'$2\'] : $V[\'$1\']); ?>', $code);
		$code = preg_replace('#\{(\$[a-z_][a-z0-9_$\->\'\"\.\[\]]*?)\}#i', '<?php echo isset($1) ? $1 : \'\'; ?>', $code);
		$code = preg_replace('#\{(\#([a-z_][a-z0-9_]*?))\}#i', '<?php echo defined(\'$2\') ? $2 : \'\'; ?>', $code);
		$code = preg_replace('#\{([a-z0-9\-_]+?)\}#i', '<?php echo isset($V[\'$1\']) ? $V[\'$1\'] : \'\'; ?>', $code);
		return $code;
	}

	//
	// Compile IF tags - much of this is from Smarty with
	// some adaptions for our block level methods
	//
	function compile_tag_if($tag_args, $elseif)
	{
		/* Tokenize args for 'if' tag. */
		preg_match_all('/(?:
										 "[^"\\\\]*(?:\\\\.[^"\\\\]*)*"         |
										 \'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'     |
										 [(),]                                  |
										 [^\s(),]+)/x', $tag_args, $match);

		$tokens = $match[0];
		$tokens_cnt = count($tokens);
		$is_arg_stack = array();

		for ($i = 0; $i < $tokens_cnt; $i++)
		{
			$token = &$tokens[$i];

			switch ($token)
			{
				case 'eq':
					$token = '==';
					break;

				case 'ne':
				case 'neq':
					$token = '!=';
					break;

				case 'lt':
					$token = '<';
					break;

				case 'le':
				case 'lte':
					$token = '<=';
					break;

				case 'gt':
					$token = '>';
					break;

				case 'ge':
				case 'gte':
					$token = '>=';
					break;

				case 'and':
					$token = '&&';
					break;

				case 'or':
					$token = '||';
					break;

				case 'not':
					$token = '!';
					break;

				case 'mod':
					$token = '%';
					break;

				case '(':
					array_push($is_arg_stack, $i);
					break;

				case 'is':
					$is_arg_start = ($tokens[$i-1] == ')') ? array_pop($is_arg_stack) : $i-1;
					$is_arg	= join('	', array_slice($tokens,	$is_arg_start, $i -	$is_arg_start));

					$new_tokens	= $this->_parse_is_expr($is_arg, array_slice($tokens, $i+1));

					array_splice($tokens, $is_arg_start, count($tokens), $new_tokens);

					$i = $is_arg_start;
					break;

				default:
					$pattern = '@^
					  (                                      #  1
					    ([a-z0-9\-_]+?\.)+?                  #  2  block tpl vars (VAR1.VAR2.) but without last
					  )?
					  (                                      #  3
					    ([a-z_][a-z0-9\-_]*)?                #  4  single tpl var or last block var (last in block)
					    (\$[a-z_][a-z0-9_$\->\'\"\.\[\]]*)?  #  5  php var
					    (\#([a-z_][a-z0-9_]*))?              #  7  php const
					  )
					$@ix';
					if (preg_match($pattern, $token, $m))
					{
						if (!empty($m[1]))
						{
							$token = $this->generate_block_data_ref(substr($m[1], 0, -1), true) . "['{$m[4]}']";
						}
						else if (!empty($m[4]))
						{
							$token = ($tokens_cnt == 1) ? "!empty(\$V['{$m[4]}'])" : "\$V['{$m[4]}']";
						}
						else if (!empty($m[5]))
						{
							$token = ($tokens_cnt == 1) ? "!empty({$m[5]})" : "{$m[5]}";
						}
						else if (!empty($m[7]))
						{
							$token = ($tokens_cnt == 1) ? "defined('{$m[7]}') && {$m[7]}" : "{$m[7]}";
						}
					}
					break;
			}
		}

		if ($elseif)
		{
			$code = '} elseif ('. join(' ', $tokens) .') {';
		}
		else
		{
			$code = 'if ('. join(' ', $tokens) .') {';
		}

		return $code;
	}

	// This is from Smarty
	function _parse_is_expr($is_arg, $tokens)
	{
		$expr_end =	0;
		$negate_expr = false;

		if (($first_token = array_shift($tokens)) == 'not')
		{
			$negate_expr = true;
			$expr_type = array_shift($tokens);
		}
		else
		{
			$expr_type = $first_token;
		}

		switch ($expr_type)
		{
			case 'even':
				if (@$tokens[$expr_end] == 'by')
				{
					$expr_end++;
					$expr_arg =	$tokens[$expr_end++];
					$expr =	"!(($is_arg	/ $expr_arg) % $expr_arg)";
				}
				else
				{
					$expr =	"!($is_arg % 2)";
				}
				break;

			case 'odd':
				if (@$tokens[$expr_end] == 'by')
				{
					$expr_end++;
					$expr_arg =	$tokens[$expr_end++];
					$expr =	"(($is_arg / $expr_arg)	% $expr_arg)";
				}
				else
				{
					$expr =	"($is_arg %	2)";
				}
				break;

			case 'div':
				if (@$tokens[$expr_end] == 'by')
				{
					$expr_end++;
					$expr_arg =	$tokens[$expr_end++];
					$expr =	"!($is_arg % $expr_arg)";
				}
				break;

			default:
				break;
		}

		if ($negate_expr)
		{
			$expr =	"!($expr)";
		}

		array_splice($tokens, 0, $expr_end,	$expr);

		return $tokens;
	}

	/**
	 * Compiles code and writes to cache if needed
	 */
	function compile2($code, $handle, $cache_file)
	{
		$code = $this->compile_code('', $code, XS_USE_ISSET);
		if ($cache_file && !empty($this->use_cache) && !empty($this->auto_compile))
		{
			$res = $this->write_cache($cache_file, $code);
			if ($handle && $res)
			{
				$this->files_cache[$handle] = $cache_file;
			}
		}
		$code = '?'.'>'.$code.'<'."?php\n";
		return $code;
	}

	/**
	 * Compiles the given string of code, and returns
	 * the result in a string.
	 * If "do_not_echo" is true, the returned code will not be directly
	 * executable, but can be used as part of a variable assignment
	 * for use in assign_code_from_handle().
	 * This function isn't used and kept only for compatibility with original template.php
	 */
	function compile($code, $do_not_echo = false, $retvar = '')
	{
		$code = ' ?'.'>' . $this->compile_code('', $code, true) . '<'."?php \n";
		if ($do_not_echo)
		{
			$code = "ob_start();\n". $code. "\n\${$retvar} = ob_get_contents();\nob_end_clean();\n";
		}
		return $code;
	}

	/**
	 * Write cache to disk
	 */
	function write_cache($filename, $code)
	{
		return file_write($code, $filename, false, true, true);
	}

	function xs_startup()
	{
		global $bb_cfg;

		if (empty($this->xs_started))
		{	// adding predefined variables
			$this->xs_started = 1;
			// adding language variable (eg: "english" or "german")
			// can be used to make truly multi-lingual templates
			$this->vars['LANG'] = isset($this->vars['LANG']) ? $this->vars['LANG'] : $bb_cfg['default_lang'];
			// adding current template
			$tpl = $this->root . '/';
			if (substr($tpl, 0, 2) === './')
			{
				$tpl = substr($tpl, 2, strlen($tpl));
			}
			$this->vars['TEMPLATE'] = isset($this->vars['TEMPLATE']) ? $this->vars['TEMPLATE'] : $tpl;
			$this->vars['TEMPLATE_NAME'] = isset($this->vars['TEMPLATE_NAME']) ? $this->vars['TEMPLATE_NAME'] : $this->tpl;
		}
	}

	function lang_error($var)
	{
		trigger_error(basename($this->cur_tpl) ." : undefined language variable {L_{$var}}", E_USER_WARNING);
		return "Undefined: {L_{$var}}";
	}
}
