<?php
/*======================================================================*\
|| #################################################################### ||
|| # vBulletin 4.2.5 - Licence Number VBF94D5E84
|| # ---------------------------------------------------------------- # ||
|| # Copyright ©2000-2026 vBulletin Solutions Inc. All Rights Reserved. ||
|| # This file may not be redistributed in whole or significant part. # ||
|| # ---------------- VBULLETIN IS NOT FREE SOFTWARE ---------------- # ||
|| #        www.vbulletin.com | www.vbulletin.com/license.html        # ||
|| #################################################################### ||
\*======================================================================*/

if (!isset($GLOBALS['vbulletin']->db))
{
	exit;
}

/**
* Class to parse the HTML generated by the WYSIWYG editor to BB code.
* Can be extended to parse additional tags or change the parsing behavior.
*
* This class can be used for generic HTML to BB code conversions, but it is
* not always ideally suited to this.
*
* @package	vBulletin
*/
class vB_WysiwygHtmlParser
{
	/**
	* Primary vBulletin registry object
	*
	* @var	vB_Registry
	*/
	protected $registry;

	/**
	* Whether HTML is allowed. If false, non parsed HTML will be stripped.
	*
	* @var	boolean
	*/
	protected $allow_html = false;

	/**
	* The number of linebreaks a <p> tag generates. This is usually 1 when
	* parsing from the WYSIWYG editors and 2 in other cases.
	* Well it was one with the vB3 editor yet ckeditor likes 0
	*
	* @var	int
	*/
	protected $p_linebreaks = 1;

	/**
	* The rules for the "normal" HTML tags that should be parsed. Only tags
	* that are matched (ie, <x>...</x>) and tags that are parsed without additional
	* context. See load_tag_rules for a format specification.
	*
	* @var	array
	*/
	protected $tags = array();

	/**
	* Arbitrary array that can be used for tracking limited tag state when parsing.
	* Use the push/pop state methods to modify and the in_state method to check.
	* Useful if you want to parse a dependent tag differently if found in an
	* unexpected place (eg, <li> tag not in a list).
	*
	* @var	array
	*/
	protected $state = array();

	/**
	* Constructor. Automatically loads the tag rules.
	*
	* @param	vB_Registry	registry
	*/
	public function __construct(vB_Registry $registry)
	{
		$this->registry = $registry;
		$this->tags = $this->load_tag_rules();
	}

	/**
	* Returns the rule set for parsing matched tags. Array key is name of
	* HTML tag to match. Value is either a simple callback or an array with
	* keys 'callback' and 'param' (an optional extra value to pass in to the
	* parsing callback function). Callbacks may refer to the string $this
	* to refer to the current class instance.
	*
	* @return	array
	*/
	public function load_tag_rules()
	{
		return array(
			'b' => array(
				'callback' => array('$this', 'parse_tag_basic'),
				'param' => 'b'
			),
			'strong' => array(
				'callback' => array('$this', 'parse_tag_basic'),
				'param' => 'b'
			),
			'i' => array(
				'callback' => array('$this', 'parse_tag_basic'),
				'param' => 'i'
			),
			'em' => array(
				'callback' => array('$this', 'parse_tag_basic'),
				'param' => 'i'
			),
			'u' => array(
				'callback' => array('$this', 'parse_tag_basic'),
				'param' => 'u'
			),
			'blockquote' => array(
				'callback' => array('$this', 'parse_tag_basic'),
				'param' => 'indent'
			),
			'sub' => array(
				'callback' => array('$this', 'parse_tag_basic'),
				'param' => 'sub'
			),
			'sup' => array(
				'callback' => array('$this', 'parse_tag_basic'),
				'param' => 'sup'
			),

			'table' => array('$this', 'parse_tag_table'),
			'tr'    => array('$this', 'parse_tag_tr'),
			'td'    => array('$this', 'parse_tag_td'),
			'th'    => array('$this', 'parse_tag_th'),

			'ol'   => array('$this', 'parse_tag_list'),
			'ul'   => array('$this', 'parse_tag_list'),
			'li'   => array('$this', 'parse_tag_li'),

			'span' => array('$this', 'parse_tag_span'),
			'font' => array('$this', 'parse_tag_font'),
			'a'    => array('$this', 'parse_tag_a'),

			'h1'   => array('$this', 'parse_tag_heading'),
			'h2'   => array('$this', 'parse_tag_heading'),
			'h3'   => array('$this', 'parse_tag_heading'),
			'h4'   => array('$this', 'parse_tag_heading'),
			'h5'   => array('$this', 'parse_tag_heading'),
			'h6'   => array('$this', 'parse_tag_heading'),

			'div'  => array('$this', 'parse_tag_div'),
			'p'    => array('$this', 'parse_tag_p'),
		);
	}

	/**
	* Sets the number of line breaks a <p> tag inserts.
	*
	* @param	int
	*/
	public function set_p_linebreaks($linebreaks)
	{
		$linebreaks = intval($linebreaks);
		if ($linebreaks < 0)
		{
			$linebreaks = 0;
		}

		$this->p_linebreaks = $linebreaks;
	}

	/**
	* Determines whether the parser is in the named state.
	* Note that a parser can be in multiple states simultaneously.
	* The state is not tracked with a stack.
	*
	* @param	string	State
	*
	* @return	boolean
	*/
	public function in_state($state)
	{
		return !empty($this->state[$state]);
	}

	/**
	* Pushes a new state into the list.
	*
	* @param	string	State
	*/
	protected function push_state($state)
	{
		if (isset($this->state[$state]))
		{
			$this->state[$state]++;
		}
		else
		{
			$this->state[$state] = 1;
		}
	}

	/**
	* Pops a state off the list.
	*
	* @param	string	State
	*/
	protected function pop_state($state)
	{
		if (isset($this->state[$state]))
		{
			$this->state[$state]--;

			if ($this->state[$state] <= 0)
			{
				unset($this->state[$state]);
			}
		}
	}

	/**
	* Parses the specified HTML into BB code
	*
	* @param	string	HTML to parse
	* @param	boolean	Whether to allow unparsable HTML to remain
	*
	* @return	string	Parsed version (BB code)
	*/
	public function parse_wysiwyg_html_to_bbcode($unparsed, $allow_html = false)
	{

		$parsed = $unparsed;
		$this->allow_html = $allow_html;

		($hook = vBulletinHook::fetch_hook('wysiwyg_parse_start')) ? eval($hook) : false;

		$parsed = $this->filter_before($parsed);
		$parsed = $this->parse_html($parsed);
		$parsed = $this->cleanup_after($parsed);

		($hook = vBulletinHook::fetch_hook('wysiwyg_parse_complete')) ? eval($hook) : false;

		return $parsed;
	}

	/**
	* Parses the specified BB Code into editor friendly HTML
	*
	* @param	string	HTML to parse
	* @param	boolean	Whether to allow unparsable HTML to remain
	* @param	mixed	Forumid/Area
	* @param	bool	Allow SMilies
	*
	* @return	string	Parsed version (BB code)
	*/
	public function parse_wysiwyg_html($html, $ishtml = 0, $forumid = 0, $allowsmilie = 1)
	{
		if ($ishtml)
		{
			// parse HTML into vbcode
			// I DON'T THINK THIS IS EVER USED NOW - KIER
			$html = $this->parse_wysiwyg_html_to_bbcode($html);
		}
		else
		{
			$html = unhtmlspecialchars($html, 0);
		}

		// parse the message back into WYSIWYG-friendly HTML
		require_once(DIR . '/includes/class_bbcode_alt.php');
		$wysiwyg_parser = new vB_BbCodeParser_Wysiwyg($this->registry, fetch_tag_list());
		$wysiwyg_parser->set_parse_userinfo($this->registry->userinfo);
		$wysiwyg_parser->set_strip_space(false);

		return $wysiwyg_parser->parse($html, $forumid, $allowsmilie);
	}

	/**
	* Template method for pre-filtering the HTML before it is parsed.
	* Filters things like BB code mixed into HTML, browser specific wrapping,
	* and HTML within BB codes that don't support nested tags.
	*
	* @param	string	Text pre-filter
	*
	* @return	string	Text post-filter
	*/
	public function filter_before($text)
	{
		$text = $this->filter_html_tags($text);
		$text = $this->filter_linebreaks_spaces($text);
		$text = $this->filter_bbcode($text);

		return $text;
	}

	/**
	* Filters the HTML tags to fix common issues (HTML intertwined with BB code).
	*
	* @param	string	Text pre-filter
	*
	* @return	string	Text post-filter
	*/
	protected function filter_html_tags($text)
	{
		$text = preg_replace_callback('#<pre(.*)>(.*)</pre>#siU', array($this, 'replace_newline_in_pre'), $text);

		// check for the WYSIWYG editor being lame with URL tags followed by bbcode tags
		$text = preg_replace('#<a href="([^"]*)\[([^"]+)"(.*)>(.*)\[\\2</a>#siU', '<a href="\1"\3>\4</a>[\2', $text);
		
		// make < and > safe in inside URL/IMG tags so they don't get stripped by strip_tags	
		$text = preg_replace_callback('#(<[^<>]+ (src|href))=(\'|"|)??(.*)(\\3)#siU', array($this, 'escape_within_url'), $text);

		return $text;
	}

	/**
	* PCRE callback for replacing newlines in <pre> with <br />
	*
	* @param	array	Matches from RE, index 2 is the stringtext to be fixed
	*
	* @return	string	Fixed Output
	*/
	protected function replace_newline_in_pre($matches)
	{
		static $find, $replace;
		if (!is_array($find))
		{
			$find =    array("\r\n", "\r", "\n");
			$replace = array('<br />', '<br />', '<br />');
		}

		return str_replace($find, $replace, $matches[2]);
	}

	/**
	* PCRE callback for escaping special HTML characters within src/href attributes
	* so they are not removed by strip_tags calls later.
	*
	* @param array $matches PCRE match array with the following indexes
	* 	* 1 -- Type of call (tag name and src/href)
	* 	* 2 -- Not used
	* 	* 3 -- Delimiter for the attribute
	* 	* 4 -- URL that will be escaped
	*
	* @return	string	Escaped output.
	*/
	protected function escape_within_url($matches)
	{
		static $find, $replace;
		if (!is_array($find))
		{
			$find =    array('<',    '>',    '\\"');
			$replace = array('&lt;', '&gt;', '"');
		}

		$delimiter = $matches[3];
		$type = $matches[1];
		$url = $matches[4];
		return $type . '=' . $delimiter . str_replace($find, $replace, $url) . $delimiter;
	}

	/**
	* Filters line breaks and spaces within the HTML. Also handles a browser-specific
	* behavior with soft wrapping.
	*
	* @param	string	Text pre-filter
	*
	* @return	string	Text post-filter
	*/
	protected function filter_linebreaks_spaces($text)
	{
		$text = str_replace('&nbsp;', ' ', $text);

		// deal with newline characters
		if (is_browser('mozilla'))
		{
			$text = preg_replace('#(?<!<br>|<br />|\r)(\r\n|\n|\r)#', ' ', $text);
		}

		$text = preg_replace('#(\r\n|\n|\r)#', '', $text);

		return $text;
	}

	/**
	* Filters BB code behaviors before the HTML is parsed. Includes removing
	* HTML from BB codes that don't support it and removing linking HTML from
	* a manually entered BB code.
	*
	* @param	string	Text pre-filter
	*
	* @return	string	Text post-filter
	*/
	protected function filter_bbcode($text)
	{

		//$text = preg_replace_callback('', array($this, 'replace_newline_in_pre'), $text);
		
		// strip html from php tags
		$text = preg_replace_callback('#\[(html|php)\]((?>[^\[]+?|(?R)|.))*\[/\\1\]#siU', array($this, 'strip_html_from_bbcode'), $text);

		// strip linked URLs from manually entered [url] tags (generic)
		$text = preg_replace(
			'#\[url=(\'|"|&quot;|)<A href="(.*)/??">\\2/??</A>#siU', 
			'[URL=$1$2', 
			$text
		);

		return $text;
	}

	/**
	* PCRE callback function to remove HTML from BB codes that don't support it.
	* Standard line break HTML is maintinaed.
	*
	* @param array $matches PCRE match string
	*
	* @param	return	Text without the HTML
	*/
	protected function strip_html_from_bbcode($matches)
	{
		return strip_tags($matches[0], '<p><br>');
	}

	/**
	* Parses the HTML tags within a string.
	* Handles matched and special unmatched tags.
	*
	* @param	string	Text pre-parsed
	*
	* @return	string	Parsed text (BB code)
	*/
	public function parse_html($text)
	{
		$text = $this->parse_unmatched_tags($text);
		$text = $this->parse_matched_tags($text);

		return $text;
	}

	/**
	* Parses special unmatched HTML tags like <img> and <br>.
	*
	* @param	string	Text pre-parsed
	*
	* @return	string	Parsed text
	*/
	protected function parse_unmatched_tags($text)
	{
		$text = preg_replace_callback('#<img[^>]+smilieid="(\d+)".*>#siU', array($this, 'translate_smilie_id_text'), $text);
		$text = preg_replace_callback('#<img[^>]+src=(\'|")(.*)(\\1).*>#siU', array($this, 'handle_wysiwyg_img'), $text);
	
		$pregfind = array
		(
			'#<br.*>#siU',                                        // <br> to newline
			'#(?:<p>\s*)?<hr\s*class=(\'|"|)previewbreak\s*(\\1)[^>]*>#si',
			'#(?:<p>\s*)?<hr.*>#siU'
		);
		$pregreplace = array
		(
			"\n",                                                 // <br> to newline
			'[PRBREAK][/PRBREAK]',
			'[HR][/HR]',
		);
		$text = preg_replace($pregfind, $pregreplace, $text);

		return $text;
	}

	/**
	* Translates the specified img link into an img bbcode
	*
	* @param array $matches PCRE match array with the following indexes
	* 	* 0 -- The full opening image tag
	* 	* 1 -- unused
	* 	* 2 -- image url
	*
	* @return	string	img bbcode
	*/
	protected function handle_wysiwyg_img($matches)
	{
		$img_url = $matches[2];
		$fulltag = $matches[0];

		// handle image attachments stored locally in our db
		if (preg_match('#attachment.php\?attachmentid=(\d+)#si', $img_url, $matches_id) AND 
			preg_match('#class=(\'|"|)([-_a-z0-9\s]+)?\s*previewthumb\s*([-_a-z0-9\s]+)?(\\1)#si', $fulltag))
		{
			return '[ATTACH=CONFIG]' . $matches_id[1] . '[/ATTACH]';
		}

		// handle images with a relative path
		else if (!preg_match('#^https?://#i', $img_url))
		{
			// prefix with the URL to this board
			return '[IMG]'. create_full_url($img_url) . '[/IMG]';
		}

		// handle fully qualified/external image urls, ex. from an rss feed
		else
		{
			return '[IMG]' . $img_url . '[/IMG]';
		}
	}

	/**
	* Translates the specified smilie ID to the text that represents that smilie.
	*
	* @param array $matches PCRE match array with the following indexes
	* 	* 1 -- Smilie ID
	*
	* @return	string	Smilie text
	*/
	protected function translate_smilie_id_text($matches)
	{
		static $smilies;
		$smilieid = $matches[1];

		// build the smilies array if we haven't already
		if (!is_array($smilies))
		{
			$smilies = array();
			// attempt to get smilies from the datastore smiliecache
			if (is_array($this->registry->smiliecache))
			{
				foreach($this->registry->smiliecache AS $smilie)
				{
					$smilies["$smilie[smilieid]"] = $smilie['smilietext'];
				}
			}
			// query smilies from the database
			else
			{
				$getsmilies = $this->registry->db->query_read_slave("SELECT smilieid, smilietext FROM " . TABLE_PREFIX . "smilie");
				while ($smilie = $this->registry->db->fetch_array($getsmilies))
				{
					$smilies["$smilie[smilieid]"] = $smilie['smilietext'];
				}
				$this->registry->db->free_result($getsmilies);
			}
		}

		// return the smilietext for this smilie
		return $smilies["$smilieid"];
	}

	/**
	* PCRE callback function to parse an <img> tag. Can only parse the src attribute.
	*
	* @param	string	The image's URL (src attribute)
	*
	* @return	string	An IMG BB code
	*/
	protected function parse_tag_img($img_url)
	{
		$img_url = str_replace('\\"', '"', $img_url);

		if (!preg_match('#^https?://#i', $img_url))
		{
			// relative URL, prefix it with the URL to this board
			$img_url = create_full_url($img_url);
		}

		return '[IMG]' . $img_url . '[/IMG]';
	}

	/**
	* Parses "normal" matched HTML tags. This function (and the individual
	* tag functions) are the primary places where the tag parsing rules are used.
	*
	* @param	string	Text pre-parsed
	*
	* @param	string	Text with matched HTML tags parsed (the ones specified in rules at least)
	*/
	protected function parse_matched_tags($text)
	{
		foreach (array_keys($this->tags) AS $tag_name)
		{
			$text = $this->parse_tag_by_name($tag_name, $text);
		}

		return $text;
	}

	/**
	* Parses a matched HTML tag by the name of the tag. This is resolved to the tag
	* parsing rules array and handled from there.
	*
	* @param	string	Name of the HTML tag to parse
	* @param	string	Text before this tag has been parsed
	* @param	mixed	Extra param info to pass to the callback; overrides the param specified in the tag rules
	*
	* @param	string	Text with tag parsed
	*/
	public function parse_tag_by_name($tag_name, $text, $force_param = null)
	{
		$tag_name = strtolower($tag_name);

		if (!isset($this->tags[$tag_name]))
		{
			return $text;
		}

		$tag_info = $this->tags[$tag_name];

		if (isset($tag_info['callback']))
		{
			$callback = $tag_info['callback'];
			$extra_param = isset($tag_info['param']) ? $tag_info['param'] : null;
		}
		else
		{
			$callback = $tag_info;
			$extra_param = null;
		}

		if (is_array($callback) AND $callback[0] == '$this')
		{
			$callback[0] = $this;
		}

		if ($force_param !== null)
		{
			$extra_param = $force_param;
		}

		$params = array($tag_name, $text, $callback);
		if ($extra_param !== null)
		{
			$params[] = $extra_param;
		}

		$text = call_user_func_array(
			array($this, 'parse_tag'),
			$params
		);

		return $text;
	}

	/**
	* Post parsing clean up. Removes unparsed HTML and sanitizes some BB codes.
	*
	* @param	string	Text pre-cleanup
	*
	* @return	string	Text post-cleanup
	*/
	public function cleanup_after($text)
	{
		$text = $this->cleanup_html($text);
		$text = $this->cleanup_smilies_from_images($text);
		$text = $this->cleanup_bbcode($text);

		return $text;
	}

	/**
	* Cleans up HTML stragglers after the parsing.
	*
	* @param	string	Text pre-cleanup
	*
	* @return	string	Text post-cleanup
	*/
	protected function cleanup_html($text)
	{
		// regex find / replace #2
		$pregfind = array(
			'#<li>(.*)((?=<li>)|</li>)#iU',    // fix some list issues
			'#<p></p>#i',                      // kill empty <p> tags
			'#<p.*>#iU',                       // kill any extra <p> tags
		);
		$pregreplace = array(
			"\\1\n",                           // fix some list issues
			'',                                // kill empty <p> tags
			"\n",                              // kill any extra <p> tags
		);
		$text = preg_replace($pregfind, $pregreplace, $text);

		// simple tag removals; mainly using PCRE for case insensitivity and /?
		$text = preg_replace('#</?(A|LI|FONT|IMG)>#siU', '', $text);

		if (!$this->allow_html)
		{
			$text = $this->cleanup_disallowed_html($text);
		}

		// basic string replacements #2; don't replace &quot; because browsers don't auto-encode quotes
		$strfind = array
		(
			'&lt;',       // un-htmlspecialchars <
			'&gt;',       // un-htmlspecialchars >
			'&amp;',      // un-htmlspecialchars &
		);
		$strreplace = array
		(
			'<',          // un-htmlspecialchars <
			'>',          // un-htmlspecialchars >
			'&',          // un-htmlspecialchars &
		);

		$text = str_replace($strfind, $strreplace, $text);

		return $text;
	}

	/**
	* Cleans up disallowed HTML. This generally removes all HTML. It is normally
	* called if HTML is not allowed.
	*
	* @param	string	Text pre-cleanup
	*
	* @return	string	Text post-cleanup
	*/
	protected function cleanup_disallowed_html($text)
	{
		$text = preg_replace('#<script[^>]*>(.*)</script>#siU', '', $text);
		$text = preg_replace('#<style[^>]*>(.*)</style>#siU', '', $text);
		$text = strip_tags($text);

		return $text;
	}

	/**
	* Translates image BB codes that represent smilies into the actual
	* smilie representation.
	*
	* @param	string	Text pre-cleanup
	*
	* @return	string	Text post-cleanup
	*/
	protected function cleanup_smilies_from_images($text)
	{
		if (is_array($this->registry->smiliecache))
		{
			$strfind = array();
			$strreplace = array();

			foreach ($this->registry->smiliecache AS $smilie)
			{
				// [IMG]images/smilies/frown.gif[/IMG]
				$strfind[] = '[IMG]' . $smilie['smiliepath'] . '[/IMG]';
				$strreplace[] = $smilie['smilietext'];

				// [IMG]http://domain.com/forum/images/smilies/frown.gif[/IMG]
				$strfind[] = '[IMG]' . create_full_url($smilie['smiliepath']) . '[/IMG]';
				$strreplace[] = $smilie['smilietext'];
			}

			$text = str_replace($strfind, $strreplace, $text);
		}

		return $text;
	}

	/**
	* General BB code cleanup after HTML parsing.
	*
	* @param	string	Text pre-cleanup
	*
	* @return	string	Text post-cleanup
	*/
	protected function cleanup_bbcode($text)
	{
		//if (is_browser('mozilla'))
		//{
		// mozilla treats line breaks before/after lists a little differently from IE (see #5774)
			$text = preg_replace('#\[(list)#i', "\n[\\1", $text);
			$text = preg_replace('#\[(/list)\]#i', "[\\1]\n", $text);
		//}

		$text = preg_replace('#(?<!\r|\n|^)\[(/list|list|\*)\]#i', "\n[\\1]", $text);

		// replace advanced URL tags that should actually be basic ones
		$text = preg_replace('#\[URL=("|\'|)(.*)\\1\]\\2\[/URL\]#siU', '[URL]$2[/URL]', $text);

		return $text;
	}

	/**
	* Parses the style attribute from a list of attributes and determines
	* if tags need to be wrapped. This does not do the wrapping, but gives you
	* the text to prepend/append.
	*
	* @param	string	Attribute string (multiple attributes within)
	* @param	string	(return) Text to prepend
	* @param	string	(return) Text to append
	*/
	protected function parse_style_attribute($tagoptions, &$prependtags, &$appendtags)
	{
		$searchlist = array(
			array('tag' => 'left', 'option' => false, 'regex' => '#text-align:\s*(left);?#i'),
			array('tag' => 'center', 'option' => false, 'regex' => '#text-align:\s*(center);?#i'),
			array('tag' => 'right', 'option' => false, 'regex' => '#text-align:\s*(right);?#i'),
			array('tag' => 'color', 'option' => true, 'regex' => '#(?<![a-z0-9-])color:\s*([^;]+);?#i', 'match' => 1),
			array('tag' => 'font', 'option' => true, 'regex' => '#font-family:\s*(\'|)([^;,\']+)\\1[^;]*;?#i', 'match' => 2),
			array('tag' => 'b', 'option' => false, 'regex' => '#font-weight:\s*(bold);?#i'),
			array('tag' => 'i', 'option' => false, 'regex' => '#font-style:\s*(italic);?#i'),
			array('tag' => 'u', 'option' => false, 'regex' => '#text-decoration:\s*(underline);?#i')
		);

		$style = $this->parse_wysiwyg_tag_attribute('style=', $tagoptions);
		$style = preg_replace_callback(
			'#(?<![a-z0-9-])color:\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)(;?)#i',
			function ($matches) 
			{
				return sprintf("color: #%02X%02X%02X" . $matches[4], $matches[1], $matches[2], $matches[3]);
			},
			$style
		);
		foreach ($searchlist AS $searchtag)
		{
			if (!$this->is_bbcode_tag_allowed($searchtag['tag']))
			{
				continue;
			}

			if (preg_match($searchtag['regex'], $style, $matches))
			{
				$prependtags .= '[' . strtoupper($searchtag['tag']) . iif($searchtag['option'] == true, '=' . $matches["$searchtag[match]"]) . ']';
				$appendtags = '[/' . strtoupper($searchtag['tag']) . "]$appendtags";
			}
		}
	}

	/**
	* Parses an <a> tag. Matches URL and EMAIL BB code.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Extra arguments passed in to parsing call or tag rules
	*/
	protected function parse_tag_a($aoptions, $text, $tag_name, $args)
	{
		$href = $this->parse_wysiwyg_tag_attribute('href=', $aoptions);

		if (!trim($href))
		{
			return $this->parse_tag_by_name('a', $text);
		}

		if (substr($href, 0, 7) == 'mailto:')
		{
			$tag = 'email';
			$href = substr($href, 7);
		}
		else
		{
			$tag = 'url';
			if (!preg_match('#^[a-z0-9]+:#i', $href))
			{
				// relative URL, prefix it with the URL to this board
				$href = create_full_url($href);
			}
		}
		$tag = strtoupper($tag);

		if ($this->is_bbcode_tag_allowed($tag))
		{
			if ($inner = $this->parse_tag_by_name('a', $text))
			{
				return "[$tag=\"$href\"]" . $inner . "[/$tag]";
			}
			else
			{
				return '';
			}
		}
		else
		{
			// can't auto link, return a plaintext version
			$inner_text = $this->parse_tag_by_name('a', $text);
			if ($inner_text != $href)
			{
				return "$inner_text ($href)";
			}
			else
			{
				return $href;
			}
		}
	}

	/**
	* Parses <h1> through <h6> tags. Simply uses bold with line breaks.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Extra arguments passed in to parsing call or tag rules
	*/
	protected function parse_tag_heading($attributes, $text, $tag_name, $args)
	{
		$tag_name = strtoupper($tag_name);

		$text = trim($text);

		if ($tag_name == 'H3')
		{
			$class = $this->parse_wysiwyg_tag_attribute('class=', $attributes);
			if ($class == 'wysiwyg_pagebreak')
			{
				return "[PAGE]{$text}[/PAGE]";
			}
		}

		if (preg_match('#^h(\d)$#i', $tag_name, $match))
		{
			$level = $match[1];
			return "[h=$level]{$text}[/h]";
		}
		else
		{
			return "$text\n\n";
		}
	}

	/**
	* Parses a <p> tag. Supports alignments and style attributes. Gives a line break.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Extra arguments passed in to parsing call or tag rules
	*/
	protected function parse_tag_p($poptions, $text, $tag_name, $args)
	{
		if (!$text)
		{
			return '';
		}

		$style = $this->parse_wysiwyg_tag_attribute('style=', $poptions);
		$align = $this->parse_wysiwyg_tag_attribute('align=', $poptions);
		$textalign = $this->parse_wysiwyg_style_attribute('text-align:', $style);

		$lessbreaks = false;

		// only allow left/center/right alignments
		switch ($align)
		{
			case 'left':
			case 'center':
			case 'right':
				$lessbreaks = true;
				break;
			default:
				$align = '';
		}

		if ($textalign)
		{
			$lessbreaks = true;
		}

		$align = strtoupper($align);

		$prepend = '';
		$append = '';

		$this->parse_style_attribute($poptions, $prepend, $append);
		if ($align AND $this->is_bbcode_tag_allowed($align))
		{
			$prepend .= "[$align]";
			$append .= "[/$align]";
		}

		if (preg_match("#^<table#si", $text))
		{
			$lessbreaks = true;
		}

		$dobreaks = $lessbreaks ? $this->p_linebreaks - 1 : $this->p_linebreaks;

		$append .= str_repeat("\n", $dobreaks);
		return $prepend . $this->parse_tag_by_name('p', $text) . $append;
	}

	/**
	* Parses a <span> tag. Supports style attributes.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Extra arguments passed in to parsing call or tag rules
	*/
	protected function parse_tag_span($spanoptions, $text, $tag_name, $args)
	{
		$prependtags = '';
		$appendtags = '';
		$this->parse_style_attribute($spanoptions, $prependtags, $appendtags);

		return $prependtags . $this->parse_tag_by_name('span', $text) . $appendtags;
	}

	/**
	* Parses a <div> tag. Supports alignments and style attributes. Gives a line break.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Extra arguments passed in to parsing call or tag rules
	*/
	protected function parse_tag_div($divoptions, $text, $tag_name, $args)
	{
		$prepend = '';
		$append = '';

		$this->parse_style_attribute($divoptions, $prepend, $append);
		$align = $this->parse_wysiwyg_tag_attribute('align=', $divoptions);
		$dir = $this->registry->stylevars['textdirection']['string'] == 'rtl' ? 'right' : 'left';
		if ($indentcount = intval(intval($this->parse_wysiwyg_style_attribute('margin-' . $dir . ':', $divoptions)) / EDITOR_INDENT))
		{
			if ($indentcount > 1)
			{
				$prepend .= "[INDENT={$indentcount}]";
			}
			else
			{
				$prepend .= '[INDENT]';
			}
			$append .= '[/INDENT]';
		}

		// only allow left/center/right alignments
		switch ($align)
		{
			case 'left':
			case 'center':
			case 'right':
				break;
			default:
				$align = '';
		}

		$align = strtoupper($align);

		if ($align AND $this->is_bbcode_tag_allowed($align))
		{
			$prepend .= "[$align]";
			$append .= "[/$align]";
		}
		$append .= "\n";

		return $prepend . $this->parse_tag_by_name('div', $text) . $append;
	}

	/**
	* Parses an <li> tag. Outputs the list element BB code if within a list state.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Extra arguments passed in to parsing call or tag rules
	*/
	protected function parse_tag_li($listoptions, $text, $tag_name, $args)
	{
		$options = '';
		$dir = $this->registry->stylevars['textdirection']['string'] == 'rtl' ? 'right' : 'left';
		if ($indentcount = intval(intval($this->parse_wysiwyg_style_attribute('margin-' . $dir . ':', $listoptions)) / EDITOR_INDENT))
		{
			$options = '=' . $indentcount;
		}

		if ($align = $this->parse_wysiwyg_style_attribute('text-align:', $listoptions) AND in_array(strtolower($align), array('right', 'center', 'left')))
		{
			$align = strtolower($align);
			$options .= $options ? "|{$align}" : "={$align}";
		}

		if (!$this->is_bbcode_tag_allowed('list') OR !$this->in_state('list'))
		{
			return "$text\n";
		}

		return  "\n[*{$options}]" . rtrim($text);
	}

	/**
	* Parses <ol> and <ul> tags.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Extra arguments passed in to parsing call or tag rules
	*/
	protected function parse_tag_list($listoptions, $text, $tagname, $args)
	{
		$longtype = $this->parse_wysiwyg_tag_attribute('class=', $listoptions);
		$listtype = trim(preg_replace('#"?LIST-STYLE-TYPE:\s*([a-z0-9_-]+);?"?#si', '\\1', $longtype));
		if (empty($listtype) AND $tagname == 'ol')
		{
			$listtype = 'decimal';
		}

		$indent = '';
		$dir = $this->registry->stylevars['textdirection']['string'] == 'rtl' ? 'right' : 'left';
		if ($indentcount = intval(intval($this->parse_wysiwyg_style_attribute('margin-' . $dir . ':', $listoptions)) / EDITOR_INDENT))
		{
			$indent = '|INDENT=' . $indentcount;
		}

		$this->push_state('list');
		$text = preg_replace('#<li([^>]*)>((?'.'>[^[<]+?|(?!</li).)*)(?=</?ol|</?ul|<li|\[list|\[/list)#siU', '<li\\1>\\2</li>', $text);
		$text = $this->parse_tag_by_name('li', $text);

		if (!$this->is_bbcode_tag_allowed('list'))
		{
			return $text;
		}

		$validtypes = array(
			'upper-alpha' => 'A',
			'lower-alpha' => 'a',
			'upper-roman' => 'I',
			'lower-roman' => 'i',
			'decimal' => '1'
		);
		if (!isset($validtypes["$listtype"]) OR strtolower($tagname) == 'ul')
		{
			if ($indent)
			{
				$opentag = "[LIST={$indent}]"; // default to bulleted
			}
			else
			{
				$opentag = '[LIST]'; // default to bulleted
			}
		}
		else
		{
			$opentag = '[LIST=' . $validtypes[$listtype] . "{$indent}]";
		}

		$text = $this->parse_tag_by_name($tagname, $text);

		$this->pop_state('list');

		return $opentag . $text . '[/LIST]';
	}

	/**
	* Parses a <font> tag. Supports font face, size, and color.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Extra arguments passed in to parsing call or tag rules
	*/
	protected function parse_tag_font($fontoptions, $text, $tag_name, $args)
	{
		$tags = array(
			'font' => 'face=',
			'size' => 'size=',
			'color' => 'color='
		);
		$prependtags = '';
		$appendtags = '';

		$fontoptionlen = strlen($fontoptions);

		foreach ($tags AS $vbcode => $locate)
		{
			$optionvalue = $this->parse_wysiwyg_tag_attribute($locate, $fontoptions);
			if ($optionvalue)
			{
				$vbcode = strtoupper($vbcode);
				$prependtags .= "[$vbcode=$optionvalue]";
				$appendtags = "[/$vbcode]$appendtags";
			}
		}

		$this->parse_style_attribute($fontoptions, $prependtags, $appendtags);

		return $prependtags . $this->parse_tag_by_name('font', $text) . $appendtags;
	}

	/**
	* Parses and does a basic HTML replacement for the named tag. The
	* argument passed in is the BB code to parse to.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Name of the BB code to parse to
	*/
	protected function parse_tag_basic($options, $text, $tagname, $parseto)
	{
		$useoptions = array(); // array of (key) tag name; (val) option to read. If tag name isn't found, no option is used

		if (trim($text) == '')
		{
			return '';
		}

		if (!$this->is_bbcode_tag_allowed($parseto))
		{
			return $text;
		}

		$parseto = strtoupper($parseto);

		if (empty($useoptions["$tagname"]))
		{
			$text = $this->parse_tag_by_name($tagname, $text);
			return "[$parseto]{$text}[/$parseto]";
		}
		else
		{
			$optionvalue = $this->parse_wysiwyg_tag_attribute($useoptions["$tagname"], $options);
			if ($optionvalue)
			{
				return "[$parseto=$optionvalue]{$text}[/$parseto]";
			}
			else
			{
				return "[$parseto]{$text}[/$parseto]";
			}
		}
	}

/**
	* Builds the key-value parameter format for table (and tr/td) BB codes.
	*
	* @param	array	Key-value array of params to specify
	*
	* @return	string	If there are options, the full BB code param (including the leading "=").
	*/
	protected function build_table_bbcode_param(array $options)
	{
		$output = array();

		foreach ($options AS $name => $value)
		{
			if ($value !== '')
			{
				$output[] = "$name: $value";
			}
		}

		$output = implode(', ', $output);

		return ($output ? "=\"$output\"" : '');
	}

	/**
	* Gets the effective class list for a BB code. A specific suffix is
	* stripped off and a prefix of 'cms_table_' is removed. The class 'wysiwyg_dashes'
	* is always ignored. For any remaining classes that aren't in the parent
	* list are returned in a space-delimited string.
	*
	* @param	string	List of classes applied to this tag
	* @param	string	List of classes applied to any parent tags
	* @param	string	Optional suffix to strip off from each class applied to this tag
	*
	* @return	string	Space-delimited list of remaining classes
	*/
	protected function get_effective_class_list($classes, $parent_classes = '', $suffix = '')
	{
		if ($classes === '')
		{
			return '';
		}

		$classes = array_unique(explode(' ', $classes));

		if ($parent_classes === '')
		{
			$parent_classes = array();
		}
		else
		{
			$parent_classes = array_unique(explode(' ', $parent_classes));
		}

		$output = array();

		foreach ($classes AS $class)
		{
			$class = trim($class);
			if (!$class)
			{
				continue;
			}

			$class = preg_replace(
				array(
					'#' . preg_quote($suffix, '#') . '$#',
					'#^wysiwyg_cms_table_#'
				), '', $class
			);

			if ($class == 'wysiwyg_dashes')
			{
				continue;
			}

			if (!in_array($class, $parent_classes))
			{
				$output[] = $class;
			}
		}

		return implode(' ', $output);
	}

	/**
	* Parses <table> tags. Supports various options. Automatically parses TRs within.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Extra arguments passed in to parsing call or tag rules
	*/
	protected function parse_tag_table($attributes, $text, $tag_name, $args)
	{
		$options = array();

		if ($class = $this->parse_wysiwyg_tag_attribute('class=', $attributes))
		{
			$options['class'] = $this->get_effective_class_list($class);
		}

		//if ($border = $this->parse_wysiwyg_tag_attribute('border=', $attributes))
		//{
		//	$options['class'] .= ($options['class'] ? ' grid' : 'grid');
		//}

		if ($style = $this->parse_wysiwyg_tag_attribute('style=', $attributes)
				AND
			$width = $this->parse_wysiwyg_style_attribute('width:', $style)
				AND
			 preg_match('#\d+\%?#', $width, $matches))
		{
			$options['width'] = $matches[0];
		}

		if ($align = $this->parse_wysiwyg_tag_attribute('align=', $attributes))
		{
			switch($align)
			{
				case 'center':
				case 'right':
				case 'left':
					$options['align'] = $align;
			}
		}

		$bbcode_param = $this->build_table_bbcode_param($options);

		$text = $this->parse_tag_by_name('table', $text);
		$text = $this->parse_tag_by_name('tr', $text, array('table_options' => $options));

		return "[TABLE{$bbcode_param}]\n" . $text . "[/TABLE]\n";
	}

	/**
	* Parses <tr> tags. Supports various options. Automatically parses TDs within.
	* Arguments passed in are usually the options applied to the parent table tag.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Extra arguments passed in to parsing call or tag rules
	*/
	protected function parse_tag_tr($attributes, $text, $tag_name, $args)
	{
		$options = array();

		$style = $this->parse_wysiwyg_tag_attribute('style=', $attributes);
		$style = preg_replace_callback(
			'#color:\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)(;?)#i',
			function ($matches) 
			{
				return sprintf("color: #%02X%02X%02X" . $matches[4], $matches[1], $matches[2], $matches[3]);
			},
			$style
		);

		if ($class = $this->parse_wysiwyg_tag_attribute('class=', $attributes))
		{
			if (!empty($args['table_options']) AND !empty($args['table_options']['class']))
			{
				$parent_classes = $args['table_options']['class'];
			}
			else
			{
				$parent_classes = '';
			}

			$options['class'] = $this->get_effective_class_list($class, $parent_classes, '_tr');
		}

		if (preg_match('#background-color:\s*([^;]+);?#i', $style, $match))
		{
			$bgcolor = $match[1];
		}
		else
		{
			$bgcolor = $this->parse_wysiwyg_tag_attribute('bgcolor=', $attributes);
		}

		if ($bgcolor)
		{
			$options['bgcolor'] = $bgcolor;
		}

		$bbcode_param = $this->build_table_bbcode_param($options);

		if (!is_array($args))
		{
			$args = array();
		}
		$args['tr_options'] = $options;

		$text = $this->parse_tag_by_name('td', $text, $args);
		$text = $this->parse_tag_by_name('th', $text, $args);

		return "[TR{$bbcode_param}]\n" . $text . "[/TR]\n";
	}

	/**
	* Parses <td> tags. Supports various options. Arguments passed in are
	* usually the options applied to the parent table and tr tags.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Extra arguments passed in to parsing call or tag rules
	*/
	protected function parse_tag_td($attributes, $text, $tag_name, $args)
	{
		$tagname = strtolower($tag_name);
		$options = array();

		$style = $this->parse_wysiwyg_tag_attribute('style=', $attributes);
		$style = preg_replace_callback(
			'#color:\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)(;?)#i',
			function ($matches) 
			{
				return sprintf("color: #%02X%02X%02X" . $matches[4], $matches[1], $matches[2], $matches[3]);
			},
			$style
		);

		if ($class = $this->parse_wysiwyg_tag_attribute('class=', $attributes))
		{
			$parent_classes = '';

			if (!empty($args['table_options']) AND !empty($args['table_options']['class']))
			{
				$parent_classes .= ' ' . $args['table_options']['class'];
			}

			if (!empty($args['tr_options']) AND !empty($args['tr_options']['class']))
			{
				$parent_classes .= ' ' . $args['tr_options']['class'];
			}

			$options['class'] = $this->get_effective_class_list($class, trim($parent_classes), "_{$tagname}");
		}

		if ($width = $this->parse_wysiwyg_tag_attribute('width=', $attributes))
		{
			$options['width'] = $width;
		}

		if (preg_match('#background-color:\s*([^;]+);?#i', $style, $match))
		{
			$bgcolor = $match[1];
		}
		else
		{
			$bgcolor = $this->parse_wysiwyg_tag_attribute('bgcolor=', $attributes);
		}

		if ($bgcolor)
		{
			$options['bgcolor'] = $bgcolor;
		}

		if ($colspan = $this->parse_wysiwyg_tag_attribute('colspan=', $attributes))
		{
			$options['colspan'] = $colspan;
		}

		if ($align = $this->parse_wysiwyg_tag_attribute('align=', $attributes))
		{
			switch($align)
			{
				case 'center':
				case 'right':
				case 'left':
					$options['align'] = $align;
			}
		}

		$textalign = $this->parse_wysiwyg_style_attribute('text-align:', $style);
		if ($textalign)
		{
			$options['align'] = $textalign;
		}

		$bbcode_param = $this->build_table_bbcode_param($options);

		if ($text == "\n")
		{
			$text = '';
		}

		$tagname = strtoupper($tagname);
		return "[{$tagname}{$bbcode_param}]" . $text . "[/{$tagname}]\n";
	}

	/**
	* Parses <th> tags. Supports various options. Arguments passed in are
	* usually the options applied to the parent table and tr tags.
	*
	* @param	string	String containing tag attributes
	* @param	string	Text within tag
	* @param	string	Name of HTML tag. Used if one function parses multiple tags
	* @param	mixed	Extra arguments passed in to parsing call or tag rules
	*/
	protected function parse_tag_th($attributes, $text, $tag_name, $args)
	{
		return $this->parse_tag_td($attributes, $text, $tag_name, $args);
	}

	/**
	* General matched tag HTML parser. Finds matched pairs of tags (outside pairs
	* first) and calls the specified call back.
	*
	* @param	string	Name of the HTML tag to search for
	* @param	string	Text to search
	* @param	callback	Callback to call when found
	* @param	mixed	Extra arguments to pass into the callback function
	*
	* @return	string	Text with named tag parsed
	*/
	protected function parse_tag($tagname, $text, $functionhandle, $extraargs = '')
	{
		$tagname = strtolower($tagname);
		$open_tag = "<$tagname";
		$open_tag_len = strlen($open_tag);
		$close_tag = "</$tagname>";
		$close_tag_len = strlen($close_tag);

		$beginsearchpos = 0;
		do {
			$textlower = strtolower($text);
			$tagbegin = @strpos($textlower, $open_tag, $beginsearchpos);
			if ($tagbegin === false)
			{
				break;
			}

			$strlen = strlen($text);

			// we've found the beginning of the tag, now extract the options
			$inquote = '';
			$found = false;
			$tagnameend = false;
			for ($optionend = $tagbegin; $optionend <= $strlen; $optionend++)
			{
				$char = $text{$optionend};
				if (($char == '"' OR $char == "'") AND $inquote == '')
				{
					$inquote = $char; // wasn't in a quote, but now we are
				}
				else if (($char == '"' OR $char == "'") AND $inquote == $char)
				{
					$inquote = ''; // left the type of quote we were in
				}
				else if ($char == '>' AND !$inquote)
				{
					$found = true;
					break; // this is what we want
				}
				else if (($char == '=' OR $char == ' ') AND !$tagnameend)
				{
					$tagnameend = $optionend;
				}
			}
			if (!$found)
			{
				break;
			}
			if (!$tagnameend)
			{
				$tagnameend = $optionend;
			}
			$offset = $optionend - ($tagbegin + $open_tag_len);
			$tagoptions = substr($text, $tagbegin + $open_tag_len, $offset);
			$acttagname = substr($textlower, $tagbegin + 1, $tagnameend - $tagbegin - 1);
			if ($acttagname != $tagname)
			{
				$beginsearchpos = $optionend;
				continue;
			}

			// now find the "end"
			$tagend = strpos($textlower, $close_tag, $optionend);
			if ($tagend === false)
			{
				break;
			}

			// if there are nested tags, this </$tagname> won't match our open tag, so we need to bump it back
			$nestedopenpos = strpos($textlower, $open_tag, $optionend);
			while ($nestedopenpos !== false AND $tagend !== false)
			{
				if ($nestedopenpos > $tagend)
				{ // the tag it found isn't actually nested -- it's past the </$tagname>
					break;
				}
				$tagend = strpos($textlower, $close_tag, $tagend + $close_tag_len);
				$nestedopenpos = strpos($textlower, $open_tag, $nestedopenpos + $open_tag_len);
			}
			if ($tagend === false)
			{
				$beginsearchpos = $optionend;
				continue;
			}

			$localbegin = $optionend + 1;
			$localtext = call_user_func($functionhandle,
				$tagoptions, substr($text, $localbegin, $tagend - $localbegin), $tagname, $extraargs
			);

			$text = substr_replace($text, $localtext, $tagbegin, $tagend + $close_tag_len - $tagbegin);

			// this adjusts for $localtext having more/less characters than the amount of text it's replacing
			$beginsearchpos = $tagbegin + strlen($localtext);
		} while ($tagbegin !== false);

		return $text;
	}

	/**
	* General attribute parser. Parses the named attribute out of a string
	* of attributes.
	*
	* @param	string	Name of attribute to parse. Should be in form "attr="
	* @param	string	Text to search
	*
	* @return	string	Value of named attribute
	*/
	protected function parse_wysiwyg_tag_attribute($option, $text)
	{
		$original_text = $text;

		$text = strtolower($text);
		$option = strtolower($option);

		if (($position = strpos($text, $option)) !== false)
		{
			$delimiter = $position + strlen($option);
			if ($text{$delimiter} == '"')
			{ // read to another "
				$delimchar = '"';
			}
			else if ($text{$delimiter} == '\'')
			{
				$delimchar = '\'';
			}
			else
			{ // read to a space
				$delimchar = ' ';
			}
			$delimloc = strpos($text, $delimchar, $delimiter + 1);
			if ($delimloc === false)
			{
				$delimloc = strlen($text);
			}
			else if ($delimchar == '"' OR $delimchar == '\'')
			{
				// don't include the delimiters
				$delimiter++;
			}
			return trim(substr($original_text, $delimiter, $delimloc - $delimiter));
		}
		else
		{
			return '';
		}
	}

	/**
	* General attribute parser. Parses the named attribute out of a string
	* of attributes.
	*
	* @param	string	Name of attribute to parse. Should be in form "attr:"
	* @param	string	Text to search
	*
	* @return	string	Value of named attribute
	*/
	protected function parse_wysiwyg_style_attribute($option, $text)
	{
		if (preg_match('#' . preg_quote($option, '#') . '\s*([\w\-_\#%]+)(\'|"|;|)#si', $text, $matches))
		{
			return $matches[1];
		}
		else
		{
			return '';
		}
	}

	/**
	* Determines if the specified BB code tag is globally enabled.
	*
	* @param	string	Tag name
	*
	* @return	bool
	*/
	protected function is_bbcode_tag_allowed($tag)
	{
		$flag_value = 0;

		switch (strtolower($tag))
		{
			case 'b':
			case 'i':
			case 'u':
			case 'h':
			case 'page':
			case 'table':
			case 'hr':
			case 'sub':
			case 'sup':
				$flag_value = ALLOW_BBCODE_BASIC;
				break;

			case 'color':
				$flag_value = ALLOW_BBCODE_COLOR;
				break;

			case 'size':
				$flag_value = ALLOW_BBCODE_SIZE;
				break;

			case 'font':
				$flag_value = ALLOW_BBCODE_FONT;
				break;

			case 'left':
			case 'right':
			case 'center':
				$flag_value = ALLOW_BBCODE_ALIGN;
				break;

			case 'list':
				$flag_value = ALLOW_BBCODE_LIST;
				break;

			case 'indent':
				// allowed if either is enabled
				$flag_value = ALLOW_BBCODE_ALIGN | ALLOW_BBCODE_LIST;
				break;

			case 'email':
			case 'url':
			case 'thread':
			case 'post':
				$flag_value = ALLOW_BBCODE_URL;
				break;

			case 'php':
				$flag_value = ALLOW_BBCODE_PHP;
				break;

			case 'code':
				$flag_value = ALLOW_BBCODE_CODE;
				break;

			case 'html':
				$flag_value = ALLOW_BBCODE_HTML;
				break;

			default:
				return true;
		}

		return ($this->registry->options['allowedbbcodes'] & $flag_value ? true : false);
	}
}

/*======================================================================*\
|| ####################################################################
|| # Downloaded: 15:27, Mon Mar 30th 2026 : $Revision: 92140 $
|| # $Date: 2016-12-30 20:26:15 -0800 (Fri, 30 Dec 2016) $
|| ####################################################################
\*======================================================================*/
