block_name = 'table-of-content';
$this->scripts = array( 'sp_smart_post_smart_toc_script' );
$this->styles = array(
'sp_smart_post_blocks_css',
'sp_smart_post_blocks_style',
'sp_smart_post_blocks_social_icons_style',
);
$this->keywords = array( 'TOC', 'contents', 'post contents', 'table of contents', 'headings' );
}
/**
* Render the block content.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param array $blocks Block instances.
* @return string
*/
public function render_block( $attributes, $content = '', $blocks = array() ) {
if ( Helper::is_editor_page() ) {
return $content;
}
if ( ! is_singular() ) {
return '';
}
// Extract attributes.
$align_class = ! empty( $attributes['align'] ) ? 'align' . $attributes['align'] : '';
$unique_id = $attributes['uniqueId'] ?? '';
$supported_heading_tag = $attributes['supportedHeadingTag'] ?? array();
$block_name = isset( $attributes['blockName'] ) ? $attributes['blockName'] : '';
$title = isset( $attributes['tocHeading'] ) ? $attributes['tocHeading'] : 'Table of Contents';
$offset_top = isset( $attributes['offsetTop'] ) ? $attributes['offsetTop'] : 50;
$list_style = isset( $attributes['listStyle'] ) ? $attributes['listStyle'] : 'bullet';
$content_alignment = isset( $attributes['tocAlignment'] ) ? $attributes['tocAlignment'] : 'left';
$copy_link = isset( $attributes['copyLink'] ) ? $attributes['copyLink'] : true;
$smooth_scroll = $attributes['smoothScroll'] ?? true;
$toc_collapsed = isset( $attributes['tocCollapsed'] ) ? $attributes['tocCollapsed'] : false;
$collapsed_initially = isset( $attributes['collapsedInitially'] ) ? $attributes['collapsedInitially'] : false;
$child_item_collapsible = isset( $attributes['childItemCollapsible'] ) ? $attributes['childItemCollapsible'] : false;
$list_hierarchy = isset( $attributes['listHierarchy'] ) ? $attributes['listHierarchy'] : true;
$display_control = isset( $attributes['displayControl'] ) ? $attributes['displayControl'] : 'static';
$floating_position = isset( $attributes['floatingPosition'] ) ? $attributes['floatingPosition'] : 'top-left';
// New attributes for collapsible icons.
$collapsible_icon_source = isset( $attributes['CollapsibleIconSource'] ) ? $attributes['CollapsibleIconSource'] : 'four';
$collapsible_button_type = isset( $attributes['CollapsibleButtonType'] ) ? $attributes['CollapsibleButtonType'] : 'icon';
$collapse_text = isset( $attributes['collapseText'] ) ? $attributes['collapseText'] : 'Collapse';
$expand_text = isset( $attributes['expandText'] ) ? $attributes['expandText'] : 'Expand';
$collapsible_color = isset( $attributes['collapsibleColor'] ) ? $attributes['collapsibleColor'] : '';
$collapsible_icon_position = isset( $attributes['collapsibleIconPosition'] ) ? $attributes['collapsibleIconPosition'] : 'right';
$child_collapsible_icon_source = isset( $attributes['childCollapsibleIconSource'] ) ? $attributes['childCollapsibleIconSource'] : 'four';
$heading_hash_url = isset( $attributes['headingHashUrl'] ) ? $attributes['headingHashUrl'] : false;
$preset = isset( $attributes['preset'] ) ? $attributes['preset'] : 'presetOne';
$headings = $this->sps_get_headings_with_id( $supported_heading_tag );
// Flatten headings if hierarchy is not enabled.
if ( ! $list_hierarchy ) {
$headings = $this->flatten_toc( $headings );
}
if ( empty( $headings ) ) {
if ( current_user_can( 'edit_posts' ) ) {
// Show message to admin/editor.
return '
No headings found in this post for table of content.
';
} else {
return '';
}
}
$floating_position_class = '';
if ( 'floating' === $display_control ) {
$floating_position_class = 'sp-toc-floating-' . $floating_position;
}
// Define icon SVGs.
$icon_svgs = array(
'one' => '',
'two' => '',
'three' => '',
'four_collapsed' => '',
'four_expanded' => '',
'five_collapsed' => '',
'five_expanded' => '',
'six' => '',
'seven_collapsed' => '',
'seven_expanded' => '',
'eight' => '',
);
// Get the appropriate icon based on source and expanded state.
$get_collapsible_icon = function ( $is_expanded ) use ( $collapsible_icon_source, $icon_svgs ) {
switch ( $collapsible_icon_source ) {
case 'one':
return $icon_svgs['one'];
case 'two':
return $icon_svgs['two'];
case 'three':
return $icon_svgs['three'];
case 'four':
return $is_expanded ? $icon_svgs['four_expanded'] : $icon_svgs['four_collapsed'];
case 'five':
return $is_expanded ? $icon_svgs['five_expanded'] : $icon_svgs['five_collapsed'];
case 'six':
return $icon_svgs['six'];
case 'seven':
return $is_expanded ? $icon_svgs['seven_expanded'] : $icon_svgs['seven_collapsed'];
case 'eight':
return $icon_svgs['eight'];
default:
return $is_expanded ? $icon_svgs['four_expanded'] : $icon_svgs['four_collapsed'];
}
};
// Get the appropriate child icon based on source and expanded state.
$get_child_collapsible_icon = function ( $is_expanded ) use ( $child_collapsible_icon_source, $icon_svgs ) {
switch ( $child_collapsible_icon_source ) {
case 'one':
return $icon_svgs['one'];
case 'two':
return $icon_svgs['two'];
case 'three':
return $icon_svgs['three'];
case 'four':
return $is_expanded ? $icon_svgs['four_expanded'] : $icon_svgs['four_collapsed'];
case 'five':
return $is_expanded ? $icon_svgs['five_expanded'] : $icon_svgs['five_collapsed'];
case 'six':
return $icon_svgs['six'];
case 'seven':
return $is_expanded ? $icon_svgs['seven_expanded'] : $icon_svgs['seven_collapsed'];
case 'eight':
return $icon_svgs['eight'];
default:
return $is_expanded ? $icon_svgs['four_expanded'] : $icon_svgs['four_collapsed'];
}
};
ob_start();
?>
render_toc_items( $headings, $list_style, 0, $content_alignment, $copy_link, $child_item_collapsible, $get_child_collapsible_icon, $heading_hash_url, $preset ); ?>
$item['id'],
'text' => $item['text'],
'level' => $item['level'],
);
// Recursively add children.
if ( ! empty( $item['children'] ) ) {
$children = $this->flatten_toc( $item['children'] );
$result = array_merge( $result, $children );
}
}
return $result;
}
/**
* Recursive function to render TOC items.
*
* @param array $items Array of TOC items to render.
* @param string $list_style The list style to apply.
* @param int $depth Current depth level.
* @param string $content_alignment for content alignment.
* @param string $copy_link to copy link.
* @param bool $child_item_collapsible Whether child items can be collapsed.
* @param callable $get_child_collapsible_icon Function to get child icon.
* @param string $heading_hash_url toggle.
* @param string $preset The current preset.
*/
private function render_toc_items( $items, $list_style = '', $depth = 0, $content_alignment = 'left', $copy_link = true, $child_item_collapsible = false, $get_child_collapsible_icon = null, $heading_hash_url = true, $preset = 'presetOne' ) {
foreach ( $items as $item ) {
if ( empty( $item['id'] ) || empty( $item['text'] ) ) {
continue;
}
$has_children = ! empty( $item['children'] ) && is_array( $item['children'] );
$is_selected = false; // This will be handled in JavaScript.
?>
render_toc_items( $item['children'], $list_style, $depth + 1, $content_alignment, $copy_link, $child_item_collapsible, $get_child_collapsible_icon, $heading_hash_url, $preset ); ?>
]*)>(.*?)<\/h\1>/is';
if ( preg_match_all( $pattern, $content, $matches, PREG_SET_ORDER ) ) {
$stack = array();
foreach ( $matches as $m ) {
$level = intval( $m[1] );
// Skip headings not in allowed_levels.
if ( ! in_array( $level, $allowed_levels ) ) {
continue;
}
$attrs = $m[2];
$inner = $m[3];
$html = $m[0];
if ( preg_match( '/id="([^"]+)"/', $attrs, $id_match ) ) {
$id = $id_match[1];
} else {
$id = $this->generate_heading_id( wp_strip_all_tags( $inner ) );
$html = preg_replace(
'/]*)>/i',
'',
$html,
1
);
}
$item = array(
'level' => $level,
'text' => wp_strip_all_tags( $inner ),
'id' => $id,
'html' => $html,
'children' => array(),
);
// Build nested structure.
while ( ! empty( $stack ) && $level <= end( $stack )['level'] ) {
array_pop( $stack );
}
if ( empty( $stack ) ) {
$headings[] = $item;
$stack[] = &$headings[ count( $headings ) - 1 ];
} else {
$parent = &$stack[ count( $stack ) - 1 ];
$parent['children'][] = $item;
$stack[] = &$parent['children'][ count( $parent['children'] ) - 1 ];
}
unset( $item, $parent );
}
}
return $headings;
}
/**
* Generate heading id function
*
* @param string $text Heading text.
* @return string
*/
private function generate_heading_id( $text ) {
$id = mb_strtolower( $text, 'UTF-8' );
$id = preg_replace( '/[\s_.,!?;:()\[\]{}"\'`~@#$%^&*+=<>\/\\\\|]+/u', '-', $id );
$id = preg_replace( '/[^\p{L}\p{N}-]/u', '', $id );
$id = preg_replace( '/-+/', '-', $id );
$id = trim( $id, '-' );
if ( empty( $id ) ) {
$id = 'heading-' . uniqid();
}
return $id;
}
}