<?php
/**
 * This file implements the Table of Contents plugin for b2evolution
 *
 * b2evolution - {@link http://b2evolution.net/}
 * Released under GNU GPL License - {@link http://b2evolution.net/about/gnu-gpl-license}
 * @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
 *
 * @package plugins
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );


/**
 * Table of Contents plugin.
 *
 * @package plugins
 */
class table_contents_plugin extends Plugin
{
	var $name;
	var $code = 'b2evoTOC';
	var $priority = 110;
	var $version = '7.2.5';
	var $group = 'rendering';
	var $subgroup = 'infoitem';
	var $short_desc;
	var $long_desc;
	var $help_topic = 'table-of-contents-plugin';
	var $widget_icon = 'list';
	var $number_of_installs = 1;


	/**
	 * Init
	 */
	function PluginInit( & $params )
	{
		$this->name = T_('Table of Contents');
		$this->short_desc = T_('Render table of contents from header html tags.');
		$this->long_desc = sprintf( T_('This renderer generates a (nested) bullet list from all %s found in the content by short tag %s'), '<code>&lt;Hx id="xxx"&gt;</code>', '<code>[toc]</code>' );
	}


	/**
	 * 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 )
	{
		return array_merge( parent::get_coll_setting_definitions( $params ),
			array(
				'offset_scroll' => array(
						'label' => T_('Anchor offset'),
						'type' => 'integer',
						'defaultvalue' => 0,
						'suffix' => ' px',
						'note' => T_('This will be used when scrolling to an anchor.'),
					),
				)
			);
	}


	/**
	 * Event handler: Called at the beginning of the skin's HTML HEAD section.
	 *
	 * Use this to add any HTML HEAD lines (like CSS styles or links to resource files (CSS, JavaScript, ..)).
	 *
	 * @param array Associative array of parameters
	 */
	function SkinBeginHtmlHead( & $params )
	{
		global $Collection, $Blog, $disp;

		if( ! isset( $Blog ) || (
		    $this->get_coll_setting( 'coll_apply_rendering', $Blog ) == 'never' &&
		    $this->get_coll_setting( 'coll_apply_comment_rendering', $Blog ) == 'never' ) )
		{	// Don't load css/js files when plugin is not enabled:
			return;
		}

		if( $disp == 'single' || $disp == 'page' )
		{	// Initialize JS for better scrolling only on Item's page:
			expose_var_to_js( 'evo_plugin_table_contents_settings', '{
					offset_scroll: '.format_to_js( intval( $this->get_coll_setting( 'offset_scroll', $Blog ) ) ).'
				}' );
		}
	}


	/**
	 * Perform rendering
	 *
	 * @param array Associative array of parameters
	 * @return boolean true if we can render something for the required output format
	 */
	function RenderItemAsHtml( & $params )
	{
		$content = & $params['data'];

		// Reset list from previous content:
		$this->cached_toc = NULL;

		// Data for using inside callback function below:
		$this->current_Item = $this->get_Item_from_params( $params );
		$this->current_content = $content;

		// Replace `[toc]` short tag with nested/bullet list from all found header anchored tags:
		$content = replace_outside_code_tags( '#\[toc\]#i', array( $this, 'callback_render_toc' ), $content, 'replace_content_callback' );

		return true;
	}


	/**
	 * Perform rendering of Message content
	 *
	 * @see Plugin::RenderMessageAsHtml()
	 */
	function RenderMessageAsHtml( & $params )
	{
		return true;
	}


	/**
	 * Perform rendering of Email content
	 *
	 * @see Plugin::RenderEmailAsHtml()
	 */
	function RenderEmailAsHtml( & $params )
	{
		return true;
	}


	function FilterCommentContent( & $params )
	{
		$Comment = & $params['Comment'];
		if( in_array( $this->code, $Comment->get_renderers_validated() ) )
		{	// Always allow rendering for comment:
			$comment_Item = & $Comment->get_Item();
			$render_params = array_merge( array( 'data' => & $Comment->content, 'Item' => & $comment_Item ), $params );
			$this->RenderItemAsHtml( $render_params );
		}
		return false;
	}


	/**
	 * Generate table of contents from content
	 *
	 * @param object Item
	 * @param string Content
	 * @return string Rendered content
	 */
	function genereate_toc( $Item, $content )
	{
		$toc = '';

		if( empty( $Item ) )
		{	// Item must be defined for initialize URL:
			return $toc;
		}

		if( preg_match_outcode( '#<h([1-6])[^>]+id=([^>\s]+)[^>]*>(.+?)</h\1>#i', $content, $header_matches ) )
		{	// If at least one `<Hx id="xxx">` is found in content:
			$item_url = $Item->get_permanent_url();
			$min_header_level = min( $header_matches[1] );
			$toc .= '<ul class="evo_plugin__table_of_contents">';
			foreach( $header_matches[3] as $h => $header_text )
			{
				$header_text = utf8_strip_tags( $header_text );
				$header_text = preg_replace( '#\[[a-z]+:[^\]`]+\]#i', '', $header_text );
				$anchor = trim( $header_matches[2][ $h ], '"\'' );
				$toc .= '<li style="margin-left:'.( ( $header_matches[1][ $h ] - $min_header_level ) * 10 ).'px">'
						.'<a href="'.$item_url.'#'.$anchor.'" data-anchor="'.$anchor.'">'.$header_text.'</a>'
					.'</li>';
			}
			$toc .= '</ul>';
		}

		return $toc;
	}


	/**
	 * Callback function to render table of contents short tag
	 *
	 * @param array Matches of `[toc]` short tag
	 */
	function callback_render_toc( $toc_matches )
	{
		if( $this->cached_toc === NULL )
		{	// Initialize table of contents once for the requested content and store into cache:
			$this->cached_toc = $this->genereate_toc( $this->current_Item, $this->current_content );
		}

		return $this->cached_toc;
	}


	/**
	 * Get keys for block/widget caching
	 *
	 * Maybe be overriden by some widgets, depending on what THEY depend on..
	 *
	 * @param integer Widget ID
	 * @return array of keys this widget depends on
	 */
	function get_widget_cache_keys( $widget_ID = 0 )
	{
		global $Collection, $Blog, $Item;

		return array(
				'wi_ID'        => $widget_ID, // Have the widget settings changed ?
				'set_coll_ID'  => isset( $Blog ) ? $Blog->ID : NULL, // Have the settings of the blog changed ? (ex: new skin)
				'cont_coll_ID' => isset( $Blog ) ? $Blog->ID : NULL, // Has the content of the displayed blog changed ?
				'item_ID'      => ( empty( $Item->ID ) ? 0 : $Item->ID ), // Has the Item page changed?
			);
	}


	/**
	 * Get definitions for widget specific editable params
	 *
	 * @see Plugin::GetDefaultSettings()
	 * @param local params like 'for_editing' => true
	 */
	function get_widget_param_definitions( $params )
	{
		return array(
			'title' => array(
				'label' => T_('Title'),
				'size' => 60,
				'defaultvalue' => T_('On this page'),
			),
		);
	}


	/**
	 * Event handler: SkinTag (widget)
	 *
	 * @param array Associative array of parameters.
	 * @return boolean did we display?
	 */
	function SkinTag( & $params )
	{
		global $Item, $disp;

		$this->init_widget_params( $params, array(
				'block_start'       => '<div class="evo_widget $wi_class$ panel panel-default">',
				'block_end'         => '</div>',
				'block_title_start' => '<div class="panel-heading"><h4 class="panel-title">',
				'block_title_end'   => '</h4></div>',
				'block_body_start'  => '<div class="panel-body">',
				'block_body_end'    => '</div>',
			) );

		if( $disp != 'single' && $disp != 'page' )
		{	// Don't display this widget for not post pages:
			$this->display_widget_debug_message( 'Plugin widget "'.$this->name.'" is hidden because no proper disp.' );
			return false;
		}

		if( empty( $Item ) )
		{	// Don't display this widget when no Item object:
			$this->display_widget_debug_message( 'Plugin widget "'.$this->name.'" is hidden because no current Item.' );
			return false;
		}

		// Generate table of contents:
		$toc = $this->genereate_toc( $Item, $Item->get_full_content( 'htmlbody', $params ) );

		if( empty( $toc ) )
		{	// Don't display widget when current Item has no anchor header tags in content:
			$this->display_widget_debug_message( 'Plugin widget "'.$this->name.'" is hidden because Item has no anchor header tags in content.' );
			return false;
		}

		echo $params['block_start'];

		$widget_title = $this->get_widget_setting( 'title', $params );
		if( ! empty( $widget_title ) )
		{	// We want to display a title for the widget block:
			echo $params['block_title_start'];
			echo $widget_title;
			echo $params['block_title_end'];
		}

		echo $params['block_body_start'];

		// Display table of contents:
		echo $toc;

		echo $params['block_body_end'];

		echo $params['block_end'];

		return true;
	}
}

?>
