- 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
1010 lines
34 KiB
PHP
1010 lines
34 KiB
PHP
<?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();
|