<?php
/*
* Ubertemplate Parser
* Version 0.2
* 
* A template parser
*
* Notes:
* As of Pure 0.2f, the bbcode parser depends on this class to work
* This version has been made slightly better documented and structured
* to facilitate future improvments or derivations of this system.
* 
* Extended by:
* bbcode.class.php (Uberbbcode Parser)
*/
class ubertemplate {
	var $template;
	var $values=array();
	var $blocks=array();
	var $generated='';
	
	/*********************************************************
	* addValues($values)
	* Adds $values to the template
	**/
	function addValues($values) {
		foreach($values as $name=>$value) {
			if(array_key_exists($name,$this->values))
				trigger_error('This template has duplicate value names',E_USER_ERROR);
			else
				$this->values[$name]=$value;
		}
	}
	
	/*********************************************************
	* addCode($string)
	* Processes $string in sequence, and adds component blocks to template
	**/
	function addCode($string) {
		//The start position and type of the first interesting block
		$start=strlen($string);
		$end=strlen($string);
		$name=false;
		$type=0; //0 - literal, 1 - boolean, 2 - inverse boolean, 3 - dynamic, 4 - variable, 5 - executable
		
		/*
		* Define our delimiters
		*/
		$delims=array(
			//Boolean
			1=>array(
				'ss'=>'<', //Start Start
				'se'=>'?>', //Start End
				'es'=>'</', //End Start
				'ee'=>'?>', //End End
			),
			//Inverse Boolean
			2=>array(
				'ss'=>'<!',
				'se'=>'?>',
				'es'=>'</!',
				'ee'=>'?>',
			),
			//Dynamic
			3=>array(
				'ss'=>'<%',
				'se'=>'%>',
				'es'=>'</%',
				'ee'=>'%>',
			),
			//Variable
			4=>array(
				's'=>'<*',
				'e'=>'*>',
			),
			//Executable
			5=>array(
				'ss'=>'<~',
				'se'=>'>',
				'es'=>'</~',
				'ee'=>'>',
			),
		);
		
		/*
		* Calculate critical lengths
		*/
		$delims[1]['frontpad']=strlen($delims[1]['ss'])+strlen($delims[1]['se']);
		$delims[1]['totalpad']=$delims[1]['frontpad']+strlen($delims[1]['es'])+strlen($delims[1]['ee']);
		
		$delims[2]['frontpad']=strlen($delims[2]['ss'])+strlen($delims[2]['se']);
		$delims[2]['totalpad']=$delims[2]['frontpad']+strlen($delims[2]['es'])+strlen($delims[2]['ee']);
		
		$delims[3]['frontpad']=strlen($delims[3]['ss'])+strlen($delims[3]['se']);
		$delims[3]['totalpad']=$delims[3]['frontpad']+strlen($delims[3]['es'])+strlen($delims[3]['ee']);
		
		$delims[4]['frontpad']=strlen($delims[4]['s']);
		$delims[4]['totalpad']=$delims[4]['frontpad']+strlen($delims[4]['e']);
		
		$delims[5]['frontpad']=strlen($delims[5]['ss'])+strlen($delims[5]['se']);
		$delims[5]['totalpad']=$delims[5]['frontpad']+strlen($delims[5]['es'])+strlen($delims[5]['ee']);
		
		//Locate the next interesting block
		$executable=$this->nextBlock($string,$delims[5]['ss'],$delims[5]['se'],$delims[5]['es'],$delims[5]['ee']);
		if($executable!==false&&$executable['startpos']<$start) {
			$start=$executable['startpos'];
			$end=$executable['endpos'];
			$name=$executable['tagname'];
			$type=5;
		}
		$variable=$this->nextTag($string,$delims[4]['s'],$delims[4]['e']);
		if($variable!==false&&$variable['startpos']<$start) {
			$start=$variable['startpos'];
			$end=$variable['endpos'];
			$name=$variable['tagname'];
			$type=4;
		}
		$dynamic=$this->nextBlock($string,$delims[3]['ss'],$delims[3]['se'],$delims[3]['es'],$delims[3]['ee']);
		if($dynamic!==false&&$dynamic['startpos']<$start) {
			$start=$dynamic['startpos'];
			$end=$dynamic['endpos'];
			$name=$dynamic['tagname'];
			$type=3;
		}
		$nboolean=$this->nextBlock($string,$delims[2]['ss'],$delims[2]['se'],$delims[2]['es'],$delims[2]['ee']);
		if($nboolean!==false&&$nboolean['startpos']<$start) {
			$start=$nboolean['startpos'];
			$end=$nboolean['endpos'];
			$name=$nboolean['tagname']; 
			$type=2;
		}
		$boolean=$this->nextBlock($string,$delims[1]['ss'],$delims[1]['se'],$delims[1]['es'],$delims[1]['ee']);
		if($boolean!==false&&$boolean['startpos']<$start) {
			$start=$boolean['startpos'];
			$end=$boolean['endpos'];
			$name=$boolean['tagname'];
			$type=1;
		}
		
		//Now we check if the entire string is just a literal
		if($type==0) {
			$this->addLiteral($string);
			return;
		}
		//Then we check if the first block of the string is a literal
		elseif($start>0) {
			//Add a literal block, since we know where the first interesting tag starts
			$this->addLiteral(substr($string,0,$start));
			$end=$start;
		}
		//If not, add the respective block, without the original tags
		else {
			switch($type) {
				case 1:
					$this->addBoolean($name,substr($string,$delims[1]['frontpad']+strlen($name),$end-$delims[1]['totalpad']-strlen($name)*2));
				break;
				case 2:
					$this->addNBoolean($name,substr($string,$delims[2]['frontpad']+strlen($name),$end-$delims[2]['totalpad']-strlen($name)*2));
				break;
				case 3:
					$this->addDynamic($name,substr($string,$delims[3]['frontpad']+strlen($name),$end-$delims[3]['totalpad']-strlen($name)*2));
				break;
				case 4:
					$this->addVariable($name);
				break;
				case 5:
					$this->addExecutable($name,substr($string,$delims[5]['frontpad']+strlen($name),$end-$delims[5]['totalpad']-strlen($name)*2));
				break;
				default:
					trigger_error('Unknown block type ('.$type.')',E_USER_ERROR);
			}
		}
		
		//Is there still some more to add?
		if($end<strlen($string)-1) {
			//Add the rest of the string
			$this->addCode(substr($string,$end));
		}
	}
	
	function addLiteral($string) {
		$this->blocks[]=new literal($string);
	}
	
	function addBoolean($name, $string) {
		$this->blocks[]=new positiveboolean($name, $string);
	}
	
	function addNBoolean($name, $string) {
		$this->blocks[]=new negativeboolean($name, $string);
	}
	
	function addDynamic($name, $string) {
		$this->blocks[]=new dynamic($name, $string);
	}
	
	function addVariable($name) {
		$this->blocks[]=new variable($name);
	}
	
	function addExecutable($name, $string) {
		$this->blocks[]=new executable($name, $string);
	}
	
	/*********************************************************
	* nextTag($string, $start, $end)
	* Locates and returns the first valid tag block
	**/
	function nextTag($string, $startstring, $endstring, $knownname=false,$offset=0,$ignore_alphanumeric=false) {
		$end_still_possible=true;
		//Find a start position
		$startpos=strpos($string,$startstring);
		//If there are no more start positions
		if($startpos===false)
			return false;
		//Find the first end position after the start pos
		$endpos=strpos($string,$endstring,$startpos+strlen($startstring));
		//If there are no more end posistions
		if($endpos===false)
			return false;
		//Get the tag name
		$tagname=substr($string,$startpos+strlen($startstring),$endpos-$startpos-strlen($startstring));
		//If this string isn't alphanumeric, search on
		if(($knownname!==false&&$tagname!==$knownname)||(!$ignore_alphanumeric&&preg_replace('/[^a-zA-Z0-9_]/','',$tagname)!=$tagname)) {
			return $this->nextTag(substr($string,$startpos+strlen($startstring)),$startstring,$endstring,$knownname,$offset+$startpos+strlen($startstring));
		}
		else {
			return array('tagname'=>$tagname,'namelength'=>strlen($tagname),'startpos'=>$offset+$startpos,'endpos'=>$offset+$endpos+strlen($endstring));
		}
	}
	
	/*********************************************************
	* nextBlock($string, $startstart, $startend, $endstart, $endend, $single_end_tag=false)
	* Locates and returns the first valid tag block
	**/
	function nextBlock($string, $startstart, $startend, $endstart, $endend, $single_end_tag=false) {
		$end_still_possible=true;
		$end_find_start=0;
		//Find a start tag
		$startpos=$this->nextTag($string,$startstart,$startend);
		if($startpos===false)
			return false;
		
		//Find the first end position after the start pos
		$remaining_string=substr($string,$startpos['endpos']);
		if($single_end_tag) {
			$endtag=$endstart.$endend;
			$endpos['endpos']=strpos($remaining_string,$endtag)+strlen($endtag);
			//Can't find an end tag?
			if($endpos['endpos']===false)
				return false;
		}
		else
			$endpos=$this->nextTag($remaining_string,$endstart,$endend,$startpos['tagname']);
		
		//If we can't find an ending tag
		if($endpos===false) {
			return false;
		}
		
		//Add the offset to the results
		$endpos['endpos']+=$startpos['endpos'];
		return array('tagname'=>$startpos['tagname'],'startpos'=>$startpos['startpos'],'endpos'=>$endpos['endpos']);
	}
	
	/*********************************************************
	* process()
	* Evaluates the template
	**/
	function process() {
		$this->generated='';
		foreach($this->blocks as $blockname => $obj) {
			$result=$obj->get($this->values);
			$this->generated=$this->generated.$result;
		}
	}
	
	/*********************************************************
	* processBlockCode()
	* Evaluates the template
	**/
	function processBlockCode() {
		$this->generated='';
		foreach($this->blocks as $blockname => $obj) {
			if(is_a($obj,'dynamic')||is_a($obj,'positiveboolean')||is_a($obj,'negativeboolean')) {
				$result=$obj->get($this->values,true);
			}
			else {
				$result=$obj->get($this->values);
			}
			$this->generated=$this->generated.$result;
		}
	}
	
	/*********************************************************
	* setBlockCode()
	* Set the blockCode
	**/
	function setBlockCode($blockCode) {
		$this->blocks=unserialize($blockCode);
	}
	
	/*********************************************************
	* getBlockCode()
	* Returns generated blockCode
	**/
	function getBlockCode() {
		//Get existing blocks
		$blocks=$this->blocks;
		//Parse dynamics further
		foreach($blocks as $index=>$obj) {
			if(is_a($obj,'dynamic')) {
				$temp=new ubertemplate();
				$temp->addCode($obj->dynamic);
				$this->blocks[$index]->dynamic=$temp->getBlockCode();
			}
		}
		//Parse booleans further
		foreach($blocks as $index=>$obj) {
			if(is_a($obj,'positiveboolean')||is_a($obj,'negativeboolean')) {
				$temp=new ubertemplate();
				$temp->addCode($obj->internal);
				$this->blocks[$index]->internal=$temp->getBlockCode();
			}
		}
		return serialize($this->blocks);
	}
	
	/*********************************************************
	* get()
	* Returns generated html
	**/
	function get() {
		return $this->generated;
	}
	
	/*********************************************************
	* write()
	* Prints generated html
	**/
	function write() {
		echo($this->generated);
	}
}

class block {
	var $generated='';
	
	function process(&$values){}
	
	/*********************************************************
	* get()
	* Get this block's value
	**/
	function get(&$values) {
		$this->process($values);
		return $this->generated;
	}
}

class literal extends block {
	
	/*********************************************************
	* literal($value)
	* Saves this value
	**/
	function literal($value) {
		$this->generated=$value;
	}
}

class variable extends block {
	
	/*********************************************************
	* variable($value)
	* Saves this value
	**/
	function variable($name) {
		$this->name=$name;
	}
	
	/*********************************************************
	* process($value)
	* Saves this value
	**/
	function process(&$value) {
		if(array_key_exists($this->name,$value))
			$this->generated=$value[$this->name];
		else
			$this->generated='';
	}
}

class dynamic extends block {
	
	/*********************************************************
	* dynamic($name, $dynamic)
	* Saves this dynamic's data
	**/
	function dynamic($name, $dynamic) {
		$this->name=$name;
		$this->dynamic=$dynamic;
	}
	
	/*********************************************************
	* get()
	* Get this block's value
	**/
	function get(&$values,$blockCode=false) {
		$this->process($values,$blockCode);
		return $this->generated;
	}
	
	/*********************************************************
	* process()
	* Processes this dynamic with the given values
	**/
	function process(&$values,$blockCode=false) {
		//Start with an empty dynamic
		$this->generated='';
		
		//Is there data to process?
		if(array_key_exists($this->name,$values)&&is_array($values[$this->name])&&count($values[$this->name])>0) {
			//Process each in a new template
			foreach($values[$this->name] as $itemid => $data) {
				$result=new ubertemplate;
				$result->addValues($data);
				if($blockCode) {
					$result->setBlockCode($this->dynamic);
					$result->processBlockCode();
				}
				else {
					$result->addCode($this->dynamic);
					$result->process();
				}
				$this->generated=$this->generated.$result->get();
			}
		}
	}
	
}

class positiveboolean extends block {
	
	/*********************************************************
	* positiveboolean($name, $internal)
	* Saves this boolean's name and internal code
	**/
	function positiveboolean($name,$internal) {
		$this->name=$name;
		$this->internal=$internal;
	}
	
	/*********************************************************
	* get()
	* Get this block's value
	**/
	function get(&$values,$blockCode=false) {
		$this->process($values,$blockCode);
		return $this->generated;
	}
	
	/*********************************************************
	* process()
	* Processes this boolean with the given values
	**/
	function process(&$values,$blockCode=false) {
		//Start with an empty result
		$this->generated='';
		//If this boolean evals to true
		if(array_key_exists($this->name,$values)&&$values[$this->name]===true) {
			//Process internals in a new template
			$result=new ubertemplate;
			if($blockCode) {
				$result->setBlockCode($this->internal);
				$result->addValues($values);
				$result->processBlockCode();
			}
			else {
				$result->addCode($this->internal);
				$result->addValues($values);
				$result->process();
			}
			$this->generated=$this->generated.$result->get();
		}
	}
}

class negativeboolean extends block {
	
	/*********************************************************
	* negativeboolean($name, internal)
	* Saves this boolean's name and internal code
	**/
	function negativeboolean($name,$internal) {
		$this->name=$name;
		$this->internal=$internal;
	}
	
	/*********************************************************
	* get()
	* Get this block's value
	**/
	function get(&$values,$blockCode=false) {
		$this->process($values,$blockCode);
		return $this->generated;
	}
	
	/*********************************************************
	* process()
	* Processes this negative boolean with the given values
	**/
	function process(&$values,$blockCode=false) {
		//Start with an empty result
		$this->generated='';
		//If this boolean evals to false
		if(array_key_exists($this->name,$values)&&$values[$this->name]===false) {
			//Process internals in a new template
			$result=new ubertemplate;
			if($blockCode) {
				$result->setBlockCode($this->internal);
				$result->addValues($values);
				$result->processBlockCode();
			}
			else {
				$result->addCode($this->internal);
				$result->addValues($values);
				$result->process();
			}
			$this->generated=$this->generated.$result->get();
		}
	}
}

class executable extends block {
	
	/*********************************************************
	* executable($name, internal)
	* Saves this executable's name and internal code
	**/
	function executable($name,$internal) {
		$this->name=$name;
		$this->internal=$internal;
	}
	
	/*********************************************************
	* process()
	* Processes this executable with the given values
	**/
	function process(&$values) {
		/*Eval code inside*/
		ob_start();
		eval($this->internal);
		$this->generated=ob_get_clean();
	}
}
?>