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;