2013-06-27 10:31:39 +00:00
|
|
|
<?php
|
|
|
|
/*
|
|
|
|
Plugin Name: Extended Table of Contents (with nextpage support)
|
|
|
|
Plugin URI: http://www.happybooking.de/wordpress/plugins/extended-toc
|
2013-08-08 13:14:53 +00:00
|
|
|
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.0
|
2013-06-27 10:31:39 +00:00
|
|
|
Author: HappyBooking UG // Daniel Boldura
|
|
|
|
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]
|
|
|
|
*/
|
2013-08-08 13:14:53 +00:00
|
|
|
|
|
|
|
define( 'EXTENDED_TOC_VERSION', '0.8.0' );
|
2013-06-27 10:31:39 +00:00
|
|
|
define( 'EXTENDED_TOC_ID', 'extended_toc' );
|
|
|
|
define( 'EXTENDED_TOC_NAME', 'Extended-ToC' );
|
2013-07-04 12:11:41 +00:00
|
|
|
define( 'TOC_MIN_START', 2 );
|
|
|
|
define( 'TOC_MAX_START', 10 );
|
2013-06-27 10:31:39 +00:00
|
|
|
|
|
|
|
if( !class_exists('ExToC') ) {
|
|
|
|
class ExToC
|
|
|
|
{
|
|
|
|
private $path;
|
|
|
|
private $content = "";
|
|
|
|
private $fullcontent = "";
|
|
|
|
private $pages = array();
|
|
|
|
private $ID = 0;
|
2013-07-04 12:11:41 +00:00
|
|
|
private $counter = array();
|
2013-06-27 10:31:39 +00:00
|
|
|
private $totalHeadings = 0;
|
|
|
|
|
|
|
|
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'),
|
2013-07-04 12:11:41 +00:00
|
|
|
'show_hierarchy' => true,
|
2013-06-27 10:31:39 +00:00
|
|
|
);
|
|
|
|
$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() ) {
|
|
|
|
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'), 10 );
|
|
|
|
add_action( 'wp_enqueue_scripts', array(&$this, 'wp_enqueue_scripts') );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function __destruct() {
|
|
|
|
}
|
|
|
|
|
|
|
|
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'],
|
2013-07-04 12:11:41 +00:00
|
|
|
'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,
|
2013-06-27 10:31:39 +00:00
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
// 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><?=__("Extended Table of Contents", EXTENDED_TOC_ID)?></h2>
|
|
|
|
|
|
|
|
<?=$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>
|
2013-07-04 12:11:41 +00:00
|
|
|
<tr>
|
|
|
|
<th><label for="show_heading_text"><?=__('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>
|
|
|
|
|
2013-06-27 10:31:39 +00:00
|
|
|
<tr>
|
|
|
|
<th><label for="heading_text"><?=__('Heading text', EXTENDED_TOC_ID); ?></label></th>
|
|
|
|
<td><input id="heading_text" type="text" class="regular-text" name="heading_text" value="<?=$this->options['heading_text']?>" /></td>
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
<tr>
|
|
|
|
<th><?=__('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="<?=$post_type?>" id="auto_insert_post_types_<?=$post_type?>" name="auto_insert_post_types[]"<?=in_array($post_type, $this->options['auto_insert_post_types'])?' checked="checked"':''?> />
|
|
|
|
<label for="auto_insert_post_types_<?=$post_type?>"><?=$post_type?></label><br />
|
|
|
|
<?php endif; ?>
|
|
|
|
<?php endforeach; ?>
|
|
|
|
</td>
|
|
|
|
</tr>
|
2013-07-04 12:11:41 +00:00
|
|
|
|
|
|
|
<tr>
|
|
|
|
<th><label for="start"><?=__('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>
|
2013-07-04 12:20:30 +00:00
|
|
|
<span>><?=__('or more headings are present', EXTENDED_TOC_ID); ?></span>
|
2013-07-04 12:11:41 +00:00
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
<tr>
|
|
|
|
<th><label for="show_hierarchy"><?=__('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>
|
2013-06-27 10:31:39 +00:00
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<p class="submit"><input class="button-primary" type="submit" value="<?=__("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;
|
|
|
|
|
|
|
|
if ( is_feed() )
|
|
|
|
return $content;
|
|
|
|
|
2013-08-08 13:14:53 +00:00
|
|
|
if( is_search() || is_archive() || is_front_page() )
|
2013-06-27 10:31:39 +00:00
|
|
|
return $content;
|
|
|
|
|
|
|
|
/** Extract the content, and extract the part content if <!--nextpage--> was used **/
|
2013-08-08 13:14:53 +00:00
|
|
|
$this->content = $content; // The original content (subpage) that is displayed
|
2013-06-27 10:31:39 +00:00
|
|
|
$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>";
|
2013-07-04 12:11:41 +00:00
|
|
|
|
2013-06-27 10:31:39 +00:00
|
|
|
if( $this->totalHeadings >= $this->options['start'] )
|
2013-08-08 13:14:53 +00:00
|
|
|
return $this->insert_toc_at_markup_position($toc_content); // $toc_content . $this->content;
|
2013-06-27 10:31:39 +00:00
|
|
|
else
|
|
|
|
return $this->content;
|
|
|
|
}
|
2013-08-08 13:14:53 +00:00
|
|
|
|
|
|
|
/** 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;
|
|
|
|
}
|
2013-06-27 10:31:39 +00:00
|
|
|
|
2013-07-04 12:11:41 +00:00
|
|
|
private function check_for_first_toc_position() {
|
|
|
|
}
|
|
|
|
|
2013-06-27 10:31:39 +00:00
|
|
|
/** 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 */
|
2013-07-04 12:11:41 +00:00
|
|
|
if( count($this->options['heading_levels']) != 6 ) {
|
2013-06-27 10:31:39 +00:00
|
|
|
$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;
|
|
|
|
}
|
|
|
|
|
|
|
|
$items = "";
|
|
|
|
|
2013-07-04 12:11:41 +00:00
|
|
|
/** Take first h-level as baseline */
|
|
|
|
$minLevel = $matches[0][2]; // lowest level e.g. h3
|
|
|
|
$currentLevel = $minLevel;
|
|
|
|
|
2013-06-27 10:31:39 +00:00
|
|
|
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];
|
|
|
|
$this->content = 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] . '>'
|
|
|
|
),
|
|
|
|
$this->content
|
2013-07-04 12:11:41 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
/** 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;
|
|
|
|
}
|
|
|
|
else if( $matches[$i][2] < $currentLevel && $matches[$i][2] >= $minLevel && $this->options['show_hierarchy'] == true) {
|
|
|
|
$currentLevel = $matches[$i][2];
|
|
|
|
$this->counter[$currentLevel] += 1;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
$this->counter[$currentLevel] += 1;
|
|
|
|
|
2013-06-27 10:31:39 +00:00
|
|
|
/** build html */
|
2013-07-04 12:11:41 +00:00
|
|
|
$items .= '<li class="header-level-' . ($currentLevel - $minLevel + 1) . '">';
|
2013-06-27 10:31:39 +00:00
|
|
|
$items .= '<a href="?p='.$this->ID.($pagenum>1?'&page='.$pagenum:'').'#' . $anchor . '">';
|
2013-07-04 12:11:41 +00:00
|
|
|
$items .= "<span class=\"toc-np-number\">";
|
|
|
|
|
|
|
|
if( $this->options['show_hierarchy'] == true ) {
|
|
|
|
for( $j = $minLevel; $j < $currentLevel; $j++ ) {
|
|
|
|
$items = $items . $this->counter[$j] . ".";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$items = $items . $this->counter[$currentLevel];
|
|
|
|
|
|
|
|
$items .= "</span>";
|
2013-06-27 10:31:39 +00:00
|
|
|
$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();
|
|
|
|
|
|
|
|
?>
|