<?php

/*
	Jerous' UBB Machine (JubbeM)
	============================
	This text contains some info about how JubbeM works and how to use it.
	
	JubbeM is a parser for UBB-formatted text, meaning that it will convert all - valid - 
	 tags between brackets ([]) to some W3C-consistent HTML equivalent. If you have e.g.
	 '[b]bold[/b]', it will be converted to '<b>bold</b>', outputting the word bold written
	 in bold.
	JubbeM itself doesn't do any formatting; it relies thus heavily on external plugins.
	 JubbeM searches for all valid tags in a text (like [b]%some text here%[/b]), checks
	 if there is a plugin for it (in the examples case the UBB_B-plugin) and processes it.
	The parsing of a text is done by using a regular expression which searches for the 
	 deepest nested tag in a text. If you have e.g. [b]bold[i]italic[/i][/b] the first
	 processed item will be '[i]italic[/i]'. JubbeM will check then if the UBB_I class
	 exists. If it does, then 'italic' will be passed to the created UBB_I-object using
	 the polymorph method process. The method process in UBB_I will then return a value
	 '<i>italic</i>' and JubbeM will modify the text, resulting in [b]bold<i>italic</i>[/b].
	 Then the deepest nested tag becomes [b]bold[...][/b]. This is then being processed
	 by UBB_B to which is passed the string 'bold<i>italic</i>' and which returns then
	 '<b>bold[...]</b>'.
	UBB-tags can take one or two arguments, being value1 and value2. The first value is 
	 value1, the second ... value2! E.g. [Quote value1]value2[/quote].
	 Each value must be of some type: if you want value1 to be nothing or a html-color,
	 and value2 to be or an integer, or floating number, then you define value1 as being
	 either EMPTY or COLOR, and value2 INTEGER or FLOAT. JubbeM then evaluates the value1
	 and value2, and will only process the tag if the typematches is all right.
	Without plugins JubbeM can't do a thing. The writing of plugins is explained in
	 ./ubb_plugins/ubb_template.inc.
	
	have fun,
	]erous.
	
	
	History
	=======
	v0.6
	 *added variable info
	 *regex now also accepts [...] inside tags which aren't made from letters; this was
	   needed to make LIST work properly (which uses [*])
	   
	TODO
	====
		v0.7: create jscript for a single item to include it in some textarea
		v0.8: optimise plugins and JubbeM
*/

/*folder in which the plugins can be found for the class.
	Only files matching the pattern ubb_*.inc will be included.
*/
define(FOLDER_UBB_PLUGINS, './ubb_plugins/');



/*this is the maximum number of nested tags that will be searched for.
	As the tagprocessing happens from inside to the outside, the tags which encircle all tags
	will be ignored. Use -1 to disable the check for the nesting level
*/
define(MAX_NESTING_LEVEL, 5);

/*this is the prefix for all childs of this class. Thus if you have a class which handles the 'B'-tag,
	you should call it class UBB_B
*/
define(UBB_CLASS_PREFIX, 'UBB_');


/*here are some definitions of some types ...
*/
define(UBB_TYPE_HTML_CHAR, '[a-zA-Z0-9\.:;\/_\?\-=&;#\<\>\'%() \t\r\n]');
define(UBB_TYPE_STRING, UBB_TYPE_HTML_CHAR."+");
define(UBB_TYPE_URL_CHAR, '[%a-zA-Z0-9\.:\/_\?=&;#+]');
define(UBB_TYPE_URL, UBB_TYPE_URL_CHAR.'*\.'.UBB_TYPE_URL_CHAR.'+');
define(UBB_TYPE_COLOR, '([a-zA-Z]{1,7}|#[0-9a-fA-F]{6})');
define(UBB_TYPE_INTEGER, '[0-9]+');
define(UBB_TYPE_FLOAT, '[0-9]+(.|,)[0-9]+');

$VALUE_TYPES = array(
	  'EMPTY' => ''
	, 'STRING' => UBB_TYPE_STRING
	, 'URL' => UBB_TYPE_URL
	, 'COLOR' => UBB_TYPE_COLOR
	, 'INTEGER' => UBB_TYPE_INTEGER
	, 'FLOAT' => UBB_TYPE_FLOAT
);



class UBB
{
	/*private var containing all loaded plugins
		The key of an item is the tag for which it is designed, and the value is the object itself
	*/
	var $m_plugins = array();
	
	var $info = array(
		  "Name"=>"JubbeM"
		, "Version"=>"0.6"
		, 'Info'=>'Framework for parsing a UBB-text and convert it to html'
	);
	
	
	//constructor
	function UBB()
	{
		$this->require_plugins();
	}
	
	//call this function when you want to UBB-parse some text ...
	function parse($text)
	{
		//this is the regex to find the deepest nested tag
		define(UBB_REGEX, '/(\[(\w+)[ ]*(( |=)([^[]+?))?\])(((?!\[[a-z]+?\])(.|\s))+?)(\[\/\2\])/i');
		//what indices do some parts have ...
		define(UBB_REGEX_TEXT, 0);
		define(UBB_REGEX_TAG, 2);
		define(UBB_REGEX_VALUE1, 5);
		define(UBB_REGEX_VALUE2, 6);
		
		$modified = true;
		
		//perform a loop as long as there has something been modified, 
		//and we've looped less as the maximum
		for($cnt=0; ($cnt<MAX_NESTING_LEVEL or MAX_NESTING_LEVEL==-1) && $modified; $cnt++)
		{
			//get all deepest nested tags
			preg_match_all(UBB_REGEX, $text, $tags, PREG_OFFSET_CAPTURE);
			$modified = false;
			
			//now start processing all tags, starting from the *end*
			for($i=count($tags[UBB_REGEX_TEXT]); $i>=0; $i--)
			{
				$result = false;
				
				//get the object for the current tag
				$p = $this->get_plugin($tags[UBB_REGEX_TAG][$i][0]);
				
				//set some vars to make reading a bit easier ...
				$v1 = $tags[UBB_REGEX_VALUE1][$i][0]; //tag
				$v2 = $tags[UBB_REGEX_VALUE2][$i][0];
				$t1 = $this->get_value_type($v1, $p->ext_types); //types this var matches with
				$t2 = $this->get_value_type($v2, $p->ext_types);
				
				$allow1 = $p->allow1; //only allow these tags
				$allow2 = $p->allow2;
				//correct the allow-arrays, as 1 value may also be given not as an array
				if (is_array($allow1)==false)		$allow1 = array($allow1);
				if (is_array($allow2)==false)		$allow2 = array($allow2);
				
				//check if the types of both value1 and value2 are allowed!
				$ok1 = $ok2 = false;
				for($j=0; $j<count($t1); $j++)
					if (in_array($t1[$j], $allow1))	$ok1 = true;
				for($j=0; $j<count($t2); $j++)
					if (in_array($t2[$j], $allow2))	$ok2 = true;
				
				//ok?
				if ($ok1 && $ok2)
					//yeah! let's process ...
					$result = $p->process($v1, $v2);
			
				if ($result != false)
				{
					$text = substr($text, 0, $tags[UBB_REGEX_TEXT][$i][1])
						.$result
						.substr($text, $tags[UBB_REGEX_TEXT][$i][1]
						+strlen($tags[UBB_REGEX_TEXT][$i][0]));
					$modified=true;
				}
			}
		}
		
		return $text;
	}
	
	/*polymorph method of the base class
		This function gets called whenever a tag is found*/
	function process($value1, $value2)
	{
		return false;
	}
	
	
	
	/*get the object for a tag (and create it if necessary)
		param plugin_name: the plugin to get (==tagname)
		return: the object for the plugin, or this class when object is not available
	*/
	function get_plugin($plugin_name)
	{
		//initialise the plugin_name and the class_name
		$plugin_name = strtoupper($plugin_name);
		$class_name = UBB_CLASS_PREFIX.$plugin_name;
		
		//object is found in the 'cache'-array
		if (isset($this->m_plugins[$plugin_name]))
			return $this->m_plugins[$plugin_name];
		
		//object is not created, but exists, ...
		if (class_exists($class_name))
		{
			//... thus let's create it!
			$this->m_plugins[$plugin_name] = new $class_name;
			return $this->m_plugins[$plugin_name];
		}
		
		//mission impossible; return this object ...
		return $this;
	}
	
	
	/*include all the ubb_plugins that are found in the ubb_plugins directory
		return: void
	*/
	function require_plugins()
	{
		//make folder end in a '/'
		$folder = FOLDER_UBB_PLUGINS;
		$folder = (substr($folder, strlen($folder), 1)=='/'? $folder:$folder.'/');
		if (is_dir($folder))
		{
			//list all files which contain (a) plugin(s)
			$ubb_class_files = glob($folder."ubb_*.inc");
			
			//require them all!
			for($i=0; $i<count($ubb_class_files); $i++)
				require_once $ubb_class_files[$i];
		}
	}
	
	/*list all the loaded plugins
		param include_me: also include the current object (thus UBB); default: false
		return: array( array("Name"=>, "Info"=>, "Version"=>)* )
	*/
	function list_plugins($include_me=false)
	{
		$ret = array();
		
		if ($include_me)	$ret[] = $this;
		
		foreach($this->m_plugins as $key=>$m_plugin)
			$ret[] = $m_plugin;
		
		return $ret;
	}
	
	/*check what types the value matches with
		param value: the value to search the types of
		param ext_types: when you want to extend the types-array with some additional values
		return: array( (type)*)
	*/
	function get_value_type($value, $ext_types = array())
	{
		global $VALUE_TYPES;
		
		//check and correct if necessary the ext_types-array
		$ret = array();
		if (is_array($ext_types)==false)
			if ($ext_types=='')	$ext_types = array();
			else				$ext_types = array($ext_types);
		
		//loop over all types ...
		foreach(array_merge($VALUE_TYPES, $ext_types) as $type=>$regex)
		{
			//... and see if this one matches ...
			$res = preg_match("/$regex/i", $value, $matches);
			//... by comparing the length of the matched part of the string
			//to the length of $value. If the two are not equal, then
			//$value belongs not to type $type, and is not added to the
			//return array.
			if (count($matches)>0 and strlen($matches[0])==strlen($value))
				$ret[] = $type;
		}
		
		return $ret;
	}
}



?>