Зміст (зміст) для великих постів (kamatoc)

Не рідко великі посади ми поділяємо логічними підзаголовками. Часом пост несе у собі якусь зібрану інформацію, розбиту на частини. Для таких постів я волію створювати “зміст” – список посилань з анкорами на підзаголовки в пості. Створювати такий зміст – заняття до того ж болюче, що простіше обійтися без нього (за рідкісним винятком, звичайно).

Написав невеликий клас, який дозволяє без шуму та пилу, а також красиво і головне швидко створювати зміст майже будь-якої складності. Для цього потрібно використовувати шоткод [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
/** @noinspection RegExpRedundantEscape */

namespace KamaWP;

interface Kama_Contents_Interface {

	/** Creates an instance by specified parameters. */
	public function __construct( array $args = [] );

	/** Processes the text, turns the shortcode in it in a table of contents. */
	public function apply_shortcode(string $content): string;

	/** Відрізняються від kamaTOC shortcode з вмісту. */
	public function strip_shortcode(string $content): string;

	/** Відображає об'єкти в $content, creates and returns a table of contents. */
	public function make_contents( string &$content, string $params = '' ): string;

}

/**
 * Contents (table of contents) for large posts.
 *
 * @author Kama
 * @ See http://wp-kama.ru/1513
 *
 * @version 4.3.10
 */
class Kama_Contents implements Kama_Contents_Interface {

	use Kama_Contents__Html;
	use Kama_Contents__Helpers;
	use Kama_Contents__Legacy;

	private static $default_opt = [
		'margin' => '2em',
		'selectors' => 'h2 h3 h4',
		'to_menu' => 'до змісту ↑',
		'title' => 'Зміст:',
		'js' => '',
		'min_found' => 1,
		'min_length' => 2000,
		'page_url' => '',
		'shortcode' => 'contents',
		'spec' => '',
		'anchor_type' => 'id',
		'anchor_attr_name' => 'id',
		'markup' => false,
		'anchor_link' => '',
		'tomenu_simcount' => 800,
		'leave_tags' => true,

		// shortcode additional params
		'as_table' => false,
		'embed' => false,
	];

	/**
	 * @var object Instance options.
	 */
	private $opt;

	/**
	 * Collects html (the contents).
	 *
	 * @var TOC_Elem[]
	 */
	protected $toc_elems;

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

	/**
	 * 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 Назва шоткоду. Типово: 'contents'.
	 * @type string $spec Залишати символи в анкорах. Для прикладу: ``.+$*=`.
	 * @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 – розрахунок для сайтів на кирилиці.
	 * @type bool|string $leave_tags Чи потрібно залишати HTML теги в елементах змісту. З версії 4.3.4.
	 * Можна вказати лише які теги потрібно залишати. Пр: ``<b><strong><var><code>'`.
	 *
	 * }
	 */
	public function __construct( array $args = [] ) {
		$this->set_opt( $args );
	}

	protected function set_opt( $args = [] ): void {
		$this->opt = (object) array_merge( self::$default_opt, (array) $args );
	}

	/**
	 * Processes the text, turns the shortcode in it in a table of contents.
	 * Use shortcode [contents] or [[contents]] до show shortcode as it is.
	 *
	 * @param string $content The text with shortcode.
	 *
	 * @Return string Processed text with a table of contents, якщо це має shotcode.
	 */
	public function apply_shortcode( string $content ): string {

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

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

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

	/**
	 * Cuts out the kamaTOC shortcode з вмісту.
	 *
	 * @param string $content
	 *
	 * @return string
	 */
	public function strip_shortcode( string $content ): string {
		return preg_replace( '~[' . $this->opt->shortcode . '[^]]*]~', '', $content );
	}

	/**
	 * Зберігати headings в тексті (за допомогою ref), твори і відновити table of contents.
	 *
	 * @param string $content The text from which you want to create a table of contents.
	 * @param string $params Array of HTML tags для перегляду в тексті.
	 * "h2 .foo" - Specify: tag names "h2 h3" або names of CSS classes ".foo .foo2".
	 * "embed" - Add "embed" mark here to get `<ul>` tag only (without header and wrapper block).
	 * "as_table="Title|Desc" - Швидка якість.
	 * Це може бути корисним для використання вмісту всередині тексту як листа.
	 *
	 * @return string Table of contents HTML.
	 */
	public function make_contents( string &$content, string $params = '' ): string {

		// Text is too short
		if( mb_strlen( strip_tags( $content ) ) < $this->opt->min_length ){
			return '';
		}

		$this->temp = new stdClass();

		$params_array = $this->parse_string_params( $params );
		$tags = $this->split_params_and_tags( $params_array );
		$tags = $this->get_actual_tags( $tags, $content );

		if(! $tags) {
			unset($this->temp);

			return '';
		}

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

		$this->collect_toc( $content, $tags );

		if( count( $this->toc_elems ) < $this->opt->min_found ){
			unset($this->temp);

			return '';
		}

		$contents = $this->toc_html();

		unset($this->temp);

		return $contents;
	}

	/**
	 * @param string $params
	 *
	 * @return array
	 */
	protected function parse_string_params( string $params ): array {

		$this->temp->original_string_params = $params;

		$ extra_tags = [];

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

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

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

		$params += $extra_tags;

		return array_filter($params);
	}

	/**
	 * Split parameters and tags.
	 *
	 * @param array $params
	 * @param string $content
	 *
	 * @return array
	 */
	protected function split_params_and_tags( array $params ): array {

		$tags = [];

		foreach( $params as $key => $val ){

			// extra tags
			if( 'as_table' === $key ){
				$this->opt->as_table = $val;
			}
			elseif( 'embed' === $val || 'embed' === $key ){
				$this->opt->embed = true;
			}
			elseif( 'no_to_menu' === $val || 'no_to_menu' === $key ){
				$this->opt->to_menu = false;
			}
			else {
				$tags[$key] = $val;
			}
		}

		if(! $tags) {
			$tags = is_array( $this->opt->selectors )
				? $this->opt->selectors
				: explode( ' ', $this->opt->selectors );
		}

		return $tags;
	}

	/**
	 * Remove tag if it's no exists in content (for performance).
	 */
	protected function get_actual_tags( array $tags, string $content ): array {

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

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

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

		return $tags;
	}

	/**
	 * Collect TOC (all titles) від визначеного вмісту.
	 * Replace HTML в спеціальному вмісті.
	 *
	 * @param string $content Changes by ref.
	 * @param array $tags HTML tags (selectors) to collect from content.
	 *
	 * @return void
	 */
	protected function collect_toc( string & $content, array $tags ): void {

		$this->toc_elems = [];

		$this->_set_tags_levels_and_regex_patt( $tags );

		$ patt_in = [];

		if( $this->temp->tag_regex_patt ){
			$tags_in = implode( '|', $this->temp->tag_regex_patt );
			$patt_in[] = "(?:<($tags_in)([^>]*)>(.*?)</1>)";
		}

		if( $this->temp->class_regex_patt ){
			$class_in = implode( '|', $this->temp->class_regex_patt );
			$patt_in[] = "(?:<([^ >]+) ([^>]*class=["'][^>]*($class_in)[^>]*["'][ ^>]*)>(.*?)</" . ( $patt_in ? '4' : '1' ) . '>)';
		}

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

		// collect and replace
		$this->temp->orig_content = $content;

		$new_content = preg_replace_callback( "/$patt_in/is", [ $this, 'collect_toc_replace_callback' ], $content, -1 );

		if( count( $this->toc_elems ) >= $this->opt->min_found ){
			$content = $new_content;
		}

	}

	/**
	 * @param array $match
	 *
	 * @return array
	 */
	protected function _replace_parse_match( $match ){

		$ full_match = $ match [0];

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

		return [$ full_match, $tag, $attrs, $level_tag, $tag_txt];
	}

	protected function _set_tags_levels_and_regex_patt( array $tags ): void {

		// group HTML classes & tags for regex patterns
		$tag_regex_patt = $class_regex_patt = $tags_levels = [];

		foreach( $tags as $tag ){
			// class
			if( $tag[0] === '.' ){
				$ tag = substr ($ tag, 1);
				$_ln = &$class_regex_patt;
			}
			// html tag
			else {
				$_ln = &$tag_regex_patt;
			}

			$_ln[] = $tag;
			$tags_levels[] = $tag;
		}

		$tags_levels = array_flip($tags_levels);

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

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

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

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

		// Set levels one by one, if they have been broken after the last operation
		$_prev_lvl = 0;
		foreach( $tags_levels 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->tags_levels = $tags_levels;
		$this->temp->tag_regex_patt = $tag_regex_patt;
		$this->temp->class_regex_patt = $class_regex_patt;
	}

	/**
	 * Callback function to replace and collect contents.
	 *
	 * @param array $match
	 *
	 * @return string
	 */
	protected function collect_toc_replace_callback( $match ): string {

		[ $full_match, $tag, $attrs, $level_tag, $tag_text ] = $this->_replace_parse_match( $match );

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

		$anchor = $this->_toc_element_anchor( $tag_text, $attrs );

		$this->toc_elems[] = new TOC_Elem( [
			'full_match' => $full_match,
			'tag' => $tag,
			'anchor' => $anchor,
			'text' => $this->_strip_tags_in_elem_txt( $tag_text ),
			'level' => $this->temp->tags_levels[ $level_tag ] ?? 0,
		]);

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

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

		$to_menu = $this->_to_menu_link( $full_match );

		return $to_menu . $new_el;
	}

	protected function _to_menu_link( $full_match ){

		if( ! $this->opt->to_menu ){
			return '';
		}

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

		// remove '$to_menu' if simbols beatween $to_menu too small (< 300)

		// mb_strpos( $this->temp->orig_content, $full_match ) - у 150 разів повільніше!
		$elpos = strpos( $this->temp->orig_content, $full_match );

		if( empty( $this->temp->elpos ) ){
			$prevpos = 0;
			$this->temp->elpos = [ $elpos ];
		}
		else {
			$prevpos = end( $this->temp->elpos );
			$this->temp->elpos[] = $elpos;
		}
		$simbols_count = $elpos - $prevpos;

		if( $simbols_count < $this->opt->tomenu_simcount ){
			$to_menu = '';
		}

		return $to_menu;
	}


}

trait Kama_Contents__Html {

	/**
	 *
	 * @return string
	 */
	protected function _toc_html(): string {

		$ toc = '';
		foreach( $this->toc_elems as $elem ){
			$elem_html = $this->toc_element_html( $elem );
			$toc .= "t$elem_htmln";
		}

		return $toc;
	}

	protected function toc_html(): string {

		// table
		if( $this->opt->as_table ){

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

			$add_wrapper = $this->opt->title && ! $this->opt->embed;
			$contents_wrap_patt = '%s';

			if( $add_wrapper ){

				$contents_wrap_patt = '
					<div class="kamatoc-wrap">
						<div class="kamatoc-wrap__title kamatoc_wrap_title_js">' . $this->opt->title . '</div>
						'. $contents_wrap_patt .'
					</div>
				';
			}

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

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

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

		$contents = $this->replace_markup( $contents );

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

	protected function toc_element_html( TOC_Elem $elem ): string {

		// table
		if( $this->opt->as_table ){

			// Take first sentence
			$quoted_match = preg_quote ($ elem-> full_match, '/');
			//preg_match( "/$quoted_matchs*<p>((?:.(?!</p>))+)/", $this->temp->orig_content, $mm )
			preg_match( "/$quoted_matchs*<p>(.+?)</p>/", $this->temp->orig_content, $mm );
			$tag_desc = $mm? $mm[1] : '';

			$elem_html = '
				<tr>
					<td {ListElement}>
						<a rel="nofollow" href="' . "{$this->opt->page_url}#$elem->anchor" . '">' . $elem->text . '</a>
						{ListElement_item}
						{ListElement_name}
						{ListElement_pos}
					</td>
					<td>'. $tag_desc. '</td>
				</tr>';
		}
		// list(li)
		else {

			if( $elem->level > 0 ){
				$unit = preg_replace( '/d/', '', $this->opt->margin )?: 'px';
				$elem_classes = "kamatoc__sub kamatoc__sub_{$elem->level}";
				$elem_attr = $this->opt->margin ? ' style="margin-left:' . ( $elem->level * (int) $this->opt->margin ) . $unit . ';"' : '';
			}
			else {
				$elem_classes = 'kamatoc__top';
				$elem_attr = '';
			}

			$elem_html = '
				<li class="'. $elem_classes .'" ' . $ elem_attr . '{ListElement}>
					<a rel="nofollow" href="' . "{$this->opt->page_url}#$elem->anchor" . '">' . $elem->text . '</a>
					{ListElement_item}
					{ListElement_name}
					{ListElement_pos}
				</li>';
		}

		$elem_html = $this->replace_elem_markup( $elem_html, $elem );

		/**
		 * Allow to change single TOC element HTML.
		 *
		 * @param string $elem_html
		 */
		return apply_filters( 'kamatoc__elem_html', $elem_html );
	}

	protected function replace_markup( $html ): string {

		$is = $this->opt->markup;

		$replace = [
			'{ItemList}' => $is ? 'itemscope itemtype="https://schema.org/ItemList"' : '',
			'{ItemName}' => $is ? '<meta itemprop="name" content="' . esc_attr( wp_strip_all_tags( $this->opt->title ) ) . '" />' : '',
		];

		return strtr($html, $replace);
	}

	protected function replace_elem_markup( $html, TOC_Elem $elem ): string {

		$is = $this->opt->markup;

		$replace = [
			'{ListElement}' => $is ? ' itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"' : '',
			'{ListElement_item}' => $is ? ' <meta itemprop="item" content="' . esc_attr( "{$this->temp->toc_page_url}#$elem->anchor" ) . '" />' : '',
			'{ListElement_name}' => $is ? ' <meta itemprop="name" content="' . esc_attr( wp_strip_all_tags( $elem->text ) ) . '" />' : '',
			'{ListElement_pos}' => $is ? ' <meta itemprop="position" content="' . $this->temp->counter . '" />' : '',
		];

		return strtr($html, $replace);
	}

}

trait Kama_Contents__Helpers {

	/**
	 * @param string $tag_txt
	 * @param string $attrs
	 *
	 * @return string
	 */
	protected function _toc_element_anchor( $tag_txt, $attrs ){

		// if tag contains id|name|... attribute it becomes anchor.
		if(
			$this->opt->anchor_attr_name
			&&
			preg_match( '/ *(' . preg_quote( $this->opt->anchor_attr_name, '/' ) . ')=(['"])(.+?)2 */i', $attrs, $ match_anchor_attr )
		) {
			// delete 'id' або 'name' attr from attrs
			if( in_array( $match_anchor_attr[1], [ 'id', 'name' ], true ) ){
				$attrs = str_replace( $match_anchor_attr[0], '', $attrs );
			}

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

		return $anchor;
	}

	/**
	 * @param string $tag_txt
	 *
	 * @return string
	 */
	protected function _strip_tags_in_elem_txt( $tag_txt ){

		// strip all tags
		if( ! $this->opt->leave_tags ){
			$ tag_txt = strip_tags ($ tag_txt);
		}
		// strip all tags, except specified
		elseif( is_string( $this->opt->leave_tags ) ){
			$tag_txt = strip_tags( $tag_txt, $this->opt->leave_tags );
		}
		// leave tags
		// $tag_txt не може містити A, IMG теги - видалимо якщо треба...
		else {

			if( false !== strpos( $tag_txt, '</a>' ) ){
				$tag_txt = preg_replace( '~<a[^>]+>|</a>~', '', $tag_txt );
			}
			if( false !== strpos( $tag_txt, '<img' ) ){
				$tag_txt = preg_replace( '~<img[^>]+>~', '', $tag_txt );
			}
		}

		return $tag_txt;
	}

	/**
	 * anchor transliteration
	 *
	 * @param string $anch
	 *
	 * @return string
	 */
	protected 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 $this->_unique_anchor($anch);
	}

	/**
	 * Adds number at end if this anchor already exists.
	 *
	 * @param string $anch
	 *
	 * @return string
	 */
	protected function _unique_anchor( string $anch ): string {

		if( ! isset( $this->temp->anchors ) ){
			$this->temp->anchors = [];
		}

		// check and unique anchor
		if( isset( $this->temp->anchors[ $anch ] ) ){

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

			return $this->{ __FUNCTION__ }( "$anch-$lastnum" );
		}

		$this->temp->anchors[ $anch ] = 1;

		return $anch;
	}

}

trait Kama_Contents__Legacy {

	/**
	 * Creates an instance з Kama_Contents for later use.
	 *
	 * @param array $args
	 *
	 * @return Kama_Contents
	 */
	public static function init( array $args = [] ) {
		static $inst;

		$args = array_intersect_key( $args, self::$default_opt ); // Leave allowed only
		$inst_key = md5(serialize( $args ) );

		if( empty( $inst[ $inst_key ] ) )){
			$inst[$inst_key] = new self();
			$inst[ $inst_key ]->set_opt( $args );
		}

		return $inst[$inst_key];
	}

	/**
	 * Alias ​​of {@see apply_shortcode()}.
	 */
	public function shortcode( string $content ): string {
		return $this->apply_shortcode( $content );
	}

}

class TOC_Elem {

	$full_match;
	public $tag;
	public $anchor;
	public $text;
	public $level;

	public function __construct( array $data ){

		foreach( $data as $key => $val ){
			$this->$key = $val;
		}
	}

}




Як користуватися класом Kama_Contents

Насамперед потрібно підключити код:

  • Створіть файл, наприклад Kama_Contents.php, і скопіюйте код. Підключіть цей файл до файлу теми functions.php:

    require_once __DIR__ .'/Kama_Contents.php';
  • Використовуйте композер:
    composer require doiftrue/wp-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 ){

	$ args = array (
		//'shortcode' => 'list', // [list] замість [contents]
		//'margin' => 30,
		//'page_url' => get_permalink(),
		'to_menu' => 'до змісту ↑',
		'title' => 'Зміст:',
		'min_length' => 300,
	);

	$toc = new KamaWPKama_Contents( $args );

	if( is_singular() ){
		return $toc->apply_shortcode( $content );
	}
	// виріжемо шорткод
	else {
		return $toc->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'),
	);

	$toc = new KamaWPKama_Contents( $args );

	$contents = $toc->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'),
	);

	$toc = new KamaWPKama_Contents( $args );

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

	// якщо потрібний по порядку роздільник знайдений у тексті
	if( isset( $ex_text[ $_sep_num ] ) ) ){

		$contents = $toc->make_contents( $ex_text[ $_sep_num ] );

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

		$text = implode($_sep, $ex_text);
	}
	// просто у вершині тексту
	else {
		$contents = $toc->make_contents( $text );

		$text = $contents. $text;
	}

	return $text;
}


#4 Зміст у сайд-барі

Ці приклади схожі на другий – тут також використовується метод make_contents(), а не apply_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;
	}

	$toc = new KamaWPKama_Contents( [
		'selectors' => [ 'h2', 'h3' ],
		'min_found' => 1,
		'margin' => 0,
		'to_menu' => false,
		'title' => false,
	]);

	$contents = $toc->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(){
	global $post;

	if( ! is_singular() ){
		return;
	}

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

	$toc = new KamaWPKama_Contents( $args );

	$GLOBALS['kc_contents'] = $toc->make_contents( $post->post_content );
}

// потім у сайдбарі виводимо: echo $GLOBALS['kc_contents'];


#5 Різні екземпляри

Якщо потрібно використовувати кілька класів із різними параметрами. Наприклад, щоб опрацювати різні тексти. То різні екземпляри класу можна створити так:

// 1
// перший текст із шорткодом [toc]

$text1 = 'текст [toc] текст';

$kamatoc = new KamaWPKama_Contents( [
	'to_menu' = 'до змісту ↑',
	'title' = 'Зміст:',
	'shortcode' = 'toc',
	//'page_url' = get_permalink(),
]);

echo $kamatoc->apply_shortcode( $text1 );

// 2
// другий текст із шорткодом [list]

$text2 = 'текст [list] текст';

$kamatoc2 = new KamaWPKama_Contents( [
	'to_menu' = 'до списку ↑',
	'title' = 'Навігація:',
	'shortcode' = 'list',
]);

echo $kamatoc2->apply_shortcode( $text2 );


Налаштування Змісту

Ви помітили закоментовані рядки в прикладах? Це налаштування. До екземпляра класу можна передавати аргументи (налаштування):

$args = [
	'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,
	'leave_tags' => true,
];

$kamatoc = new KamaWPKama_Contents( $args );
$args
(масив)

Параметри.

  • margin (рядок)
    Відступ зліва у підрозділів px|em|rem.

  • selectors (рядок)
    HTML теги яким будуватиметься зміст: 'h2 h3 h4'. Порядок визначає рівень вкладеності. Можна вказати рядок або масив: [ 'h2', 'h3', 'h4' ]або 'h2 h3 h4'. Можна вказати атрибут class: 'h2 .class_name'. Якщо потрібно, щоб різні теги були на одному рівні, вказуємо їх через |: 'h2|dt h3'або [ 'h2|dt', 'h3' ].

  • to_menu (рядок)
    Посилання на повернення до змісту. ''– Забрати посилання.

  • title (рядок)
    Заголовок. ''– Забрати заголовок.

  • js (рядок)
    JS код (додається після HTML коду)

  • min_found (int)
    Мінімальна кількість знайдених тегів, щоб вміст виводився.

  • min_length (int)
    Мінімальна довжина (символів) тексту, щоб вміст виводився.

  • page_url (рядок)
    Посилання на сторінку для якої збирається зміст. Якщо вміст виводиться на іншій сторінці…

  • shortcode (рядок)
    Назва шоткоду.

  • spec (рядок)
    Залишати символи в анкорах. Для прикладу: ‘.+$*= .

  • anchor_type (рядок)
    Який тип анкору використовувати: ‘a’ – <a name=”anchor”></a> або ‘id’.

  • anchor_attr_name (рядок)
    Назва атрибута тега зі значення якого буде братися анкор (якщо цей атрибут є у тега). Ставимо ”, щоб вимкнути таку перевірку…

  • markup (true|false)
    Включити мікророзмітку?

  • anchor_link (рядок)
    Додати ‘знак’ перед підзаголовком статті з посиланням на поточний анкор заголовка. Вкажіть ‘#’, ‘&’ або що вам подобається.

  • tomenu_simcount (int)
    Мінімальна кількість символів між заголовками змісту, для яких потрібно виводити посилання “до змісту”. Немає сенсу, якщо параметр ‘to_menu’ вимкнено. З метою продуктивності кирилиця вважається без урахування кодування. Тому 800 символів кирилиці – це приблизно 1600 символів у цьому параметрі. 800 – розрахунок для сайтів на кирилиці.

  • leave_tags (true|false|рядок)
    Чи потрібно залишати HTML теги в елементах змісту. З версії 4.3.4. Можна вказати тільки теги, які потрібно залишати. Пр: ‘<b><strong><var><code>’ .


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-doc.com/handbook/codex/data-types/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-doc.com/handbook/codex/data-types/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-doc.com/handbook/codex/data-types/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-metadannych">Функції метаданих</a>
			<meta itemprop="name" content="Функції метаданих" />
			<meta itemprop="url" content="https://wp-doc.com/handbook/codex/data-types/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-doc.com/handbook/codex/data-types/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 )
			} )
	})

})


«Приховати/показати» зміст (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. При цьому Вам не доведеться витрачати свій час на пошук справді надійного сервісу. Тут Ви знайдете все, що потрібно: якісні послуги, приємні ціни, гарантії та ввічливе обслуговування.

Залишити коментар

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *