PluginProbe
SimpleTOC – Table of Contents Block / 7.4.1
SimpleTOC – Table of Contents Block v7.4.1
7.4.1 7.4.0 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 All 162 releases
← All changes | plugin.php +207 -72 7.1.0 → 7.4.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.4.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.4.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.
@@ -56,9 +57,10 @@
56 57 wp_add_inline_script(
57 58 'simpletoc-toc-editor-script',
58 59 'window.simpletocEditorSettings = ' . wp_json_encode(
59 60 array(
60 - 'settingsUrl' => admin_url( 'options-general.php?page=simpletoc' ),
61 + 'settingsUrl' => admin_url( 'options-general.php?page=simpletoc' ),
62 + 'scrollSpyEnabled' => simpletoc_scroll_spy_enabled(),
61 63 )
62 64 ) . ';',
63 65 'before'
64 66 );
@@ -76,8 +78,10 @@
76 78 */
77 79 function add_simpletoc_block_editor_settings( $editor_settings, $editor_context ) {
78 80 $editor_settings['simpletocSettingsUrl'] = admin_url( 'options-general.php?page=simpletoc' );
79 81
82 + $editor_settings['simpletocScrollSpyEnabled'] = simpletoc_scroll_spy_enabled();
83 +
80 84 return $editor_settings;
81 85 }
82 86
83 87 add_filter( 'block_editor_settings_all', __NAMESPACE__ . '\add_simpletoc_block_editor_settings', 10, 2 );
@@ -162,11 +166,11 @@
162 166 2
163 167 );
164 168
165 169 /**
166 - * Filter to add plugins to the TOC list for Rank Math plugin
170 + * Filter to add plugins to the TOC list for Rank Math plugin.
167 171 *
168 - * @param array TOC plugins.
172 + * @param array $toc_plugins TOC plugins.
169 173 */
170 174 add_filter(
171 175 'rank_math/researches/toc_plugins',
172 176 function ( $toc_plugins ) {
@@ -193,9 +197,9 @@
193 197
194 198 return $content;
195 199 }
196 200
197 -add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 1 );
201 +add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 1 );
198 202
199 203 /**
200 204 * Recursively adds IDs to the headings of a nested block structure.
201 205 *
@@ -216,20 +220,22 @@
216 220 * @param array $supported_blocks The array of supported blocks.
217 221 */
218 222 $supported_blocks = apply_filters( 'simpletoc_supported_blocks_for_ids', $supported_blocks );
219 223
220 - // Need two separate instances so that IDs aren't double coubnted.
224 + // Need two separate instances so that IDs aren't double counted.
221 225 $inner_html_id_instance = new SimpleTOC_Headline_Ids();
222 226 $inner_content_id_instance = new SimpleTOC_Headline_Ids();
223 227
228 + $ignored_blocks = simpletoc_get_excluded_blocks();
229 +
224 230 foreach ( $blocks as &$block ) {
231 + if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $ignored_blocks, true ) ) {
232 + continue;
233 + }
234 +
225 235 if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $supported_blocks, true ) && isset( $block['innerHTML'] ) && isset( $block['innerContent'] ) && isset( $block['innerContent'][0] ) ) {
226 236 $block['innerHTML'] = add_anchor_attribute( $block['innerHTML'], $inner_html_id_instance, $block );
227 237 $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 238 } elseif ( ! empty( $block['innerBlocks'] ) ) {
233 239 // search in groups.
234 240 $block['innerBlocks'] = add_ids_to_blocks_recursive( $block['innerBlocks'] );
235 241 }
@@ -247,21 +253,30 @@
247 253 function render_callback_simpletoc( $attributes ) {
248 254 $is_backend = defined( 'REST_REQUEST' ) && REST_REQUEST && 'edit' === filter_input( INPUT_GET, 'context' );
249 255 $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : __( 'Table of Contents', 'simpletoc' );
250 256 $alignclass = ! empty( $attributes['align'] ) ? 'align' . $attributes['align'] : '';
251 - $class_name = ! empty( $attributes['className'] ) ? wp_strip_all_tags( $attributes['className'] ) : '';
252 257 $title_level = $attributes['title_level'];
253 258 $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'] );
259 + $legacy_box_style_enabled = ! empty( $attributes['box_style'] );
260 + $typography_enabled = ! empty( $attributes['fontSize'] ) || ! empty( $attributes['style']['typography'] );
255 261 $wrapper_classes = array( 'simpletoc' );
256 262 $wrapper_style = '';
257 263
258 - if ( $box_style_enabled ) {
264 + if ( simpletoc_scroll_spy_enabled() || ! empty( $attributes['scroll_spy'] ) ) {
265 + $wrapper_classes[] = 'has-simpletoc-scroll-spy';
266 + }
267 +
268 + if ( $typography_enabled ) {
269 + $wrapper_classes[] = 'has-simpletoc-typography';
270 + }
271 +
272 + if ( $global_box_style_enabled || $legacy_box_style_enabled ) {
273 + $wrapper_classes[] = 'is-style-boxed';
259 274 $wrapper_classes[] = 'has-simpletoc-box-style';
260 275
261 276 if ( $global_box_style_enabled ) {
262 277 $wrapper_classes[] = 'has-background';
263 - $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
278 + $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
264 279 } elseif ( ! empty( $attributes['box_color'] ) ) {
265 280 $wrapper_classes[] = 'has-background';
266 281 $wrapper_style = safecss_filter_attr( 'background-color:' . $attributes['box_color'] . ';' );
267 282 } else {
@@ -269,9 +284,8 @@
269 284 $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
270 285 }
271 286 }
272 287
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 288 $wrapper_attrs = get_block_wrapper_attributes(
275 289 array(
276 290 'class' => implode( ' ', $wrapper_classes ),
277 291 'style' => $wrapper_style,
@@ -276,11 +290,10 @@
276 290 'class' => implode( ' ', $wrapper_classes ),
277 291 'style' => $wrapper_style,
278 292 )
279 293 );
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>' : '';
294 + $pre_html = '<div role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>';
295 + $post_html = '</div>';
283 296
284 297 $post = get_post();
285 298 $blocks = ! is_null( $post ) && ! is_null( $post->post_content ) ? parse_blocks( $post->post_content ) : '';
286 299
@@ -289,17 +302,17 @@
289 302 $headings_clean = array_map( 'trim', $headings );
290 303 $toc_html = generate_toc( $headings_clean, $attributes );
291 304
292 305 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 );
306 + 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 307 }
295 308
296 309 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 );
310 + 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 311 }
299 312
300 313 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 );
314 + 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 315 }
303 316
304 317 return $pre_html . $toc_html . $post_html;
305 318 }
@@ -313,27 +326,20 @@
313 326 * @param string $alignclass The CSS class for alignment of the Table of Contents block.
314 327 * @param string $title_text The text for the Table of Contents title.
315 328 * @param string $warning_text1 The first part of the warning message to be displayed.
316 329 * @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.
330 + * @param string $wrapper_attrs Block wrapper attributes.
319 331 *
320 332 * @return string The HTML output for the empty blocks message.
321 333 */
322 -function get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, $warning_text1, $warning_text2, $wrapper_attrs = '', $has_wrapper = false ) {
334 +function get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, $warning_text1, $warning_text2, $wrapper_attrs = '' ) {
323 335 $html = '';
324 336
325 337 if ( $is_backend ) {
326 - if ( $has_wrapper ) {
327 - $html .= '<div role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>';
328 - }
329 -
338 + $html .= '<div role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>';
330 339 $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 - }
340 + $html .= sprintf( '<p class="components-notice is-warning %s">%s %s</p>', esc_attr( $alignclass ), esc_html( $warning_text1 ), esc_html( $warning_text2 ) );
341 + $html .= '</div>';
336 342 }
337 343
338 344 return $html;
339 345 }
@@ -361,9 +367,9 @@
361 367 if ( isset( $blocks[ $block ]['blockName'] ) && 'core/heading' === $blocks[ $block ]['blockName'] ) {
362 368 // make sure its a headline.
363 369 foreach ( $headings as $heading => &$inner_heading ) {
364 370 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'] );
371 + $inner_heading = simpletoc_add_page_number_to_headline( $blocks[ $block ]['innerHTML'], $pages );
366 372 }
367 373 }
368 374 }
369 375 }
@@ -370,8 +376,22 @@
370 376 return $headings;
371 377 }
372 378
373 379 /**
380 + * Gets blocks excluded from both anchor generation and TOC collection.
381 + *
382 + * Query templates render repeatedly in a different post context.
383 + *
384 + * @return string[] Excluded block names.
385 + */
386 +function simpletoc_get_excluded_blocks() {
387 + return apply_filters(
388 + 'simpletoc_excluded_blocks',
389 + array( 'core/query', 'generateblocks/query-loop', 'generateblocks/query' )
390 + );
391 +}
392 +
393 +/**
374 394 * Return all headings with a recursive walk through all blocks.
375 395 * This includes groups and reusable block with groups within reusable blocks.
376 396 *
377 397 * @param array[] $blocks The blocks to filter headings from.
@@ -384,9 +404,9 @@
384 404 return $arr;
385 405 }
386 406
387 407 // allow developers to ignore specific blocks.
388 - $ignored_blocks = apply_filters( 'simpletoc_excluded_blocks', array() );
408 + $ignored_blocks = simpletoc_get_excluded_blocks();
389 409
390 410 foreach ( $blocks as $inner_block ) {
391 411 if ( is_array( $inner_block ) ) {
392 412 // if block is ignored, skip.
@@ -405,13 +425,10 @@
405 425 // search in groups.
406 426 $arr = array_merge( filter_headings_recursive( $inner_block ), $arr );
407 427 }
408 428 } 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 - }
429 + if ( isset( $blocks['blockName'] ) && ( 'core/heading' === $blocks['blockName'] ) && 'core/heading' !== $inner_block && simpletoc_is_heading_html( $inner_block ) ) {
430 + $arr[] = $inner_block;
414 431 }
415 432
416 433 $supported_third_party_blocks = array(
417 434 'generateblocks/headline', /* GenerateBlocks 1.x */
@@ -428,14 +445,11 @@
428 445 'simpletoc_supported_third_party_blocks',
429 446 $supported_third_party_blocks
430 447 );
431 448
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 - }
449 + if ( isset( $blocks['blockName'] ) && in_array( $blocks['blockName'], $supported_third_party_blocks, true ) && 'core/heading' !== $inner_block && simpletoc_is_heading_html( $inner_block ) ) {
450 + $inner_block = simpletoc_maybe_replace_generateblocks_dynamic_tags( $inner_block, $blocks );
451 + $arr[] = $inner_block;
438 452 }
439 453 }
440 454 }
441 455
@@ -488,12 +502,11 @@
488 502 $html_wo_nbs = str_replace( '&nbsp;', ' ', $zero_punctuation );
489 503 // remove umlauts and accents.
490 504 $string_without_accents = remove_accents( $html_wo_nbs );
491 505 // 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;
506 + // Already returns a URL-safe, percent-encoded slug for non-ASCII input, so no
507 + // further rawurlencode() is needed (that would double-encode the string).
508 + return sanitize_title_with_dashes( $string_without_accents );
496 509 }
497 510
498 511 /**
499 512 * Add additional plugin meta links to the SimpleTOC plugin page.
@@ -534,8 +547,113 @@
534 547 return class_exists( '\WP_HTML_Tag_Processor' );
535 548 }
536 549
537 550 /**
551 + * Creates an HTML Tag Processor for a valid HTML fragment.
552 + *
553 + * Parsed block content can contain non-string placeholders for nested blocks.
554 + * The WordPress HTML API accepts strings only.
555 + *
556 + * @param mixed $html The HTML fragment to inspect.
557 + * @return \WP_HTML_Tag_Processor|null The processor, or null when unavailable or invalid.
558 + */
559 +function simpletoc_get_html_tag_processor( $html ) {
560 + if ( ! is_string( $html ) || ! simpletoc_load_html_tag_processor() ) {
561 + return null;
562 + }
563 +
564 + return new \WP_HTML_Tag_Processor( $html );
565 +}
566 +
567 +/**
568 + * Returns true when the provided HTML contains a heading tag.
569 + *
570 + * @param string $html The HTML to inspect.
571 + * @return bool True when the HTML contains a heading tag.
572 + */
573 +function simpletoc_is_heading_html( $html ) {
574 + return false !== simpletoc_get_heading_depth( $html );
575 +}
576 +
577 +/**
578 + * Gets the first heading depth from an HTML fragment.
579 + *
580 + * @param string $html The HTML to inspect.
581 + * @return int|false The heading depth, or false when no heading was found.
582 + */
583 +function simpletoc_get_heading_depth( $html ) {
584 + $processor = simpletoc_get_html_tag_processor( $html );
585 +
586 + if ( $processor ) {
587 +
588 + while ( $processor->next_tag() ) {
589 + $tag_name = $processor->get_tag();
590 +
591 + if ( in_array( $tag_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
592 + return (int) substr( $tag_name, 1 );
593 + }
594 + }
595 + }
596 +
597 + return false;
598 +}
599 +
600 +/**
601 + * Adds a data-page attribute to the first heading in an HTML fragment.
602 + *
603 + * @param string $html The heading HTML.
604 + * @param int $page_number The page number to set.
605 + * @return string The updated HTML.
606 + */
607 +function simpletoc_add_page_number_to_headline( $html, $page_number ) {
608 + $processor = simpletoc_get_html_tag_processor( $html );
609 +
610 + if ( $processor ) {
611 +
612 + while ( $processor->next_tag() ) {
613 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
614 + continue;
615 + }
616 +
617 + $processor->set_attribute( 'data-page', (string) $page_number );
618 + return $processor->get_updated_html();
619 + }
620 + }
621 +
622 + return $html;
623 +}
624 +
625 +/**
626 + * Checks whether the first heading in an HTML fragment has the provided class.
627 + *
628 + * @param string $html The heading HTML.
629 + * @param string $class_name The class name to find.
630 + * @return bool True when the class exists.
631 + */
632 +function simpletoc_heading_has_class( $html, $class_name ) {
633 + $processor = simpletoc_get_html_tag_processor( $html );
634 +
635 + if ( $processor ) {
636 +
637 + while ( $processor->next_tag() ) {
638 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
639 + continue;
640 + }
641 +
642 + $class_attribute = $processor->get_attribute( 'class' );
643 +
644 + if ( ! is_string( $class_attribute ) ) {
645 + return false;
646 + }
647 +
648 + return in_array( $class_name, preg_split( '/\s+/', trim( $class_attribute ) ), true );
649 + }
650 + }
651 +
652 + return false;
653 +}
654 +
655 +/**
538 656 * Adds an ID attribute to all Heading tags in the provided HTML.
539 657 *
540 658 * @param string $html The HTML content to modify.
541 659 * @param SimpleTOC_Headline_Ids $headline_class_instance The instance of the SimpleTOC_Headline_Ids class.
@@ -542,8 +660,11 @@
542 660 * @param array $block The parsed block data.
543 661 * @return string The modified HTML content with ID attributes added to the Heading tags
544 662 */
545 663 function add_anchor_attribute( $html, $headline_class_instance = null, $block = array() ) {
664 + if ( ! is_string( $html ) ) {
665 + return $html;
666 + }
546 667
547 668 // remove non-breaking space entites from input HTML.
548 669 $html_wo_nbs = str_replace( '&nbsp;', ' ', $html );
549 670
@@ -551,11 +672,12 @@
551 672 if ( ! $html_wo_nbs ) {
552 673 return $html;
553 674 }
554 675
555 - if ( simpletoc_load_html_tag_processor() ) {
556 - $processor = new \WP_HTML_Tag_Processor( $html_wo_nbs );
676 + $processor = simpletoc_get_html_tag_processor( $html_wo_nbs );
557 677
678 + if ( $processor ) {
679 +
558 680 while ( $processor->next_tag() ) {
559 681 if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
560 682 continue;
561 683 }
@@ -666,13 +788,9 @@
666 788 * @param int $this_depth The depth level of the headline.
667 789 * @return bool True if the headline should be excluded, false otherwise.
668 790 */
669 791 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 - }
792 + $exclude_headline = simpletoc_heading_has_class( $headline, 'simpletoc-hidden' );
675 793
676 794 return ( $this_depth > $attributes['max_level'] || $exclude_headline || $this_depth < $attributes['min_level'] );
677 795 }
678 796
@@ -687,9 +805,14 @@
687 805 $toc_headings = array();
688 806 $headline_ids = new SimpleTOC_Headline_Ids();
689 807
690 808 foreach ( $headings as $headline ) {
691 - $this_depth = (int) $headline[2];
809 + $this_depth = simpletoc_get_heading_depth( $headline );
810 +
811 + if ( false === $this_depth ) {
812 + continue;
813 + }
814 +
692 815 $title = trim( wp_strip_all_tags( $headline ) );
693 816 $custom_id = extract_id( $headline );
694 817 $link = $custom_id ? $custom_id : $headline_ids->get_headline_anchor( $title );
695 818
@@ -739,10 +862,12 @@
739 862 }
740 863 $list .= "</li>\n<li>";
741 864 }
742 865
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;
866 + $page = get_page_number_from_headline( $toc_heading['headline'] );
867 + $url = $absolute_url . $page . '#' . $toc_heading['link'];
868 + $href = $absolute_url ? esc_url( $url ) : esc_attr( $url );
869 + $list .= '<a href="' . $href . '">' . esc_html( $toc_heading['title'] ) . '</a>' . PHP_EOL;
745 870 }
746 871
747 872 if ( null !== $current_depth ) {
748 873 for ( $current_depth; $current_depth > $min_depth; $current_depth-- ) {
@@ -776,9 +901,9 @@
776 901 wp_enqueue_script(
777 902 'simpletoc-accordion',
778 903 plugin_dir_url( __FILE__ ) . 'assets/accordion.js',
779 904 array(),
780 - '6.9.0',
905 + SIMPLETOC_VERSION,
781 906 true
782 907 );
783 908
784 909 wp_enqueue_style(
@@ -784,9 +909,9 @@
784 909 wp_enqueue_style(
785 910 'simpletoc-accordion',
786 911 plugin_dir_url( __FILE__ ) . 'assets/accordion.css',
787 912 array(),
788 - '6.9.0'
913 + SIMPLETOC_VERSION
789 914 );
790 915 }
791 916
792 917 /**
@@ -802,9 +927,9 @@
802 927
803 928 if ( $is_hidden_enabled ) {
804 929 $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : esc_html__( 'Table of Contents', 'simpletoc' );
805 930 $hidden_start = '<details class="simpletoc">
806 - <summary style="cursor: pointer;">' . $title_text . '</summary>';
931 + <summary>' . $title_text . '</summary>';
807 932 $html .= $hidden_start;
808 933 }
809 934
810 935 // If there are no items in the table of contents, return an empty string.
@@ -849,9 +974,9 @@
849 974 // Start and end HTML for accordion, if enabled.
850 975 $accordion_start = '';
851 976 if ( $is_accordion_enabled ) {
852 977 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">';
978 + $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 979 }
855 980
856 981 // Add the accordion start HTML to the output.
857 982 $html .= $accordion_start;
@@ -899,17 +1024,26 @@
899 1024 /**
900 1025 * Extracts the ID value from the provided heading HTML string.
901 1026 *
902 1027 * @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.
1028 + * @return string|false Returns the extracted ID value, or false if no ID value is found.
904 1029 */
905 1030 function extract_id( $headline ) {
906 - $pattern = '/id="([^"]*)"/';
907 - preg_match( $pattern, $headline, $matches );
908 - $id_value = $matches[1] ?? false;
1031 + $processor = simpletoc_get_html_tag_processor( $headline );
909 1032
910 - if ( false !== $id_value ) {
911 - return $id_value;
1033 + if ( $processor ) {
1034 +
1035 + while ( $processor->next_tag() ) {
1036 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
1037 + continue;
1038 + }
1039 +
1040 + $id_value = $processor->get_attribute( 'id' );
1041 +
1042 + if ( is_string( $id_value ) ) {
1043 + return $id_value;
1044 + }
1045 + }
912 1046 }
913 1047
914 1048 return false;
915 1049 }
@@ -920,10 +1054,11 @@
920 1054 * @param string $headline The headline string.
921 1055 * @return string The page number (in the format "X/") if it exists and is greater than 1, or an empty string otherwise.
922 1056 */
923 1057 function get_page_number_from_headline( $headline ) {
924 - if ( simpletoc_load_html_tag_processor() ) {
925 - $processor = new \WP_HTML_Tag_Processor( $headline );
1058 + $processor = simpletoc_get_html_tag_processor( $headline );
1059 +
1060 + if ( $processor ) {
926 1061
927 1062 while ( $processor->next_tag() ) {
928 1063 if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
929 1064 continue;