WordPress как на ладони
Недорогой хостинг для сайтов на WordPress: wordpress.jino.ru

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

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

Написал небольшой класс, который позволяет без шума и пыли, а также красиво и главное быстро создавать оглавления почти любой сложности. Для этого нужно использовать шоткод [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

GitHub
<?php

/**
 * Содержание (оглавление) для больших постов.
 *
 * @author:  Kama
 * @info:    http://wp-kama.ru/?p=1513
 * @version: 4.2
 *
 * @changelog: https://github.com/doiftrue/Kama_Contents/blob/master/CHANGELOG.md
 */
class Kama_Contents {

	public $opt = [

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

	public $contents; // collects html (the contents)

	private $temp;

	public static $inst;

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

	/**
	 * Create instance.
	 *
	 * @param  array $args Options
	 * @return object Instance
	 */
	static function init( $args = [] ){

		is_null( self::$inst ) && self::$inst = new self( $args );

		if( $args )
			self::$inst->set_opt( $args );

		return self::$inst;
	}

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

	/**
	 * Processes the text, turns the shortcode in it into a table of contents.
	 * Use shortcode [contents] or [[contents]] to show shortcode as it is.
	 *
	 * @param string $content      The text, which has a shortcode.
	 * @param string $contents_cb  Сallback function that will process the contents list.
	 *
	 * @return string Processed text with a table of contents, if it has a shotcode.
	 */
	public function shortcode( $content, $contents_cb = '' ){

		$shortcode = $this->opt->shortcode;

		if( false === strpos( $content, "[$shortcode" ) )
			return $content;

		// get contents data
		// use `[[contents` to escape the shortcode
		if( ! preg_match( "/^(.*)(?<!\[)\[$shortcode([^\]]*)\](.*)$/su", $content, $m ) )
			return $content;

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

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

		$content = $m[1] . $contents . $m[3];

		return $content;
	}

	/**
	 * Replaces the headings in the passed text (by ref), creates and returns a table of contents.
	 *
	 * @param string        $content The text from which you want to create a table of contents.
	 * @param array|string  $tags    Array of HTML tags to look for in the passed text.
	 *                               You can specify: tag names "h2 h3" or names of CSS classes ".foo .foo2".
	 *                               You can add "embed" mark here to get <ul> tag only (without header and wrapper block).
	 *                               It can be useful for use contents inside the text as a list.
	 *
	 * @return string table of contents HTML code.
	 */
	public function make_contents( & $content, $tags = '' ){

		// 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;

		// for shortcode
		if( is_string( $tags ) ){
			$extra_tags = [];

			if( preg_match( '/(as_table)="([^"]+)"/', $tags, $mm ) ){

				$extra_tags[ $mm[1] ] = explode( '|', $mm[2] );
				$tags = str_replace( " {$mm[0]}", '', $tags ); // cut
			}

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

			$tags += $extra_tags;
		}

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

		// check tags
		foreach( $tags as $key => $tag ){

			// extra tags
			if( $key === 'as_table' ){
				$this->temp->as_table = $tag;

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

			// remove special marker tags and set $args
			if( in_array( $tag, [ 'embed', 'no_to_menu' ] ) ){

				if( $tag === 'embed' )
					$this->temp->embed = true;

				if( $tag === 'no_to_menu' )
					$this->opt->to_menu = false;

				unset( $tags[ $key ] );
				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[ $key ] );
				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 = [];

		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;

		$this->opt->toc_page_url = $this->opt->page_url ?: home_url( parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) );

		// collect and replace
		$_content = preg_replace_callback( "/$patt_in/is", [ $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 for check reasone

		// html
		$embed = isset( $this->temp->embed );
		$title = & $this->opt->title;
		$is_title = ! $embed && $title;

		// markup
		$ItemList = $this->opt->markup ? ' itemscope itemtype="https://schema.org/ItemList"' : '';
		$ItemName = $this->opt->markup ? '<meta itemprop="name" content="'. wp_strip_all_tags( $title ) .'" />' : '';

		if( isset( $this->temp->as_table ) ){

			$contents = '
			<table class="kamatoc" id="tocmenu"'. $ItemList .'>
				'. $ItemName .'
				<thead>
					<tr>
						<th>'. esc_html( $this->temp->as_table[0] ) .'</th>
						<th>'. esc_html( $this->temp->as_table[1] ) .'</th>
					</tr>
				</thead>
				<tbody>
					'. implode( '', $this->contents ) .'
				</tbody>
			</table>';
		}
		else {

			if( $is_title ){
				$contents_wrap_patt = '
					<div class="kamatoc-wrap">
						<span class="kamatoc-wrap__title">' . $title . '</span>
						%s
					</div>
				';
			}
			else{
				$contents_wrap_patt = '%s';
			}

			$contents = '
				<ul class="kamatoc" id="tocmenu" '. $ItemList .'>
					'. $ItemName .'
					'. implode( '', $this->contents ) .'
				</ul>';

			$contents = sprintf( $contents_wrap_patt, $contents );
		}

		$css_code = $js_code = '';

		if( $this->opt->css ){
			$css_code = '<style>' . preg_replace( '/[\n\t ]+/', ' ', $this->opt->css ) . '</style>';
		}

		if( $this->opt->js ){
			$js_code = '<script>' . preg_replace( '/[\n\t ]+/', ' ', $this->opt->js ) . '</script>';
		}

		$this->contents = $css_code . $contents . $js_code;

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

		return $this->contents;
	}

	/**
	 * Cut the kamaTOC shortcode from the content.
	 *
	 * @param string $text
	 *
	 * @return string
	 */
	public function strip_shortcode( $text ){
		return preg_replace( '~\[' . $this->opt->shortcode . '[^\]]*\]~', '', $text );
	}

	/**
	 * Callback function to replace and collect contents.
	 *
	 * @param array $match
	 *
	 * @return string
	 */
	private function _make_contents_callback( $match ){
		$temp = & $this->temp;

		// it's only class selector in pattern
		if( count( $match ) === 5 ){
			list( $tag, $attrs, $level_tag, $tag_txt ) = array_slice( $match, 1 );
		}
		// it's found tag selector
		elseif( count( $match ) === 4 ){
			list( $tag, $attrs, $tag_txt ) = array_slice( $match, 1 );

			$level_tag = $tag; // class name
		}
		// it's found class selector
		else{
			list( $tag, $attrs, $level_tag, $tag_txt ) = array_slice( $match, 4 );
		}

		if( isset( $this->temp->as_table ) ){
			$tag_desc = '';
			//if( preg_match( '/'. preg_quote($match[0], '/') .'\s*<p>((?:.(?!<\/p>))+)./', $this->temp->content, $mm ) ){
			if( preg_match( '/' . preg_quote( $match[0], '/' ) . '\s*<p>(.+?)<\/p>/', $this->temp->content, $mm ) ){
				$tag_desc = $mm[1];
			}
		}

		$opt = $this->opt; // short love

		// if tag contains id attribute it become anchor
		if(
			$opt->anchor_attr_name
			&&
			preg_match( '/ *(' . preg_quote( $opt->anchor_attr_name, '/' ) . ')=([\'"])(.+?)\2 */i', $attrs, $id_match )
		){
			// delete 'id' or 'name' attr from attrs
			if( in_array( $id_match[1], [ 'id', 'name' ] ) )
				$attrs = str_replace( $id_match[0], '', $attrs );

			$anchor = $this->_sanitaze_anchor( $id_match[3] );
		}
		else{
			$anchor = $this->_sanitaze_anchor( $tag_txt );
		}

		$level = @ $temp->level_tags[ $level_tag ];
		if( $level > 0 ){
			$unit = preg_replace( '/\d/', '', $opt->margin ) ?: 'px';
			$sub = ( $opt->margin ? ' class="kamatoc__sub kamatoc__sub_' . $level . '" style="margin-left:' . ( $level * (int) $opt->margin ) . $unit .';"' : '' );
		}
		else{
			$sub = ' class="kamatoc__top"';
		}

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

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

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

		if( isset( $this->temp->as_table ) ){

			$this->contents[] = "\t".'
				<tr>
					<td'. ( $_is_mark ? ' itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"' : '' ) .'>
						<a rel="nofollow" href="'. "$opt->page_url#$anchor" .'">'. $cont_elem_txt .'</a>
						'.( $_is_mark ? ' <meta itemprop="name" content="'. $cont_elem_txt .'" />':'' ).'
						'.( $_is_mark ? ' <meta itemprop="url" content="'. "$opt->toc_page_url#$anchor" .'" />':'' ).'
						'.( $_is_mark ? ' <meta itemprop="position" content="'. $temp->counter .'" />':'' ).'
					</td>
					<td>'. $tag_desc .'</td>
				</tr>'. "\n";
		}
		else {

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

		if( $opt->anchor_link )
			$tag_txt = '<a rel="nofollow" class="kamatoc-anchlink" href="#'. $anchor .'">'. $opt->anchor_link .'</a> ' . $tag_txt;

		// anchor type: 'a' or 'id'
		if( $opt->anchor_type === 'a' )
			$new_el = '<a class="kamatoc-anchor" name="'. $anchor .'"></a>'."\n<$tag $attrs>$tag_txt</$tag>";
		else
			$new_el = "\n<$tag id=\"$anchor\" $attrs>$tag_txt</$tag>";

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

			// remove '$to_menu' if simbols beatween $to_menu too small (< 300)
			// mb_strpos( $temp->content, $match[0] ) - в 150 раз медленнее!
			$pos = strpos( $temp->content, $match[0] );
			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;
	}

	/**
	 * anchor transliteration
	 *
	 * @param string $anch
	 *
	 * @return string
	 */
	private function _sanitaze_anchor( $anch ){
		$anch = strip_tags( $anch );

		$anch = apply_filters( 'kama_cont::sanitaze_anchor_before', $anch, $this );

		$anch = html_entity_decode( $anch );

		// iso9
		$anch = strtr( $anch, [
			'А'=>'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'
		] );

		$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 = apply_filters( 'kama_cont::sanitaze_anchor', $anch, $this );

		$anch = self::_unique_anchor( $anch );

		return $anch;
	}

	/**
	 * Adds number at the end if this anchor already exists.
	 *
	 * @param string $anch
	 *
	 * @return string
	 */
	static function _unique_anchor( $anch ){
		static $anchors = [];

		// check and unique anchor
		if( isset( $anchors[ $anch ] ) ){

			$lastnum = substr( $anch, -1 );
			$lastnum = is_numeric( $lastnum ) ? $lastnum + 1 : 2;
			$anch = preg_replace( '/-\d$/', '', $anch );

			return call_user_func( __METHOD__, "$anch-$lastnum" );
		}

		$anchors[ $anch ] = 1;

		return $anch;
	}

}
меню

Как пользоваться классом 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;

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

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

	// погнали...
	$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(), как в первом.

Вариант 1

Добавьте эту функцию рядом с классом и используйте там где нужно вывести оглавление. В функцию нужно передать объект поста (по умолчанию передается global $post) для которого нужно получить оглавление или можно передать сам текст для которого нужно вывести оглавление (текст нужно передавать в переменной, которая потом будет использована для вывода текста, именно эта переменная, потому что в ней по ссылке изменяется текст - к его заголовкам добавляются анкоры).

// для вывода оглавления
function get_kama_contents( & $post = false ){
	if( ! $post ) $post = & $GLOBALS['post'];

	if( is_string($post) )
		$post_content = & $post;
	else
		$post_content = & $post->post_content;

	$args = array(
		'selectors' => array('h2','h3'),
		'min_found' => 1,
		'margin'    => 0,
		'to_menu'   => false,
		'title'     => false,
	);

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

	// чтобы правильно работала the_content() которая работатет на основе get_the_content()
	global $pages;
	if( $pages && count($pages) == 1 ){
		$pages[0] = $post_content;
	}
	else{
		// об этом варианте молчит наука. Тут надо выдумывать...
	}

	return $contents;
}

Теперь выводим оглавление, например в сайдбаре:

echo get_kama_contents();

Заметка: get_kama_contents() нужно вызывать раньше чем выводиться контент. Если в HTML содержание нужно вывести ниже чем выводится контент, то вызовите функцию сохраните оглавление и выведите его ниже.

$contents = get_kama_contents();

// код код
the_content();

// выводим оглавление
echo $contents;
Вариант 2

Разместив этот код рядом с основным классом, оглавление можно вывести в любом месте шаблона, например в сайдбаре. Для этого используйте строку: 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'] = '';

// Какой тип анкора использовать: 'a' - <a name="anchor"></a> или 'id' -
$args['anchor_type'] = 'id';

// Название атрибута тега из значения которого будет браться анкор
// (если этот атрибут есть у тега). Ставим '', чтобы отключить такую проверку...
// пример: у заголовка есть атрибут id="fooo". Анкором станет текст #fooo, а не текст заголовка
$args['anchor_attr_name'] = 'id';
меню

HTML и CSS

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

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

<div class="kamatoc-wrap">
	<span class="kamatoc-wrap__title">Оглавление:</span>

	<ul class="kamatoc" id="tocmenu"  itemscope itemtype="https://schema.org/ItemList">
		<meta itemprop="name" content="Оглавление:" />

		<li class="kamatoc__top" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
			<a rel="nofollow" href="#chto-takoe-metadannye">Что такое метаданные?</a>
			<meta itemprop="name" content="Что такое метаданные?" />
			<meta itemprop="url" content="https://wp-kama.ru/handbook/codex/metadata#chto-takoe-metadannye" />
			<meta itemprop="position" content="1" />
		</li>

		<li class="kamatoc__sub kamatoc__sub_1" style="margin-left:2em;" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
			<a rel="nofollow" href="#tablitsy-metadannyh-v-baze-dannyh-wp">Таблицы метаданных в Базе Данных WP</a>
			<meta itemprop="name" content="Таблицы метаданных в Базе Данных WP" />
			<meta itemprop="url" content="https://wp-kama.ru/handbook/codex/metadata#tablitsy-metadannyh-v-baze-dannyh-wp" />
			<meta itemprop="position" content="2" />
		</li>

		<li class="kamatoc__sub kamatoc__sub_1" style="margin-left:2em;" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
			<a rel="nofollow" href="#skrytye-metapolya">Скрытые (защищенные) метаполя</a>
			<meta itemprop="name" content="Скрытые (защищенные) метаполя" />
			<meta itemprop="url" content="https://wp-kama.ru/handbook/codex/metadata#skrytye-metapolya" />
			<meta itemprop="position" content="3" />
		</li>

		<li class="kamatoc__top" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
			<a rel="nofollow" href="#funktsii-metadannyh">Функции метаданных</a>
			<meta itemprop="name" content="Функции метаданных" />
			<meta itemprop="url" content="https://wp-kama.ru/handbook/codex/metadata#funktsii-metadannyh" />
			<meta itemprop="position" content="4" />
		</li>

		<li class="kamatoc__top" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
			<a rel="nofollow" href="#ochistka-znachenij-metapolej-pri-sohranenii">Очистка значений метаполей при сохранении</a>
			<meta itemprop="name" content="Очистка значений метаполей при сохранении" />
			<meta itemprop="url" content="https://wp-kama.ru/handbook/codex/metadata#ochistka-znachenij-metapolej-pri-sohranenii" />
			<meta itemprop="position" content="5" />
		</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) '.'; }
меню

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

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

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

	// Прокрутка на все якоря (анкоры) (#) и на a[name]. v1.3
	$(document).on( 'click.smoothscroll', 'a[href*="#"]', function( e ){

		let hash    = this.hash
		let _hash   = hash.replace( /#/, '' )
		let theHref = $(this).attr('href').replace( /#.*/, '' )

		// у кнопки есть атрибут onclick значит у нее другая задача
		if( this.onclick )
			return

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

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

		if( ! $target.length )
			return

		e.preventDefault()

		let scrollTo = $target.offset().top - 50

		$('html:first, body:first')
			.stop()
			.animate( { scrollTop: scrollTo }, 200, 'swing', function(){
				window.history.replaceState( null, document.title, hash )
			} )
	})

})
меню

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

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

/**
 * Показать/скрыть Содержание. Кнопка добавляется после Текста в заголовок - "Содержание: [скрыть]"
 * v 0.3
 */
// document.ready
jQuery(function($){

	let $title  = $('.kc__title')
	let showtxt = '[показать]'
	let hidetxt = '[скрыть]'
	let $but    = $('<span class="kc-show-hide" style="cursor:pointer;margin-left:.5em;font-size:80%;">'+ hidetxt +'</span>')

	$but.on( 'click', function(){

		let $the = $(this)
		let $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 1.0
 */
jQuery(document).ready(function($){

	let $title  = $('.kc__title').css({ cursor:'pointer' })
	let showico = ' ▾'
	let hideico = ' ▴'
	let collapsedKey = 'contents_collapse'
	let setIco = function( $that, type ){
		$that.text( type === 'hide' ? $that.text().replace( showico, hideico ) : $that.text().replace( hideico, showico ) )
	}

	$title.each(function(){

		let $the = $(this);

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

		$the.on( 'click', function(){

			let $cont = $the.next('.contents')

			if( $cont.is(':visible') ){
				$cont.slideUp(function(){
					$the.addClass('collapsed')
					setIco( $the, 'show' )
					window.localStorage.setItem( collapsedKey, '1' )
				})
			}
			else {
				$cont.slideDown(function(){
					$the.removeClass('collapsed')
					setIco( $the, 'hide' )
					window.localStorage.removeItem( collapsedKey )
				})
			}
		})

		// свернем/развернем на основе куков
		if( window.localStorage.getItem(collapsedKey) === '1' ){
			setIco( $the, 'show' )
			$the.next('.contents').hide()
		}
	})
});

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

меню

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

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

  • Easy Table of Contents - удобный и функциональный плагин, который позволяет вам вставлять оглавление в ваши посты, страницы и пользовательские типы постов.

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

  • LuckyWP Table of Contents — генерирует содержание для записей, страниц и произвольных типов постов. Множество настроек, Gutenbeg-блок, кнопка в классическом редакторе. Поддерживает как ручное, так и автоматическое добавление содержания в посты.
меню

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

  • Добавить ответ
Результаты

Что было сделано благодаря опросу:
- Добавить Schema разметку и оптимизировать код для поисковиков (30 голосов)

Заказать по очень недорогой цене комментарии в Инстаграм Вы можете на сайте Avi1.ru. При этом Вам не придется тратить свое время на поиск действительно надежного сервиса. Здесь Вы найдете все, что нужно: качественные услуги, приятные цены, гарантии и вежливое обслуживание.

379 комментов
Полезные 31 Вопросы 16 Все
    Войти