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
/**
 * Contents (table of contents) for large posts.
 *
 * @author:  Kama
 * @info:    http://wp-kama.ru/?p=1513
 * @version: 4.3.3
 *
 * @changelog: https://github.com/doiftrue/Kama_Contents/blob/master/CHANGELOG.md
 */
class Kama_Contents {

	public $opt = [
		'margin'           => '2em',
		'selectors'        => 'h2 h3 h4',
		'to_menu'          => 'к оглавлению ↑',
		'title'            => 'Оглавление:',
		'js'               => '',
		'min_found'        => 2,
		'min_length'       => 2000,
		'page_url'         => '',
		'shortcode'        => 'contents',
		'spec'             => '',
		'anchor_type'      => 'id',
		'anchor_attr_name' => 'id',
		'markup'           => false,
		'anchor_link'      => '',
		'tomenu_simcount'  => 800,
	];

	/**
	 * Collects html (the contents).
	 *
	 * @var array
	 */
	public $contents;

	/**
	 * @var array
	 */
	private $temp;

	/**
	 * @var Kama_Contents
	 */
	public static $inst;


	public static function init( $args = [] ){

		self::$inst || self::$inst = new self();

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

		return self::$inst;
	}

	/**
	 * Create instance.
	 *
	 * @param array $args {
	 *     Parameters.
	 *
	 *     @type string       $margin            Отступ слева у подразделов в px|em|rem.
	 *     @type string       $selectors         HTML теги по котором будет строиться оглавление: 'h2 h3 h4'.
	 *                                           Порядок определяет уровень вложености.
	 *                                           Можно указать строку или массив: [ 'h2', 'h3', 'h4' ] или 'h2 h3 h4'.
	 *                                           Можно указать атрибут class: 'h2 .class_name'.
	 *                                           Если нужно чтобы разные теги были на одном уровне,
	 *                                           указываем их через |: 'h2|dt h3' или [ 'h2|dt', 'h3' ].
	 *     @type string       $to_menu           Ссылка на возврат к оглавлению. '' - убрать ссылку.
	 *     @type string       $title             Заголовок. '' - убрать заголовок.
	 *     @type string       $js                JS код (добавляется после HTML кода)
	 *     @type int          $min_found         Минимальное количество найденных тегов, чтобы оглавление выводилось.
	 *     @type int          $min_length        Минимальная длина (символов) текста, чтобы оглавление выводилось.
	 *     @type string       $page_url          Ссылка на страницу для которой собирается оглавление.
	 *                                           Если оглавление выводиться на другой странице...
	 *     @type string       $shortcode         Название шоткода.
	 *     @type string       $spec              Оставлять символы в анкорах. For example: `'.+$*=`.
	 *     @type string       $anchor_type       Какой тип анкора использовать: 'a' - `<a name="anchor"></a>` или 'id'.
	 *     @type string       $anchor_attr_name  Название атрибута тега из значения которого будет браться
	 *                                           анкор (если этот атрибут есть у тега). Ставим '', чтобы отключить такую проверку...
	 *     @type bool         $markup            Включить микроразметку?
	 *     @type string       $anchor_link       Добавить 'знак' перед подзаголовком статьи со ссылкой
	 *                                           на текущий анкор заголовка. Укажите '#', '&' или что вам нравится.
	 *     @type int          $tomenu_simcount   Минимальное количество символов между заголовками содержания,
	 *                                           для которых нужно выводить ссылку "к содержанию".
	 *                                           Не имеет смысла, если параметр 'to_menu' отключен. С целью производительности,
	 *                                           кириллица считается без учета кодировки. Поэтому 800 символов кириллицы -
	 *                                           это примерно 1600 символов в этом параметре. 800 - расчет для сайтов на кириллице.
	 * }
	 *
	 * @return Kama_Contents
	 */
	public function __construct( $args = [] ){
		$this->set_opt( $args );
	}

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

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

	/**
	 * 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 );
		}

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

	/**
	 * 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 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( string & $content, string $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;
		}

		$this->temp->original_tags = $tags;

		// prase tags as string
		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 );

		// get parameters from tags
		foreach( $tags as $key => $tag ){

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

				unset( $tags[ $key ] );
			}
			elseif( 'embed' === $tag ){
				$this->temp->embed = true;

				unset( $tags[ $key ] );
			}
			elseif( 'no_to_menu' === $tag ){
				$this->opt->to_menu = false;

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

		// remove tag if it's not exists in content
		foreach( $tags as $key => $tag ){

			$patt = ( $tag[0] === '.' )
				? 'class=[\'"][^\'"]*' . substr( $tag, 1 )
				: "<$tag";

			if( ! preg_match( "/$patt/i", $content ) ){
				unset( $tags[ $key ] );
			}
		}

		if( ! $tags ){
			return '';
		}

		// PREPARE TAGS ---
		$this->collect_contents( $content, $tags );

		// 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="'. esc_attr( wp_strip_all_tags( $title ) ) .'" />' : '';

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

			$contents = '
			<table class="kamatoc kamatoc_js" 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 kamatoc_wrap_title_js">' . $title . '</span>
						%s
					</div>
				';
			}
			else{
				$contents_wrap_patt = '%s';
			}

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

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

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

		/**
		 * Allow to change result contents string.
		 *
		 * @param string        $contents
		 * @param Kama_Contents $inst
		 */
		$contents = apply_filters( 'kamatoc__contents', "$contents\n$js_code", $this );

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

		return $contents;
	}

	private function collect_contents( string & $content, array $tags ): void {

		// group HTML classes & tags for regex patterns
		$class_patt = $tag_patt = [];
		// collect levels
		$level_tags = [];
		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;
		}

		$level_tags = array_flip( $level_tags );

		// fix levels if it's not start from zero
		if( reset( $level_tags ) !== 0 ){
			while( reset( $level_tags ) !== 0 ){
				$level_tags = array_map( static function( $val ){ return $val - 1; }, $level_tags );
			}
		}

		// set equal level if tags specified with tag1|tag2
		$_prev_tag = '';
		foreach( $level_tags as $tag => $lvl ){

			if( $_prev_tag && false !== strpos( $this->temp->original_tags, "$_prev_tag|$tag" ) ){
				$level_tags[ $tag ] = $_prev_lvl;
			}

			$_prev_tag = $tag;
			$_prev_lvl = $lvl;
		}

		// set the levels one by one if they were broken after the last operation
		$_prev_lvl = 0;
		foreach( $level_tags as & $lvl ){

			// fix next lvl - it's wrong
			if( ! in_array( $lvl, [ $_prev_lvl, $_prev_lvl + 1 ], true ) ){

				$lvl = $_prev_lvl + 1;
			}

			$_prev_lvl = $lvl;
		}
		unset( $lvl );

		$this->temp->level_tags = $level_tags;

		// COLLECT CONTENTS ---

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

	}

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

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

			$level_tag = $tag; // class name
		}
		// it's class selector
		else{
			[ $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; // simplify

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

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

		// table
		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="item" content="'. esc_attr( "$opt->toc_page_url#$anchor" ) .'" />':'' ).'
						'.( $_is_mark ? ' <meta itemprop="name" content="'. esc_attr( wp_strip_all_tags( $cont_elem_txt ) ) .'" />':'' ).'
						'.( $_is_mark ? ' <meta itemprop="position" content="'. $temp->counter .'" />':'' ).'
					</td>
					<td>'. $tag_desc .'</td>
				</tr>'. "\n";
		}
		// list (li)
		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="item" content="'. esc_attr( "$opt->toc_page_url#$anchor" ) .'" />':'' ).'
					'.( $_is_mark ? ' <meta itemprop="name" content="'. esc_attr( wp_strip_all_tags( $cont_elem_txt ) ) .'" />':'' ).'
					'.( $_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( 'kamatoc__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( 'kamatoc__sanitaze_anchor', $anch, $this );

		return self::_unique_anchor( $anch );
	}

	/**
	 * Adds number at the end if this anchor already exists.
	 *
	 * @param string $anch
	 *
	 * @return string
	 */
	public 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. При этом Вам не придется тратить свое время на поиск действительно надежного сервиса. Здесь Вы найдете все, что нужно: качественные услуги, приятные цены, гарантии и вежливое обслуживание.

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