PluginProbe
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News / 4.0.7
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News v4.0.7
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 2.3.5 2.3.6 2.4.0 2.4.1 2.4.10 2.4.11 2.4.12 2.4.13 2.4.14 2.4.15 2.4.16 2.4.17 2.4.18 2.4.19 2.4.2 2.4.20 2.4.21 All 88 releases
post-carousel / blocks / includes / table-of-content / Table_Of_Content.php

Table_Of_Content.php in Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News 4.0.7, at blocks/includes/table-of-content/Table_Of_Content.php

436 lines 21.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Table of Contents block for Smart Post Show.
4 *
5 * @package Smart_Post_Show_Pro
6 * @subpackage Smart_Post_Show_Pro/blocks/includes
7 */
8
9 namespace SmartPostShow\Blocks;
10
11 use SmartPostShow\Blocks\Helper;
12
13 use SmartPostShow\Blocks\Block_Base;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18 /**
19 * Table of Contents block class.
20 */
21 class Table_Of_Content extends Block_Base {
22
23 /**
24 * Set block properties.
25 */
26 protected function set_block_properties() {
27 $this->block_name = 'table-of-content';
28 $this->scripts = array( 'sp_smart_post_smart_toc_script' );
29 $this->styles = array(
30 'sp_smart_post_blocks_css',
31 'sp_smart_post_blocks_style',
32 'sp_smart_post_blocks_social_icons_style',
33 );
34
35 $this->keywords = array( 'TOC', 'contents', 'post contents', 'table of contents', 'headings' );
36 }
37
38
39
40 /**
41 * Render the block content.
42 *
43 * @param array $attributes Block attributes.
44 * @param string $content Block content.
45 * @param array $blocks Block instances.
46 * @return string
47 */
48 public function render_block( $attributes, $content = '', $blocks = array() ) {
49 if ( Helper::is_editor_page() ) {
50 return $content;
51 }
52 if ( ! is_singular() ) {
53 return '';
54 }
55
56 // Extract attributes.
57 $align_class = ! empty( $attributes['align'] ) ? 'align' . $attributes['align'] : '';
58 $unique_id = $attributes['uniqueId'] ?? '';
59 $supported_heading_tag = $attributes['supportedHeadingTag'] ?? array();
60 $block_name = isset( $attributes['blockName'] ) ? $attributes['blockName'] : '';
61 $title = isset( $attributes['tocHeading'] ) ? $attributes['tocHeading'] : 'Table of Contents';
62 $offset_top = isset( $attributes['offsetTop'] ) ? $attributes['offsetTop'] : 50;
63 $list_style = isset( $attributes['listStyle'] ) ? $attributes['listStyle'] : 'bullet';
64 $content_alignment = isset( $attributes['tocAlignment'] ) ? $attributes['tocAlignment'] : 'left';
65 $copy_link = isset( $attributes['copyLink'] ) ? $attributes['copyLink'] : true;
66 $smooth_scroll = $attributes['smoothScroll'] ?? true;
67 $toc_collapsed = isset( $attributes['tocCollapsed'] ) ? $attributes['tocCollapsed'] : false;
68 $collapsed_initially = isset( $attributes['collapsedInitially'] ) ? $attributes['collapsedInitially'] : false;
69 $child_item_collapsible = isset( $attributes['childItemCollapsible'] ) ? $attributes['childItemCollapsible'] : false;
70 $list_hierarchy = isset( $attributes['listHierarchy'] ) ? $attributes['listHierarchy'] : true;
71 $display_control = isset( $attributes['displayControl'] ) ? $attributes['displayControl'] : 'static';
72 $floating_position = isset( $attributes['floatingPosition'] ) ? $attributes['floatingPosition'] : 'top-left';
73
74 // New attributes for collapsible icons.
75 $collapsible_icon_source = isset( $attributes['CollapsibleIconSource'] ) ? $attributes['CollapsibleIconSource'] : 'four';
76 $collapsible_button_type = isset( $attributes['CollapsibleButtonType'] ) ? $attributes['CollapsibleButtonType'] : 'icon';
77 $collapse_text = isset( $attributes['collapseText'] ) ? $attributes['collapseText'] : 'Collapse';
78 $expand_text = isset( $attributes['expandText'] ) ? $attributes['expandText'] : 'Expand';
79 $collapsible_color = isset( $attributes['collapsibleColor'] ) ? $attributes['collapsibleColor'] : '';
80 $collapsible_icon_position = isset( $attributes['collapsibleIconPosition'] ) ? $attributes['collapsibleIconPosition'] : 'right';
81 $child_collapsible_icon_source = isset( $attributes['childCollapsibleIconSource'] ) ? $attributes['childCollapsibleIconSource'] : 'four';
82 $heading_hash_url = isset( $attributes['headingHashUrl'] ) ? $attributes['headingHashUrl'] : false;
83 $preset = isset( $attributes['preset'] ) ? $attributes['preset'] : 'presetOne';
84 $headings = $this->sps_get_headings_with_id( $supported_heading_tag );
85
86 // Flatten headings if hierarchy is not enabled.
87 if ( ! $list_hierarchy ) {
88 $headings = $this->flatten_toc( $headings );
89 }
90
91 if ( empty( $headings ) ) {
92 if ( current_user_can( 'edit_posts' ) ) {
93 // Show message to admin/editor.
94 return '<div class="sps-toc-empty">No headings found in this post for table of content.</div>';
95 } else {
96 return '';
97 }
98 }
99 $floating_position_class = '';
100 if ( 'floating' === $display_control ) {
101 $floating_position_class = 'sp-toc-floating-' . $floating_position;
102 }
103
104 // Define icon SVGs.
105 $icon_svgs = array(
106 'one' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 9L12 15L18 9" stroke="' . $collapsible_color['color'] . '" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></svg>',
107 'two' => '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 14 8" fill="none"><path d="M1 1L7 7L13 1" stroke="' . $collapsible_color['color'] . '" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></svg>',
108 'three' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M11.325 16.325L4.225 9.225C3.925 8.925 3.925 8.325 4.225 8.025L5.02499 7.225C5.32499 6.925 5.925 6.925 6.225 7.225L11.925 12.825L17.625 7.225C17.925 6.925 18.525 6.925 18.825 7.225L19.625 8.025C19.925 8.325 19.925 8.925 19.625 9.225L12.525 16.325C12.225 16.725 11.625 16.725 11.325 16.325Z" fill="' . $collapsible_color['color'] . '" /></svg>',
109 'four_collapsed' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M11.75 4C12.1642 4 12.5 4.33579 12.5 4.75V11H18.75C19.1642 11 19.5 11.3358 19.5 11.75C19.5 12.1642 19.1642 12.5 18.75 12.5H12.5V18.75C12.5 19.1642 12.1642 19.5 11.75 19.5C11.3358 19.5 11 19.1642 11 18.75V12.5H4.75C4.33579 12.5 4 12.1642 4 11.75C4 11.3358 4.33579 11 4.75 11H11V4.75C11 4.33579 11.3358 4 11.75 4Z" fill="' . $collapsible_color['color'] . '"/></svg>',
110 'four_expanded' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M18.75 11C19.1642 11 19.5 11.3358 19.5 11.75C19.5 12.1642 19.1642 12.5 18.75 12.5H4.75C4.33579 12.5 4 12.1642 4 11.75C4 11.3358 4.33579 11 4.75 11H18.75Z" fill="' . $collapsible_color['color'] . '"/></svg>',
111 'five_collapsed' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M12 4C12.5523 4 13 4.44772 13 5V10.5H18.5C19.0523 10.5 19.5 10.9477 19.5 11.5V12C19.5 12.5523 19.0523 13 18.5 13H13V18.5C13 19.0523 12.5523 19.5 12 19.5H11.5C10.9477 19.5 10.5 19.0523 10.5 18.5V13H5C4.44772 13 4 12.5523 4 12V11.5C4 10.9477 4.44772 10.5 5 10.5H10.5V5C10.5 4.44772 10.9477 4 11.5 4H12Z" fill="' . $collapsible_color['color'] . '"/></svg>',
112 'five_expanded' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M18.5625 10.675C19.1148 10.675 19.5625 11.1228 19.5625 11.675V12.175C19.5625 12.7273 19.1148 13.175 18.5625 13.175H5.0625C4.51022 13.175 4.0625 12.7273 4.0625 12.175V11.675C4.0625 11.1228 4.51022 10.675 5.0625 10.675H18.5625Z" fill="' . $collapsible_color['color'] . '"/></svg>',
113 'six' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M16.925 11.1L17.5249 11.7C17.8249 12 17.8249 12.4 17.5249 12.6L12.325 17.8C12.025 18.1 11.625 18.1 11.425 17.8L6.225 12.6C5.925 12.3 5.925 11.9 6.225 11.7L6.82498 11.1C7.12498 10.8 7.525 10.9 7.725 11.1L10.825 14.3V6.6C10.825 6.2 11.125 6 11.425 6H12.325C12.725 6 12.925 6.3 12.925 6.6V14.3L16.0249 11.1C16.2249 10.9 16.625 10.9 16.925 11.1Z" fill="' . $collapsible_color['color'] . '"/></svg>',
114 'seven_collapsed' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M18.2002 10C18.2002 5.47126 14.5287 1.7998 10 1.7998C5.47126 1.7998 1.7998 5.47126 1.7998 10C1.7998 14.5287 5.47126 18.2002 10 18.2002V20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20V18.2002C14.5287 18.2002 18.2002 14.5287 18.2002 10Z" fill="' . $collapsible_color['color'] . '"/><path d="M14.2994 8.94904H11.051V5.70064C11.051 5.31847 10.7325 5 10.3503 5H9.64968C9.26752 5 8.94904 5.31847 8.94904 5.70064V8.94904H5.70064C5.31847 8.94904 5 9.26752 5 9.64968V10.3503C5 10.7325 5.31847 11.051 5.70064 11.051H8.94904V14.2994C8.94904 14.6815 9.26752 15 9.64968 15H10.3503C10.7325 15 11.051 14.6815 11.051 14.2994V11.051H14.2994C14.6815 11.051 15 10.7325 15 10.3503V9.64968C15 9.26752 14.6815 8.94904 14.2994 8.94904Z" fill="' . $collapsible_color['color'] . '"/></svg>',
115 'seven_expanded' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M18.2002 10C18.2002 5.47126 14.5287 1.7998 10 1.7998C5.47126 1.7998 1.7998 5.47126 1.7998 10C1.7998 14.5287 5.47126 18.2002 10 18.2002V20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20V18.2002C14.5287 18.2002 18.2002 14.5287 18.2002 10Z" fill="' . $collapsible_color['color'] . '"/><path d="M14.3082 9H5.69182C5.31446 9 5 9.30303 5 9.66667V10.3333C5 10.697 5.31446 11 5.69182 11H14.3082C14.6855 11 15 10.697 15 10.3333V9.66667C15 9.30303 14.6855 9 14.3082 9Z" fill="' . $collapsible_color['color'] . '"/></svg>',
116 'eight' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 20C7.32891 20 4.8177 18.9598 2.92891 17.0711C1.0402 15.1823 0 12.6711 0 10C0 7.32891 1.0402 4.8177 2.92891 2.92891C4.8177 1.0402 7.32891 0 10 0C12.6711 0 15.1823 1.0402 17.0711 2.92891C18.9598 4.81766 20 7.32891 20 10C20 12.6711 18.9598 15.1823 17.0711 17.0711C15.1823 18.9598 12.6711 20 10 20ZM10 1.5625C7.74625 1.5625 5.62742 2.44016 4.03379 4.03379C2.44016 5.62742 1.5625 7.74629 1.5625 10C1.5625 12.2537 2.44016 14.3726 4.03379 15.9662C5.62742 17.5598 7.74629 18.4375 10 18.4375C12.2537 18.4375 14.3726 17.5598 15.9662 15.9662C17.5598 14.3726 18.4375 12.2537 18.4375 10C18.4375 7.74629 17.5598 5.62742 15.9662 4.03379C14.3726 2.44016 12.2537 1.5625 10 1.5625Z" fill="' . $collapsible_color['color'] . '"/><path d="M14.9308 6.79996C14.7309 6.79996 14.5309 6.87625 14.3784 7.02879L10.001 11.4061L5.62371 7.02879C5.31859 6.72367 4.82394 6.72367 4.51886 7.02879C4.21378 7.33391 4.21375 7.82855 4.51886 8.13363L9.44863 13.0634C9.75375 13.3685 10.2484 13.3685 10.5535 13.0634L15.4832 8.13363C15.7884 7.82852 15.7884 7.33387 15.4832 7.02879C15.3307 6.87625 15.1308 6.79996 14.9308 6.79996Z" fill="' . $collapsible_color['color'] . '"/></svg>',
117 );
118
119 // Get the appropriate icon based on source and expanded state.
120 $get_collapsible_icon = function ( $is_expanded ) use ( $collapsible_icon_source, $icon_svgs ) {
121 switch ( $collapsible_icon_source ) {
122 case 'one':
123 return $icon_svgs['one'];
124 case 'two':
125 return $icon_svgs['two'];
126 case 'three':
127 return $icon_svgs['three'];
128 case 'four':
129 return $is_expanded ? $icon_svgs['four_expanded'] : $icon_svgs['four_collapsed'];
130 case 'five':
131 return $is_expanded ? $icon_svgs['five_expanded'] : $icon_svgs['five_collapsed'];
132 case 'six':
133 return $icon_svgs['six'];
134 case 'seven':
135 return $is_expanded ? $icon_svgs['seven_expanded'] : $icon_svgs['seven_collapsed'];
136 case 'eight':
137 return $icon_svgs['eight'];
138 default:
139 return $is_expanded ? $icon_svgs['four_expanded'] : $icon_svgs['four_collapsed'];
140 }
141 };
142
143 // Get the appropriate child icon based on source and expanded state.
144 $get_child_collapsible_icon = function ( $is_expanded ) use ( $child_collapsible_icon_source, $icon_svgs ) {
145 switch ( $child_collapsible_icon_source ) {
146 case 'one':
147 return $icon_svgs['one'];
148 case 'two':
149 return $icon_svgs['two'];
150 case 'three':
151 return $icon_svgs['three'];
152 case 'four':
153 return $is_expanded ? $icon_svgs['four_expanded'] : $icon_svgs['four_collapsed'];
154 case 'five':
155 return $is_expanded ? $icon_svgs['five_expanded'] : $icon_svgs['five_collapsed'];
156 case 'six':
157 return $icon_svgs['six'];
158 case 'seven':
159 return $is_expanded ? $icon_svgs['seven_expanded'] : $icon_svgs['seven_collapsed'];
160 case 'eight':
161 return $icon_svgs['eight'];
162 default:
163 return $is_expanded ? $icon_svgs['four_expanded'] : $icon_svgs['four_collapsed'];
164 }
165 };
166
167 ob_start();
168 ?>
169 <div id="<?php echo esc_attr( $unique_id ); ?>"
170 class="sp-table-of-content-toc sp-smart-post-<?php echo esc_attr( $block_name ); ?> <?php echo esc_attr( $align_class ); ?> sp-toc-position-<?php echo esc_attr( $display_control ); ?> <?php echo esc_attr( $floating_position_class ); ?>"
171 data-smooth-scroll="<?php echo esc_attr( $smooth_scroll ? 'true' : 'false' ); ?>"
172 data-offset-top="<?php echo esc_attr( $offset_top ); ?>"
173 data-button-type="<?php echo esc_attr( $collapsible_button_type ); ?>"
174 data-collapse-text="<?php echo esc_attr( $collapse_text ); ?>"
175 data-expand-text="<?php echo esc_attr( $expand_text ); ?>"
176 data-icon-source="<?php echo esc_attr( $collapsible_icon_source ); ?>"
177 data-child-icon-source="<?php echo esc_attr( $child_collapsible_icon_source ); ?>"
178 data-preset="<?php echo esc_attr( $preset ); ?>"
179 data-hash-url="<?php echo esc_attr( $heading_hash_url ); ?>">
180 <div class="sps-toc-wrapper"
181 data-icons='<?php echo esc_attr( wp_json_encode( $icon_svgs ) ); ?>'>
182
183
184 <div class="sps-toc-header-row">
185 <div class="sps-toc-header">
186 <strong class="sps-toc-title"><?php echo esc_html( $title ); ?></strong>
187
188 <?php if ( 'icon' === $collapsible_button_type && 'besideTitle' === $collapsible_icon_position && $toc_collapsed ) : ?>
189 <button
190 class="sps-toc-main-toggle <?php echo ! $collapsed_initially ? 'expanded' : ''; ?>"
191 aria-label="<?php echo $collapsed_initially ? esc_html( $expand_text ) : esc_html( $collapse_text ); ?>"
192 aria-expanded="<?php echo ! $collapsed_initially ? 'true' : 'false'; ?>">
193
194 <?php
195 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
196 echo $get_collapsible_icon( ! $collapsed_initially );
197 ?>
198
199 </button>
200 <?php endif; ?>
201
202 </div>
203 <?php if ( $toc_collapsed ) : ?>
204 <div class="sp-main-collapse-toggle">
205
206 <?php if ( 'right' === $collapsible_icon_position || 'icon' !== $collapsible_button_type ) : ?>
207 <button
208 class="sps-toc-main-toggle <?php echo ! $collapsed_initially ? 'expanded' : ''; ?>"
209 aria-label="<?php echo $collapsed_initially ? esc_html( $expand_text ) : esc_html( $collapse_text ); ?>"
210 aria-expanded="<?php echo ! $collapsed_initially ? 'true' : 'false'; ?>">
211
212 <?php if ( 'icon' === $collapsible_button_type ) : ?>
213 <?php
214 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
215 echo $get_collapsible_icon( ! $collapsed_initially );
216 ?>
217
218 <?php elseif ( 'text' === $collapsible_button_type ) : ?>
219 <?php echo $collapsed_initially ? esc_html( $expand_text ) : esc_html( $collapse_text ); ?>
220 <?php endif; ?>
221
222 </button>
223 <?php endif; ?>
224
225 </div>
226
227 <?php endif; ?>
228 </div>
229
230 <!-- Always render the container but hide it with CSS when initially collapsed -->
231 <div class="sps-toc-container <?php echo esc_attr( $preset ); ?><?php echo ( $toc_collapsed && $collapsed_initially ) ? ' sps-toc-hidden' : ''; ?>">
232 <?php if ( in_array( $preset, array( 'presetThree', 'presetFour', 'presetFive' ) ) ) : ?>
233 <div class="sps-toc-indicator"></div>
234 <?php endif; ?>
235 <ul class="sps-toc-list sps-toc-style-<?php echo esc_attr( $list_style ); ?>">
236 <?php $this->render_toc_items( $headings, $list_style, 0, $content_alignment, $copy_link, $child_item_collapsible, $get_child_collapsible_icon, $heading_hash_url, $preset ); ?>
237 </ul>
238 </div>
239 </div>
240 </div>
241 <?php
242 return ob_get_clean();
243 }
244
245 /**
246 * Flatten TOC structure if hierarchy is disabled.
247 *
248 * @param array $items TOC items.
249 * @return array Flattened TOC items.
250 */
251 private function flatten_toc( $items ) {
252 $result = array();
253
254 foreach ( $items as $item ) {
255 // Add current item.
256 $result[] = array(
257 'id' => $item['id'],
258 'text' => $item['text'],
259 'level' => $item['level'],
260 );
261
262 // Recursively add children.
263 if ( ! empty( $item['children'] ) ) {
264 $children = $this->flatten_toc( $item['children'] );
265 $result = array_merge( $result, $children );
266 }
267 }
268
269 return $result;
270 }
271
272 /**
273 * Recursive function to render TOC items.
274 *
275 * @param array $items Array of TOC items to render.
276 * @param string $list_style The list style to apply.
277 * @param int $depth Current depth level.
278 * @param string $content_alignment for content alignment.
279 * @param string $copy_link to copy link.
280 * @param bool $child_item_collapsible Whether child items can be collapsed.
281 * @param callable $get_child_collapsible_icon Function to get child icon.
282 * @param string $heading_hash_url toggle.
283 * @param string $preset The current preset.
284 */
285 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' ) {
286 foreach ( $items as $item ) {
287 if ( empty( $item['id'] ) || empty( $item['text'] ) ) {
288 continue;
289 }
290
291 $has_children = ! empty( $item['children'] ) && is_array( $item['children'] );
292 $is_selected = false; // This will be handled in JavaScript.
293 ?>
294 <li class="sps-toc-item <?php echo $has_children ? 'has-children' : ''; ?> sps-toc-level-<?php echo esc_attr( $item['level'] ); ?>"
295 data-depth="<?php echo esc_attr( $depth ); ?>"
296 data-item-id="<?php echo esc_attr( $item['id'] ); ?>">
297 <div class="sps-toc-link-wrapper sps-toc-<?php echo esc_attr( $content_alignment ); ?> ">
298 <a href="#<?php echo esc_attr( $item['id'] ); ?>" class="sps-toc-link">
299 <?php if ( 'none' !== $list_style && 'bullet' !== $list_style ) : ?>
300 <span class="sps-toc-number"></span>
301 <?php endif; ?>
302 <span class="sps-toc-text"><?php echo esc_html( $item['text'] ); ?></span>
303 </a>
304
305 <div class="sps-toc-actions">
306 <?php if ( $copy_link ) : ?>
307 <button class="sps-toc-copy"
308 data-clipboard-text="<?php echo esc_url( get_permalink() . '#' . $item['id'] ); ?>"
309 title="Copy link"
310 aria-label="Copy link to this section">
311 #
312 </button>
313 <?php endif; ?>
314
315 <?php if ( $has_children && $child_item_collapsible && is_callable( $get_child_collapsible_icon ) ) : ?>
316 <button
317 class="sps-toc-toggle expanded"
318 aria-label="<?php echo esc_attr__( 'Collapse section', 'post-carousel' ); ?>"
319 aria-expanded="true">
320 <?php
321 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
322 echo $get_child_collapsible_icon( true );
323 ?>
324 </button>
325
326 <?php endif; ?>
327 </div>
328 </div>
329
330 <?php if ( $has_children ) : ?>
331 <ul class="sps-toc-sublist">
332 <?php $this->render_toc_items( $item['children'], $list_style, $depth + 1, $content_alignment, $copy_link, $child_item_collapsible, $get_child_collapsible_icon, $heading_hash_url, $preset ); ?>
333 </ul>
334 <?php endif; ?>
335 </li>
336 <?php
337 }
338 }
339
340
341 /**
342 * Sps_get_headings_with_id function
343 *
344 * @param array $supported_heading_tag All heading tags.
345 * @return array
346 */
347 private function sps_get_headings_with_id( $supported_heading_tag ) {
348 $content = get_the_content();
349 $headings = array();
350
351 // Extract allowed heading numbers from $supported_heading_tag.
352 $allowed_levels = array_map(
353 function ( $item ) {
354 return intval( substr( $item['value'], 1 ) ); // 'h2' → 2
355 },
356 $supported_heading_tag
357 );
358
359 $pattern = '/<h([1-6])([^>]*)>(.*?)<\/h\1>/is';
360
361 if ( preg_match_all( $pattern, $content, $matches, PREG_SET_ORDER ) ) {
362 $stack = array();
363
364 foreach ( $matches as $m ) {
365 $level = intval( $m[1] );
366
367 // Skip headings not in allowed_levels.
368 if ( ! in_array( $level, $allowed_levels ) ) {
369 continue;
370 }
371
372 $attrs = $m[2];
373 $inner = $m[3];
374 $html = $m[0];
375
376 if ( preg_match( '/id="([^"]+)"/', $attrs, $id_match ) ) {
377 $id = $id_match[1];
378 } else {
379 $id = $this->generate_heading_id( wp_strip_all_tags( $inner ) );
380 $html = preg_replace(
381 '/<h([1-6])([^>]*)>/i',
382 '<h$1 id="' . $id . '"$2>',
383 $html,
384 1
385 );
386 }
387
388 $item = array(
389 'level' => $level,
390 'text' => wp_strip_all_tags( $inner ),
391 'id' => $id,
392 'html' => $html,
393 'children' => array(),
394 );
395
396 // Build nested structure.
397 while ( ! empty( $stack ) && $level <= end( $stack )['level'] ) {
398 array_pop( $stack );
399 }
400
401 if ( empty( $stack ) ) {
402 $headings[] = $item;
403 $stack[] = &$headings[ count( $headings ) - 1 ];
404 } else {
405 $parent = &$stack[ count( $stack ) - 1 ];
406 $parent['children'][] = $item;
407 $stack[] = &$parent['children'][ count( $parent['children'] ) - 1 ];
408 }
409
410 unset( $item, $parent );
411 }
412 }
413
414 return $headings;
415 }
416
417 /**
418 * Generate heading id function
419 *
420 * @param string $text Heading text.
421 * @return string
422 */
423 private function generate_heading_id( $text ) {
424 $id = mb_strtolower( $text, 'UTF-8' );
425 $id = preg_replace( '/[\s_.,!?;:()\[\]{}"\'`~@#$%^&*+=<>\/\\\\|]+/u', '-', $id );
426 $id = preg_replace( '/[^\p{L}\p{N}-]/u', '', $id );
427 $id = preg_replace( '/-+/', '-', $id );
428 $id = trim( $id, '-' );
429
430 if ( empty( $id ) ) {
431 $id = 'heading-' . uniqid();
432 }
433 return $id;
434 }
435 }
436