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

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

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

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

  • не показывать заголовок: "Содержание:";
  • не показывать ссылки на оглавление в тексте;
  • настроить под себя CSS стили;
  • изменить HTML теги по которым будет строиться оглавление. Можно указать любые теги не только H1, H2 ... но и strong, em и т.д. Или вообще, указать классы html тега, например: .foo, .bar;
  • указать минимальное количество заголовков для того, чтобы оглавление выводилось.
  • указать минимальную длину текста, чтобы оглавление выводилось.
  • указать название шоткода, который будет использоваться в тексте для создания оглавления.
к началу

Код класса Kama_Contents

/**
 * Содержание (оглавление) для больших постов.
 *
 * Author: Kama
 * Page: http://wp-kama.ru/?p=1513
 * ver: 3.10
 *
 * 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 тег - ссылку, удалим если она есть...
		$cont_title = $title;
		if( strpos( $cont_title, '</a>') ) $cont_title = preg_replace('~<a[^>]+>|</a>~', '', $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( $str ){
		$str = strip_tags( $str );

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

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

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

		return strtolower( $str );
	}

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

/**
 * 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 ){
	$args = array();

	//$args['shortcode'] = 'list'; // [list] вместо [contents]

	if( is_singular() ){
		//$args['margin'] = 30;
		//$args['page_url'] = get_permalink();
		$args['to_menu']  = 'к оглавлению ↑';
		$args['title']    = 'Оглавление:';

		return Kama_Contents::init( $args )->shortcode( $content );
	}
	// вырежем шорткод
	else
		return Kama_Contents::init( $args )->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 }, 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" к верхнему уровню списка.

Оглавление (содержание) для больших постов 346 комментариев
Полезные 24 Вопросы 12 Все
  • Alex
    @

    Подскажите, можно ли сделать анкор для заголовков вида: hh-1, hh-2, hh-3 и т.д
    То есть чтобы добавлялись цифры к ключевому слову, которое можно указать в настройках?
    Сейчас скрипт делает транслитерацию и все. Это не всегда удобно, если длина заголовков большая.

    Ответить13 дней назад #
    • Kama4192

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

      Ответить6 дней назад #
  • Akira Tsukizawa
    @

    Спасибо вам добрый человек за очень полезную функцию! mail

    Ответить12 дней назад #
  • Андрей cайт: not

    Доброго времени суток! Помогите пожалуйста, я сделал содержание липким(при прокрутке статьи оно следует за статью в свернутом состоянии.) По нажатию оно раскрывается и человек может перейти по анкору. Но проблема такая, необходимо, чтобы при клике по анкору/заголовку - чтобы список(содержание) снова сворачивался. Чтобы человек при прокрутке человеку не приходилось его снова закрывать.

    Ответить10 дней назад #
  • Владимир cайт: appleiwatch.name
    @

    Приветствую!

    А каким-образом сделать так, чтобы содержание не выводилось для страниц, а выводилось только для постов?

    Спасибо!

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

      При инициализации используй условие is_single()

      Ответитьвчера #
      • Владимир
        @

        А можешь подробнее указать, где происходит инициализация?! А то не совсем понятно мне((

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

          Например "#2 Оглавление вверху каждого поста" - это код запуска класса - инициализация. В нем можно вставить проверку... В прочем она там уже есть:

          if( ! is_singular() ) return $content;

          В твоем случае нужно заменить на

          if( ! is_single() ) return $content;

          или на

          if( ! is_singular('post') ) return $content;
          Ответитьвчера #

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

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