<?php /* Plugin Name: Extended Table of Contents (with nextpage support) Plugin URI: http://www.happybooking.de/wordpress/plugins/extended-toc Description: This plugin automatically generates and inserts a table of contents (ToC) to your pages and posts, based on tags h1-h6. Whenever the plugin discovers more than a certain amount of headings (default: 3) the ToC is inserted at the top of the page. This plugin also can handle posts that are divided into pages by the nextpage-wordpress-tag. By using the markups [extoc] you can decide where to insert the ToC. Also you can use a whitelist by disable general ToC insertion and insert the ToC to special pages/subpages/posts by [extoc]. Otherwise you can use a blacklist and disable the ToC only on special pages/subpages/posts by using the [noextoc] markup. Any feedback or suggestions are welcome. Version: 0.8.6 Author: Daniel Boldura, HappyBooking UG Author URI: http://www.happybooking.de/ /* Copyright 2013 HappyBooking UG // Daniel Boldura (email: info at happybooking.de or daniel at boldura.de) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /** * Planed features and todos: * * 1. Collision detection for anchors * 2. Header hierarchie * 3. Support markups for show or hide the ToC on single pages/posts * 4. Config the ToC within a markup e.g. [extoc start=5 headers=1,2,3 title="My table of contents"] oder [extoc start=5 headers=1,2,3 notitle] */ define( 'EXTENDED_TOC_VERSION', '0.8.6' ); define( 'EXTENDED_TOC_ID', 'extended_toc' ); define( 'EXTENDED_TOC_NAME', 'Extended-ToC' ); define( 'TOC_MIN_START', 2 ); define( 'TOC_MAX_START', 10 ); if( !class_exists('ExToC') ) { class ExToC { private $path; private $content = ""; private $fullcontent = ""; private $pages = array(); private $ID = 0; private $counter = array(); private $totalHeadings = 0; private $minLevel = null; public function __construct() { $this->path = plugins_url( '', __FILE__ ); $this->exclude_post_types = array( 'attachment', 'revision', 'nav_menu_item', 'safecss' ); // get options $defaults = array( // default options 'heading_text' => 'Contents', 'start' => 3, 'show_heading_text' => true, 'auto_insert_post_types' => array('page', 'post'), 'heading_levels' => array('1', '2', '3', '4', '5', '6'), 'show_hierarchy' => true, 'number_list_items' => true, ); $options = get_option( EXTENDED_TOC_ID, $defaults ); $this->options = wp_parse_args( $options, $defaults ); add_action( 'plugins_loaded', array(&$this, 'plugins_loaded') ); if( is_admin() ) { //Additional links on the plugin page add_filter('plugin_row_meta', array(&$this, 'register_plugin_links'), 10, 2); add_action('admin_init', array(&$this, 'admin_init')); add_action('admin_menu', array(&$this, 'admin_menu')); } else { /** Add the content filter and enqueue css **/ add_filter( 'the_content', array(&$this, 'the_content'), 100 ); add_action( 'wp_enqueue_scripts', array(&$this, 'wp_enqueue_scripts') ); } } public function __destruct() { } public function register_plugin_links($links, $file) { if( $file == plugin_basename(__FILE__) ) { $links[] = '<a href="http://www.happybooking.de/wordpress/plugins/extended-toc/donate">' . __('Donate', EXTENDED_TOC_ID) . '</a>'; } return $links; } public function admin_init() { wp_register_style( EXTENDED_TOC_ID, $this->path . '/admin-style.css', array(), EXTENDED_TOC_VERSION ); wp_enqueue_style(EXTENDED_TOC_ID); } public function admin_menu() { // Create menu tab $page = add_submenu_page( 'plugins.php', EXTENDED_TOC_NAME, EXTENDED_TOC_NAME, 'manage_options', EXTENDED_TOC_ID, array(&$this, 'admin_options') ); } private function save_admin_options() { global $post_id; // security check if ( !wp_verify_nonce( @$_POST[EXTENDED_TOC_ID], plugin_basename(__FILE__) ) ) return false; // require an administrator level to save if ( !current_user_can( 'manage_options', $post_id ) ) return false; $this->options = array_merge( $this->options, array( 'heading_text' => stripslashes( trim($_POST['heading_text']) ), 'auto_insert_post_types' => @(array)$_POST['auto_insert_post_types'], 'start' => intval($_POST['start']), 'show_heading_text' => (isset($_POST['show_heading_text']) && $_POST['show_heading_text']) ? true : false, 'show_hierarchy' => (isset($_POST['show_hierarchy']) && $_POST['show_hierarchy']) ? true : false, 'number_list_items' => (isset($_POST['number_list_items']) && $_POST['number_list_items']) ? true : false, ) ); // update_option will return false if no changes were made update_option( EXTENDED_TOC_ID, $this->options ); return true; } public function admin_options() { if( isset($_GET['update']) ) { if( $this->save_admin_options() ) $msg = '<div id="message" class="updated fade"><p>' . __('Options saved.', EXTENDED_TOC_ID) . '</p></div>'; else $msg = '<div id="message" class="error fade"><p>' . __('Save failed.', EXTENDED_TOC_ID) . '</p></div>'; } ?> <div class="wrap"> <div id="icon-plugins" class="icon32"> <br /> </div> <h2><?php echo __("Extended Table of Contents", EXTENDED_TOC_ID)?></h2> <?php echo $msg; ?> <form method="post" action="<?php echo htmlentities('?page=' . $_GET['page'] . '&update'); ?>"> <?php wp_nonce_field( plugin_basename(__FILE__), EXTENDED_TOC_ID ); ?> <div class="form_container"> <table class="form-table"> <tbody> <tr> <th><label for="show_heading_text"><?php echo __('Show heading text', EXTENDED_TOC_ID); ?></label></th> <td> <input id="show_heading_text" type="checkbox" name="show_heading_text" <?php if ( $this->options['show_heading_text'] ) echo ' checked="checked"'; ?> /> </td> </tr> <tr> <th><label for="heading_text"><?php echo __('Heading text', EXTENDED_TOC_ID); ?></label></th> <td><input id="heading_text" type="text" class="regular-text" name="heading_text" value="<?php echo $this->options['heading_text']; ?>" /></td> </tr> <tr> <th><?php echo __('Add table of contents to following content types', EXTENDED_TOC_ID); ?></th> <td> <?php foreach( get_post_types() as $post_type ): ?> <?php if( !in_array($post_type, $this->exclude_post_types) ): ?> <input type="checkbox" value="<?php echo $post_type?>" id="auto_insert_post_types_<?php echo $post_type?>" name="auto_insert_post_types[]"<?php echo in_array($post_type, $this->options['auto_insert_post_types'])?' checked="checked"':''?> /> <label for="auto_insert_post_types_<?php echo $post_type?>"><?php echo $post_type?></label><br /> <?php endif; ?> <?php endforeach; ?> </td> </tr> <tr> <th><label for="start"><?php echo __('Show when', EXTENDED_TOC_ID); ?></label></th> <td> <select name="start" id="start"> <?php for ($i = TOC_MIN_START; $i <= TOC_MAX_START; $i++) { echo '<option value="' . $i . '"'; if ( $i == $this->options['start'] ) echo ' selected="selected"'; echo '>' . $i . '</option>' . "\n"; } ?> </select> <span>><?php echo __('or more headings are present', EXTENDED_TOC_ID); ?></span> </td> </tr> <tr> <th><label for="show_hierarchy"><?php echo __('Show hierarchy', EXTENDED_TOC_ID); ?></label></th> <td> <input id="show_hierarchy" type="checkbox" name="show_hierarchy" <?php if ( $this->options['show_hierarchy'] ) echo ' checked="checked"'; ?> /> </td> </tr> <tr> <th><label for="number_list_items"><?php echo __('Number list items', EXTENDED_TOC_ID); ?></label></th> <td> <input id="number_list_items" type="checkbox" name="number_list_items" <?php if ( $this->options['number_list_items'] ) echo ' checked="checked"'; ?> /> </td> </tr> </tbody> </table> </div> <p class="submit"><input class="button-primary" type="submit" value="<?php echo __("Save Options", EXTENDED_TOC_ID)?>" name="submit" /></p> </form> </div> <?php } public function wp_enqueue_scripts() { wp_register_style(EXTENDED_TOC_ID, $this->path . '/style.css', array(), POWER_TOC_VERSION); wp_enqueue_style(EXTENDED_TOC_ID); } public function plugins_loaded() { load_plugin_textdomain( EXTENDED_TOC_ID, false, dirname(plugin_basename(__FILE__)) . '/locale/' ); } public function the_content($content) { global $post; // Reset the counter $this->counter = array(); if ( is_feed() ) return $content; if( is_search() || is_archive() || is_front_page() ) return $content; /** Extract the content, and extract the part content if <!--nextpage--> was used **/ $this->content = $content; // The original content (subpage) that is displayed $this->extract_full_post_content(); $toc_content = "<div id=\"toc-np-container\">"; if( $this->options['show_heading_text'] == true ) $toc_content .= "<p id=\"toc-np-title\">" . $this->options["heading_text"] . "</p>"; $toc_content .= "<ul class=\"no-bullets\">"; $toc_content .= $this->extract_toc(); $toc_content .= "</ul></div>"; if( $this->totalHeadings >= $this->options['start'] ) return $this->insert_toc_at_markup_position($toc_content); // $toc_content . $this->content; else return $this->content; } /** returns the content for display added by the ToC */ private function insert_toc_at_markup_position($toc_content) { // clean content without markups for returning $content = $this->content; $content = preg_replace("/\[extoc\]|\[noextoc\]/", "", $content); // [noextoc] has priority. If this is found, return the original if( strpos($this->content, '[noextoc]') !== false ) return $content; // try to find the markup for the ToC $pos = strpos($this->content, '[extoc]'); if( $pos === false ) { // There was no markup, so insert at top or return original if this type does not need a ToC if( !in_array(get_post_type($post), $this->options['auto_insert_post_types']) ) return $content; else return $toc_content . $content; } if( is_numeric($pos) && $pos >= 0 ) { return substr($content, 0, $pos) . $toc_content . substr($content, $pos); } // Absolute backup, return the content. This point should actually never be reached return $content; } /** Extract the full unshortened content from the post **/ private function extract_full_post_content() { global $post; $this->fullcontent = $post->post_content; $this->ID = $post->ID; } private function extract_toc() { /** check within the full content how many pages exists */ $this->extract_pages(); $headers = ""; /** Extract headings from every pages */ for( $pagenum = 1; $pagenum <= count($this->pages); $pagenum++ ) { $headers .= $this->exctract_headings($pagenum); } return $headers; } private function extract_pages() { /** Split the content by "nextpage"-tags if some exists */ $this->pages = preg_split("/<!--nextpage-->/msuU", $this->fullcontent); } private function exctract_headings($pagenum) { /** find all header tags within the page **/ preg_match_all('/(<h([1-6]{1})[^>]*>).*<\/h\2>/msuU', $this->pages[$pagenum-1], $matches, PREG_SET_ORDER); /** Check the headings that are desired */ if( count($this->options['heading_levels']) != 6 ) { $new_matches = array(); for ($i = 0; $i < count($matches); $i++) { if ( in_array($matches[$i][2], $this->options['heading_levels']) ) $new_matches[] = $matches[$i]; } $matches = $new_matches; } // echo "<pre>"; print_r($matches); echo "</pre>"; $items = ""; /** Take first h-level as baseline */ if( $this->minLevel == null ) $this->minLevel = $matches[0][2]; // lowest level e.g. h3 $currentLevel = $this->minLevel; // $minLevel; for( $i = 0; $i < count($matches); $i++ ) { /** get anchor and add to find and replace arrays **/ $anchor = $this->url_encode_anchor($matches[$i][0]); $find = $matches[$i][0]; $replace = str_replace( array( $matches[$i][1], // start of heading '</h' . $matches[$i][2] . '>' // end of heading ), array( $matches[$i][1] . '<span id="' . $anchor . '">', '</span></h' . $matches[$i][2] . '>' ), $matches[$i][0] ); $this->content = str_replace($find, $replace, $this->content); /** Check if header lower current header, then add level and update current header */ if( $matches[$i][2] > $currentLevel && $this->options['show_hierarchy'] == true) { $currentLevel = $matches[$i][2]; $this->counter[$currentLevel] = 1; // echo '$this->counter['.$currentLevel.'] = 1 <br>'; } else if( $matches[$i][2] < $currentLevel && $matches[$i][2] >= $this->minLevel && $this->options['show_hierarchy'] == true) { $currentLevel = $matches[$i][2]; $this->counter[$currentLevel] += 1; // echo '$this->counter['.$currentLevel.'] += 1 <br>'; } else { $this->counter[$currentLevel] += 1; // echo '$this->counter['.$currentLevel.'] += 1 <br>'; } /** build html */ $items .= '<li class="header-level-' . ($currentLevel - $this->minLevel + 1) . '">'; // echo $currentLevel . ' - ' . $this->minLevel . ' <br>'; $items .= '<a href="?p='.$this->ID.($pagenum>0?'&page='.$pagenum:'').'#' . $anchor . '">'; // Show numbers only if user wants it if( $this->options['number_list_items'] ) { $items .= "<span class=\"toc-np-number\">"; if( $this->options['show_hierarchy'] == true ) { for( $j = $this->minLevel; $j < $currentLevel; $j++ ) { $items = $items . $this->counter[$j] . "."; } } $items = $items . $this->counter[$currentLevel]; $items .= "</span>"; } $items .= strip_tags($matches[$i][0]) . '</a>'; $items .= '</li>'; $this->totalHeadings++; } return $items; } private function url_encode_anchor($anchor) { $return = false; if(!empty($anchor) ) { /** Remove tags */ $return = trim( strip_tags($anchor) ); /** remove & */ $return = str_replace( '&', '', $return ); /** remove all unknown chars **/ $return = preg_replace("/[^0-9a-zA-Z \-_]+/", "", $return); /** Remove backspace etc */ $return = preg_replace("/[\s]+/", "-", $return); /** If we now start or end with a - or _ remove it */ $return = preg_replace("/^[-_]/", "", $return); $return = preg_replace("/[-_]$/", "", $return); } return $return; } } } /** Initialise the class */ $tocPlugin = new ExToC(); ?>