Files
acrib.es/wp-content/plugins/kadence-blocks/includes/class-kadence-blocks-table-of-contents.php
Malin f3ff7b7186 feat: initial ACRIB WordPress deployment
- WordPress 6.9.4 (es_ES) with Kadence theme
- Homepage: Hero, La Asociación, Pilares, Beneficios, Eventos, Miembros, Hazte Miembro, Contacto
- Brand identity: #13294b navy, #a12932 burgundy, #c69c48 gold
- Fonts: Raleway (headings) + Source Sans 3 (body) + Lato (UI)
- Plugins: Kadence Blocks, Polylang, Contact Form 7
- Custom CSS with full brand styling and responsive layout
- HTTPS enforced via wp-config.php proxy detection
2026-05-19 19:25:59 +02:00

1010 lines
34 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Setup the Table of Contents Block.
*
* This Block was largely based on the Core Table of Contents block.
*
* @since 1.9.0
* @package Kadence Blocks
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class to Build the Table of Contents.
*
* @category class
*/
class Kadence_Blocks_Table_Of_Contents {
/**
* Headers that needs ids.
*
* @var null
*/
public static $headings = array();
/**
* Anchors that are used, prevent duplicates.
*
* @var null
*/
public static $anchors = array();
/**
* Post content.
*
* @var string
*/
public static $output_content = '';
/**
* Instance of this class
*
* @var null
*/
private static $instance = null;
/**
* headings that are used, prevent duplicates.
*
* @var null
*/
public static $the_headings = null;
/**
* The block names to ignore.
*
* @var null
*/
public static $ignore_list = null;
/**
* The block names to ignore.
*
* @var null
*/
public static $inner_ignore_list = null;
/**
* The heading classes to ignore.
*
* @var null
*/
public static $ignore_classes = null;
/**
* Instance Control
*/
public static function get_instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
public function __construct() {
add_action( 'wp_footer', array( $this, 'headings_enqueue' ), 1 );
}
/**
* Helper function to remove the children of a node.
*
* @access private
*
* @param object $node The node to remove children from.
*/
public function table_of_contents_delete_node_children( $node ) {
/* phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase */
// Disabled because of PHP DOMDoument and DOMXPath APIs using camelCase.
// Whenever the 1st child node is removed, the 2nd one becomes the 1st.
while ( isset( $node->firstChild ) ) {
$this->table_of_contents_delete_node_children( $node->firstChild );
$node->removeChild( $node->firstChild );
}
/* phpcs:enable */
}
/**
* Helper function to remove a node and all of its children.
*
* @access private
*
* @param object $node The node to remove along with its children.
*/
public function table_of_contents_delete_node_and_children( $node ) {
/* phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase */
// Disabled because of PHP DOMDoument and DOMXPath APIs using camelCase.
$this->table_of_contents_delete_node_children( $node );
$node->parentNode->removeChild( $node );
/* phpcs:enable */
}
/**
* Gets the content, anchor, level, and page of headings from a post. Returns
* data from all headings in a paginated post if $current_page_only is false;
* otherwise, returns only data from headings on the current page being
* rendered.
*
* @access private
*
* @param WP_Post $post The post to extract headings from.
* @param array $attributes the block attributes.
*
* @return array The list of headings.
*/
private function table_of_contents_get_headings( $post, $attributes = array() ) {
global $multipage, $page, $pages;
if ( $multipage ) {
// Creates a list of heading lists, one list per page.
$pages_of_headings = array_map(
function( $page_content, $page_index ) use ( $page, $attributes ) {
return $this->table_of_contents_get_headings_from_content(
$page_content,
$page_index + 1,
$page,
$attributes
);
},
$pages,
array_keys( $pages )
);
$current_page_only = true;
if ( $current_page_only ) {
// Return the headings from the current page.
return $pages_of_headings[ $page - 1 ];
} else {
// Concatenate the heading lists into a single array and return it.
return array_merge( ...$pages_of_headings );
}
} else {
// Only one page, so return headings from entire post_content.
if ( is_null( self::$the_headings ) ) {
if ( isset( $attributes['enableDynamicSearch'] ) && $attributes['enableDynamicSearch'] ) {
if ( isset( $attributes['enableTemplateSearch'] ) && $attributes['enableTemplateSearch'] && class_exists( 'Kadence_Woo_Block_Editor_Templates' ) && is_singular( 'product' ) ) {
$kadence_woo_pro = Kadence_Woo_Block_Editor_Templates::get_instance();
$page_content = '';
if ( ! empty( $kadence_woo_pro::$single_override ) ) {
$template_post = get_post( $kadence_woo_pro::$single_override );
$blocks = parse_blocks( $template_post->post_content );
self::$output_content = '';
foreach ( $blocks as $block ) {
$this->recursively_parse_blocks( $block );
}
if ( self::$output_content ) {
$page_content .= do_shortcode( self::$output_content );
}
} else {
$blocks = parse_blocks( $post->post_content );
self::$output_content = '';
foreach ( $blocks as $block ) {
$this->recursively_parse_blocks( $block );
}
if ( self::$output_content ) {
$page_content .= do_shortcode( self::$output_content );
} else {
$page_content .= do_shortcode( $post->post_content );
}
}
} else if ( isset( $attributes['enableTemplateSearch'] ) && $attributes['enableTemplateSearch'] && class_exists( 'Kadence_Pro\Elements_Controller' ) && is_singular() ) {
$kadence_pro = Kadence_Pro\Elements_Controller::get_instance();
$page_content = '';
if ( ! empty( $kadence_pro::$single_template ) ) {
$blocks = parse_blocks( $post->post_content );
self::$output_content = '';
$the_post_content = '';
foreach ( $blocks as $block ) {
$this->recursively_parse_blocks( $block );
}
if ( self::$output_content ) {
$the_post_content .= do_shortcode( self::$output_content );
} else {
$the_post_content .= do_shortcode( $post->post_content );
}
$template_post = get_post( $kadence_pro::$single_template );
$blocks = parse_blocks( $template_post->post_content );
self::$output_content = '';
foreach ( $blocks as $block ) {
$this->recursively_parse_blocks( $block, $the_post_content );
}
if ( self::$output_content ) {
$page_content .= do_shortcode( self::$output_content );
}
} else {
$blocks = parse_blocks( $post->post_content );
self::$output_content = '';
foreach ( $blocks as $block ) {
$this->recursively_parse_blocks( $block );
}
if ( self::$output_content ) {
$page_content .= do_shortcode( self::$output_content );
} else {
$page_content .= do_shortcode( $post->post_content );
}
}
} else {
$blocks = parse_blocks( $post->post_content );
self::$output_content = '';
foreach ( $blocks as $block ) {
$this->recursively_parse_blocks( $block );
}
if ( self::$output_content ) {
$page_content = do_shortcode( self::$output_content );
} else {
$page_content = do_shortcode( $post->post_content );
}
}
} else {
$page_content = $post->post_content;
}
self::$the_headings = $this->table_of_contents_get_headings_from_content( $page_content, 1, 1, $attributes );
}
$headings = self::$the_headings;
if ( $attributes && isset( $attributes['allowedHeaders'] ) && isset( $attributes['allowedHeaders'][0] ) && is_array( $attributes['allowedHeaders'][0] ) ) {
foreach ( array( 'h1' => 1, 'h2' => 2, 'h3' => 3, 'h4' => 4, 'h5' => 5, 'h6' => 6 ) as $tag => $level ) {
foreach ( $headings as $headkey => $headvalue ) {
// User is specifically excluding this heading.
if( false === $headvalue['include'] ) {
unset( $headings[$headkey] );
}
// User is specifically including this heading, or, it's in the allowed headers list.
if ( true === $headvalue['include'] || ( !isset( $attributes['allowedHeaders'][0][$tag] ) || $attributes['allowedHeaders'][0][$tag] ) ) {
continue;
}
// Heading is at an unwanted level.
if ( $headvalue['level'] === $level ) {
unset( $headings[$headkey] );
}
}
}
}
return array_values( $headings );
}
}
/**
* Get the ignore list of blocks.
*
* @access private
*/
private function get_ignore_list() {
if ( is_null( self::$ignore_list ) ) {
self::$ignore_list = apply_filters( 'kadence_toc_block_ignore_array', array( 'kadence/tableofcontents', 'kadence/tabs', 'kadence/modal', 'kadence/repeater', 'core/post-content' ) );
}
return self::$ignore_list;
}
/**
* Get the ignore list of blocks.
*
* @access private
*/
private function get_inner_block_ignore_list() {
if ( is_null( self::$inner_ignore_list ) ) {
self::$inner_ignore_list = apply_filters( 'kadence_toc_inner_block_ignore_array', array( 'kadence/pane' ) );
}
return self::$inner_ignore_list;
}
/**
* Get the ignore list of classes.
*
* @access private
*/
private function get_ignore_classes() {
if ( is_null( self::$ignore_classes ) ) {
self::$ignore_classes = apply_filters( 'kadence_toc_exclude_classes_array', array( 'kt-testimonial-title', 'image-overlay-title', 'image-overlay-subtitle', 'toc-ignore' ) );
}
return self::$ignore_classes;
}
/**
* Renders blocks to allow dynamic blocks that bring in headings to be accounted for.
*
* @access private
*
* @param string $block The page content content.
*/
private function recursively_parse_blocks( $block, $the_post_content = '' ) {
if ( ! in_array( $block['blockName'], $this->get_ignore_list() ) ) {
if ( isset( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
// Can't link to inner blocks so ignore everything past.
if ( in_array( $block['blockName'], $this->get_inner_block_ignore_list() ) ) {
self::$output_content .= $block['innerHTML'];
} else {
foreach( $block['innerBlocks'] as $inner_block ) {
$this->recursively_parse_blocks( $inner_block );
}
}
} else {
self::$output_content .= render_block( $block );
}
} else if ( $block['blockName'] === 'core/post-content' ) {
self::$output_content .= $the_post_content;
} else if ( $block['blockName'] === 'kadence/repeater' ) {
//repeater needs to render all together to properly render dynamic data with context.
self::$output_content .= render_block( $block );
}
}
/**
* Registers scripts and styles.
*/
public function headings_enqueue() {
// If in the backend, bail out.
if ( is_admin() ) {
return;
}
wp_localize_script(
'kadence-blocks-tableofcontents',
'kadence_blocks_toc',
array(
'headings' => wp_json_encode( self::$headings ),
'expandText' => esc_attr__( 'Expand Table of Contents', 'kadence-blocks' ),
'collapseText' => esc_attr__( 'Collapse Table of Contents', 'kadence-blocks' ),
)
);
}
/**
* Extracts heading content, anchor, and level from the given post content.
*
* @access private
*
* @param string $content The heading content.
*
* @return string The sanitized heading content for anchor link.
*/
private function convert_text_to_anchor( $content ) {
$content = remove_accents( $content );
$content = strtolower( $content );
$content = preg_replace( "/[^A-Za-z0-9 ]/", '', $content );
$content = str_replace( ' ', '-', $content );
return $content;
}
/**
* Process content through Kadence Blocks conditional logic (logged in/out, RCP membership, etc.).
*
* @access private
*
* @param string $content The post content to process.
*
* @return string The processed content with conditionally hidden blocks removed.
*/
private function process_content_through_kadence_conditional_logic( $content ) {
$blocks = parse_blocks( $content );
if ( empty( $blocks ) ) {
return $content;
}
// Get the Kadence Blocks Frontend instance to access conditionally_render_block
$frontend_instance = Kadence_Blocks_Frontend::get_instance();
if ( ! $frontend_instance ) {
return $content;
}
// Process each block through conditional logic
$processed_blocks = array();
foreach ( $blocks as $block ) {
if ( empty( $block['blockName'] ) ) {
$processed_blocks[] = $block;
continue;
}
$wp_block = new stdClass();
$wp_block->context = array();
// Check if this block should be conditionally hidden using Kadence Blocks logic
// We pass a placeholder content to test if the block would be hidden
$block_content = $frontend_instance->conditionally_render_block( 'PLACEHOLDER_CONTENT', $block, $wp_block );
if ( $block_content !== '' ) {
$processed_blocks[] = $block;
}
}
// Re-render the content from the processed blocks
$processed_content = '';
foreach ( $processed_blocks as $block ) {
$processed_content .= serialize_block( $block );
}
return $processed_content;
}
/**
* Process content through Kadence Blocks Pro conditional hiding logic.
*
* @access private
*
* @param string $content The post content to process.
*
* @return string The processed content with conditionally hidden blocks removed.
*/
private function process_content_through_conditional_hiding( $content ) {
// Check if Kadence Blocks Pro is available
if ( ! class_exists( 'Kadence_Blocks_Pro_Dynamic_Content' ) ) {
return $content;
}
$dynamic_content = Kadence_Blocks_Pro_Dynamic_Content::get_instance();
if ( ! $dynamic_content ) {
return $content;
}
// Parse the content into blocks and process each block through conditional display logic
$blocks = $dynamic_content->kadence_parse_blocks( $content );
if ( empty( $blocks ) ) {
return $content;
}
$processed_blocks = array();
foreach ( $blocks as $block ) {
$wp_block = new stdClass();
$wp_block->context = array();
$should_hide = $dynamic_content->should_conditionally_hide_block( '', $block, $wp_block, null, null );
if ( ! $should_hide ) {
$processed_blocks[] = $block;
}
}
// Convert the filtered block array back to serialized content format
$processed_content = '';
foreach ( $processed_blocks as $block ) {
$processed_content .= serialize_block( $block );
}
return $processed_content;
}
/**
* Extracts heading content, anchor, and level from the given post content.
*
* @access private
*
* @param string $content The post content to extract headings from.
* @param int $headings_page The page of the post where the headings are
* located.
* @param int $current_page The page of the post currently being rendered.
* @param array $attributes The block attributes.
*
* @return array The list of headings.
*/
private function table_of_contents_get_headings_from_content(
$content,
$headings_page = 1,
$current_page = 1,
$attributes = array()
) {
// Process content through Kadence Blocks conditional logic first
$content = $this->process_content_through_kadence_conditional_logic( $content );
// Then process content through Kadence Blocks Pro conditional hiding before parsing HTML
$content = $this->process_content_through_conditional_hiding( $content );
/* phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase */
// Disabled because of PHP DOMDoument and DOMXPath APIs using camelCase.
// Create a document to load the post content into.
//$doc = new DOMDocument();
$wp_charset = get_bloginfo( 'charset' );
$doc = new DOMDocument( '1.0', $wp_charset );
// Enable user error handling for the HTML parsing. HTML5 elements aren't
// supported (as of PHP 7.4) and There's no way to guarantee that the markup
// is valid anyway, so we're just going to ignore all errors in parsing.
// Nested heading elements will still be parsed.
// The lack of HTML5 support is a libxml2 issue:
// https://bugzilla.gnome.org/show_bug.cgi?id=761534.
libxml_use_internal_errors( true );
// Parse the post content into an HTML document.
$content = mb_encode_numericentity( $content, [0x80, 0x10FFFF, 0, ~0], 'UTF-8' );
// loadHTML expects ISO-8859-1, so we need to convert the post content to
// that format. We use htmlentities to encode Unicode characters not
// supported by ISO-8859-1 as HTML entities. However, this function also
// converts all special characters like < or > to HTML entities, so we use
// htmlspecialchars_decode to decode them.
$doc->loadHTML(
htmlspecialchars_decode(
mb_convert_encoding(
htmlentities(
'<!DOCTYPE html><html><head><title>:D</title><body>' .
htmlentities( $content ) .
'</body></html>',
ENT_COMPAT,
'UTF-8',
false
), 'ISO-8859-1', 'UTF-8'
),
ENT_COMPAT
)
);
// We're done parsing, so we can disable user error handling. This also
// clears any existing errors, which helps avoid a memory leak.
libxml_use_internal_errors( false );
$document_el = $doc->documentElement;
// IE11 treats template elements like divs, so to avoid extracting heading
// elements from them, we first have to remove the template elements and
// their children.
// We can't use foreach directly on the $templates DOMNodeList because it's a
// dynamic list, and removing nodes confuses the foreach iterator. So
// instead, we create a static array of the nodes we want to remove and then
// iterate over that.
$templates = iterator_to_array(
$document_el->getElementsByTagName( 'template' )
);
foreach ( $templates as $template ) {
$this->table_of_contents_delete_node_and_children( $template );
}
$xpath = new DOMXPath( $doc );
$query = '//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]';
// Remove this since the headings are cached, instead strip out the ones we don't want from the cached response.
/*
if ( $attributes && isset( $attributes['allowedHeaders'] ) && isset( $attributes['allowedHeaders'][0] ) && is_array( $attributes['allowedHeaders'][0] ) ) {
$query = '//*[';
$arr = array();
if ( isset( $attributes['allowedHeaders'][0]['h1'] ) && $attributes['allowedHeaders'][0]['h1'] ) {
$arr[] = 'self::h1';
}
if ( isset( $attributes['allowedHeaders'][0]['h2'] ) && $attributes['allowedHeaders'][0]['h2'] ) {
$arr[] = 'self::h2';
}
if ( isset( $attributes['allowedHeaders'][0]['h3'] ) && $attributes['allowedHeaders'][0]['h3'] ) {
$arr[] = 'self::h3';
}
if ( isset( $attributes['allowedHeaders'][0]['h4'] ) && $attributes['allowedHeaders'][0]['h4'] ) {
$arr[] = 'self::h4';
}
if ( isset( $attributes['allowedHeaders'][0]['h5'] ) && $attributes['allowedHeaders'][0]['h5'] ) {
$arr[] = 'self::h5';
}
if ( isset( $attributes['allowedHeaders'][0]['h6'] ) && $attributes['allowedHeaders'][0]['h6'] ) {
$arr[] = 'self::h6';
}
$query .= implode( ' or ', $arr );
$query .= ']';
}
*/
// Get all heading elements in the post content.
$temp_headings = iterator_to_array(
$xpath->query( $query )
);
$headings = array();
foreach ( $temp_headings as $temp_heading ) {
if ( isset( $temp_heading->attributes ) ) {
$heading_classes_string = $temp_heading->attributes->getNamedItem( 'class' );
if ( null !== $heading_classes_string ) {
$heading_classes = explode( ' ', trim( $heading_classes_string->nodeValue ) );
$exclude_classes = $this->get_ignore_classes();
$exclude = false;
foreach ( $exclude_classes as $exclude_class ) {
if ( in_array( $exclude_class, $heading_classes ) ) {
$exclude = true;
break;
}
}
if ( $exclude ) {
continue;
}
}
}
$headings[] = $temp_heading;
}
return array_map(
function ( $heading ) use ( $headings_page, $current_page ) {
$anchor_string = false;
$anchor = '';
if ( isset( $heading->attributes ) ) {
$id_attribute = $heading->attributes->getNamedItem( 'id' );
if ( null !== $id_attribute ) {
// The id attribute may contain many ids, so just use the first.
$first_id = explode( ' ', trim( $id_attribute->nodeValue ) )[0];
if ( $headings_page === $current_page ) {
$anchor = '#' . $first_id;
} elseif ( 1 !== $headings_page && 1 === $current_page ) {
$anchor = './' . $headings_page . '/#' . $first_id;
} elseif ( 1 === $headings_page && 1 !== $current_page ) {
$anchor = '../#' . $first_id;
} else {
$anchor = '../' . $headings_page . '/#' . $first_id;
}
} else {
$anchor_string = $this->convert_text_to_anchor( $heading->textContent );
if ( ! $anchor_string ) {
$anchor_string = uniqid( 'hd-' );
}
if ( in_array( $anchor_string, self::$anchors ) ) {
$anchor_string = $anchor_string . uniqid( '-hd-' );
}
self::$anchors[] = $anchor_string;
$anchor = '#' . $anchor_string;
}
} else {
$anchor_string = $this->convert_text_to_anchor( $heading->textContent );
if ( ! $anchor_string ) {
$anchor_string = uniqid( 'hd-' );
}
if ( in_array( $anchor_string, self::$anchors ) ) {
$anchor_string = $anchor_string . uniqid( '-hd-' );
}
self::$anchors[] = $anchor_string;
$anchor = '#' . $anchor_string;
}
switch ( $heading->nodeName ) {
case 'h1':
$level = 1;
break;
case 'h2':
$level = 2;
break;
case 'h3':
$level = 3;
break;
case 'h4':
$level = 4;
break;
case 'h5':
$level = 5;
break;
case 'h6':
$level = 6;
break;
}
if ( $heading->getAttribute( 'data-alt-title' ) ) {
$heading->textContent = $heading->getAttribute( 'data-alt-title' );
}
$headingWrapper = [
'element' => $heading,
'include' => ''
];
if( $heading->getAttribute( 'data-toc-include' ) === 'true' ){
$headingWrapper['include'] = true;
} else if ( $heading->getAttribute( 'data-toc-include' ) === 'false' ) {
$headingWrapper['include'] = false;
}
if ( $anchor_string ) {
$add = true;
foreach ( self::$headings as $v ) {
if ( $v['anchor'] == $anchor_string ) {
$add = false;
}
}
if ( $add ) {
self::$headings[] = array(
'anchor' => $anchor_string,
'content' => $this->convert_smart_quotes( $heading->textContent ),
'level' => $level,
'include' => $headingWrapper['include'],
'page' => $headings_page,
);
}
}
return array(
'anchor' => $anchor,
'content' => $this->convert_smart_quotes( $heading->textContent ),
'level' => $level,
'include' => $headingWrapper['include'],
'page' => $headings_page,
);
},
$headings
);
/* phpcs:enable */
}
/**
* converts special quotes characters in string to normal.
*
* @param string $string The string to convert.
* @return string The string converted.
*/
public function convert_smart_quotes( $string ) {
$search = array("×",
"",
"",
"",
"",
"",
"",
"");
$replace = array("x",
"'",
"'",
'"',
'"',
'-',
'-',
'...');
return str_replace( $search, $replace, $string );
}
/**
* Sets the current post for usage in template blocks.
*
* @return WP_Post|null The post if any, or null otherwise.
*/
public function get_post_from_context() {
// TODO: Without this temporary fix, an infinite loop can occur where
// posts with post content blocks render themselves recursively.
if ( is_admin() || defined( 'REST_REQUEST' ) ) {
return null;
}
// Prevent an issue with the terms and conditions page in the woocommerce checkout.
if ( class_exists( 'woocommerce' ) && is_checkout() ) {
return '';
}
// Bug when called in via a hook on the front page.
// if ( ! in_the_loop() && ! is_front_page() ) {
// rewind_posts();
// the_post();
// }
return get_post();
}
/**
* Converts a flat list of heading parameters to a hierarchical nested list
* based on each header's immediate parent's level.
*
* @access private
*
* @param array $heading_list Flat list of heading parameters to nest.
* @param int $index The current list index.
*
* @return array A hierarchical nested list of heading parameters.
*/
private function table_of_contents_linear_to_nested_heading_list(
$heading_list,
$index = 0
) {
$nested_heading_list = array();
foreach ( $heading_list as $key => $heading ) {
if ( ! isset( $heading['content'] ) ) {
break;
}
// Make sure we are only working with the same level as the first
// iteration in our set.
if ( $heading['level'] === $heading_list[0]['level'] ) {
// Check that the next iteration will return a value.
// If it does and the next level is greater than the current level,
// the next iteration becomes a child of the current interation.
if (
isset( $heading_list[ $key + 1 ] ) &&
$heading_list[ $key + 1 ]['level'] > $heading['level']
) {
// We need to calculate the last index before the next iteration
// that has the same level (siblings). We then use this last index
// to slice the array for use in recursion. This prevents duplicate
// nodes.
$heading_list_length = count( $heading_list );
$end_of_slice = $heading_list_length;
for ( $i = $key + 1; $i < $heading_list_length; $i++ ) {
if ( $heading_list[ $i ]['level'] === $heading['level'] ) {
$end_of_slice = $i;
break;
}
}
// Found a child node: Push a new node onto the return array with
// children.
$nested_heading_list[] = array(
'heading' => $heading,
'index' => $index + $key,
'children' => $this->table_of_contents_linear_to_nested_heading_list(
array_slice(
$heading_list,
$key + 1,
$end_of_slice - ( $key + 1 )
),
$index + $key + 1
),
);
} else {
// No child node: Push a new node onto the return array.
$nested_heading_list[] = array(
'heading' => $heading,
'index' => $index + $key,
'children' => null,
);
}
}
}
return $nested_heading_list;
}
/**
* Renders the heading list of the kadence table of contents block on server.
*
* @access private
*
* @param array $nested_heading_list Nested list of heading data.
* @param bool $wrap if the list should be wrapped.
* @param string $list_tag the tag to be used if wrapped.
*
* @return string The heading list rendered as HTML.
*/
private function table_of_contents_render_list( $nested_heading_list, $wrap = false, $list_tag = 'ul' ) {
$entry_class = 'kb-table-of-contents__entry';
$child_nodes = array_map(
function ( $child_node ) use ( $entry_class, $list_tag ) {
$anchor = $child_node['heading']['anchor'];
$content = $child_node['heading']['content'];
if ( isset( $anchor ) && '' !== $anchor ) {
$entry = sprintf(
'<a class="%1$s" href="%2$s">%3$s</a>',
$entry_class,
esc_attr( $anchor ),
esc_html( $content )
);
} else {
$entry = sprintf(
'<span class="%1$s">%2$s</span>',
$entry_class,
esc_html( $content )
);
}
return sprintf(
'<li>%1$s%2$s</li>',
$entry,
$child_node['children']
? $this->table_of_contents_render_list( $child_node['children'], true, $list_tag )
: null
);
},
$nested_heading_list
);
return $wrap ? '<' . $list_tag . ' class="kb-table-of-contents-list-sub">' . implode( $child_nodes ) . '</' . $list_tag . '>' : implode( $child_nodes );
}
/**
* Render Table of Contents Block
*
* @param array $attributes the blocks attribtues.
*/
public function render_table_of_content( $attributes, $content ) {
$the_post = $this->get_post_from_context();
// CSS class string.
$class = 'wp-block-kadence-tableofcontents kb-table-of-content-nav kb-table-of-content-id' . $attributes['uniqueID'];
// Add custom CSS classes to class string.
if ( isset( $attributes['className'] ) ) {
$class .= ' ' . $attributes['className'];
}
if ( ! $the_post ) {
return '';
}
$attributes = apply_filters( 'kadence_blocks_toc_block_build_only_attributes', $attributes );
$headings = $this->table_of_contents_get_headings(
$the_post,
$attributes
);
$list_tag = ( isset( $attributes['listStyle'] ) && 'numbered' === $attributes['listStyle'] ? 'ol' : 'ul' );
// If there are no headings or the only heading is empty.
if ( count( $headings ) === 0 || '' === $headings[0]['content'] ) {
return '';
}
if ( ! isset( $attributes['enableTitle'] ) || isset( $attributes['enableTitle'] ) && $attributes['enableTitle'] ) {
if ( isset( $attributes['enableToggle'] ) && $attributes['enableToggle'] ) {
$enable_toggle = true;
$start_closed = ( isset( $attributes['startClosed'] ) && $attributes['startClosed'] ? true : false );
$title_toggle = ( isset( $attributes['enableTitleToggle'] ) && $attributes['enableTitleToggle'] ? true : false );
} else {
$enable_toggle = false;
$start_closed = false;
$title_toggle = false;
}
} else {
$enable_toggle = false;
$start_closed = false;
$title_toggle = false;
}
$enable_scroll = false;
$scroll_offset = '40';
if ( isset( $attributes['enableSmoothScroll'] ) && true === $attributes['enableSmoothScroll'] ) {
$enable_scroll = true;
$scroll_offset = ( isset( $attributes['smoothScrollOffset'] ) && is_numeric( $attributes['smoothScrollOffset'] ) ? $attributes['smoothScrollOffset'] : '40' );
$class .= ' kb-toc-smooth-scroll';
}
$output = '<nav class="' . esc_attr( $class ) . ( $enable_toggle ? ' kb-collapsible-toc kb-toc-toggle-' . ( $start_closed ? 'hidden' : 'active' ) : '' ) . '" role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'kadence-blocks' ) . '"' . ( $enable_scroll ? ' data-scroll-offset="' . esc_attr( $scroll_offset ) . '"' : '' ) . ( isset( $attributes['enableScrollSpy'] ) && true === $attributes['enableScrollSpy'] ? ' data-scroll-spy="true"' : '' ) . '>';
$output .= '<div class="kb-table-of-content-wrap">';
if ( ! isset( $attributes['enableTitle'] ) || isset( $attributes['enableTitle'] ) && $attributes['enableTitle'] ) {
$output .= '<div class="kb-table-of-contents-title-wrap kb-toggle-icon-style-' . ( $enable_toggle && isset( $attributes['toggleIcon'] ) && $attributes['toggleIcon'] ? $attributes['toggleIcon'] : 'arrow' ) . '">';
if ( $title_toggle ) {
$output .= '<button class="kb-table-of-contents-title-btn kb-table-of-contents-toggle" aria-expanded="' . esc_attr( $start_closed ? 'false' : 'true' ) . '" aria-label="' . ( $start_closed ? esc_attr__( 'Expand Table of Contents', 'kadence-blocks' ) : esc_attr__( 'Collapse Table of Contents', 'kadence-blocks' ) ) . '">';
}
$output .= '<span class="kb-table-of-contents-title">';
$output .= ( isset( $attributes['title'] ) && $attributes['title'] !== 'Table of Contents' ? $attributes['title'] : __( 'Table of Contents', 'kadence-blocks' ) );
$output .= '</span>';
if ( $enable_toggle ) {
if ( $title_toggle ) {
$output .= '<span class="kb-table-of-contents-icon-trigger"></span>';
} else {
$output .= '<button class="kb-table-of-contents-icon-trigger kb-table-of-contents-toggle" aria-expanded="' . esc_attr( $start_closed ? 'false' : 'true' ) . '" aria-label="' . ( $start_closed ? esc_attr__( 'Expand Table of Contents', 'kadence-blocks' ) : esc_attr__( 'Collapse Table of Contents', 'kadence-blocks' ) ) . '"></button>';
}
}
if ( $title_toggle ) {
$output .= '</button>';
}
$output .= '</div>';
}
$output .= '<' . $list_tag . ' class="kb-table-of-content-list kb-table-of-content-list-columns-' . esc_attr( isset( $attributes['columns'] ) && ! empty( $attributes['columns'] ) ? $attributes['columns'] : '1' ) . ' kb-table-of-content-list-style-' . esc_attr( isset( $attributes['listStyle'] ) && ! empty( $attributes['listStyle'] ) ? $attributes['listStyle'] : 'disc' ) . ' kb-table-of-content-link-style-' . esc_attr( isset( $attributes['linkStyle'] ) && ! empty( $attributes['linkStyle'] ) ? $attributes['linkStyle'] : 'underline' ) . '">';
$output .= $this->table_of_contents_render_list(
$this->table_of_contents_linear_to_nested_heading_list( $headings, false ),
false,
$list_tag
);
$output .= '</' . $list_tag . '>';
$output .= '</div>';
$output .= '</nav>';
$content = $output . $content;
if ( ! wp_style_is( 'kadence-blocks-table-of-contents', 'enqueued' ) ) {
wp_enqueue_style( 'kadence-blocks-table-of-contents' );
}
if ( isset( $attributes['uniqueID'] ) ) {
$unique_id = $attributes['uniqueID'];
$style_id = 'kb-tableofcontents' . esc_attr( $unique_id );
if ( ! wp_style_is( $style_id, 'enqueued' ) && apply_filters( 'kadence_blocks_render_inline_css', true, 'tableofcontent', $unique_id ) ) {
if ( kadence_blocks_is_not_amp() ) {
if ( isset( $attributes['enableScrollSpy'] ) && $attributes['enableScrollSpy'] ) {
wp_enqueue_script( 'kadence-blocks-gumshoe' );
//need to laod this script with the gumshoe dependency if scrollspy is enabled
wp_enqueue_script( 'kadence-blocks-table-of-contents', KADENCE_BLOCKS_URL . 'includes/assets/js/kb-table-of-contents.min.js', array('kadence-blocks-gumshoe') , KADENCE_BLOCKS_VERSION, true );
} else {
wp_enqueue_script( 'kadence-blocks-table-of-contents' );
}
}
if ( ! doing_filter( 'the_content' ) ) {
if ( ! wp_style_is( 'kadence-blocks-table-of-contents', 'done' ) ) {
wp_print_styles( 'kadence-blocks-table-of-contents' );
}
} else {
if ( ! wp_style_is( 'kadence-blocks-table-of-contents', 'done' ) ) {
ob_start();
wp_print_styles( 'kadence-blocks-table-of-contents' );
$content = ob_get_clean() . $content;
}
}
}
}
return $content;
}
/*
* Reset the instance properties for unit tests
*/
private static function reset_instance() {
self::$headings = array();
self::$anchors = array();
self::$the_headings = null;
self::$output_content = '';
}
}
Kadence_Blocks_Table_Of_Contents::get_instance();