PluginProbe
SimpleTOC – Table of Contents Block / 7.3.1
SimpleTOC – Table of Contents Block v7.3.1
7.3.1 7.2.0 7.3.0 7.1.1 7.1.0 7.0.10 7.0.9 7.0.8 7.0.7 7.0.6 7.0.4 7.0.5 5.0.21.1 5.0.22 5.0.23 5.0.24 5.0.25 5.0.25.1 5.0.27 5.0.28 5.0.29 5.0.3 5.0.30 5.0.31 5.0.32 All 160 releases
← All changes | plugin.php +178 -70 7.1.07.3.1 View file →
@@ -2,9 +2,9 @@
2 2 /**
3 3 * Plugin Name: SimpleTOC - Table of Contents Block
4 4 * Plugin URI: https://marc.tv/simpletoc-wordpress-inhaltsverzeichnis-plugin-gutenberg/
5 5 * Description: SEO-friendly Table of Contents Gutenberg block. No JavaScript or CSS by default.
6 - * Version: 7.1.0
6 + * Version: 7.3.1
7 7 * Requires at least: 6.2
8 8 * Requires PHP: 7.3
9 9 * Author: Marc Tönsing
10 10 * Author URI: https://toensing.com
@@ -20,8 +20,9 @@
20 20 require_once __DIR__ . '/simpletoc-admin-settings.php';
21 21 require_once __DIR__ . '/simpletoc-class-headline-ids.php';
22 22
23 23 const DEFAULT_BOX_COLOR = '#ebebeb';
24 +const SIMPLETOC_VERSION = '7.3.1';
24 25
25 26 /**
26 27 * Prevents direct execution of the plugin file.
27 28 * If a WordPress function does not exist, it means that the file has not been run by WordPress.
@@ -162,11 +163,11 @@
162 163 2
163 164 );
164 165
165 166 /**
166 - * Filter to add plugins to the TOC list for Rank Math plugin
167 + * Filter to add plugins to the TOC list for Rank Math plugin.
167 168 *
168 - * @param array TOC plugins.
169 + * @param array $toc_plugins TOC plugins.
169 170 */
170 171 add_filter(
171 172 'rank_math/researches/toc_plugins',
172 173 function ( $toc_plugins ) {
@@ -193,9 +194,9 @@
193 194
194 195 return $content;
195 196 }
196 197
197 -add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 1 );
198 +add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 1 );
198 199
199 200 /**
200 201 * Recursively adds IDs to the headings of a nested block structure.
201 202 *
@@ -216,9 +217,9 @@
216 217 * @param array $supported_blocks The array of supported blocks.
217 218 */
218 219 $supported_blocks = apply_filters( 'simpletoc_supported_blocks_for_ids', $supported_blocks );
219 220
220 - // Need two separate instances so that IDs aren't double coubnted.
221 + // Need two separate instances so that IDs aren't double counted.
221 222 $inner_html_id_instance = new SimpleTOC_Headline_Ids();
222 223 $inner_content_id_instance = new SimpleTOC_Headline_Ids();
223 224
224 225 foreach ( $blocks as &$block ) {
@@ -224,12 +225,8 @@
224 225 foreach ( $blocks as &$block ) {
225 226 if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $supported_blocks, true ) && isset( $block['innerHTML'] ) && isset( $block['innerContent'] ) && isset( $block['innerContent'][0] ) ) {
226 227 $block['innerHTML'] = add_anchor_attribute( $block['innerHTML'], $inner_html_id_instance, $block );
227 228 $block['innerContent'][0] = add_anchor_attribute( $block['innerContent'][0], $inner_content_id_instance, $block );
228 - } elseif ( isset( $block['attrs']['ref'] ) ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElseif
229 - // search in reusable blocks (this is not finished because I ran out of ideas.)
230 - // $reusable_block_id = $block['attrs']['ref'];
231 - // $reusable_block_content = parse_blocks(get_post($reusable_block_id)->post_content);.
232 229 } elseif ( ! empty( $block['innerBlocks'] ) ) {
233 230 // search in groups.
234 231 $block['innerBlocks'] = add_ids_to_blocks_recursive( $block['innerBlocks'] );
235 232 }
@@ -247,21 +244,26 @@
247 244 function render_callback_simpletoc( $attributes ) {
248 245 $is_backend = defined( 'REST_REQUEST' ) && REST_REQUEST && 'edit' === filter_input( INPUT_GET, 'context' );
249 246 $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : __( 'Table of Contents', 'simpletoc' );
250 247 $alignclass = ! empty( $attributes['align'] ) ? 'align' . $attributes['align'] : '';
251 - $class_name = ! empty( $attributes['className'] ) ? wp_strip_all_tags( $attributes['className'] ) : '';
252 248 $title_level = $attributes['title_level'];
253 249 $global_box_style_enabled = apply_filters( 'simpletoc_box_style_enabled', false ) || true === (bool) get_option( 'simpletoc_box_style_enabled', false );
254 - $box_style_enabled = $global_box_style_enabled || ! empty( $attributes['box_style'] );
250 + $legacy_box_style_enabled = ! empty( $attributes['box_style'] );
251 + $typography_enabled = ! empty( $attributes['fontSize'] ) || ! empty( $attributes['style']['typography'] );
255 252 $wrapper_classes = array( 'simpletoc' );
256 253 $wrapper_style = '';
257 254
258 - if ( $box_style_enabled ) {
255 + if ( $typography_enabled ) {
256 + $wrapper_classes[] = 'has-simpletoc-typography';
257 + }
258 +
259 + if ( $global_box_style_enabled || $legacy_box_style_enabled ) {
260 + $wrapper_classes[] = 'is-style-boxed';
259 261 $wrapper_classes[] = 'has-simpletoc-box-style';
260 262
261 263 if ( $global_box_style_enabled ) {
262 264 $wrapper_classes[] = 'has-background';
263 - $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
265 + $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
264 266 } elseif ( ! empty( $attributes['box_color'] ) ) {
265 267 $wrapper_classes[] = 'has-background';
266 268 $wrapper_style = safecss_filter_attr( 'background-color:' . $attributes['box_color'] . ';' );
267 269 } else {
@@ -269,9 +271,8 @@
269 271 $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
270 272 }
271 273 }
272 274
273 - $wrapper_enabled = apply_filters( 'simpletoc_wrapper_enabled', false ) || true === (bool) get_option( 'simpletoc_wrapper_enabled', false ) || true === (bool) get_option( 'simpletoc_accordion_enabled', false );
274 275 $wrapper_attrs = get_block_wrapper_attributes(
275 276 array(
276 277 'class' => implode( ' ', $wrapper_classes ),
277 278 'style' => $wrapper_style,
@@ -276,11 +277,10 @@
276 277 'class' => implode( ' ', $wrapper_classes ),
277 278 'style' => $wrapper_style,
278 279 )
279 280 );
280 - $has_wrapper = ! empty( $class_name ) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper'] || $box_style_enabled;
281 - $pre_html = $has_wrapper ? '<div role="navigation" aria-label="' . __( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>' : '';
282 - $post_html = $has_wrapper ? '</div>' : '';
281 + $pre_html = '<div role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>';
282 + $post_html = '</div>';
283 283
284 284 $post = get_post();
285 285 $blocks = ! is_null( $post ) && ! is_null( $post->post_content ) ? parse_blocks( $post->post_content ) : '';
286 286
@@ -289,17 +289,17 @@
289 289 $headings_clean = array_map( 'trim', $headings );
290 290 $toc_html = generate_toc( $headings_clean, $attributes );
291 291
292 292 if ( empty( $blocks ) ) {
293 - return get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No blocks found.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ), $wrapper_attrs, $has_wrapper );
293 + return get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No blocks found.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ), $wrapper_attrs );
294 294 }
295 295
296 296 if ( empty( $headings_clean ) ) {
297 - return get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No headings found.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ), $wrapper_attrs, $has_wrapper );
297 + return get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No headings found.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ), $wrapper_attrs );
298 298 }
299 299
300 300 if ( empty( $toc_html ) ) {
301 - return get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No headings found.', 'simpletoc' ), __( 'Check minimal and maximum level block settings.', 'simpletoc' ), $wrapper_attrs, $has_wrapper );
301 + return get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No headings found.', 'simpletoc' ), __( 'Check minimal and maximum level block settings.', 'simpletoc' ), $wrapper_attrs );
302 302 }
303 303
304 304 return $pre_html . $toc_html . $post_html;
305 305 }
@@ -313,27 +313,20 @@
313 313 * @param string $alignclass The CSS class for alignment of the Table of Contents block.
314 314 * @param string $title_text The text for the Table of Contents title.
315 315 * @param string $warning_text1 The first part of the warning message to be displayed.
316 316 * @param string $warning_text2 The second part of the warning message to be displayed.
317 - * @param string $wrapper_attrs Wrapper attributes for the optional block wrapper.
318 - * @param bool $has_wrapper Indicates if the wrapper should be rendered.
317 + * @param string $wrapper_attrs Block wrapper attributes.
319 318 *
320 319 * @return string The HTML output for the empty blocks message.
321 320 */
322 -function get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, $warning_text1, $warning_text2, $wrapper_attrs = '', $has_wrapper = false ) {
321 +function get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, $warning_text1, $warning_text2, $wrapper_attrs = '' ) {
323 322 $html = '';
324 323
325 324 if ( $is_backend ) {
326 - if ( $has_wrapper ) {
327 - $html .= '<div role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>';
328 - }
329 -
325 + $html .= '<div role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>';
330 326 $html .= sprintf( '<h%d class="%s">%s</h%d>', $title_level, esc_attr( trim( 'simpletoc-title ' . $alignclass ) ), $title_text, $title_level );
331 - $html .= sprintf( '<p class="components-notice is-warning %s">%s %s</p>', $alignclass, $warning_text1, $warning_text2 );
332 -
333 - if ( $has_wrapper ) {
334 - $html .= '</div>';
335 - }
327 + $html .= sprintf( '<p class="components-notice is-warning %s">%s %s</p>', esc_attr( $alignclass ), esc_html( $warning_text1 ), esc_html( $warning_text2 ) );
328 + $html .= '</div>';
336 329 }
337 330
338 331 return $html;
339 332 }
@@ -361,9 +354,9 @@
361 354 if ( isset( $blocks[ $block ]['blockName'] ) && 'core/heading' === $blocks[ $block ]['blockName'] ) {
362 355 // make sure its a headline.
363 356 foreach ( $headings as $heading => &$inner_heading ) {
364 357 if ( $inner_heading === $blocks[ $block ]['innerHTML'] ) {
365 - $inner_heading = preg_replace( '/(<h1|<h2|<h3|<h4|<h5|<h6)/i', '$1 data-page="' . $pages . '"', $blocks[ $block ]['innerHTML'] );
358 + $inner_heading = simpletoc_add_page_number_to_headline( $blocks[ $block ]['innerHTML'], $pages );
366 359 }
367 360 }
368 361 }
369 362 }
@@ -405,13 +398,10 @@
405 398 // search in groups.
406 399 $arr = array_merge( filter_headings_recursive( $inner_block ), $arr );
407 400 }
408 401 } else {
409 - if ( isset( $blocks['blockName'] ) && ( 'core/heading' === $blocks['blockName'] ) && 'core/heading' !== $inner_block ) {
410 - // make sure it's a headline.
411 - if ( preg_match( '/(<h1|<h2|<h3|<h4|<h5|<h6)/i', $inner_block ) ) {
412 - $arr[] = $inner_block;
413 - }
402 + if ( isset( $blocks['blockName'] ) && ( 'core/heading' === $blocks['blockName'] ) && 'core/heading' !== $inner_block && simpletoc_is_heading_html( $inner_block ) ) {
403 + $arr[] = $inner_block;
414 404 }
415 405
416 406 $supported_third_party_blocks = array(
417 407 'generateblocks/headline', /* GenerateBlocks 1.x */
@@ -428,14 +418,11 @@
428 418 'simpletoc_supported_third_party_blocks',
429 419 $supported_third_party_blocks
430 420 );
431 421
432 - if ( isset( $blocks['blockName'] ) && in_array( $blocks['blockName'], $supported_third_party_blocks, true ) && 'core/heading' !== $inner_block ) {
433 - // make sure it's a headline.
434 - if ( preg_match( '/(<h1|<h2|<h3|<h4|<h5|<h6)/i', $inner_block ) ) {
435 - $inner_block = simpletoc_maybe_replace_generateblocks_dynamic_tags( $inner_block, $blocks );
436 - $arr[] = $inner_block;
437 - }
422 + if ( isset( $blocks['blockName'] ) && in_array( $blocks['blockName'], $supported_third_party_blocks, true ) && 'core/heading' !== $inner_block && simpletoc_is_heading_html( $inner_block ) ) {
423 + $inner_block = simpletoc_maybe_replace_generateblocks_dynamic_tags( $inner_block, $blocks );
424 + $arr[] = $inner_block;
438 425 }
439 426 }
440 427 }
441 428
@@ -488,12 +475,11 @@
488 475 $html_wo_nbs = str_replace( '&nbsp;', ' ', $zero_punctuation );
489 476 // remove umlauts and accents.
490 477 $string_without_accents = remove_accents( $html_wo_nbs );
491 478 // Sanitizes a title, replacing whitespace and a few other characters with dashes.
492 - $sanitized_string = sanitize_title_with_dashes( $string_without_accents );
493 - // Encode for use in an url.
494 - $urlencoded = rawurlencode( $sanitized_string );
495 - return $urlencoded;
479 + // Already returns a URL-safe, percent-encoded slug for non-ASCII input, so no
480 + // further rawurlencode() is needed (that would double-encode the string).
481 + return sanitize_title_with_dashes( $string_without_accents );
496 482 }
497 483
498 484 /**
499 485 * Add additional plugin meta links to the SimpleTOC plugin page.
@@ -534,8 +520,113 @@
534 520 return class_exists( '\WP_HTML_Tag_Processor' );
535 521 }
536 522
537 523 /**
524 + * Creates an HTML Tag Processor for a valid HTML fragment.
525 + *
526 + * Parsed block content can contain non-string placeholders for nested blocks.
527 + * The WordPress HTML API accepts strings only.
528 + *
529 + * @param mixed $html The HTML fragment to inspect.
530 + * @return \WP_HTML_Tag_Processor|null The processor, or null when unavailable or invalid.
531 + */
532 +function simpletoc_get_html_tag_processor( $html ) {
533 + if ( ! is_string( $html ) || ! simpletoc_load_html_tag_processor() ) {
534 + return null;
535 + }
536 +
537 + return new \WP_HTML_Tag_Processor( $html );
538 +}
539 +
540 +/**
541 + * Returns true when the provided HTML contains a heading tag.
542 + *
543 + * @param string $html The HTML to inspect.
544 + * @return bool True when the HTML contains a heading tag.
545 + */
546 +function simpletoc_is_heading_html( $html ) {
547 + return false !== simpletoc_get_heading_depth( $html );
548 +}
549 +
550 +/**
551 + * Gets the first heading depth from an HTML fragment.
552 + *
553 + * @param string $html The HTML to inspect.
554 + * @return int|false The heading depth, or false when no heading was found.
555 + */
556 +function simpletoc_get_heading_depth( $html ) {
557 + $processor = simpletoc_get_html_tag_processor( $html );
558 +
559 + if ( $processor ) {
560 +
561 + while ( $processor->next_tag() ) {
562 + $tag_name = $processor->get_tag();
563 +
564 + if ( in_array( $tag_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
565 + return (int) substr( $tag_name, 1 );
566 + }
567 + }
568 + }
569 +
570 + return false;
571 +}
572 +
573 +/**
574 + * Adds a data-page attribute to the first heading in an HTML fragment.
575 + *
576 + * @param string $html The heading HTML.
577 + * @param int $page_number The page number to set.
578 + * @return string The updated HTML.
579 + */
580 +function simpletoc_add_page_number_to_headline( $html, $page_number ) {
581 + $processor = simpletoc_get_html_tag_processor( $html );
582 +
583 + if ( $processor ) {
584 +
585 + while ( $processor->next_tag() ) {
586 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
587 + continue;
588 + }
589 +
590 + $processor->set_attribute( 'data-page', (string) $page_number );
591 + return $processor->get_updated_html();
592 + }
593 + }
594 +
595 + return $html;
596 +}
597 +
598 +/**
599 + * Checks whether the first heading in an HTML fragment has the provided class.
600 + *
601 + * @param string $html The heading HTML.
602 + * @param string $class_name The class name to find.
603 + * @return bool True when the class exists.
604 + */
605 +function simpletoc_heading_has_class( $html, $class_name ) {
606 + $processor = simpletoc_get_html_tag_processor( $html );
607 +
608 + if ( $processor ) {
609 +
610 + while ( $processor->next_tag() ) {
611 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
612 + continue;
613 + }
614 +
615 + $class_attribute = $processor->get_attribute( 'class' );
616 +
617 + if ( ! is_string( $class_attribute ) ) {
618 + return false;
619 + }
620 +
621 + return in_array( $class_name, preg_split( '/\s+/', trim( $class_attribute ) ), true );
622 + }
623 + }
624 +
625 + return false;
626 +}
627 +
628 +/**
538 629 * Adds an ID attribute to all Heading tags in the provided HTML.
539 630 *
540 631 * @param string $html The HTML content to modify.
541 632 * @param SimpleTOC_Headline_Ids $headline_class_instance The instance of the SimpleTOC_Headline_Ids class.
@@ -542,8 +633,11 @@
542 633 * @param array $block The parsed block data.
543 634 * @return string The modified HTML content with ID attributes added to the Heading tags
544 635 */
545 636 function add_anchor_attribute( $html, $headline_class_instance = null, $block = array() ) {
637 + if ( ! is_string( $html ) ) {
638 + return $html;
639 + }
546 640
547 641 // remove non-breaking space entites from input HTML.
548 642 $html_wo_nbs = str_replace( '&nbsp;', ' ', $html );
549 643
@@ -551,11 +645,12 @@
551 645 if ( ! $html_wo_nbs ) {
552 646 return $html;
553 647 }
554 648
555 - if ( simpletoc_load_html_tag_processor() ) {
556 - $processor = new \WP_HTML_Tag_Processor( $html_wo_nbs );
649 + $processor = simpletoc_get_html_tag_processor( $html_wo_nbs );
557 650
651 + if ( $processor ) {
652 +
558 653 while ( $processor->next_tag() ) {
559 654 if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
560 655 continue;
561 656 }
@@ -666,13 +761,9 @@
666 761 * @param int $this_depth The depth level of the headline.
667 762 * @return bool True if the headline should be excluded, false otherwise.
668 763 */
669 764 function should_exclude_headline( $headline, $attributes, $this_depth ) {
670 - $exclude_headline = false;
671 - preg_match( '/class="([^"]+)"/', $headline, $matches );
672 - if ( ! empty( $matches[1] ) && strpos( $matches[1], 'simpletoc-hidden' ) !== false ) {
673 - $exclude_headline = true;
674 - }
765 + $exclude_headline = simpletoc_heading_has_class( $headline, 'simpletoc-hidden' );
675 766
676 767 return ( $this_depth > $attributes['max_level'] || $exclude_headline || $this_depth < $attributes['min_level'] );
677 768 }
678 769
@@ -687,9 +778,14 @@
687 778 $toc_headings = array();
688 779 $headline_ids = new SimpleTOC_Headline_Ids();
689 780
690 781 foreach ( $headings as $headline ) {
691 - $this_depth = (int) $headline[2];
782 + $this_depth = simpletoc_get_heading_depth( $headline );
783 +
784 + if ( false === $this_depth ) {
785 + continue;
786 + }
787 +
692 788 $title = trim( wp_strip_all_tags( $headline ) );
693 789 $custom_id = extract_id( $headline );
694 790 $link = $custom_id ? $custom_id : $headline_ids->get_headline_anchor( $title );
695 791
@@ -739,10 +835,12 @@
739 835 }
740 836 $list .= "</li>\n<li>";
741 837 }
742 838
743 - $page = get_page_number_from_headline( $toc_heading['headline'] );
744 - $list .= '<a href="' . $absolute_url . $page . '#' . $toc_heading['link'] . '">' . $toc_heading['title'] . '</a>' . PHP_EOL;
839 + $page = get_page_number_from_headline( $toc_heading['headline'] );
840 + $url = $absolute_url . $page . '#' . $toc_heading['link'];
841 + $href = $absolute_url ? esc_url( $url ) : esc_attr( $url );
842 + $list .= '<a href="' . $href . '">' . esc_html( $toc_heading['title'] ) . '</a>' . PHP_EOL;
745 843 }
746 844
747 845 if ( null !== $current_depth ) {
748 846 for ( $current_depth; $current_depth > $min_depth; $current_depth-- ) {
@@ -776,9 +874,9 @@
776 874 wp_enqueue_script(
777 875 'simpletoc-accordion',
778 876 plugin_dir_url( __FILE__ ) . 'assets/accordion.js',
779 877 array(),
780 - '6.9.0',
878 + SIMPLETOC_VERSION,
781 879 true
782 880 );
783 881
784 882 wp_enqueue_style(
@@ -784,9 +882,9 @@
784 882 wp_enqueue_style(
785 883 'simpletoc-accordion',
786 884 plugin_dir_url( __FILE__ ) . 'assets/accordion.css',
787 885 array(),
788 - '6.9.0'
886 + SIMPLETOC_VERSION
789 887 );
790 888 }
791 889
792 890 /**
@@ -802,9 +900,9 @@
802 900
803 901 if ( $is_hidden_enabled ) {
804 902 $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : esc_html__( 'Table of Contents', 'simpletoc' );
805 903 $hidden_start = '<details class="simpletoc">
806 - <summary style="cursor: pointer;">' . $title_text . '</summary>';
904 + <summary>' . $title_text . '</summary>';
807 905 $html .= $hidden_start;
808 906 }
809 907
810 908 // If there are no items in the table of contents, return an empty string.
@@ -849,9 +947,9 @@
849 947 // Start and end HTML for accordion, if enabled.
850 948 $accordion_start = '';
851 949 if ( $is_accordion_enabled ) {
852 950 enqueue_accordion_frontend();
853 - $accordion_start = '<h2 style="margin: 0;"><button type="button" aria-expanded="false" aria-controls="simpletoc-content-container" class="simpletoc-collapsible">' . $title_text . '<span class="simpletoc-icon" aria-hidden="true"></span></button></h2><div id="simpletoc-content-container" class="simpletoc-content">';
951 + $accordion_start = '<h2 class="simpletoc-accordion-heading"><button type="button" aria-expanded="false" aria-controls="simpletoc-content-container" class="simpletoc-collapsible">' . $title_text . '<span class="simpletoc-icon" aria-hidden="true"></span></button></h2><div id="simpletoc-content-container" class="simpletoc-content">';
854 952 }
855 953
856 954 // Add the accordion start HTML to the output.
857 955 $html .= $accordion_start;
@@ -899,17 +997,26 @@
899 997 /**
900 998 * Extracts the ID value from the provided heading HTML string.
901 999 *
902 1000 * @param string $headline The heading HTML string to extract the ID value from.
903 - * @return mixed Returns the extracted ID value, or false if no ID value is found.
1001 + * @return string|false Returns the extracted ID value, or false if no ID value is found.
904 1002 */
905 1003 function extract_id( $headline ) {
906 - $pattern = '/id="([^"]*)"/';
907 - preg_match( $pattern, $headline, $matches );
908 - $id_value = $matches[1] ?? false;
1004 + $processor = simpletoc_get_html_tag_processor( $headline );
909 1005
910 - if ( false !== $id_value ) {
911 - return $id_value;
1006 + if ( $processor ) {
1007 +
1008 + while ( $processor->next_tag() ) {
1009 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
1010 + continue;
1011 + }
1012 +
1013 + $id_value = $processor->get_attribute( 'id' );
1014 +
1015 + if ( is_string( $id_value ) ) {
1016 + return $id_value;
1017 + }
1018 + }
912 1019 }
913 1020
914 1021 return false;
915 1022 }
@@ -920,10 +1027,11 @@
920 1027 * @param string $headline The headline string.
921 1028 * @return string The page number (in the format "X/") if it exists and is greater than 1, or an empty string otherwise.
922 1029 */
923 1030 function get_page_number_from_headline( $headline ) {
924 - if ( simpletoc_load_html_tag_processor() ) {
925 - $processor = new \WP_HTML_Tag_Processor( $headline );
1031 + $processor = simpletoc_get_html_tag_processor( $headline );
1032 +
1033 + if ( $processor ) {
926 1034
927 1035 while ( $processor->next_tag() ) {
928 1036 if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
929 1037 continue;