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');
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' );
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' );
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" к верхнему уровню списка.

Оглавление (содержание) для больших постов 373 комментария
Полезные 25 Вопросы 6 Все
  • Виталий cайт: wp-r.ru

    Еще один нюанс.
    Содержание автоматически выводится. И текст содержания попадает в the_excerpt на Главной и рубриках. Т.е. подменяет собой начало текста статьи.

    • Kama4464

      И какое решение предлагаешь, вырезать из excerpt фильтром?

      Можно попробовать доп проверку обработки шорткода добавить - is_main_query()

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

        Такой же вопрос, как у Виталия. Нужно этот код вставить, чтобы в excerpt содержание не попало?

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

        И еще вопрос, как вы добавили стрелочку от родительского комментария к дочернему?

        Ответить11 дней назад #
  • Алексей cайт: ww.ru

    Здравствуйте, скрипт просто шикарен. Возник такой вопрос: как можно дописать скрипт вида [contents p22] для отображения содержания страницы ID которой 22. Чтобы собрать содержание из нужных страниц такого плана [contents p22] [contents p23].
    Определить "p" легко но как подставить в $page_url

    • Алексей cайт: www.ru

      А раз нельзя несколько раз на странице использовать [contents] то тогда так [contents p22,p23,p25]

      • Kama4464

        Надо брать контент и обрабатывать и получать оглавление и собирать их. Я не понял что тебе нужно. Напиши в личку, решим вопрос за символическую плату...

  • campusboy1844 cайт: wp-plus.ru

    Хотелось бы видеть данный класс в виде плагина в репозитории, чтобы иметь актуальную версию. Так же мне не очень понравилась реализация плавной прокрутки. По дефолту сразу лучше написать $target.offset().top - 100, чтобы промотка осуществлялась до момента чуть выше, чем находится заголовок - так эстетически приятнее. Плюс, если человек авторизован и у него показывается дефолтый тулбокс вверху, то он перекроет при промотке заголовок и будет точно не понятно, что произошло. Так же не понравилось использование hash.replace, так как теперь, если нажать "Назад", то страница вверх (где щелкнули по пункту меню) не прокрутится, всё останется как есть.

    • campusboy1844 cайт: wp-plus.ru

      Потому использую такой (обычный) код:

      jQuery(document).ready(function($) {
        $('a[href^="#"]').click(function(){
      	var el = $(this).attr('href');
      	$('html, body').animate({scrollTop: $(el).offset().top - 100}, 500);
        });
      });
      • Kama4464

        offset - 100 добавил, спасибо! thank_you Насчет хэша - это палка о двух концах, пусть так будет, кому надо изменят.

  • Егор

    Доброго времени суток. Благодарю за ваши старания. К сожалению возникла проблема. Необходимо вывести содержание и в теле записи и в сайдбаре. Однако, для содержаний назначаются разные анкоры. Отчего то тому, что в записи, вместо кавычек и дефиса, назначаются цифры.

    • Kama4464

      Покажи скрин, так не особо понятно в чем проблема...

      • Егор

        Да я уже TOC поставил. Ваш скрипт меня привлек тем, что можно было в записи выводить заголовки, скажем, h2, h3, h4, h5. А для сайтбара только h2 и h3. Ну это я для примера. В плагинах же такой настройки я не нашел. Трабла у меня заключалась в том, что для заголовка типа:
        Трам трам трам - "Трам трам трам".
        В записи добавлялся анкор
        #tram-tram-tram812-126tram-tram-tram127
        А в сайтбаре
        #tram-tram-tram---tram-tram-tram-

      • Егор

        Забыл добавить, в теле записи заголовок отображается с кавычками такого типа <>, а в сайтбаре такого "". То же самое и с дефисом. В записи он длинный, а в сайтбаре короткий.

  • Akira @

    Спасибо за полезную функцию! Разрешите вопрос. Замечал что в google при поиске, выдаёт на некоторых сайтах "Перейти к разделу". Выглядит это примерно так.

    Так вот, возможно ли такое сделать? При клике, мы попадаем на сайт и в нужный раздел, это очень удобно.

    • campusboy1844 cайт: wp-plus.ru

      Почитайте о микроразметке smile

      2
      • Akira @

        Вроде стоит

        // Включить микроразметку?
        		'markup'      => true,
        

        Или надо заменить строку на?

        $args['markup']   = true;
        

        sorry Спасибо за ответ.

        • campusboy1844 cайт: wp-plus.ru

          Я именно к тому примеру, что Вы привели. Обычно это достигается хлебными крошками или же просто шаблон страницы размечается в соответствии со стандартами микроразметки. Этот плагин содержания не для того создан. Ну и вдобавок, Гугл любит экспериментировать и то, что Вы показали на скрине похоже на это. Во всяком случае я нашёл данный сайт по разным запросам и такой ссылочки в сниппете не было.

          2
          • Akira @

            Ещё раз спасибо Вам! Да, дело оказалось в другом, страницы которые я проверял, не были проиндексированы, в гугл вебмастере разметка есть, и в поиске выдаёт нормально.

  • Дмитрий

    Здравствуйте, подскажите как можно использовать оглавление в виджете из одной определенной статьи?

    Ответить4 месяца назад #
  • Виктор

    Здравствуйте! Подскажите как быть в таком случае:

    <h2>Заголовок1</h2>
    <h3>Пункт1</h3>
    <h3>Пункт2</h3>
    <h2>Заголовок2</h2>
    <h3>Пункт1</h3>
    <h3>Пункт2</h3>

    То есть в блоках h3 под h2 имеются 2 пункта, которые абсолютно одинаковы в обоих случаях. Как результат при нажатии на Пункт1 во 2 блоке перекидывает на Пункт1, но в первом блоке потому что ссылки в обоих местах одинаковы: #punkt1.

    Ответить4 месяца назад #
  • Александр @

    Добрый день! По неизвестной причине добавляется 2 одинаковых ID, к самому заголовку и к пункту меню.

    Ответить3 месяца назад #
    • Александр @

      Всё, разобрался. По какой то причине к заголовкам добавились ID в не зависимости используется в нём содержание или нет. Поэтому скрипт просто копировал в меню заголовок как есть, вместе со span и его id
      <h2><span id="link">Заголовок</span></h2>

      Зашел просто в редактирование поста и удалил лишнее. Если еще раз поймаю этот "прикол" попробую его детальнее описать и выяснить причину.

      Ответить3 месяца назад #
      • Александр @

        В общем суть была вот в чём, предыдущий программист уже использовал этот скрипт и при переносе контента на новый дизайн в заголовках сохранились все обёртки, классы, id и и прочее.

        Возможно данный участок

        $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 );

        имеет смысл немного скорректировать в сторону полной очистки от тегов, классов, id и прочего, т.к. они могут содержать с собой какие-то стили.

        Например

        <h2><em>Заголовок</em></h2>

        в таком виде будет перенесен в меню, и в итоге в меню будет пункт выделенный курсивом. Тоже самое и с классами будет.

        Как вариант перечислить в все существующие теги, но я думаю есть более элегантное решение...

        Ответить3 месяца назад #
        • Kama4464

          теги нужны иногда. Например я так жирным выделяю намеренно. Тоже самое может быть удобно сделать с EM... Следите за тем что в заголовках... Теги которые откровенно могут ломать все удаляются уже. Может туда DIV еще нужно добавить на всякий - но это очень уж редкий случай

          Ответить3 месяца назад #
  • prosto_kvasno @

    Если в статью вставляется шоткод [contents], то в html-разметке перед и после блока оглавления появляются лишние теги <p></p>

    получается вот так:

    <p></p>
    <div class="kc__wrap" itemscope="" itemtype="http://schema.org/ItemList">...</div>
    <p></p>

    Вопрос, как убрать <p></p>? Спасибо

    Ответить2 месяца назад #
    • Kama4464

      Как кусок кода статьи выглядит в html редакторе? Там навернео & nbsp; есть.

      Ответить2 месяца назад #
      • prosto_kvasno @

        В консоли разработчика гугл показывается код, приведенный выше. Сейчас глянул html код и увидел, что в тег <p> вкладывается <div>. Хотя два тега блочные по умолчанию, но обычно они вкладываются наоборот: в <div> вкладывается <p>. Из-за этого в хроме верстка немного ломается. Надо либо поменять порядок вложения тегов, либо блок оглавления оборачивать не в <div>.

        Ответить2 месяца назад #
        • Kama4464

          Проблема не в моем коде, а на сайте где-то, или плагин или какой-то код что-то делает с контентом... Я этот код много где использую, все нормально работает... Поищи еще проблемные плагины или коды, ради эксперемента попробуй тему смени...

          Ответить2 месяца назад #
          • prosto_kvasno @

            Позже проверю и, если найду причину, отпишу. Спасибо.

            Ответить2 месяца назад #

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

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