WordPress как на ладони
wordpress jino

Оглавление (содержание) для больших постов

Не редко большие посты мы разделяем логическими подзаголовками. Порой пост несет в себе какую-то собранную информацию, разбитую на части. Для таких постов я предпочитаю создавать "оглавление" — список ссылок с анкорами на подзаголовки в посте. Создавать такое оглавление - занятие до того муторное, что проще обойтись без него (за редким исключение, конечно).

Написал небольшой класс, который позволяет без шума и пыли, а также красиво и главное быстро создавать оглавления почти любой сложности. Для этого нужно использовать шоткод [contents], в том месте, где оно нам нужно. В начале этого поста вы видите то самое оглавление.

Кроме этого написанный мною класс позволяет создавать оглавление для любого текста, без использования шоткода в нем. А затем выводить это оглавление, например, в начале поста или в боковой колонке (сайдбаре).

"Оглавление" можно всячески настроить:

  • не показывать заголовок: "Содержание:" - [contents embed];
  • не показывать ссылки "К содержанию" в тексте - параметр to_menu;
  • настроить под себя CSS стили - параметр css;
  • изменить HTML теги по которым будет строиться оглавление. Можно указать любые теги не только H1, H2 ... но и strong, em и т.д. Или вообще, указать классы html тега, например: .foo, .bar - [contents h1 em .foo];
  • указать минимальное количество заголовков для того, чтобы оглавление выводилось - параметр min_found.
  • указать минимальную длину текста, чтобы оглавление выводилось - параметр min_length.
  • указать название шоткода, который будет использоваться в тексте для создания оглавления - параметр shortcode.
  • другие параметры, см. в коде класса Kama_Contents.

Код класса Kama_Contents

/**
 * Содержание (оглавление) для больших постов.
 *
 * Author: Kama
 * Page: http://wp-kama.ru/?p=1513
 * ver: 3.12
 *
 * Changelog: http://wp-kama.ru/?p=1513#obnovleniya
 */
class Kama_Contents {

	// defaults options
	public $opt = array(
		// Отступ слева у подразделов в px.
		'margin'     => 40,
		// Теги по умолчанию по котором будет строиться оглавление. Порядок имеет значение.
		// Кроме тегов, можно указать атрибут classа: array('h2','.class_name'). Можно указать строкой: 'h2 h3 .class_name'
		'selectors'  => array('h2','h3','h4'),
		// Ссылка на возврат к оглавлению. '' - убрать ссылку
		'to_menu'    => 'к содержанию ↑',
		// Заголовок. '' - убрать заголовок
		'title'      => 'Содержание:',
		// Css стили. '' - убрать стили
		'css'        => '.kc__gotop{ display:block; text-align:right; }
						 .kc__title{ font-style:italic; padding:1em 0; }
						 .kc__anchlink{ color:#ddd!important; position:absolute; margin-left:-1em; }',
		// Минимальное количество найденных тегов, чтобы оглавление выводилось.
		'min_found'  => 2,
		// Минимальная длина (символов) текста, чтобы оглавление выводилось.
		'min_length' => 2000,
		// Ссылка на страницу для которой собирается оглавление. Если оглавление выводиться на другой странице...
		'page_url'   => '',
		// Название шоткода
		'shortcode'  => 'contents',
		// Оставлять символы в анкорах
		'spec'       => '\'.+$*~=',
		// Какой тип анкора использовать: 'a' - <a name="anchor"></a> или 'id' -
		'anchor_type' => 'id',
		// Включить микроразметку?
		'markup'      => false,
		// Добавить 'знак' перед подзаголовком статьи со ссылкой на текущий анкор заголовка. Укажите '#', '&' или что вам нравится :)
		'anchor_link' => '',
		// минимальное количество символов между заголовками содержания, для которых нужно выводить ссылку "к содержанию".
		// Не имеет смысла, если параметр 'to_menu' отключен. С целью производительности, кириллица считается без учета кодировки.
		// Поэтому 800 символов кириллицы - это примерно 1600 символов в этом параметре. 800 - расчет для сайтов на кириллице...
		'tomenu_simcount' => 800,
	);

	public $contents; // collect html contents

	private $temp;

	static $inst;

	function __construct( $args = array() ){
		$this->set_opt( $args );
		return $this;
	}

	/**
	 * Create instance
	 * @param  array [$args = array()] Options
	 * @return object Instance
	 */
	static function init( $args = array() ){
		is_null( self::$inst ) && self::$inst = new self( $args );
		if( $args ) self::$inst->set_opt( $args );
		return self::$inst;
	}

	function set_opt( $args = array() ){
		$this->opt = (object) array_merge( (array) $this->opt, (array) $args );
	}

	/**
	 * Обрабатывает текст, превращает шоткод в нем в оглавление.
	 * @param (string) $content текст, в котором есть шоткод.
	 * @param (string) $contents_cb callback функция, которая обработает список оглавления.
	 * @return Обработанный текст с оглавлением, если в нем есть шоткод.
	 */
	function shortcode( $content, $contents_cb = '' ){
		if( false === strpos( $content, '['. $this->opt->shortcode ) )
			return $content;

		// get contents data
		if( ! preg_match('~^(.*)\['. $this->opt->shortcode .'([^\]]*)\](.*)$~s', $content, $m ) )
			return $content;

		$contents = $this->make_contents( $m[3], $m[2] );

		if( $contents && $contents_cb && is_callable($contents_cb) )
			$contents = $contents_cb( $contents );

		return $m[1] . $contents . $m[3];
	}

	/**
	 * Заменяет заголовки в переданном тексте (по ссылке), создает и возвращает оглавление.
	 * @param (string)        $content текст на основе которого нужно создать оглавление.
	 * @param (array/string)  $tags    массив тегов, которые искать в переданном тексте.
	 *                                 Можно указать: имена тегов "h2 h3" или классы элементов ".foo .foo2".
	 *                                 Если в теги добавить маркер "embed" то вернется только тег <ul>
	 *                                 без заголовка и оборачивающего блока. Нужно для использования внутри текста, как список.
	 * @return                html код оглавления.
	 */
	function make_contents( & $content, $tags = '' ){
		// return if text is too short
		if( mb_strlen( strip_tags($content) ) < $this->opt->min_length )
			return;

		$this->temp     = $this->opt;
		$this->contents = array();

		if( ! $tags )
			$tags = $this->opt->selectors;

		if( is_string($tags) )
			$tags = array_map('trim', preg_split('/[ ,]+/', $tags ) );

		$tags = array_filter($tags); // del empty

		// check tags
		foreach( $tags as $k => $tag ){
			// remove special marker tags and set $args
			if( in_array( $tag, array('embed','no_to_menu') ) ){
				if( $tag == 'embed' ) $this->temp->embed = true;
				if( $tag == 'no_to_menu' ) $this->opt->to_menu = false;

				unset( $tags[ $k ] );
				continue;
			}

			// remove tag if it's not exists in content
			$patt = ( ($tag[0] == '.') ? 'class=[\'"][^\'"]*'. substr($tag, 1) : "<$tag" );
			if( ! preg_match("/$patt/i", $content ) ){
				unset( $tags[ $k ] );
				continue;
			}
		}

		if( ! $tags ) return;

		// set patterns from given $tags
		// separate classes & tags & set
		$class_patt = $tag_patt = $level_tags = array();
		foreach( $tags as $tag ){
			// class
			if( $tag{0} == '.' ){
				$tag  = substr( $tag, 1 );
				$link = & $class_patt;
			}
			// html tag
			else
				$link = & $tag_patt;

			$link[] = $tag;
			$level_tags[] = $tag;
		}

		$this->temp->level_tags = array_flip( $level_tags );

		// replace all titles & collect contents to $this->contents
		$patt_in = array();
		if( $tag_patt )   $patt_in[] = '(?:<('. implode('|', $tag_patt) .')([^>]*)>(.*?)<\/\1>)';
		if( $class_patt ) $patt_in[] = '(?:<([^ >]+) ([^>]*class=["\'][^>]*('. implode('|', $class_patt) .')[^>]*["\'][^>]*)>(.*?)<\/'. ($patt_in?'\4':'\1') .'>)';

		$patt_in = implode('|', $patt_in );

		$this->temp->content = $content;

		// collect and replace
		$_content = preg_replace_callback("/$patt_in/is", array( &$this, '_make_contents_callback'), $content, -1, $count );

		if( ! $count || $count < $this->opt->min_found ){
			unset($this->temp); // clear cache
			return;
		}

		$this->temp->content = $content = $_content; // $_content was for check reasone

		// html
		static $css;
		$embed   = isset($this->temp->embed);
		$_tit    = & $this->opt->title;
		$_is_tit = ! $embed && $_tit;

		// markup
		$ItemList = $this->opt->markup ? ' itemscope itemtype="http://schema.org/ItemList"' : '';

		$contents =
			( $_is_tit ? '<div class="kc__wrap"'. $ItemList .' >' : '' ) .
			( ( ! $css && $this->opt->css ) ? '<style>'. preg_replace('/[\n\t ]+/', ' ', $this->opt->css ) .'</style>' : '' ) .
			( $_is_tit ? '<span style="display:block;" class="kc-title kc__title" id="kcmenu"'. ($ItemList?' itemprop="name"':'') .'>'. $_tit .'</span>'. "\n" : '' ) .
				'<ul class="contents"'. ( (! $_tit || $embed) ? ' id="kcmenu"' : '' ) . ( ($ItemList && ! $_is_tit ) ? $ItemList : '' ) .'>'. "\n".
					implode('', $this->contents ) .
				'</ul>'."\n" .
			( $_is_tit ? '</div>' : '' );

		unset($this->temp); // clear cache

		return $this->contents = $contents;
	}

	## callback function to replace and collect contents
	private function _make_contents_callback( $match ){
		$temp = & $this->temp;

		// it's only class selector in pattern
		if( count($match) == 5 ){
			$tag   = $match[1];
			$attrs = $match[2];
			$title = $match[4];

			$level_tag = $match[3]; // class_name
		}
		// it's found tag selector
		elseif( count($match) == 4 ){
			$tag   = $match[1];
			$attrs = $match[2];
			$title = $match[3];

			$level_tag = $tag;
		}
		// it's found class selector
		else{
			$tag   = $match[4];
			$attrs = $match[5];
			$title = $match[7];

			$level_tag = $match[6]; // class_name
		}

		$anchor = $this->_sanitaze_anchor( $title );
		$opt = $this->opt; // make live easier

		$level = @ $temp->level_tags[ $level_tag ];
		if( $level > 0 )
			$sub = ( $opt->margin ? ' style="margin-left:'. ($level*$opt->margin) .'px;"' : '') . ' class="sub sub_'. $level .'"';
		else
			$sub = ' class="top"';

		// collect contents
		// markup
		$_is_mark = $opt->markup;

		$temp->counter = empty($temp->counter) ? 1 : $temp->counter+1;

		// $title не может содержать A, IMG теги - удалим если надо...
		$cont_title = $title;
		if( false !== strpos($cont_title, '</a>') ) $cont_title = preg_replace('~<a[^>]+>|</a>~', '', $cont_title );
		if( false !== strpos($cont_title, '<img') ) $cont_title = preg_replace('~<img[^>]+>~', '', $cont_title );

		$this->contents[] = "\t".
			'<li'. $sub . ($_is_mark?' itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem"':'') .'>
				<a rel="nofollow"'. ($_is_mark?' itemprop="item"':'') .' href="'. $opt->page_url .'#'. $anchor .'">
					'.( $_is_mark ? '<span itemprop="name">'. $cont_title .'</span>' : $cont_title ).'
				</a>
				'.( $_is_mark ? ' <meta itemprop="position" content="'. $temp->counter .'" />':'' ).'
			</li>'. "\n";

		$anchlink = $opt->anchor_link ? '<a rel="nofollow" class="kc__anchlink" href="#'. $anchor .'">'. $opt->anchor_link .'</a> ' : '';
		if( $anchlink ) $title = $anchlink . $title;

		$new_el = "\n<$tag id=\"$anchor\" $attrs>$anchlink$title</$tag>";
		if( $opt->anchor_type == 'a' )
			$new_el = '<a class="kc__anchor" name="'. $anchor .'"></a>'."\n<$tag $attrs>$title</$tag>";

		$to_menu = '';
		if( $opt->to_menu ){
			// go to contents
			$to_menu = '<a rel="nofollow" class="kc-gotop kc__gotop" href="'. $opt->page_url .'#kcmenu">'. $opt->to_menu .'</a>';

			// remove '$to_menu' if simbols beatween $to_menu too small (< 300)
			$pos = strpos( $temp->content, $match[0] ); // mb_strpos( $temp->content, $match[0] ) - в 150 раз медленнее!
			if( empty($temp->elpos) ){
				$prevpos = 0;
				$temp->elpos = array( $pos );
			}
			else {
				$prevpos = end($temp->elpos);
				$temp->elpos[] = $pos;
			}
			$simbols_count = $pos - $prevpos;
			if( $simbols_count < $opt->tomenu_simcount ) $to_menu = '';
		}

		return $to_menu . $new_el;
	}

	## URL transliteration
	function _sanitaze_anchor( $anch ){
		$anch = strip_tags( $anch );

		$iso9 = array(
			'А'=>'A', 'Б'=>'B', 'В'=>'V', 'Г'=>'G', 'Д'=>'D', 'Е'=>'E', 'Ё'=>'YO', 'Ж'=>'ZH',
			'З'=>'Z', 'И'=>'I', 'Й'=>'J', 'К'=>'K', 'Л'=>'L', 'М'=>'M', 'Н'=>'N', 'О'=>'O',
			'П'=>'P', 'Р'=>'R', 'С'=>'S', 'Т'=>'T', 'У'=>'U', 'Ф'=>'F', 'Х'=>'H', 'Ц'=>'TS',
			'Ч'=>'CH', 'Ш'=>'SH', 'Щ'=>'SHH', 'Ъ'=>'', 'Ы'=>'Y', 'Ь'=>'', 'Э'=>'E', 'Ю'=>'YU', 'Я'=>'YA',
			// small
			'а'=>'a', 'б'=>'b', 'в'=>'v', 'г'=>'g', 'д'=>'d', 'е'=>'e', 'ё'=>'yo', 'ж'=>'zh',
			'з'=>'z', 'и'=>'i', 'й'=>'j', 'к'=>'k', 'л'=>'l', 'м'=>'m', 'н'=>'n', 'о'=>'o',
			'п'=>'p', 'р'=>'r', 'с'=>'s', 'т'=>'t', 'у'=>'u', 'ф'=>'f', 'х'=>'h', 'ц'=>'ts',
			'ч'=>'ch', 'ш'=>'sh', 'щ'=>'shh', 'ъ'=>'', 'ы'=>'y', 'ь'=>'', 'э'=>'e', 'ю'=>'yu', 'я'=>'ya',
			// other
			'Ѓ'=>'G', 'Ґ'=>'G', 'Є'=>'YE', 'Ѕ'=>'Z', 'Ј'=>'J', 'І'=>'I', 'Ї'=>'YI', 'Ќ'=>'K', 'Љ'=>'L', 'Њ'=>'N', 'Ў'=>'U', 'Џ'=>'DH',
			'ѓ'=>'g', 'ґ'=>'g', 'є'=>'ye', 'ѕ'=>'z', 'ј'=>'j', 'і'=>'i', 'ї'=>'yi', 'ќ'=>'k', 'љ'=>'l', 'њ'=>'n', 'ў'=>'u', 'џ'=>'dh'
		);

		$anch = strtr( $anch, $iso9 );

		$spec = preg_quote( $this->opt->spec );
		$anch = preg_replace("/[^a-zA-Z0-9_$spec\-]+/", '-', $anch ); // все ненужное на '-'
		$anch = strtolower( trim( $anch, '-') );
		$anch = substr( $anch, 0, 70 ); // shorten
		$anch = $this->_unique_anchor( $anch );

		return $anch;
	}

	## adds number at the end if this anchor already exists
	function _unique_anchor( $anch ){
		$temp = & $this->temp;

		// check and unique anchor
		if( empty($temp->anchors) ){
			$temp->anchors = array( $anch => 1 );
		}
		elseif( isset($temp->anchors[ $anch ]) ){
			$lastnum = substr( $anch, -1 );
			$lastnum = is_numeric($lastnum) ? $lastnum + 1 : 2;
			return $this->_unique_anchor( "$anch-$lastnum" );
		}
		else {
			$temp->anchors[ $anch ] = 1;
		}

		return $anch;
	}

	## cut the shortcode from the content
	function strip_shortcode( $text ){
		return preg_replace('~\['. $this->opt->shortcode .'[^\]]*\]~', '', $text );
	}
}

/**
 * 3.12 - уникализация одинаковых якорей - _unique_anchor()
 * 3.11 - удаляется IMG тег из заголовка в оглавлении...
 * 3.10 - удаляется A тег из заголовка в оглавлении...
 * 3.9 - при 'anchor_type=a' не работал параметр 'anchor_link'
 * 3.8 - баг синтаксиса при заполнении свойства $this->contents в PHP 7.1
 * 3.7 - добавил элемент position при маркировке schema.org
 * 3.6.1 - тег заголовка "Содержание" изменил с DIV на SPAN
 * 3.6 - исправление парсинга тегов - удаление пустых при разбиении по [ ,]
 * 3.5 - стабильность. в параметр selectors можно указывать строку с элементами через запятую.
 * 3.4 - параметр 'tomenu_simcount'
 * 3.3 - smart 'to contents' link show - not show next link if symbols between prev smaller than 500
 */

Как пользоваться классом Kama_Contents

Прежде всего нужно подключить код, можно добавить его в файл темы functions.php или создать плагин. Далее выбирайте подходящий вам код примера и добавьте его рядом с основным (выше или ниже неважно).

#1 Оглавление в тексте (шоткод [contents])

Разместите этот код рядом с основным и при написании поста используйте шоткод [contents] или [contents h3] или [contents h3 h5]. На месте шоткода появится содержание, текста который следует после шоткода:

## Обработка шоткода [contents] в тексте
add_filter( 'the_content', 'kama_contents_shortcode', 20 );
function kama_contents_shortcode( $content ){
	if( is_singular() ){
		$args = array(
			//'shortcode' => 'list', // [list] вместо [contents]
			//'margin'   => 30,
			//'page_url' => get_permalink(),
			'to_menu'    => 'к оглавлению ↑',
			'title'      => 'Оглавление:',
			'min_length' => 300,
		);

		return Kama_Contents::init( $args )->shortcode( $content );
	}
	// вырежем шорткод
	else
		return Kama_Contents::init()->strip_shortcode( $content );
}

#2 Оглавление вверху каждого поста

Разместите этот код рядом с основным и в начале каждого поста у вас будет выводится содержание, по указанным тегам array('h2','h3'), т.е. если в тексте будут найдены теги h2 или h3, то из них будет собрано содержание:

## Вывод содержания вверху, автоматом для всех постов
add_filter( 'the_content', 'contents_on_post_top', 20 );
function contents_on_post_top( $content ){
	if( ! is_singular() )
		return $content;

	$args = array(
		//'margin'    => 50,
		//'to_menu'   => false,
		//'title'     => false,
		'selectors' => array('h2','h3'),
	);

	$contents = Kama_Contents::init( $args )->make_contents( $content );

	return $contents . $content;
}

#3 Оглавление вверху каждого поста, после разделителя

Этот пример предложили в комментариях. Но код я переделал, упростил...

Этот код будет вставлять оглавление в начале каждой записи. Но не в самом начале, а после первого параграфа. Номер параграфа (разделителя) и сам разделитель можно изменить в переменных: $_sep_num и $_sep соответственно.

## Вывод содержания вверху после указанного параграфа, автоматом для всех записей
add_filter( 'the_content', 'contents_at_top_after_nsep', 20 );
function contents_at_top_after_nsep( $text ){
	if( ! is_singular() )
		return $text;

	// параметры оглавления
	$args = array(
		'min_length' => 4000,
		'css'        => false,
		'markup'     => true,
		'selectors'  => array('h2','h3'),
	);

	// настройки разделителя
	$_sep = '</p>'; // разделитель в тексте
	$_sep_num = 1;  // после какого по порядку разделителя вставлять оглавление?

	// погнали...
	$ex_text = explode( $_sep, $text, $_sep_num + 1 );

	// если подходящий по порядку разделитель найден в тексте
	if( isset($ex_text[$_sep_num]) ){
		$contents = Kama_Contents::init( $args )->make_contents( $ex_text[$_sep_num] );

		$ex_text[$_sep_num] = $contents . $ex_text[$_sep_num];

		$text = implode( $_sep, $ex_text );
	}
	// просто в верху текста
	else {
		$contents = Kama_Contents::init( $args )->make_contents( $text );

		$text = $contents . $text;
	}

	return $text;
}

#4 Оглавление в сайд-баре

Этот пример похож на второй - тут также используется метод make_contents(), а не shortcode(), как в первом. Разместив этот код рядом с основным классом, оглавление можно вывести в любом месте шаблона, например в сайдбаре. Для этого используйте строку: echo $GLOBALS['kc_contents'];

## вывод содержания в сайдбаре
add_action( 'wp_head', 'sidebar_contents' );
function sidebar_contents(){
	if( ! is_singular() ) return;

	global $post;

	$args = array();
	$args['selectors'] = array('h2','h3');
	//$args['margin'] = 50;
	//$args['to_menu'] = false;
	//$args['title'] = false;

	$GLOBALS['kc_contents'] = Kama_Contents::init( $args )->make_contents( $post->post_content );   
}
// затем в сайдбаре выводим: echo $GLOBALS['kc_contents'];

#5 Разные экземпляры

Если нужно использовать несколько классов с разными параметрами. Например, чтобы обработать разные тексты. То разные экземпляры класса можно создать так:

// первый текст
$text1 = 'текст [contents] текст';

$kcone = new Kama_Contents( array(
	'to_menu'  = 'к оглавлению ↑',
	'title'    = 'Оглавление:',
	//'page_url' = get_permalink(),
) );

echo $kcone->shortcode( $text1 );

// второй текст
$text2 = 'текст [list] текст';

$kctwo = new Kama_Contents( array(
		'to_menu'   = 'к списку ↑',
		'title'     = 'Навигация:',
		'shortcode' = 'list',
) );

echo $kctwo->shortcode( $text2 );

Настройки Оглавления

Вы же заметили закомментированные строки в примерах? Это настройки. В экземпляр класса можно передавать аргументы (настройки): Kama_Contents::init( $args ):

// число. отступ слева у подразделов в пикселях.
$args['margin'] = 40;

// массив/строка. Теги по умолчанию по котором будет строиться содержание. Порядок имеет значение. 
// Кроме тегов, можно указать атрибут class: array('h2','.class_name'). Можно указать строкой: 'h2 h3 .class_name'
$args['selectors'] = array('h2','h3','h4');

// строка. ссылка на возврат к содержанию. '' - убрать ссылку
$args['to_menu'] = 'к содержанию ↑';

// строка. Заголовок содержания. '' - убрать заголовок 
$args['title'] = 'Содержание:';

// строка. css стили. '' - убрать стили
$args['css'] = '.kc-gotop{ display:block; text-align:right; } .kc-title{ text-align:italic; }';

// число. Сколько минимум заголовков должен содержать текст, чтобы оглавление отобразилось.
$args['min_found'] = 2;

// Минимальная длина (символов) текста, чтобы содержание выводилось.
$args['min_length'] = 2000;

// Ссылка на страницу для которой собирается содержание. Если содержание выводиться на другой странице...
$args['page_url']   = '';

// Название шоткода
$args['shortcode']   = 'contents';

// Включение микроразметки. Установите true чтобы включить микроразметку типа http://schema.org/ItemList
$args['markup']   = false;

// Добавить 'знак' перед под-заголовком статьи со ссылкой на текущий анкор под-заголовка.
// Чтобы включить, укажите '#', '&' или что вам нравится :)
$args['anchor_link'] = '',

HTML и CSS

Все содержание идет сплошными <li>, а для уровней указываются CSS классы и левый отступ - margin. С помощью классов можно настроить отображение как угодно...

Вот так выглядит HTML код, который генерирует Kama_Contents:

<div class="kc__wrap">
	<div class="kc__title" id="kcmenu">Содержание:</div>
	<ul class="contents">
	<li class="top"><a href="#h2_1">заглавие 2, №1</a></li>
	<li style="margin-left:40px;" class="sub sub_1"><a href="#h3_2">заглавие 3, №1</a></li>
	<li style="margin-left:80px;" class="sub sub_2"><a href="#h4_3">заглавие 4, №1</a></li>
	<li style="margin-left:80px;" class="sub sub_2"><a href="#h4_4">заглавие 4, №2</a></li>
	<li style="margin-left:80px;" class="sub sub_2"><a href="#h4_5">заглавие 4, №3</a></li>
	<li style="margin-left:40px;" class="sub sub_1"><a href="#h3_6">заглавие 3, №2</a></li>
	<li style="margin-left:40px;" class="sub sub_1"><a href="#h3_7">заглавие 3, №3</a></li>
	<li class="top"><a href="#h2_8">заглавие 2, №2</a></li>
	<li class="top"><a href="#h2_9">заглавие 2, №3</a></li>
	<li style="margin-left:40px;" class="sub sub_1"><a href="#h3_10">заглавие 3, №4</a></li>
	<li style="margin-left:40px;" class="sub sub_1"><a href="#h3_11">заглавие 3, №5</a></li>
	</ul>
</div>

style="margin-left:40px;" добавляется автоматически на основе настройки $args['margin'] = 40;. Нет связи между числами в заголовках (h1, h2), уровни выставляются в зависимости от порядка указного в настройке: $args['def_tags'] = array('h2','h3','h4');. Т.е. если поменять на  array('h3','h2','h4');, то h3 будет в содержании верхним уровнем - это нужно, чтобы указывать отличные от h* теги: strong, em.

Древовидная нумерация списка

contents-numeric

Чтобы сделать древовидную нумерацию списка, как на картинке, установите для списка такие CSS стили:

.contents{ list-style-type:none; counter-reset:list; }
/* цвет чисел */
.contents li:before{ color:#555; }
/* уровень 0 */
.contents li.top{ counter-increment:list; counter-reset:list1; }
.contents li.top:before{ content:counter(list) '. '; }
/* уровень 1 */
.contents li.sub_1{ counter-increment:list1; counter-reset:list2; }
.contents li.sub_1:before{ content:counter(list) '.' counter(list1) '. '; }
/* уровень 2 */
.contents li.sub_2{ counter-increment:list2; }
.contents li.sub_2:before{ content:counter(list) '.' counter(list1) '.' counter(list2) '. '; }

Нумерация идет только до 3 уровня: верхний и два под ним. Если нужно больше, то по аналогии допишите стили так:

/* уровень 3 */
.contents li.sub_3{ counter-increment:list3; }
.contents li.sub_3:before{ content:counter(list) '.' counter(list1) '.' counter(list2) '.' counter(list3) '.'; }

Плавная прокрутка

Howtomake в комментариях предложил сделать плавную прокрутку для якорей страницы (ссылок с # диезом) и он же дал ссылку на статью своего блога, с jQuery кодом, реализующим идею плавного прокручивания.

В целом, можно ставить этот код на любой сайт где подключен jQuery и будет плавная прокрутка к якорям, а в частности, он хорошо сочетается с "содержанием" (см. пример, жмите там на ссылки). А это и js код:

jQuery(document).ready(function($){

	// Прокрутка на все якоря (#) и на a[name]. v1.1
	$('a[href*="#"]').on('click.smoothscroll', function( e ){
		var hash    = this.hash,
			_hash   = hash.replace(/#/,''),
			theHref = $(this).attr('href').replace(/#.*/, '');

		if( theHref && location.href.replace(/#.*/,'') != theHref ) return; // не текущая страница

		var $target = _hash === '' ? $('body') : $( hash + ', a[name="'+ _hash +'"]').first();

		if( ! $target.length ) return;

		e.preventDefault();

		$('html, body').stop().animate({ scrollTop: $target.offset().top - 100 }, 400, 'swing', function(){
			window.location.hash = hash;
		});
	});

});

«Cкрыть/Показать» оглавление (jQuery код)

В комментариях попросили - сделал. Получилось два варианта "скрыть/показать" оглавление.

Вариант 1 ("как-в-википедии"):

/**
 * Показать/скрыть Содержание. Кнопка добавляется после Текста в заголовок - "Содержание: [скрыть]"
 * v 0.2
 */
jQuery(document).ready(function($){
	var $title = $('.kc__title'),
		showtxt = '[показать]',
		hidetxt = '[скрыть]',
		$but = $('<span class="kc-show-hide" style="cursor:pointer;margin-left:.5em;font-size:80%;">'+ hidetxt +'</span>')
		$but.click(function(){
			var $the = $(this),
				$cont = $the.parent().next('.contents');

			if( $the.text() == hidetxt ){
				$the.text( showtxt );
				$cont.slideUp();
			}
			else{
				$the.text( hidetxt );
				$cont.slideDown();      
			}
		});

	$title.append( $but );
});

Вариант 2 (заголовок-кнопка):

/**
 * Показать/скрыть Содержание. Заголовок является кнопкой и к нему приписывается текст - "Содержание ▴"
 * v 0.2
 */
jQuery(document).ready(function($){
	var $title = $('.kc__title').css({ cursor:'pointer' }),
		showico = ' ▾',
		hideico = ' ▴',
		setIco = function( $that, type ){
			$that.text( (type === 'hide') ?  $that.text().replace( showico, hideico ) : $that.text().replace( hideico, showico ) );
		};

	$title.each(function(){
		var $tit = $(this);

		$tit.text( $tit.text().replace(':','').trim() + hideico )

		$tit.click(function(){
			var $the  = $(this),
				$cont = $the.next('.contents');

			if( $cont.is(':visible') ){
				$cont.slideUp(function(){
					setIco( $the, 'show' );
				});
			}
			else{               
				$cont.slideDown(function(){
					setIco( $the, 'hide' );
				});
			}
		});
	});
});

Коды нужно добавить к имеющимся js скриптам. Код должен срабатывать после того как подключилась jQuery библиотека.

Опрос по скрипту

Что добавить в скрипт "Содержание для больших постов"?

  • Добавить ответ

Плагины для создания оглавления

Есть не мало причин использовать готовые плагины, даже для тех кто может использовать материал из этой статьи. Потому что - это удобно! Вот плагины для создания такого же содержания:

  • Simple TOC - простой в использовании, результат выглядит легко и не нагружено. Есть кнопка в виз. редакторе.

  • Table of Contents Plus - очень гибко настраиваемые плагин содержания в статьях. Также есть кнопка в виз. редакторе.

Обновления

3.2
Добавил параметр 'anchor_link'. Позволяет добавить '#' перед подзаголовком статьи со ссылкой на текущий анкор заголовка

3.0
Добавил поддержку микроразметки. Смотрите параметр markup

2.9.4
Добавил параметр $contents_cb в метод shortcode( $content, $contents_cb = '' ), чтобы можно было указать функцию которая обработает созданное оглавление.

Например заменим UL на OL в оглавлении:

$content = $kc->shortcode( $content, function( $contents ){
	return str_replace( array('<ul','ul>'), array('<ol','ol>'), $contents );
} );

2.9.3

  • Перенес встроенный <style> внутрь оборачивателя.

2.9.2

  • Добавил метку embed, которую можно добавить в шорткод вместе с указанными тегами ([contents h3 embed]), тогда содержание будет выведено без оборачивающего тега и заголовка, т.е. только <ul>...</ul>. Нужно это когда содержание используется внутри контекста, не как обычное оглавление.

2.9.1

  • Добавил параметр 'anchor_type' - Какой тип анкора использовать: 'a' - <a name="anchor"></a> или 'id' - <h2 id="anchor">

2.9.0

  • В 100 раз ускорил скорость работы, если в тегах поиска указан класс.
  • Объединил поиски: теперь в тегах можно указывать одновременно и теги и классы, пр: 'h2 .class_name'
  • Добавил удаление верхних уровней, если они указаны, но их нет в тексте, т.е. указано: "h2 h3 h4" а в тексте есть только "h3 h4" тогда они шли как подуровни с ненужным отступом слева.

2.8.8

  • Добавил параметр spec в который можно указать символы, которые нужно оставлять в анкорах.
  • Подправил функцию транслитерации анкора __sanitaze_anchor().

2.8.7
Добавил strip_tags() для текста при сравнении его с параметром min_length

2.8.6
Плюс: Параметры можно передавать в виде объекта данных, не только массива.

2.8.5
Плюс: div оборачивающий содержание с классом .kc__wrap.
Правка: корректная работа параметра page_url для ссылки "к содержанию".

2.8.4

  • опция shortcode, в которой можно указать название шоткода, например изменить на "оглавление" в результате в тексте нужно будет указывать так: [оглавление h2]
  • метод класса strip_shortcode() - вырезает шоткод, для страниц архивов.
  • метод класса __construct(), чтобы можно было использовать несколько экземпляров класса.
    fix: правки кода

2.8.3

  • Опция page_url

2.8.2

  • свойство класса contents там сохраняется html код содержания.

2.8.1
Фиксы багов из версии 2.8.0

  • фильтрация анкоров: добавил понимание точки и удалил замену крайних "-". Так должно поддерживаться больше строк...

2.8.0

  • добавил возможность указывать в качестве селекторов атрибут класс. См. парам. $tags.
    баг: возможность установить разные настройки для разных выводов одного экземпляра
  • удаление html тегов при очистке текста якоря

2.7.3 (14 6 2015)

  • символы (~+=$) в очистку анкора (транслитерация). Иногда эти символы нужно учитывать, а не заменять на -

2.7.1 (14 апрель 2015)

  • класс (kc_anchor) к анкору, чтобы можно было его стилизовать, если вдруг понадобится: <a class="kc_anchor" name="

2.7 (31 марта 2015)
Добавил: опцию min_length, в которую нужно указывать минимальную длину текста, чтобы содержание собиралось. По умолчанию 4000 символов. Т.е. если текст меньше, то содержание просто не будет работать.

2.6 (14 марта 2015)
Добавил: обернул все содержание в <div class="contents-wrap">.

2.5 (13 марта 2015)
Баг: был серьезный баг связанный с параметром min_found - если заголовков меньше чем в min_found, то контент дублировался.

2.4 (6 марта  2015)
Изменил: убрал атрибут ID у заменяемых тегов: было <h2 id="anchor">Заголовок</h2> а стало <a name="anchor"></a><h2>Заголовок</h2>

2.3 (6 марта  2015)
Добавил: транслитерацию анкоров (#punkt-menu). Теперь анкоры в УРЛ можно читать и при изменении порядка, ссылка на пункт продолжает работать. А то раньше анкоры были #h-1, #h-2.

2.2 (9 фев. 2015)
Добавил: функцию чтобы можно было указывать минимальное количество найденных тегов, для того чтобы скрипт работал.
Мелкие правки кода.

2.1.4 (30.10.2014):
Исправление: поправил css стили для заголовка (была ошибка).

2.1.3 (27.10.2014):
Добавил: class="top" к верхнему уровню списка.

Оглавление (содержание) для больших постов 386 комментариев
Полезные 28 Вопросы 6 Все
  • Александр

    Добрый день! Получаю такой результат по "шорткоду":

    Появляются теги <br>:

    Ответить3 дня назад #
    • Kama4662

      Попробуй приоритет 20 или больше поставить для хука:

      add_filter( 'the_content', 'kama_contents_shortcode', 20 );
      1
      Ответить2 дня назад #
      • Александр cайт: blogforest.ru

        Больше спасибо, стало намного лучше, однако, не до конца (приоритет максимальный):

        Ответитьвчера #
        • Kama4662

          У тебя в коде скорее всего есть пустые <h2> <h3> удали их из кода...

          Ответитьвчера #
          • Александр cайт: blogforest.ru

            Точно, картинки были в "заголовках". Спасибо еще раз. smile Очень полезный скрипт!

            Ответитьвчера #

Здравствуйте, !

Ваш комментарий