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. ?>
  • ]*)>(.*?)<\/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; } }