| 
          <?php | 
        
        
           | 
          /* | 
        
        
           | 
           * Plugin Name: F1 Table of Contents | 
        
        
           | 
           * Version: 0.1.4 | 
        
        
           | 
           * Description: | 
        
        
           | 
           * Author: Forum One, Russell Heimlich | 
        
        
           | 
           * GitHub Plugin URI: https://github.com/forumone/f1-table-of-contents | 
        
        
           | 
           */ | 
        
        
           | 
          
 | 
        
        
           | 
          class F1_Table_Of_Contents { | 
        
        
           | 
          
 | 
        
        
           | 
          	// Properties | 
        
        
           | 
          	/** | 
        
        
           | 
          	 * Holds objects with information about each heading. | 
        
        
           | 
          	 * @var array | 
        
        
           | 
          	 */ | 
        
        
           | 
          	public $headings = array(); | 
        
        
           | 
          
 | 
        
        
           | 
          	/** | 
        
        
           | 
          	 * Collection of strings that need to be found and replaced. | 
        
        
           | 
          	 * @var array | 
        
        
           | 
          	 */ | 
        
        
           | 
          	public $find = array(); | 
        
        
           | 
          
 | 
        
        
           | 
          	/** | 
        
        
           | 
          	 * Collection of strings that are replacements to be found. | 
        
        
           | 
          	 * @var array | 
        
        
           | 
          	 */ | 
        
        
           | 
          	public $replace = array(); | 
        
        
           | 
          
 | 
        
        
           | 
          	/** | 
        
        
           | 
          	 * Collection of integers representing $header indexes that have already been processed during build_html_tree() | 
        
        
           | 
          	 * @var array | 
        
        
           | 
          	 */ | 
        
        
           | 
          	public $html_tree_processed = array(); | 
        
        
           | 
          
 | 
        
        
           | 
          	// Methods | 
        
        
           | 
          	public function __construct() {} | 
        
        
           | 
          
 | 
        
        
           | 
          	/** | 
        
        
           | 
          	 * Sets up hooks this class uses. | 
        
        
           | 
          	 */ | 
        
        
           | 
          	public function setup() { | 
        
        
           | 
          		add_filter( 'the_content', array( $this, 'the_content' ), 100 ); // run after shortcodes are interpretted (level 10) | 
        
        
           | 
          	} | 
        
        
           | 
          
 | 
        
        
           | 
          	/** | 
        
        
           | 
          	 * Replaces opening heading tags to add an anchor element that can be linked to. | 
        
        
           | 
          	 * @param  string $the_content The post content to be modified. | 
        
        
           | 
          	 * @return string	The modified content. | 
        
        
           | 
          	 */ | 
        
        
           | 
          	public function the_content( $the_content ) { | 
        
        
           | 
          		if( !is_singular() ) { | 
        
        
           | 
          			return $the_content; | 
        
        
           | 
          		} | 
        
        
           | 
          
 | 
        
        
           | 
          		$this->get_headings( $the_content ); | 
        
        
           | 
          
 | 
        
        
           | 
          		return $this->mb_find_replace( $the_content ); | 
        
        
           | 
          	} | 
        
        
           | 
          
 | 
        
        
           | 
          	/** | 
        
        
           | 
          	 * Extract heading information from a string of text. | 
        
        
           | 
          	 * @param  string $text Text with headings in it to be extracted. | 
        
        
           | 
          	 * @return object	Return information about the headings extracted. | 
        
        
           | 
          	 */ | 
        
        
           | 
          	public function get_headings( $text ) { | 
        
        
           | 
          		$matches = array(); | 
        
        
           | 
          		$toc_anchor_text = apply_filters( 'f1_toc_anchor_text', '' ); // If you want to add text within the anchor link then hook into this filter. | 
        
        
           | 
          
 | 
        
        
           | 
          		if( preg_match_all( '/(<h([1-6]{1})[^>]*>).*<\/h\2>/msuU', $text, $matches, PREG_SET_ORDER ) ) { | 
        
        
           | 
          			/* | 
        
        
           | 
          			$matches[x][0] - Full HTML heading, <h1>Hello World!</h1> | 
        
        
           | 
          			$matches[x][1] - Opening headling tag, <h1> or <h2> etc. | 
        
        
           | 
          			$matches[x][2] - Numeric heading level, 1 or 2 | 
        
        
           | 
          			*/ | 
        
        
           | 
          
 | 
        
        
           | 
          			$output = array(); | 
        
        
           | 
          			for( $i = 0; $i < count( $matches ); $i++ ) { | 
        
        
           | 
          				// Get anchor and add to find and replace arrays | 
        
        
           | 
          				$anchor = $this->url_anchor_target( $matches[ $i ][0] ); | 
        
        
           | 
          				$this->find[] = $matches[ $i ][0]; | 
        
        
           | 
          				$this->replace[] = str_replace( | 
        
        
           | 
          					array( | 
        
        
           | 
          						$matches[ $i ][1], // Start of heading | 
        
        
           | 
          						'</h' . intval( $matches[ $i ][2] ) . '>',	// End of heading | 
        
        
           | 
          					), | 
        
        
           | 
          					array( | 
        
        
           | 
          						$matches[ $i ][1] . '<a id="' . esc_attr( $anchor ) . '" href="#' .  esc_attr( $anchor ) .'" class="toc-anchor">' . $toc_anchor_text . '</a>', | 
        
        
           | 
          						'</h' . intval( $matches[ $i ][2] ) . '>', | 
        
        
           | 
          					), | 
        
        
           | 
          					$matches[ $i ][0] | 
        
        
           | 
          				); | 
        
        
           | 
          
 | 
        
        
           | 
          				$output[] = (object) array( | 
        
        
           | 
          					'anchor' => $anchor, | 
        
        
           | 
          					'text' => strip_tags( $matches[ $i ][0] ), | 
        
        
           | 
          					'html' => $matches[ $i ][0], | 
        
        
           | 
          					'opening_tag' => $matches[ $i ][1], | 
        
        
           | 
          					'level' => intval($matches[ $i ][2]), | 
        
        
           | 
          				); | 
        
        
           | 
          			} | 
        
        
           | 
          
 | 
        
        
           | 
          			$this->headings = $output; | 
        
        
           | 
          
 | 
        
        
           | 
          			return $output; | 
        
        
           | 
          		} | 
        
        
           | 
          	} | 
        
        
           | 
          
 | 
        
        
           | 
          	/** | 
        
        
           | 
          	 * Convert a string to a hypenated slug. | 
        
        
           | 
          	 * @param  string $title The string to slugify. | 
        
        
           | 
          	 * @return string        Slugified version of the string. | 
        
        
           | 
          	 */ | 
        
        
           | 
          	private function url_anchor_target( $title = '' ) { | 
        
        
           | 
          		$return = false; | 
        
        
           | 
          
 | 
        
        
           | 
          		if ( $title ) { | 
        
        
           | 
          			$return = trim( strip_tags( $title ) ); | 
        
        
           | 
          
 | 
        
        
           | 
          			// Replace newlines with spaces (eg when headings are split over multiple lines) | 
        
        
           | 
          			$return = str_replace( array("\r", "\n", "\n\r", "\r\n"), ' ', $return ); | 
        
        
           | 
          
 | 
        
        
           | 
          			$return = sanitize_file_name( $return ); | 
        
        
           | 
          			$return = strtolower( $return ); | 
        
        
           | 
          		} | 
        
        
           | 
          
 | 
        
        
           | 
          		return $return; | 
        
        
           | 
          	} | 
        
        
           | 
          
 | 
        
        
           | 
          	/** | 
        
        
           | 
          	 * Multibyte-aware find and replace. | 
        
        
           | 
          	 * @param  string $string String to be searched | 
        
        
           | 
          	 * @return string	Modified string | 
        
        
           | 
          	 */ | 
        
        
           | 
          	private function mb_find_replace( &$string = '' ) { | 
        
        
           | 
          		// Check if multibyte strings are supported | 
        
        
           | 
          
 | 
        
        
           | 
          		$find = $this->find; | 
        
        
           | 
          		$replace = $this->replace; | 
        
        
           | 
          
 | 
        
        
           | 
          		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; | 
        
        
           | 
          	} | 
        
        
           | 
          
 | 
        
        
           | 
          	public function build_html_tree( $headers, $index = 0, $depth = 0 ) { | 
        
        
           | 
          		if( in_array( $index, $this->html_tree_processed ) ) { | 
        
        
           | 
          			return ''; | 
        
        
           | 
          		} | 
        
        
           | 
          
 | 
        
        
           | 
          		$tree = ''; | 
        
        
           | 
          		if( $depth === 0 ) { | 
        
        
           | 
          			$depth++; | 
        
        
           | 
          			foreach( $headers as $i => $header ) { | 
        
        
           | 
          				$tree .= $this->build_html_tree( $headers, $i, $depth ); | 
        
        
           | 
          			} | 
        
        
           | 
          
 | 
        
        
           | 
          			if( $tree ) { | 
        
        
           | 
          				$tree = '<ol class="f1-toc">' . $tree . '</ol>'; | 
        
        
           | 
          			} | 
        
        
           | 
          			return $tree; | 
        
        
           | 
          		} | 
        
        
           | 
          
 | 
        
        
           | 
          		$header = $headers[ $index ]; | 
        
        
           | 
          		$next_header = false; | 
        
        
           | 
          		if( isset( $headers[ $index + 1 ] ) ) { | 
        
        
           | 
          			$next_header = $headers[ $index + 1 ]; | 
        
        
           | 
          		} | 
        
        
           | 
          
 | 
        
        
           | 
          		$this->html_tree_processed[] = $index; | 
        
        
           | 
          		$tree .= '<li>'; | 
        
        
           | 
          		$tree .= '<a href="#' . $header->anchor . '">' . $header->text . '</a>'; | 
        
        
           | 
          		if( $next_header && $header->level < $next_header->level ) { | 
        
        
           | 
          			$node = $this->build_html_tree( $headers, $index + 1, $depth++ ); | 
        
        
           | 
          			if( $node ) { | 
        
        
           | 
          				$tree .= '<ol>' . $node . '</ol>'; | 
        
        
           | 
          			} | 
        
        
           | 
          		} | 
        
        
           | 
          		$tree .= '</li>'; | 
        
        
           | 
          
 | 
        
        
           | 
          		if( $next_header && $header->level == $next_header->level ) { | 
        
        
           | 
          			$node = $this->build_html_tree( $headers, $index + 1, $depth++ ); | 
        
        
           | 
          			if( $node ) { | 
        
        
           | 
          				$tree .= $node; | 
        
        
           | 
          			} | 
        
        
           | 
          		} | 
        
        
           | 
          
 | 
        
        
           | 
          		return $tree; | 
        
        
           | 
          	} | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          global $f1_table_of_contents; | 
        
        
           | 
          $f1_table_of_contents = new F1_Table_Of_Contents(); | 
        
        
           | 
          $f1_table_of_contents->setup(); | 
        
        
           | 
          
 | 
        
        
           | 
          
 | 
        
        
           | 
          
 | 
        
        
           | 
          /* Helper Functions */ | 
        
        
           | 
          
 | 
        
        
           | 
          
 | 
        
        
           | 
          /** | 
        
        
           | 
           * Gets header data from the content. | 
        
        
           | 
           * @return array List of objects containing header data | 
        
        
           | 
           */ | 
        
        
           | 
          function get_f1_toc() { | 
        
        
           | 
          	global $f1_table_of_contents; | 
        
        
           | 
          	return $f1_table_of_contents->headings; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          /** | 
        
        
           | 
           * Generate HTML markup based on the heading structure of the post content with anchor links to different headers. | 
        
        
           | 
           * @param  integer $limit The deepest level of headings to show | 
        
        
           | 
           * @return string	HTML output | 
        
        
           | 
           */ | 
        
        
           | 
          function f1_toc( $limit = 6 ) { | 
        
        
           | 
          	global $f1_table_of_contents; | 
        
        
           | 
          	$raw_headers = get_f1_toc(); | 
        
        
           | 
          	if( !$raw_headers ) { | 
        
        
           | 
          		return; | 
        
        
           | 
          	} | 
        
        
           | 
          
 | 
        
        
           | 
          	$headers = []; | 
        
        
           | 
          	foreach( $raw_headers as $header ) { | 
        
        
           | 
          		if( $header->level <= $limit ) { | 
        
        
           | 
          			$headers[] = $header; | 
        
        
           | 
          		} | 
        
        
           | 
          	} | 
        
        
           | 
          	$headers = apply_filters( 'f1_toc_headers', $headers ); | 
        
        
           | 
          
 | 
        
        
           | 
          	// Reset the array that keeps track of which headers have already been processed | 
        
        
           | 
          	$f1_table_of_contents->html_tree_processed = array(); | 
        
        
           | 
          	return $f1_table_of_contents->build_html_tree( $headers ); | 
        
        
           | 
          } |