<?php
/**
 * This file implements the Auto P plugin for b2evolution
 *
 * @author blueyed: Daniel HAHLER - {@link http://daniel.hahler.de/}
 *
 * @package plugins
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );


/**
 * The Auto-P Plugin.
 *
 * It wraps text blocks, which are devided by newline(s) into HTML P tags (paragraphs)
 * and optionally replaces single newlines with BR tags (line breaks).
 *
 * @package plugins
 */
class auto_p_plugin extends Plugin
{
	var $code = 'b2WPAutP';
	var $name = 'Auto P';
	var $priority = 80;
	var $version = '7.2.3';
	var $group = 'rendering';
	var $short_desc;
	var $long_desc;
	var $help_topic = 'auto-p-plugin';
	var $number_of_installs = 1;

	/**
	 * List of block elements (we want a paragraph before and after), excludes: address, added: td, th
	 * @var string
	 */
	var $block_tags = 'blockquote|dd|div|dl|dt|fieldset|form|h[1-6]|hr|li|object|ol|p|pre|select|script|table|td|th|ul';


	var $p_allowed_in = array('address', 'applet', 'blockquote', 'body', 'button', 'center', 'dd', 'del', 'div', 'fieldset', 'form', 'iframe', 'ins', 'li', 'map', 'noframes', 'noscript', 'object', 'td', 'th' );


	var $br_allowed_in = array(
		// Block level:
		'address', 'center', 'dd', 'dir', 'div', 'dt', 'fieldset', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'isindex', 'menu', 'noframes', 'noscript', 'p', 'pre',
		// Inline:
		'a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo', 'big', 'button', 'cite', 'code', 'dfn', 'em', 'font', 'i', 'img', 'input', 'iframe', 'kbd', 'label', 'li', 'map', 'object', 'q', 'samp', 'select', 'small', 'span', 'strong', 'sub', 'sup', 'textarea', 'td', 'th', 'tt', 'var' );


	/**
	 * Init
	 */
	function PluginInit( & $params )
	{
		$this->short_desc = T_('Automatic &lt;P&gt; and &lt;BR&gt; tags');
		$this->long_desc = T_('This renderer will automatically detect paragraphs on double line-breaks and mark them with appropriate HTML &lt;P&gt; tags.<br />
Optionally, it will also mark single line breaks with HTML &lt;BR&gt; tags.');
	}


	/**
	 * Define the GLOBAL settings of the plugin here. These can then be edited in the backoffice in System > Plugins.
	 *
	 * @param array Associative array of parameters (since v1.9).
	 *    'for_editing': true, if the settings get queried for editing;
	 *                   false, if they get queried for instantiating {@link Plugin::$Settings}.
	 * @return array see {@link Plugin::GetDefaultSettings()}.
	 * The array to be returned should define the names of the settings as keys (max length is 30 chars)
	 * and assign an array with the following keys to them (only 'label' is required):
	 */
	function GetDefaultSettings( & $params )
	{
		return array(
				'br' => array(
					'label' => T_('Line breaks'),
					'type' => 'checkbox',
					'defaultvalue' => 1,
					'note' => T_('Make line breaks (&lt;br /&gt;) for single newlines.'),
				),
				'add_p_in_block' => array(
					'label' => T_('Add P tags in blocks (e.g. DIV)'),
					'type' => 'checkbox',
					'defaultvalue' => 1,
					'note' => '',
				),
				'skip_tags' => array(
					'label' => T_('Ignore tags'),
					'type' => 'text',
					'defaultvalue' => 'pre',
					'note' => T_('A list of tags, in which no P or BR tags should get added.'),
				),
			);
	}


	/**
	 * Perform rendering
	 */
	function RenderItemAsHtml( & $params )
	{
		$content = & $params['data'];

		// Render outside multiline short tags:
		if( preg_match( '/\[[a-z]+:[^\]`]+\]/i', $content ) )
		{	// Call replace_content() on everything outside multiline short tags:
			$content = callback_on_non_matching_blocks( $content,
				'~\[[a-z]+:[^\]`]+\]~is',
				array( $this, 'render_autop' ) );
		}
		else
		{	// No short tags, replace on the whole content:
			$content = $this->render_autop( $content );
		}

		return true;
	}


	/**
	 * Render content to apply auto <p> and <br>
	 *
	 * @param string Source content
	 * @return string Rendered content
	 */
	function render_autop( $content )
	{
		$this->use_auto_br = $this->Settings->get('br');
		$this->add_p_in_block = $this->Settings->get('add_p_in_block');
		$this->skip_tags = preg_split( '~\s+~', $this->Settings->get('skip_tags'), -1, PREG_SPLIT_NO_EMPTY );

		$content = preg_replace( "~(\r\n|\r)~", "\n", $content ); // cross-platform newlines

		// Handle blocks, splitted by content separators: [teaserbreak] or [pagebreak]
		$content_parts = split_outcode( array( '[teaserbreak]', '[pagebreak]' ), $content, true );
		$content_parts[] = '';

		$content = '';
		for( $i = 0; $i < count( $content_parts ); $i = $i + 2 )
		{
			$content .= $this->handle_blocks( $content_parts[ $i ] );
			$content .= $content_parts[ $i + 1 ];
		}

		return $content;
	}


	/**
	 * Define here default collection/blog settings that are to be made available in the backoffice.
	 *
	 * @param array Associative array of parameters.
	 * @return array See {@link Plugin::get_coll_setting_definitions()}.
	 */
	function get_coll_setting_definitions( & $params )
	{
		$default_params = array(
				'default_comment_rendering' => 'stealth',
				'default_post_rendering' => 'opt-out'
			);

		if( ! empty( $params['blog_type'] ) )
		{	// Set default settings depending on collection type:
			switch( $params['blog_type'] )
			{
				case 'forum':
					$default_params['default_comment_rendering'] = 'never';
					$default_params['default_post_rendering'] = 'never';
					break;
			}
		}

		$default_params = array_merge( $params, $default_params );

		return parent::get_coll_setting_definitions( $default_params );
	}


	/**
	 * Define here default message settings that are to be made available in the backoffice.
	 *
	 * @param array Associative array of parameters.
	 * @return array See {@link Plugin::GetDefaultSettings()}.
	 */
	function get_msg_setting_definitions( & $params )
	{
		// set params to allow rendering for messages by default
		$default_params = array_merge( $params, array( 'default_msg_rendering' => 'stealth' ) );
		return parent::get_msg_setting_definitions( $default_params );
	}


	/**
	 * Define here default email settings that are to be made available in the backoffice.
	 *
	 * @param array Associative array of parameters.
	 * @return array See {@link Plugin::GetDefaultSettings()}.
	 */
	function get_email_setting_definitions( & $params )
	{
		// set params to allow rendering for emails by default:
		$default_params = array_merge( $params, array( 'default_email_rendering' => 'stealth' ) );
		return parent::get_email_setting_definitions( $default_params );
	}


	/**
	 * Define here default shared settings that are to be made available in the backoffice.
	 *
	 * @param array Associative array of parameters.
	 * @return array See {@link Plugin::GetDefaultSettings()}.
	 */
	function get_shared_setting_definitions( & $params )
	{
		// set params to allow rendering for shared container widgets by default:
		$default_params = array_merge( $params, array( 'default_shared_rendering' => 'stealth' ) );
		return parent::get_shared_setting_definitions( $default_params );
	}


	/**
	 * - Split text into blocks, using $block_tags pattern.
	 *
	 * @param string Text
	 * @return string
	 */
	function handle_blocks( $text )
	{
		$blocks = preg_split( '~(</?('.$this->block_tags.')(\b[^>]*)?>)~is', $text, -1, PREG_SPLIT_DELIM_CAPTURE );

		$new_text = '';
		$tag_position = false;
		$opened_tags = array();
		foreach( $blocks as $b => $block )
		{
			if( $block === '' )
			{	// Skip empty string:
				continue;
			}

			if( $tag_position !== false && $b <= $tag_position + 2 )
			{	// Skip tag name and attributes:
				if( $b == $tag_position + 2 )
				{	// Tag position is ended here, next is text:
					$tag_position = false;
				}
				continue;
			}

			if( strpos( $block, '<' ) === 0 &&
			    isset( $blocks[ $b + 1 ] ) &&
			    preg_match( '~^('.$this->block_tags.')$~i', $blocks[ $b + 1 ] ) )
			{	// Block tag is opening or closing here:
				$tag_position = $b;
				if( substr( $block, 1, 1 ) == '/' )
				{	// This is a closing of opened tag,
					// Remove last opened tag from the array:
					array_pop( $opened_tags );
				}
				elseif( substr( $block, -2, 1 ) != '/' )
				{	// This is a start of new tag AND this tag is not single/autoclosed tag like <hr />, <img /> and etc.,
					// Put tag name in array to know what tags are opened and not closed yet:
					$opened_tags[] = $blocks[ $b + 1 ];
				}
				// Append start or end of block tag with all attributes:
				$new_text .= $block;
				continue;
			}

			// Handle text which may contain inline tags:
			$current_tag = isset( $opened_tags[ count( $opened_tags ) - 1 ] ) ? $opened_tags[ count( $opened_tags ) - 1 ] : '';
			$new_text .= $this->handle_pre_blocks( $block, $current_tag );
		}

		return $new_text;
	}


	/**
	 * Handle text which may contain inline tags
	 *
	 * - Explode by \n\n
	 * - Merge blocks that span over multiple tags
	 * - Apply BR to blocks
	 * - Wrap block in P
	 *
	 * @param string Text
	 * @param string Tag where $text is in
	 * @return string
	 */
	function handle_pre_blocks( $text, $in_tag )
	{
		#echo '<h2>HANDLE_PRE_BLOCKS</h2>'; pre_dump( $text, $in_tag );

		if( $in_tag )
		{
			if( in_array($in_tag, $this->skip_tags) )
			{
				return $text;
			}

			if( ! in_array($in_tag, $this->p_allowed_in) )
			{ // we're in a tag, where no P tags are allowed, so just do the BRs:
				return $this->handle_br( $text, $in_tag );
			}
		}

		$text_lines = preg_split( '~(\n\n+)~', $text, -1, /*PREG_SPLIT_NO_EMPTY |*/ PREG_SPLIT_DELIM_CAPTURE );
		$text_lines[] = ''; // dummy

		#echo '<strong>text_lines</strong><br />'; pre_dump( $text_lines, $in_tag );

		$new_blocks = array();
		$count_new_blocks = 0;
		for( $i = 0, $n = count($text_lines); $i < $n; $i = $i+2 /* every second block is a real one */ )
		{
			if( ! isset($new_blocks[$count_new_blocks]) )
			{
				$new_blocks[$count_new_blocks] = '';
			}
			if( $text_lines[$i] == '' )
			{
				$new_blocks[$count_new_blocks] .= $text_lines[$i+1];
				$new_blocks[$count_new_blocks+1] = ''; // dummy
				continue;
			}
			$new_blocks[$count_new_blocks] .= $text_lines[$i];
			$new_blocks[$count_new_blocks+1] = $text_lines[$i+1];
			$count_new_blocks = $count_new_blocks+2;
		}

		$text_lines = $new_blocks;
		#echo '<strong>new text_lines</strong><br />'; pre_dump( $new_blocks );

		if( trim($text) == '' )
		{ // there's only whitespace
			return $text;
		}


		// fix it, so no (inline) tags span across multiple blocks:
		$new_blocks = array();
		$new_blocks_nowrap = array(); // blocks that should not be wrapped in P (opening without closing tag or vice versa)
		$count_new_blocks = 0;
		$looking_for_close_tag = array();

		for( $i = 0, $n = count($text_lines); $i < $n; $i = $i+2 /* every 2nd line is a real block */ )
		{
			$line = $text_lines[$i];
			if( ! isset($new_blocks[$count_new_blocks]) )
			{
				$new_blocks[$count_new_blocks] = '';

				$line_copy = $line;

				while( preg_match( '~^(.*?)(<\s*/\s*(\w+)(\s+[^>]*?)?>)~is', $line_copy, $match )
					&& !(preg_match( '~^(.*?)(<\s*'.$match[3].'(\s+[^>]*?(\s*/\s*)?)?>)~is', $new_blocks[$count_new_blocks].$match[1] )) )
				{ // a closing tag:
					$new_blocks[$count_new_blocks] .= $match[0];
					$line_copy = substr($line_copy, strlen($match[0]));
				}
				if( ! empty($new_blocks[$count_new_blocks]) )
				{ // we've found a closing tag with no opening tag, this must not get wrapped in P:
					$new_blocks_nowrap[] = $count_new_blocks;
					$new_blocks[$count_new_blocks+1] = ''; // dummy
					$new_blocks[$count_new_blocks+2] = ''; // init new
					$line = substr( $line, strlen($new_blocks[$count_new_blocks]) );
					$count_new_blocks = $count_new_blocks+2;
				}
			}
			else
			{ // we're looking for a closing tag:
				// Find closing tag:
				$line_copy = $line;
				list( $text_in_tag, $closing_tag, $NL_before, $NL_after ) = $this->split_text_for_tag( $looking_for_close_tag['tag'], $line_copy /* by ref */ );
				if( empty($closing_tag) )
				{ // not in this whole block:
					$new_blocks[ $count_new_blocks ] .= $line.$text_lines[$i+1];
					continue;
				}

				// Tag has been found:
				$looking_for_close_tag = array();
			}

			if( preg_match( '~^(.*?)(<\s*(\w+)(\s+[^>/]*)?(\s*/\s*)?>)~is', $line, $match ) )
			{ // a opening tag:
				$tag = $match[3];
				$pos_after_tag = strlen($match[0]);

				while( ! empty($match[5]) /* "/" */ )
				{ // self-closing tag, find next:
					$tag = false;
					if( preg_match( '~^(.*?)(<\s*(\w+)(\s+[^>/]*)?(\s*/\s*)?>)~is', substr($line, $pos_after_tag), $match ) )
					{
						$tag = $match[3];
						$pos_after_tag += strlen($match[0]);
					}
				}
				$text_after_tag = substr($line, $pos_after_tag);

				if( $tag )
				{
					// Find closing tag:
					list( $text_in_tag, $closing_tag, $NL_before, $NL_after ) = $this->split_text_for_tag($tag, $text_after_tag /* by ref */ );
					if( empty($closing_tag) )
					{
						$looking_for_close_tag = array(
								'tag' => $tag,
								'block' => $i,
								'pos' => strlen($new_blocks[ $count_new_blocks ])+strlen($match[1]), // position where the unclosed tag begins
							);
						$new_blocks[ $count_new_blocks ] .= $line.$text_lines[$i+1];
						continue;
					}
				}
			}
			$new_blocks[ $count_new_blocks ] .= $line;
			$new_blocks[ ++$count_new_blocks ] = $text_lines[$i+1];

			$count_new_blocks++;
		}
		if( $looking_for_close_tag )
		{
			#echo '<h1>looking_for_close_tag</h1>'; pre_dump( $looking_for_close_tag );
			$new_blocks[ $count_new_blocks+1 ] = ''; // dummy

			if( $looking_for_close_tag['pos'] > 0 )
			{ // move part of last block without closing tag to an own block:
				$new_blocks[ $count_new_blocks+2 ] = substr($new_blocks[ $count_new_blocks ], $looking_for_close_tag['pos']);
				$new_blocks[ $count_new_blocks ] = substr($new_blocks[ $count_new_blocks ], 0, $looking_for_close_tag['pos']);
				$new_blocks[ $count_new_blocks+3 ] = ''; // dummy

				$new_blocks_nowrap[] = $count_new_blocks+2;
			}
			else
			{ // the whole block should not get wrapped!
				$new_blocks_nowrap[] = $count_new_blocks;
			}
		}

		#echo '<h1>new_blocks:</h1>'; pre_dump( $new_blocks, $new_blocks_nowrap );

		$after_block_wp = '';
		$before_block_wp = '';
		if( empty($in_tag) )
		{
			$wrap_in_p = true;
		}
		elseif( in_array($in_tag, $this->p_allowed_in) )
		{
			if( ! $this->add_p_in_block )
			{
				$wrap_in_p = false;
			}
			elseif( count($new_blocks) > 2 )
			{
				$wrap_in_p = true;
			}
			else
			{
				$wrap_in_p = false;
				if( substr( $new_blocks[0], 0, 1 ) == "\n" )
				{
					$before_block_wp = "\n";
					$new_blocks[0] = substr( $new_blocks[0], 1 );
					$wrap_in_p = true;
				}
				if( substr( $new_blocks[0], -1 ) == "\n" )
				{
					$after_block_wp = "\n";
					$new_blocks[0] = substr( $new_blocks[0], 0, -1 );
					$wrap_in_p = true;
				}
			}
		}
		else
		{
			$wrap_in_p = false;
		}

		if( $new_blocks[count($new_blocks)-2] == '' )
		{
			array_pop($new_blocks);
			array_pop($new_blocks);
		}

		$new_text = '';

		for( $i = 0, $n = count($new_blocks); $i < $n; $i = $i+2 )
		{
			#echo '<h2>--new_blocks['.$i.']: '; pre_dump( $new_blocks[$i] ); echo '</h2>';

			$this_wrap_in_p = $wrap_in_p && ! in_array( $i, $new_blocks_nowrap ); // only wrap this, if it's a valid block

			if( empty($new_blocks[$i]) )
			{
				if( $this_wrap_in_p )
				{ // not the last one
					$block = '<p></p>';
				}
				else
				{
					$block = '';
				}
				$new_text .= $before_block_wp.$block.$after_block_wp.$new_blocks[$i+1];
				continue;
			}

			list($new_block, $has_p) = $this->handle_pre_blocks_helper( $new_blocks[$i], $in_tag, $this_wrap_in_p );


			$new_text .= $before_block_wp.$new_block.$after_block_wp.$new_blocks[$i+1];
		}

		#pre_dump( 'HANDLE_PRE_BLOCKS return: ', $new_text, $in_tag );
		return $new_text;
	}


	/**
	 * This is a helper for handling blocks from {@link handle_pre_blocks_helper()}.
	 *
	 * What comes here is supposed to have no block tags.
	 *
	 * @return array array( $text, $has_p )
	 */
	function handle_pre_blocks_helper( $block, $in_tag, $wrap_in_p, $ignore_NL = true )
	{
		#pre_dump( 'HANDLE_PRE_BLOCKS_HELPER begin', $block, $in_tag );
		$has_p = NULL;
		$r = '';

		if( $in_tag == 'blockquote' )
		{ // XHTML strict: blockquote content needs to be in block tag
			$in_tag = 'p';
			$wrap_in_p = true; // at the end
		}

		// Remove newlines at start and end (will get re-applied later):
		$NL_start = '';
		$NL_end = '';
		if( $ignore_NL )
		{
			while( isset( $block[0] ) && $block[0] == "\n" )
			{
				$NL_start .= $block[0];
				$block = substr($block, 1);
			}
			while( substr($block, -1) == "\n" )
			{
				$NL_end .= substr($block, -1);
				$block = substr($block, 0, -1);
			}
		}

		if( preg_match( '~^(.*?)(<\s*(\w+)(\s+[^>]*)?>)~is', $block, $match ) )
		{ // a tag:
			$tag = $match[3];
			$before_tag = $match[1];

			if( ! empty($before_tag) )
			{ // Delegate to handle_br:
				$r .= $this->handle_br( $before_tag, $in_tag );
			}

			// Opening tag:
			$r .= $match[2];

			$text_after_tag = substr( $block, strlen($match[0]) );

			// Find closing tag:
			list( $text_in_tag, $closing_tag, $NL_before, $NL_after ) = $this->split_text_for_tag($tag, $text_after_tag);

			if( ! empty($text_in_tag) )
			{ // Recurse (same level) - with the optional newlines at start and end, because in an inline tag every linebreak should become a BR:
				list($text_in_tag, $sub_has_p) = $this->handle_pre_blocks_helper( $NL_before.$text_in_tag.$NL_after, $tag, false, false );
			}
			$r .= $text_in_tag;

			$r .= $closing_tag;

			if( ! empty($text_after_tag) )
			{
				#echo '<h1>RECURSE: text_after_tag (handle_pre_blocks)</h1>';
				// Recurse (same level):
				list( $text_after_tag, $sub_has_p ) = $this->handle_pre_blocks_helper( $text_after_tag, $in_tag, false, false );
				$r .= $text_after_tag;
			}
		}
		else
		{ // No tags in this $text:
			$r .= $this->handle_br( $block, $in_tag );
		}

		if( ! empty($wrap_in_p) )
		{
			$r = '<p>'.$r.'</p>';
			$has_p = true;
		}

		// re-apply newlines from start and end:
		$r = $NL_start.$r.$NL_end;

		#pre_dump( 'HANDLE_PRE_BLOCKS_HELPER return: ', $r, $has_p );
		return array( $r, $has_p );
	}


	/**
	 * Handles adding BR.
	 *
	 * @return string
	 */
	function handle_br( $text, $in_tag )
	{
		#echo '<h3>LEVEL>1 (BR)</h3>'; pre_dump( $text );

		if( empty($in_tag) || in_array($in_tag, $this->br_allowed_in) )
		{
			$new_text = $this->autobr($text);
		}
		else
		{
			$new_text = $text;
		}

		#pre_dump( 'HANDLE_BR return: ', $new_text, $in_tag );
		return $new_text;
	}


	/**
	 * Split the text for a given tag, mainly to find the closing tag.
	 *
	 * @return array
	 */
	function split_text_for_tag($tag, & $text_after_tag)
	{
		#echo '<strong>split_text_for_tag</strong><br />'; pre_dump( $tag, $text_after_tag );
		$depth = 1;
		$text_in_tag = '';

		$loop_text = $text_after_tag;
		while( 1 )
		{
			#echo '<hr />loop_text:'; pre_dump( $loop_text );
			if( preg_match( '~^(.*?)(<\s*(/)?\s*'.preg_quote( $tag, '~' ).'\s*(/\s*)?>)~is', $loop_text, $after_match ) )
			{
				#pre_dump( 'after_match', $after_match );
				$text_in_tag .= $after_match[1];
				$found_tag = $after_match[2];
				$is_closing = ( ! empty($after_match[3]) || ! empty($after_match[4]) /* self-closing */ );

				if( $is_closing )
				{
					$depth--;
					if( $depth == 0 )
					{ // found the matching closing tag:
						$closing_tag = $found_tag;
						break;
					}
					else
					{ // this closing tag is part of the outer:
						$text_in_tag .= $found_tag;
					}
				}
				else
				{ // found the same, but opening tag (nested)
					$text_in_tag .= $found_tag;
					$depth++;
				}

				// skip what we've matched:
				$loop_text = substr($loop_text, strlen($after_match[0]) );
			}
			else
			{ // did not find the closing tag.. :/
				$closing_tag = '';
				return array( false, false, false, false );
			}
		}

		// remove newline at start and end:
		if( substr($text_in_tag, 0, 1) == "\n" )
		{
			$NL_before = "\n";
			$text_in_tag = substr($text_in_tag, 1);
		}
		else
		{
			$NL_before = '';
		}
		if( substr($text_in_tag, -1) == "\n" )
		{
			$NL_after = "\n";
			$text_in_tag = substr($text_in_tag, 0, -1);
		}
		else
		{
			$NL_after = '';
		}

		$text_after_tag = substr( $text_after_tag, strlen($NL_before.$text_in_tag.$NL_after)+strlen($closing_tag) );
		$r = array( $text_in_tag, $closing_tag, $NL_before, $NL_after );

		#pre_dump( 'return: ', $r, $text_after_tag );
		return $r;
	}


	/**
	 * Add "<br />" to the end of newlines, which do not end with "<br />" already and which aren't
	 * the last line, if the "Auto-BR" setting is enabled.
	 *
	 * @return string
	 */
	function autobr( $text, $replace_last = true )
	{
		if( ! $this->use_auto_br )
		{ // don't make <br />'s
			return $text;
		}

		return preg_replace( '~(?<!<br />)\n'.( $replace_last ? '' : '(?!\z)' ).'~i', "<br />\n", $text );
	}

}

?>