show_toc = true;
$this->exclude_post_types = [ 'attachment', 'revision', 'nav_menu_item', 'safecss' ];
$this->collision_collector = [];
// get options
$defaults = [ // default options
'fragment_prefix' => 'i',
'position' => TOC_POSITION_BEFORE_FIRST_HEADING,
'start' => 4,
'show_heading_text' => true,
'heading_text' => 'Contents',
'auto_insert_post_types' => [ 'page' ],
'show_heirarchy' => true,
'ordered_list' => true,
'smooth_scroll' => false,
'smooth_scroll_offset' => TOC_SMOOTH_SCROLL_OFFSET,
'visibility' => true,
'visibility_show' => 'show',
'visibility_hide' => 'hide',
'visibility_hide_by_default' => false,
'width' => 'Auto',
'width_custom' => '275',
'width_custom_units' => 'px',
'wrapping' => TOC_WRAPPING_NONE,
'font_size' => '95',
'font_size_units' => '%',
'theme' => TOC_THEME_GREY,
'custom_background_colour' => TOC_DEFAULT_BACKGROUND_COLOUR,
'custom_border_colour' => TOC_DEFAULT_BORDER_COLOUR,
'custom_title_colour' => TOC_DEFAULT_TITLE_COLOUR,
'custom_links_colour' => TOC_DEFAULT_LINKS_COLOUR,
'custom_links_hover_colour' => TOC_DEFAULT_LINKS_HOVER_COLOUR,
'custom_links_visited_colour' => TOC_DEFAULT_LINKS_VISITED_COLOUR,
'lowercase' => false,
'hyphenate' => false,
'bullet_spacing' => false,
'include_homepage' => false,
'exclude_css' => false,
'exclude' => '',
'heading_levels' => [ 1, 2, 3, 4, 5, 6 ],
'restrict_path' => '',
'css_container_class' => '',
'sitemap_show_page_listing' => true,
'sitemap_show_category_listing' => true,
'sitemap_heading_type' => 3,
'sitemap_pages' => 'Pages',
'sitemap_categories' => 'Categories',
'show_toc_in_widget_only' => false,
'show_toc_in_widget_only_post_types' => [ 'page' ],
];
$options = get_option( 'toc-options', $defaults );
$this->options = wp_parse_args( $options, $defaults );
add_action( 'plugins_loaded', [ $this, 'plugins_loaded' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'wp_enqueue_scripts' ] );
add_action( 'admin_init', [ $this, 'admin_init' ] );
add_action( 'admin_menu', [ $this, 'admin_menu' ] );
add_action( 'widgets_init', [ $this, 'widgets_init' ] );
add_action( 'sidebar_admin_setup', [ $this, 'sidebar_admin_setup' ] );
add_action( 'init', [ $this, 'init' ] );
add_filter( 'the_content', [ $this, 'the_content' ], 100 ); // run after shortcodes are interpreted (level 10)
add_filter( 'plugin_action_links', [ $this, 'plugin_action_links' ], 10, 2 );
add_filter( 'widget_text', 'do_shortcode' );
add_shortcode( 'toc', [ $this, 'shortcode_toc' ] );
add_shortcode( 'no_toc', [ $this, 'shortcode_no_toc' ] );
add_shortcode( 'sitemap', [ $this, 'shortcode_sitemap' ] );
add_shortcode( 'sitemap_pages', [ $this, 'shortcode_sitemap_pages' ] );
add_shortcode( 'sitemap_categories', [ $this, 'shortcode_sitemap_categories' ] );
add_shortcode( 'sitemap_posts', [ $this, 'shortcode_sitemap_posts' ] );
}
public function __destruct() {}
public function get_options() {
return $this->options;
}
public function set_option( $options ) {
$this->options = array_merge( $this->options, $options );
}
/**
* Allows the developer to disable TOC execution
*/
public function disable() {
$this->show_toc = false;
}
/**
* Allows the developer to enable TOC execution
*/
public function enable() {
$this->show_toc = true;
}
public function set_show_toc_in_widget_only( $value = false ) {
if ( $value ) {
$this->options['show_toc_in_widget_only'] = true;
} else {
$this->options['show_toc_in_widget_only'] = false;
}
update_option( 'toc-options', $this->options );
}
public function set_show_toc_in_widget_only_post_types( $value = false ) {
if ( $value ) {
$this->options['show_toc_in_widget_only_post_types'] = $value;
} else {
$this->options['show_toc_in_widget_only_post_types'] = [];
}
update_option( 'toc-options', $this->options );
}
public function get_exclude_post_types() {
return $this->exclude_post_types;
}
public function plugin_action_links( $links, $file ) {
if ( 'table-of-contents-plus/toc.php' === $file ) {
$settings_link = '' . __( 'Settings', 'table-of-contents-plus' ) . '';
$links = array_merge( [ $settings_link ], $links );
}
return $links;
}
public function shortcode_toc( $attributes ) {
$atts = shortcode_atts(
[
'label' => $this->options['heading_text'],
'label_show' => $this->options['visibility_show'],
'label_hide' => $this->options['visibility_hide'],
'no_label' => false,
'class' => false,
'wrapping' => $this->options['wrapping'],
'heading_levels' => $this->options['heading_levels'],
'exclude' => $this->options['exclude'],
'collapse' => false,
'no_numbers' => false,
'start' => $this->options['start'],
],
$attributes
);
$re_enqueue_scripts = false;
if ( $atts['no_label'] ) {
$this->options['show_heading_text'] = false;
}
if ( $atts['label'] ) {
$this->options['heading_text'] = wp_kses_post( html_entity_decode( $atts['label'] ) );
}
if ( $atts['label_show'] ) {
$this->options['visibility_show'] = wp_kses_post( html_entity_decode( $atts['label_show'] ) );
$re_enqueue_scripts = true;
}
if ( $atts['label_hide'] ) {
$this->options['visibility_hide'] = wp_kses_post( html_entity_decode( $atts['label_hide'] ) );
$re_enqueue_scripts = true;
}
if ( $atts['class'] ) {
$this->options['css_container_class'] = wp_kses_post( html_entity_decode( $atts['class'] ) );
}
if ( $atts['wrapping'] ) {
switch ( strtolower( trim( $atts['wrapping'] ) ) ) {
case 'left':
$this->options['wrapping'] = TOC_WRAPPING_LEFT;
break;
case 'right':
$this->options['wrapping'] = TOC_WRAPPING_RIGHT;
break;
default:
// do nothing
}
}
if ( $atts['exclude'] ) {
$this->options['exclude'] = $atts['exclude'];
}
if ( $atts['collapse'] ) {
$this->options['visibility_hide_by_default'] = true;
$re_enqueue_scripts = true;
}
if ( $atts['no_numbers'] ) {
$this->options['ordered_list'] = false;
}
if ( is_numeric( $atts['start'] ) ) {
$this->options['start'] = $atts['start'];
}
if ( $re_enqueue_scripts ) {
do_action( 'wp_enqueue_scripts' );
}
// if $atts['heading_levels'] is an array, then it came from the global options
// and wasn't provided by per instance
if ( $atts['heading_levels'] && ! is_array( $atts['heading_levels'] ) ) {
// make sure they are numbers between 1 and 6 and put into
// the $clean_heading_levels array if not already
$clean_heading_levels = [];
foreach ( explode( ',', $atts['heading_levels'] ) as $heading_level ) {
if ( is_numeric( $heading_level ) ) {
$heading_level = (int) $heading_level;
if ( 1 <= $heading_level && $heading_level <= 6 ) {
if ( ! in_array( $heading_level, $clean_heading_levels, true ) ) {
$clean_heading_levels[] = (int) $heading_level;
}
}
}
}
if ( count( $clean_heading_levels ) > 0 ) {
$this->options['heading_levels'] = $clean_heading_levels;
}
}
if ( ! is_search() && ! is_archive() && ! is_feed() ) {
return '';
} else {
return '';
}
}
public function shortcode_no_toc( $atts ) {
$this->show_toc = false;
return '';
}
public function shortcode_sitemap( $atts ) {
$html = '';
// only do the following if enabled
if ( $this->options['sitemap_show_page_listing'] || $this->options['sitemap_show_category_listing'] ) {
$html = '
';
if ( $this->options['sitemap_show_page_listing'] ) {
$html .=
'
options['sitemap_heading_type'] . ' class="toc_sitemap_pages">' . htmlentities( $this->options['sitemap_pages'], ENT_COMPAT, 'UTF-8' ) . 'options['sitemap_heading_type'] . '>' .
'
' .
wp_list_pages(
[
'title_li' => '',
'echo' => false,
]
) .
'
';
}
if ( $this->options['sitemap_show_category_listing'] ) {
$html .=
'
options['sitemap_heading_type'] . ' class="toc_sitemap_categories">' . htmlentities( $this->options['sitemap_categories'], ENT_COMPAT, 'UTF-8' ) . 'options['sitemap_heading_type'] . '>' .
'
' .
wp_list_categories(
[
'title_li' => '',
'echo' => false,
]
) .
'
';
}
$html .= '
';
}
return $html;
}
public function shortcode_sitemap_pages( $attributes ) {
$atts = shortcode_atts(
[
'heading' => $this->options['sitemap_heading_type'],
'label' => $this->options['sitemap_pages'],
'no_label' => false,
'exclude' => '',
'exclude_tree' => '',
'child_of' => 0,
],
$attributes
);
$atts['heading'] = intval( $atts['heading'] ); // make sure it's an integer
if ( $atts['heading'] < 1 || $atts['heading'] > 6 ) { // h1 to h6 are valid
$atts['heading'] = $this->options['sitemap_heading_type'];
}
if ( 'current' === strtolower( $atts['child_of'] ) ) {
$atts['child_of'] = get_the_ID();
} elseif ( is_numeric( $atts['child_of'] ) ) {
$atts['child_of'] = intval( $atts['child_of'] );
} else {
$atts['child_of'] = 0;
}
$html = '';
if ( ! $atts['no_label'] ) {
$html .= '
' . htmlentities( $atts['label'], ENT_COMPAT, 'UTF-8' ) . '';
}
$html .=
'
' .
wp_list_pages(
[
'title_li' => '',
'echo' => false,
'exclude' => $atts['exclude'],
'exclude_tree' => $atts['exclude_tree'],
'hierarchical' => true,
'child_of' => $atts['child_of'],
]
) .
'
' .
'
';
return $html;
}
public function shortcode_sitemap_categories( $attributes ) {
$atts = shortcode_atts(
[
'heading' => $this->options['sitemap_heading_type'],
'label' => $this->options['sitemap_categories'],
'no_label' => false,
'exclude' => '',
'exclude_tree' => '',
],
$attributes
);
$atts['heading'] = intval( $atts['heading'] ); // make sure it's an integer
if ( $atts['heading'] < 1 || $atts['heading'] > 6 ) { // h1 to h6 are valid
$atts['heading'] = $this->options['sitemap_heading_type'];
}
$html = '';
if ( ! $atts['no_label'] ) {
$html .= '
' . htmlentities( $atts['label'], ENT_COMPAT, 'UTF-8' ) . '';
}
$html .=
'
' .
wp_list_categories(
[
'title_li' => '',
'echo' => false,
'exclude' => $atts['exclude'],
'exclude_tree' => $atts['exclude_tree'],
]
) .
'
' .
'
';
return $html;
}
public function shortcode_sitemap_posts( $attributes ) {
$atts = shortcode_atts(
[
'order' => 'ASC',
'orderby' => 'title',
'separate' => true,
],
$attributes
);
$articles = new WP_Query(
[
'post_type' => 'post',
'post_status' => 'publish',
'order' => $atts['order'],
'orderby' => $atts['orderby'],
'posts_per_page' => -1,
]
);
$html = '';
$letter = '';
$atts['separate'] = strtolower( $atts['separate'] );
if ( 'false' === $atts['separate'] || 'no' === $atts['separate'] ) {
$atts['separate'] = false;
}
while ( $articles->have_posts() ) {
$articles->the_post();
$title = wp_strip_all_tags( get_the_title() );
if ( $atts['separate'] ) {
if ( strtolower( $title[0] ) !== $letter ) {
if ( $letter ) {
$html .= '';
}
$html .= '' . strtolower( $title[0] ) . '
';
$letter = strtolower( $title[0] );
}
}
$html .= '- ' . $title . '
';
}
if ( $html ) {
if ( $atts['separate'] ) {
$html .= '
';
} else {
$html = '';
}
}
wp_reset_postdata();
return $html;
}
/**
* Register and load CSS and javascript files for frontend.
*/
public function wp_enqueue_scripts() {
$js_vars = [];
// register our CSS and scripts
wp_register_style( 'toc-screen', TOC_PLUGIN_PATH . '/screen.min.css', [], TOC_VERSION );
wp_register_script( 'toc-front', TOC_PLUGIN_PATH . '/front.min.js', [ 'jquery' ], TOC_VERSION, true );
// enqueue them!
if ( ! $this->options['exclude_css'] ) {
wp_enqueue_style( 'toc-screen' );
// add any admin GUI customisations
$custom_css = $this->get_custom_css();
if ( $custom_css ) {
wp_add_inline_style( 'toc-screen', $custom_css );
}
}
if ( $this->options['smooth_scroll'] ) {
$js_vars['smooth_scroll'] = true;
}
wp_enqueue_script( 'toc-front' );
if ( $this->options['show_heading_text'] && $this->options['visibility'] ) {
$width = ( 'User defined' !== $this->options['width'] ) ? $this->options['width'] : $this->options['width_custom'] . $this->options['width_custom_units'];
$js_vars['visibility_show'] = esc_js( $this->options['visibility_show'] );
$js_vars['visibility_hide'] = esc_js( $this->options['visibility_hide'] );
if ( $this->options['visibility_hide_by_default'] ) {
$js_vars['visibility_hide_by_default'] = true;
}
$js_vars['width'] = esc_js( $width );
}
if ( TOC_SMOOTH_SCROLL_OFFSET !== $this->options['smooth_scroll_offset'] ) {
$js_vars['smooth_scroll_offset'] = esc_js( $this->options['smooth_scroll_offset'] );
}
if ( count( $js_vars ) > 0 ) {
wp_localize_script(
'toc-front',
'tocplus',
$js_vars
);
}
}
public function plugins_loaded() {
load_plugin_textdomain( 'table-of-contents-plus', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
}
public function admin_init() {
wp_register_script( 'toc_admin_script', TOC_PLUGIN_PATH . '/admin.js', [], TOC_VERSION, true );
wp_register_style( 'toc_admin_style', TOC_PLUGIN_PATH . '/admin.css', [], TOC_VERSION );
}
public function admin_menu() {
$page = add_submenu_page(
'options-general.php',
__( 'TOC', 'table-of-contents-plus' ) . '+',
__( 'TOC', 'table-of-contents-plus' ) . '+',
'manage_options',
'toc',
[ $this, 'admin_options' ]
);
add_action( 'admin_print_styles-' . $page, [ $this, 'admin_options_head' ] );
}
public function widgets_init() {
register_widget( 'toc_widget' );
}
/**
* Remove widget options on widget deletion
*/
public function sidebar_admin_setup() {
// this action is loaded at the start of the widget screen
// so only do the following when a form action has been initiated
if ( 'post' === strtolower( $_SERVER['REQUEST_METHOD'] ) ) {
if ( isset( $_POST['id_base'] ) && 'toc-widget' === $_POST['id_base'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( isset( $_POST['delete_widget'] ) && 1 === (int) $_POST['delete_widget'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
$this->set_show_toc_in_widget_only( false );
$this->set_show_toc_in_widget_only_post_types( [ 'page' ] );
}
}
}
}
public function init() {
// Add compatibility with Rank Math SEO
if ( class_exists( 'RankMath' ) ) {
add_filter(
'rank_math/researches/toc_plugins',
function ( $toc_plugins ) {
$toc_plugins['table-of-contents-plus/toc.php'] = 'Table of Contents Plus';
return $toc_plugins;
}
);
}
}
/**
* Load needed scripts and styles only on the toc administration interface.
*/
public function admin_options_head() {
wp_enqueue_style( 'farbtastic' );
wp_enqueue_script( 'farbtastic' );
wp_enqueue_script( 'jquery' );
wp_enqueue_script( 'toc_admin_script' );
wp_enqueue_style( 'toc_admin_style' );
}
/**
* Tries to convert $input into a valid hex colour.
* Returns $default if $input is not a hex value, otherwise returns verified hex.
*/
private function hex_value( $input = '', $default = '#' ) {
$return = $default;
if ( $input ) {
// strip out non hex chars
$return = preg_replace( '/[^a-fA-F0-9]*/', '', $input );
switch ( strlen( $return ) ) {
case 3: // do next
case 6:
$return = '#' . $return;
break;
default:
if ( strlen( $return ) > 6 ) {
$return = '#' . substr( $return, 0, 6 ); // if > 6 chars, then take the first 6
} elseif ( strlen( $return ) > 3 && strlen( $return ) < 6 ) {
$return = '#' . substr( $return, 0, 3 ); // if between 3 and 6, then take first 3
} else {
$return = $default; // not valid, return $default
}
}
}
return $return;
}
private function save_admin_options() {
global $post_id;
// security check
if ( ! isset( $_POST['toc-admin-options'] ) ) {
return false;
}
if ( ! wp_verify_nonce( $_POST['toc-admin-options'], plugin_basename( __FILE__ ) ) ) {
return false;
}
// require an administrator level to save
if ( ! current_user_can( 'manage_options', $post_id ) ) {
return false;
}
// use stripslashes on free text fields that can have ' " \
// WordPress automatically slashes these characters as part of
// wp-includes/load.php::wp_magic_quotes()
$custom_background_colour = $this->hex_value( trim( $_POST['custom_background_colour'] ), TOC_DEFAULT_BACKGROUND_COLOUR );
$custom_border_colour = $this->hex_value( trim( $_POST['custom_border_colour'] ), TOC_DEFAULT_BORDER_COLOUR );
$custom_title_colour = $this->hex_value( trim( $_POST['custom_title_colour'] ), TOC_DEFAULT_TITLE_COLOUR );
$custom_links_colour = $this->hex_value( trim( $_POST['custom_links_colour'] ), TOC_DEFAULT_LINKS_COLOUR );
$custom_links_hover_colour = $this->hex_value( trim( $_POST['custom_links_hover_colour'] ), TOC_DEFAULT_LINKS_HOVER_COLOUR );
$custom_links_visited_colour = $this->hex_value( trim( $_POST['custom_links_visited_colour'] ), TOC_DEFAULT_LINKS_VISITED_COLOUR );
$restrict_path = trim( $_POST['restrict_path'] );
if ( $restrict_path ) {
if ( strpos( $restrict_path, '/' ) !== 0 ) {
// restrict path did not start with a / so unset it
$restrict_path = '';
}
}
$this->options = array_merge(
$this->options,
[
'fragment_prefix' => trim( $_POST['fragment_prefix'] ),
'position' => intval( $_POST['position'] ),
'start' => intval( $_POST['start'] ),
'show_heading_text' => ( isset( $_POST['show_heading_text'] ) && $_POST['show_heading_text'] ) ? true : false,
'heading_text' => stripslashes( trim( $_POST['heading_text'] ) ),
'auto_insert_post_types' => ( isset( $_POST['auto_insert_post_types'] ) ) ? (array) $_POST['auto_insert_post_types'] : array(),
'show_heirarchy' => ( isset( $_POST['show_heirarchy'] ) && $_POST['show_heirarchy'] ) ? true : false,
'ordered_list' => ( isset( $_POST['ordered_list'] ) && $_POST['ordered_list'] ) ? true : false,
'smooth_scroll' => ( isset( $_POST['smooth_scroll'] ) && $_POST['smooth_scroll'] ) ? true : false,
'smooth_scroll_offset' => intval( $_POST['smooth_scroll_offset'] ),
'visibility' => ( isset( $_POST['visibility'] ) && $_POST['visibility'] ) ? true : false,
'visibility_show' => stripslashes( trim( $_POST['visibility_show'] ) ),
'visibility_hide' => stripslashes( trim( $_POST['visibility_hide'] ) ),
'visibility_hide_by_default' => ( isset( $_POST['visibility_hide_by_default'] ) && $_POST['visibility_hide_by_default'] ) ? true : false,
'width' => trim( $_POST['width'] ),
'width_custom' => floatval( $_POST['width_custom'] ),
'width_custom_units' => trim( $_POST['width_custom_units'] ),
'wrapping' => intval( $_POST['wrapping'] ),
'font_size' => floatval( $_POST['font_size'] ),
'font_size_units' => trim( $_POST['font_size_units'] ),
'theme' => intval( $_POST['theme'] ),
'custom_background_colour' => $custom_background_colour,
'custom_border_colour' => $custom_border_colour,
'custom_title_colour' => $custom_title_colour,
'custom_links_colour' => $custom_links_colour,
'custom_links_hover_colour' => $custom_links_hover_colour,
'custom_links_visited_colour' => $custom_links_visited_colour,
'lowercase' => ( isset( $_POST['lowercase'] ) && $_POST['lowercase'] ) ? true : false,
'hyphenate' => ( isset( $_POST['hyphenate'] ) && $_POST['hyphenate'] ) ? true : false,
'bullet_spacing' => ( isset( $_POST['bullet_spacing'] ) && $_POST['bullet_spacing'] ) ? true : false,
'include_homepage' => ( isset( $_POST['include_homepage'] ) && $_POST['include_homepage'] ) ? true : false,
'exclude_css' => ( isset( $_POST['exclude_css'] ) && $_POST['exclude_css'] ) ? true : false,
'heading_levels' => ( isset( $_POST['heading_levels'] ) ) ? array_map( 'intval', (array) $_POST['heading_levels'] ) : array(),
'exclude' => stripslashes( trim( $_POST['exclude'] ) ),
'restrict_path' => $restrict_path,
'sitemap_show_page_listing' => ( isset( $_POST['sitemap_show_page_listing'] ) && $_POST['sitemap_show_page_listing'] ) ? true : false,
'sitemap_show_category_listing' => ( isset( $_POST['sitemap_show_category_listing'] ) && $_POST['sitemap_show_category_listing'] ) ? true : false,
'sitemap_heading_type' => intval( $_POST['sitemap_heading_type'] ),
'sitemap_pages' => stripslashes( trim( $_POST['sitemap_pages'] ) ),
'sitemap_categories' => stripslashes( trim( $_POST['sitemap_categories'] ) ),
]
);
// update_option will return false if no changes were made
update_option( 'toc-options', $this->options );
return true;
}
public function admin_options() {
$msg = '';
// was there a form submission, if so, do security checks and try to save form
if ( isset( $_GET['update'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $this->save_admin_options() ) {
$msg = '' . __( 'Options saved.', 'table-of-contents-plus' ) . '
';
} else {
$msg = '' . __( 'Save failed.', 'table-of-contents-plus' ) . '
';
}
}
?>
options['exclude_css'] ) {
if ( TOC_THEME_CUSTOM === $this->options['theme'] || 'Auto' !== $this->options['width'] ) {
$css .= 'div#toc_container {';
if ( TOC_THEME_CUSTOM === $this->options['theme'] ) {
$css .= 'background: ' . $this->options['custom_background_colour'] . ';border: 1px solid ' . $this->options['custom_border_colour'] . ';';
}
if ( 'Auto' !== $this->options['width'] ) {
$css .= 'width: ';
if ( 'User defined' !== $this->options['width'] ) {
$css .= $this->options['width'];
} else {
$css .= $this->options['width_custom'] . $this->options['width_custom_units'];
}
$css .= ';';
}
$css .= '}';
}
if ( '95%' !== $this->options['font_size'] . $this->options['font_size_units'] ) {
$css .= 'div#toc_container ul li {font-size: ' . $this->options['font_size'] . $this->options['font_size_units'] . ';}';
}
if ( TOC_THEME_CUSTOM === $this->options['theme'] ) {
if ( TOC_DEFAULT_TITLE_COLOUR !== $this->options['custom_title_colour'] ) {
$css .= 'div#toc_container p.toc_title {color: ' . $this->options['custom_title_colour'] . ';}';
}
if ( TOC_DEFAULT_LINKS_COLOUR !== $this->options['custom_links_colour'] ) {
$css .= 'div#toc_container p.toc_title a,div#toc_container ul.toc_list a {color: ' . $this->options['custom_links_colour'] . ';}';
}
if ( TOC_DEFAULT_LINKS_HOVER_COLOUR !== $this->options['custom_links_hover_colour'] ) {
$css .= 'div#toc_container p.toc_title a:hover,div#toc_container ul.toc_list a:hover {color: ' . $this->options['custom_links_hover_colour'] . ';}';
}
if ( TOC_DEFAULT_LINKS_HOVER_COLOUR !== $this->options['custom_links_hover_colour'] ) {
$css .= 'div#toc_container p.toc_title a:hover,div#toc_container ul.toc_list a:hover {color: ' . $this->options['custom_links_hover_colour'] . ';}';
}
if ( TOC_DEFAULT_LINKS_VISITED_COLOUR !== $this->options['custom_links_visited_colour'] ) {
$css .= 'div#toc_container p.toc_title a:visited,div#toc_container ul.toc_list a:visited {color: ' . $this->options['custom_links_visited_colour'] . ';}';
}
}
}
return $css;
}
/**
* Returns a clean url to be used as the destination anchor target
*/
private function url_anchor_target( $title ) {
$return = false;
if ( $title ) {
$return = trim( wp_strip_all_tags( $title ) );
// convert accented characters to ASCII
$return = remove_accents( $return );
// replace newlines with spaces (eg when headings are split over multiple lines)
$return = str_replace( [ "\r", "\n", "\n\r", "\r\n" ], ' ', $return );
// remove &
$return = str_replace( '&', '', $return );
// remove non alphanumeric chars
$return = preg_replace( '/[^a-zA-Z0-9 \-_]*/', '', $return );
// convert spaces to _
$return = str_replace(
[ ' ', ' ' ],
'_',
$return
);
// remove trailing - and _
$return = rtrim( $return, '-_' );
// lowercase everything?
if ( $this->options['lowercase'] ) {
$return = strtolower( $return );
}
// if blank, then prepend with the fragment prefix
// blank anchors normally appear on sites that don't use the latin charset
if ( ! $return ) {
$return = ( $this->options['fragment_prefix'] ) ? $this->options['fragment_prefix'] : '_';
}
// hyphenate?
if ( $this->options['hyphenate'] ) {
$return = str_replace( '_', '-', $return );
$return = str_replace( '--', '-', $return );
}
}
if ( array_key_exists( $return, $this->collision_collector ) ) {
$this->collision_collector[ $return ]++;
$return .= '-' . $this->collision_collector[ $return ];
} else {
$this->collision_collector[ $return ] = 1;
}
return apply_filters( 'toc_url_anchor_target', $return );
}
private function build_hierarchy( &$matches ) {
$current_depth = 100; // headings can't be larger than h6 but 100 as a default to be sure
$html = '';
$numbered_items = [];
$numbered_items_min = null;
$count_matches = count( $matches );
// reset the internal collision collection
$this->collision_collector = [];
// find the minimum heading to establish our baseline
for ( $i = 0; $i < $count_matches; $i++ ) {
if ( $current_depth > $matches[ $i ][2] ) {
$current_depth = (int) $matches[ $i ][2];
}
}
$numbered_items[ $current_depth ] = 0;
$numbered_items_min = $current_depth;
for ( $i = 0; $i < $count_matches; $i++ ) {
if ( $current_depth === (int) $matches[ $i ][2] ) {
$html .= '';
}
// start lists
if ( $current_depth !== (int) $matches[ $i ][2] ) {
for ( $current_depth; $current_depth < (int) $matches[ $i ][2]; $current_depth++ ) {
$numbered_items[ $current_depth + 1 ] = 0;
$html .= '';
$numbered_items[ $current_depth ] = 0;
}
}
if ( (int) @$matches[ $i + 1 ][2] === $current_depth ) {
$html .= '';
}
} else {
// this is the last item, make sure we close off all tags
for ( $current_depth; $current_depth >= $numbered_items_min; $current_depth-- ) {
$html .= '';
if ( $current_depth !== $numbered_items_min ) {
$html .= '';
}
}
}
}
return $html;
}
/**
* Returns a string with all items from the $find array replaced with their matching
* items in the $replace array. This does a one to one replacement (rather than
* globally).
*
* This function is multibyte safe.
*
* $find and $replace are arrays, $string is the haystack. All variables are
* passed by reference.
*/
private function mb_find_replace( &$find = false, &$replace = false, &$string = '' ) {
if ( is_array( $find ) && is_array( $replace ) && $string ) {
$count_find = count( $find );
// check if multibyte strings are supported
if ( function_exists( 'mb_strpos' ) ) {
for ( $i = 0; $i < $count_find; $i++ ) {
$string =
mb_substr( $string, 0, mb_strpos( $string, $find[ $i ] ) ) . // everything before $find
$replace[ $i ] . // its replacement
mb_substr( $string, mb_strpos( $string, $find[ $i ] ) + mb_strlen( $find[ $i ] ) ); // everything after $find
}
} else {
for ( $i = 0; $i < $count_find; $i++ ) {
$string = substr_replace(
$string,
$replace[ $i ],
strpos( $string, $find[ $i ] ),
strlen( $find[ $i ] )
);
}
}
}
return $string;
}
/**
* This function extracts headings from the html formatted $content. It will pull out
* only the required headings as specified in the options. For all qualifying headings,
* this function populates the $find and $replace arrays (both passed by reference)
* with what to search and replace with.
*
* Returns a html formatted string of list items for each qualifying heading. This
* is everything between and NOT including
*/
public function extract_headings( &$find, &$replace, $content = '' ) {
$matches = [];
$anchor = '';
$items = false;
// reset the internal collision collection as the_content may have been triggered elsewhere
// eg by themes or other plugins that need to read in content such as metadata fields in
// the head html tag, or to provide descriptions to twitter/facebook
$this->collision_collector = [];
if ( is_array( $find ) && is_array( $replace ) && $content ) {
// filter the content
$content = apply_filters( 'toc_extract_headings', $content );
// get all headings
// the html spec allows for a maximum of 6 heading depths
if ( preg_match_all( '/(]*>).*<\/h\2>/msuU', $content, $matches, PREG_SET_ORDER ) ) {
// remove undesired headings (if any) as defined by heading_levels
if ( count( $this->options['heading_levels'] ) !== 6 ) {
$new_matches = [];
$count_matches = count( $matches );
for ( $i = 0; $i < $count_matches; $i++ ) {
if ( in_array( (int) $matches[ $i ][2], $this->options['heading_levels'], true ) ) {
$new_matches[] = $matches[ $i ];
}
}
$matches = $new_matches;
}
// remove specific headings if provided via the 'exclude' property
if ( $this->options['exclude'] ) {
$excluded_headings = explode( '|', $this->options['exclude'] );
$count_excluded_headings = count( $excluded_headings );
if ( $count_excluded_headings > 0 ) {
for ( $j = 0; $j < $count_excluded_headings; $j++ ) {
// escape some regular expression characters
// others: http://www.php.net/manual/en/regexp.reference.meta.php
$excluded_headings[ $j ] = str_replace(
[ '*' ],
[ '.*' ],
trim( $excluded_headings[ $j ] )
);
}
$new_matches = [];
$count_matches = count( $matches );
for ( $i = 0; $i < $count_matches; $i++ ) {
$found = false;
$count_excluded_headings = count( $excluded_headings );
for ( $j = 0; $j < $count_excluded_headings; $j++ ) {
if ( @preg_match( '/^' . $excluded_headings[ $j ] . '$/imU', wp_strip_all_tags( $matches[ $i ][0] ) ) ) {
$found = true;
break;
}
}
if ( ! $found ) {
$new_matches[] = $matches[ $i ];
}
}
if ( count( $matches ) !== count( $new_matches ) ) {
$matches = $new_matches;
}
}
}
// remove empty headings
$new_matches = [];
$count_matches = count( $matches );
for ( $i = 0; $i < $count_matches; $i++ ) {
if ( trim( wp_strip_all_tags( $matches[ $i ][0] ) ) !== false ) {
$new_matches[] = $matches[ $i ];
}
}
if ( count( $matches ) !== count( $new_matches ) ) {
$matches = $new_matches;
}
// check minimum number of headings
if ( count( $matches ) >= $this->options['start'] ) {
$count_matches = count( $matches );
for ( $i = 0; $i < $count_matches; $i++ ) {
// get anchor and add to find and replace arrays
$anchor = $this->url_anchor_target( $matches[ $i ][0] );
$find[] = $matches[ $i ][0];
$replace[] = str_replace(
[
$matches[ $i ][1], // start of heading
'', // end of heading
],
[
$matches[ $i ][1] . '',
'',
],
$matches[ $i ][0]
);
// assemble flat list
if ( ! $this->options['show_heirarchy'] ) {
$items .= '';
if ( $this->options['ordered_list'] ) {
$items .= count( $replace ) . ' ';
}
$items .= wp_strip_all_tags( $matches[ $i ][0] ) . '';
}
}
// build a hierarchical toc?
// we could have tested for $items but that var can be quite large in some cases
if ( $this->options['show_heirarchy'] ) {
$items = $this->build_hierarchy( $matches );
}
}
}
}
return $items;
}
/**
* Returns true if the table of contents is eligible to be printed, false otherwise.
*/
public function is_eligible( $shortcode_used = false ) {
global $post;
// do not trigger the TOC on REST Requests
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
return false;
}
// do not trigger the TOC when displaying an XML/RSS feed
if ( is_feed() ) {
return false;
}
// if the shortcode was used, this bypasses many of the global options
if ( false !== $shortcode_used ) {
// shortcode is used, make sure it adheres to the exclude from
// homepage option if we're on the homepage
if ( ! $this->options['include_homepage'] && is_front_page() ) {
return false;
} else {
return true;
}
} else {
if (
( in_array( get_post_type( $post ), $this->options['auto_insert_post_types'], true ) && $this->show_toc && ! is_search() && ! is_archive() && ! is_front_page() ) ||
( $this->options['include_homepage'] && is_front_page() )
) {
if ( $this->options['restrict_path'] ) {
if ( strpos( $_SERVER['REQUEST_URI'], $this->options['restrict_path'] ) === 0 ) {
return true;
} else {
return false;
}
} else {
return true;
}
} else {
return false;
}
}
}
public function the_content( $content ) {
global $post;
$items = '';
$css_classes = '';
$anchor = '';
$find = [];
$replace = [];
$custom_toc_position = strpos( $content, '' );
if ( $this->is_eligible( $custom_toc_position ) ) {
$items = $this->extract_headings( $find, $replace, $content );
if ( $items ) {
// do we display the toc within the content or has the user opted
// to only show it in the widget? if so, then we still need to
// make the find/replace call to insert the anchors
if ( $this->options['show_toc_in_widget_only'] && ( in_array( get_post_type(), $this->options['show_toc_in_widget_only_post_types'], true ) ) ) {
$content = $this->mb_find_replace( $find, $replace, $content );
} else {
// wrapping css classes
switch ( $this->options['wrapping'] ) {
case TOC_WRAPPING_LEFT:
$css_classes .= ' toc_wrap_left';
break;
case TOC_WRAPPING_RIGHT:
$css_classes .= ' toc_wrap_right';
break;
case TOC_WRAPPING_NONE:
default:
// do nothing
}
// colour themes
switch ( $this->options['theme'] ) {
case TOC_THEME_LIGHT_BLUE:
$css_classes .= ' toc_light_blue';
break;
case TOC_THEME_WHITE:
$css_classes .= ' toc_white';
break;
case TOC_THEME_BLACK:
$css_classes .= ' toc_black';
break;
case TOC_THEME_TRANSPARENT:
$css_classes .= ' toc_transparent';
break;
case TOC_THEME_GREY:
default:
// do nothing
}
// bullets?
if ( $this->options['bullet_spacing'] ) {
$css_classes .= ' have_bullets';
} else {
$css_classes .= ' no_bullets';
}
if ( $this->options['css_container_class'] ) {
$css_classes .= ' ' . $this->options['css_container_class'];
}
$css_classes = trim( $css_classes );
// an empty class="" is invalid markup!
if ( ! $css_classes ) {
$css_classes = ' ';
}
// add container, toc title and list items
$html = '';
if ( $this->options['show_heading_text'] ) {
$toc_title = htmlentities( $this->options['heading_text'], ENT_COMPAT, 'UTF-8' );
if ( false !== strpos( $toc_title, '%PAGE_TITLE%' ) ) {
$toc_title = str_replace( '%PAGE_TITLE%', get_the_title(), $toc_title );
}
if ( false !== strpos( $toc_title, '%PAGE_NAME%' ) ) {
$toc_title = str_replace( '%PAGE_NAME%', get_the_title(), $toc_title );
}
$html .= '
' . $toc_title . '
';
}
$html .= '
' . "\n";
if ( false !== $custom_toc_position ) {
$find[] = '';
$replace[] = $html;
$content = $this->mb_find_replace( $find, $replace, $content );
} else {
if ( count( $find ) > 0 ) {
switch ( $this->options['position'] ) {
case TOC_POSITION_TOP:
$content = $html . $this->mb_find_replace( $find, $replace, $content );
break;
case TOC_POSITION_BOTTOM:
$content = $this->mb_find_replace( $find, $replace, $content ) . $html;
break;
case TOC_POSITION_AFTER_FIRST_HEADING:
$replace[0] = $replace[0] . $html;
$content = $this->mb_find_replace( $find, $replace, $content );
break;
case TOC_POSITION_BEFORE_FIRST_HEADING:
default:
$replace[0] = $html . $replace[0];
$content = $this->mb_find_replace( $find, $replace, $content );
}
}
}
}
}
} else {
// remove (inserted from shortcode) from content
$content = str_replace( '', '', $content );
}
return $content;
}
} // end class
endif;