PluginProbe
SimpleTOC – Table of Contents Block / trunk
SimpleTOC – Table of Contents Block vtrunk
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 5.0.33 All 159 releases
← All changes | plugin.php +210 -189 7.0.10trunk View file →
@@ -2,9 +2,11 @@
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.0.10
6 + * Version: 7.3.0
7 + * Requires at least: 6.2
8 + * Requires PHP: 7.3
7 9 * Author: Marc Tönsing
8 10 * Author URI: https://toensing.com
9 11 * Text Domain: simpletoc
10 12 * License: GPL v2 or later
@@ -18,8 +20,9 @@
18 20 require_once __DIR__ . '/simpletoc-admin-settings.php';
19 21 require_once __DIR__ . '/simpletoc-class-headline-ids.php';
20 22
21 23 const DEFAULT_BOX_COLOR = '#ebebeb';
24 +const SIMPLETOC_VERSION = '7.3.0';
22 25
23 26 /**
24 27 * Prevents direct execution of the plugin file.
25 28 * If a WordPress function does not exist, it means that the file has not been run by WordPress.
@@ -160,11 +163,11 @@
160 163 2
161 164 );
162 165
163 166 /**
164 - * 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.
165 168 *
166 - * @param array TOC plugins.
169 + * @param array $toc_plugins TOC plugins.
167 170 */
168 171 add_filter(
169 172 'rank_math/researches/toc_plugins',
170 173 function ( $toc_plugins ) {
@@ -191,9 +194,9 @@
191 194
192 195 return $content;
193 196 }
194 197
195 -add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 1 );
198 +add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 1 );
196 199
197 200 /**
198 201 * Recursively adds IDs to the headings of a nested block structure.
199 202 *
@@ -214,9 +217,9 @@
214 217 * @param array $supported_blocks The array of supported blocks.
215 218 */
216 219 $supported_blocks = apply_filters( 'simpletoc_supported_blocks_for_ids', $supported_blocks );
217 220
218 - // Need two separate instances so that IDs aren't double coubnted.
221 + // Need two separate instances so that IDs aren't double counted.
219 222 $inner_html_id_instance = new SimpleTOC_Headline_Ids();
220 223 $inner_content_id_instance = new SimpleTOC_Headline_Ids();
221 224
222 225 foreach ( $blocks as &$block ) {
@@ -222,12 +225,8 @@
222 225 foreach ( $blocks as &$block ) {
223 226 if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $supported_blocks, true ) && isset( $block['innerHTML'] ) && isset( $block['innerContent'] ) && isset( $block['innerContent'][0] ) ) {
224 227 $block['innerHTML'] = add_anchor_attribute( $block['innerHTML'], $inner_html_id_instance, $block );
225 228 $block['innerContent'][0] = add_anchor_attribute( $block['innerContent'][0], $inner_content_id_instance, $block );
226 - } elseif ( isset( $block['attrs']['ref'] ) ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElseif
227 - // search in reusable blocks (this is not finished because I ran out of ideas.)
228 - // $reusable_block_id = $block['attrs']['ref'];
229 - // $reusable_block_content = parse_blocks(get_post($reusable_block_id)->post_content);.
230 229 } elseif ( ! empty( $block['innerBlocks'] ) ) {
231 230 // search in groups.
232 231 $block['innerBlocks'] = add_ids_to_blocks_recursive( $block['innerBlocks'] );
233 232 }
@@ -245,21 +244,26 @@
245 244 function render_callback_simpletoc( $attributes ) {
246 245 $is_backend = defined( 'REST_REQUEST' ) && REST_REQUEST && 'edit' === filter_input( INPUT_GET, 'context' );
247 246 $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : __( 'Table of Contents', 'simpletoc' );
248 247 $alignclass = ! empty( $attributes['align'] ) ? 'align' . $attributes['align'] : '';
249 - $class_name = ! empty( $attributes['className'] ) ? wp_strip_all_tags( $attributes['className'] ) : '';
250 248 $title_level = $attributes['title_level'];
251 249 $global_box_style_enabled = apply_filters( 'simpletoc_box_style_enabled', false ) || true === (bool) get_option( 'simpletoc_box_style_enabled', false );
252 - $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'] );
253 252 $wrapper_classes = array( 'simpletoc' );
254 253 $wrapper_style = '';
255 254
256 - 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';
257 261 $wrapper_classes[] = 'has-simpletoc-box-style';
258 262
259 263 if ( $global_box_style_enabled ) {
260 264 $wrapper_classes[] = 'has-background';
261 - $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
265 + $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
262 266 } elseif ( ! empty( $attributes['box_color'] ) ) {
263 267 $wrapper_classes[] = 'has-background';
264 268 $wrapper_style = safecss_filter_attr( 'background-color:' . $attributes['box_color'] . ';' );
265 269 } else {
@@ -267,9 +271,8 @@
267 271 $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
268 272 }
269 273 }
270 274
271 - $wrapper_enabled = apply_filters( 'simpletoc_wrapper_enabled', false ) || true === (bool) get_option( 'simpletoc_wrapper_enabled', false ) || true === (bool) get_option( 'simpletoc_accordion_enabled', false );
272 275 $wrapper_attrs = get_block_wrapper_attributes(
273 276 array(
274 277 'class' => implode( ' ', $wrapper_classes ),
275 278 'style' => $wrapper_style,
@@ -274,11 +277,10 @@
274 277 'class' => implode( ' ', $wrapper_classes ),
275 278 'style' => $wrapper_style,
276 279 )
277 280 );
278 - $has_wrapper = ! empty( $class_name ) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper'] || $box_style_enabled;
279 - $pre_html = $has_wrapper ? '<div role="navigation" aria-label="' . __( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>' : '';
280 - $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>';
281 283
282 284 $post = get_post();
283 285 $blocks = ! is_null( $post ) && ! is_null( $post->post_content ) ? parse_blocks( $post->post_content ) : '';
284 286
@@ -287,17 +289,17 @@
287 289 $headings_clean = array_map( 'trim', $headings );
288 290 $toc_html = generate_toc( $headings_clean, $attributes );
289 291
290 292 if ( empty( $blocks ) ) {
291 - 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 );
292 294 }
293 295
294 296 if ( empty( $headings_clean ) ) {
295 - 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 );
296 298 }
297 299
298 300 if ( empty( $toc_html ) ) {
299 - 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 );
300 302 }
301 303
302 304 return $pre_html . $toc_html . $post_html;
303 305 }
@@ -311,27 +313,20 @@
311 313 * @param string $alignclass The CSS class for alignment of the Table of Contents block.
312 314 * @param string $title_text The text for the Table of Contents title.
313 315 * @param string $warning_text1 The first part of the warning message to be displayed.
314 316 * @param string $warning_text2 The second part of the warning message to be displayed.
315 - * @param string $wrapper_attrs Wrapper attributes for the optional block wrapper.
316 - * @param bool $has_wrapper Indicates if the wrapper should be rendered.
317 + * @param string $wrapper_attrs Block wrapper attributes.
317 318 *
318 319 * @return string The HTML output for the empty blocks message.
319 320 */
320 -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 = '' ) {
321 322 $html = '';
322 323
323 324 if ( $is_backend ) {
324 - if ( $has_wrapper ) {
325 - $html .= '<div role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>';
326 - }
327 -
325 + $html .= '<div role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>';
328 326 $html .= sprintf( '<h%d class="%s">%s</h%d>', $title_level, esc_attr( trim( 'simpletoc-title ' . $alignclass ) ), $title_text, $title_level );
329 - $html .= sprintf( '<p class="components-notice is-warning %s">%s %s</p>', $alignclass, $warning_text1, $warning_text2 );
330 -
331 - if ( $has_wrapper ) {
332 - $html .= '</div>';
333 - }
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>';
334 329 }
335 330
336 331 return $html;
337 332 }
@@ -359,9 +354,9 @@
359 354 if ( isset( $blocks[ $block ]['blockName'] ) && 'core/heading' === $blocks[ $block ]['blockName'] ) {
360 355 // make sure its a headline.
361 356 foreach ( $headings as $heading => &$inner_heading ) {
362 357 if ( $inner_heading === $blocks[ $block ]['innerHTML'] ) {
363 - $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 );
364 359 }
365 360 }
366 361 }
367 362 }
@@ -403,13 +398,10 @@
403 398 // search in groups.
404 399 $arr = array_merge( filter_headings_recursive( $inner_block ), $arr );
405 400 }
406 401 } else {
407 - if ( isset( $blocks['blockName'] ) && ( 'core/heading' === $blocks['blockName'] ) && 'core/heading' !== $inner_block ) {
408 - // make sure it's a headline.
409 - if ( preg_match( '/(<h1|<h2|<h3|<h4|<h5|<h6)/i', $inner_block ) ) {
410 - $arr[] = $inner_block;
411 - }
402 + if ( isset( $blocks['blockName'] ) && ( 'core/heading' === $blocks['blockName'] ) && 'core/heading' !== $inner_block && simpletoc_is_heading_html( $inner_block ) ) {
403 + $arr[] = $inner_block;
412 404 }
413 405
414 406 $supported_third_party_blocks = array(
415 407 'generateblocks/headline', /* GenerateBlocks 1.x */
@@ -426,14 +418,11 @@
426 418 'simpletoc_supported_third_party_blocks',
427 419 $supported_third_party_blocks
428 420 );
429 421
430 - if ( isset( $blocks['blockName'] ) && in_array( $blocks['blockName'], $supported_third_party_blocks, true ) && 'core/heading' !== $inner_block ) {
431 - // make sure it's a headline.
432 - if ( preg_match( '/(<h1|<h2|<h3|<h4|<h5|<h6)/i', $inner_block ) ) {
433 - $inner_block = simpletoc_maybe_replace_generateblocks_dynamic_tags( $inner_block, $blocks );
434 - $arr[] = $inner_block;
435 - }
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;
436 425 }
437 426 }
438 427 }
439 428
@@ -511,8 +500,134 @@
511 500 return $links;
512 501 }
513 502
514 503 /**
504 + * Loads the WordPress HTML Tag Processor when available.
505 + *
506 + * @return bool True when the HTML Tag Processor can be used.
507 + */
508 +function simpletoc_load_html_tag_processor() {
509 + if ( class_exists( '\WP_HTML_Tag_Processor' ) ) {
510 + return true;
511 + }
512 +
513 + if ( defined( 'ABSPATH' ) && defined( 'WPINC' ) ) {
514 + $html_tag_processor_file = ABSPATH . WPINC . '/html-api/class-wp-html-tag-processor.php';
515 +
516 + if ( file_exists( $html_tag_processor_file ) ) {
517 + require_once $html_tag_processor_file;
518 + }
519 + }
520 +
521 + return class_exists( '\WP_HTML_Tag_Processor' );
522 +}
523 +
524 +/**
525 + * Creates an HTML Tag Processor for a valid HTML fragment.
526 + *
527 + * Parsed block content can contain non-string placeholders for nested blocks.
528 + * The WordPress HTML API accepts strings only.
529 + *
530 + * @param mixed $html The HTML fragment to inspect.
531 + * @return \WP_HTML_Tag_Processor|null The processor, or null when unavailable or invalid.
532 + */
533 +function simpletoc_get_html_tag_processor( $html ) {
534 + if ( ! is_string( $html ) || ! simpletoc_load_html_tag_processor() ) {
535 + return null;
536 + }
537 +
538 + return new \WP_HTML_Tag_Processor( $html );
539 +}
540 +
541 +/**
542 + * Returns true when the provided HTML contains a heading tag.
543 + *
544 + * @param string $html The HTML to inspect.
545 + * @return bool True when the HTML contains a heading tag.
546 + */
547 +function simpletoc_is_heading_html( $html ) {
548 + return false !== simpletoc_get_heading_depth( $html );
549 +}
550 +
551 +/**
552 + * Gets the first heading depth from an HTML fragment.
553 + *
554 + * @param string $html The HTML to inspect.
555 + * @return int|false The heading depth, or false when no heading was found.
556 + */
557 +function simpletoc_get_heading_depth( $html ) {
558 + $processor = simpletoc_get_html_tag_processor( $html );
559 +
560 + if ( $processor ) {
561 +
562 + while ( $processor->next_tag() ) {
563 + $tag_name = $processor->get_tag();
564 +
565 + if ( in_array( $tag_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
566 + return (int) substr( $tag_name, 1 );
567 + }
568 + }
569 + }
570 +
571 + return false;
572 +}
573 +
574 +/**
575 + * Adds a data-page attribute to the first heading in an HTML fragment.
576 + *
577 + * @param string $html The heading HTML.
578 + * @param int $page_number The page number to set.
579 + * @return string The updated HTML.
580 + */
581 +function simpletoc_add_page_number_to_headline( $html, $page_number ) {
582 + $processor = simpletoc_get_html_tag_processor( $html );
583 +
584 + if ( $processor ) {
585 +
586 + while ( $processor->next_tag() ) {
587 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
588 + continue;
589 + }
590 +
591 + $processor->set_attribute( 'data-page', (string) $page_number );
592 + return $processor->get_updated_html();
593 + }
594 + }
595 +
596 + return $html;
597 +}
598 +
599 +/**
600 + * Checks whether the first heading in an HTML fragment has the provided class.
601 + *
602 + * @param string $html The heading HTML.
603 + * @param string $class_name The class name to find.
604 + * @return bool True when the class exists.
605 + */
606 +function simpletoc_heading_has_class( $html, $class_name ) {
607 + $processor = simpletoc_get_html_tag_processor( $html );
608 +
609 + if ( $processor ) {
610 +
611 + while ( $processor->next_tag() ) {
612 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
613 + continue;
614 + }
615 +
616 + $class_attribute = $processor->get_attribute( 'class' );
617 +
618 + if ( ! is_string( $class_attribute ) ) {
619 + return false;
620 + }
621 +
622 + return in_array( $class_name, preg_split( '/\s+/', trim( $class_attribute ) ), true );
623 + }
624 + }
625 +
626 + return false;
627 +}
628 +
629 +/**
515 630 * Adds an ID attribute to all Heading tags in the provided HTML.
516 631 *
517 632 * @param string $html The HTML content to modify.
518 633 * @param SimpleTOC_Headline_Ids $headline_class_instance The instance of the SimpleTOC_Headline_Ids class.
@@ -519,8 +634,11 @@
519 634 * @param array $block The parsed block data.
520 635 * @return string The modified HTML content with ID attributes added to the Heading tags
521 636 */
522 637 function add_anchor_attribute( $html, $headline_class_instance = null, $block = array() ) {
638 + if ( ! is_string( $html ) ) {
639 + return $html;
640 + }
523 641
524 642 // remove non-breaking space entites from input HTML.
525 643 $html_wo_nbs = str_replace( '&nbsp;', ' ', $html );
526 644
@@ -528,19 +646,12 @@
528 646 if ( ! $html_wo_nbs ) {
529 647 return $html;
530 648 }
531 649
532 - if ( ! class_exists( '\WP_HTML_Tag_Processor' ) && defined( 'ABSPATH' ) && defined( 'WPINC' ) ) {
533 - $html_tag_processor_file = ABSPATH . WPINC . '/html-api/class-wp-html-tag-processor.php';
650 + $processor = simpletoc_get_html_tag_processor( $html_wo_nbs );
534 651
535 - if ( file_exists( $html_tag_processor_file ) ) {
536 - require_once $html_tag_processor_file;
537 - }
538 - }
652 + if ( $processor ) {
539 653
540 - if ( class_exists( '\WP_HTML_Tag_Processor' ) ) {
541 - $processor = new \WP_HTML_Tag_Processor( $html_wo_nbs );
542 -
543 654 while ( $processor->next_tag() ) {
544 655 if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
545 656 continue;
546 657 }
@@ -558,37 +669,9 @@
558 669
559 670 return $processor->get_updated_html();
560 671 }
561 672
562 - libxml_use_internal_errors( true );
563 - $dom = new \DOMDocument();
564 - try {
565 - $dom->loadHTML( '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $html_wo_nbs, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
566 - } catch ( \Exception $e ) {
567 - return $html;
568 - }
569 -
570 - // use xpath to select the Heading html tags.
571 - $xpath = new \DOMXPath( $dom );
572 - $tags = $xpath->evaluate( '//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]' );
573 -
574 - // Loop through all the found tags.
575 - foreach ( $tags as $tag ) {
576 - // if tag already has an attribute "id" defined, no need for creating a new one.
577 - if ( ! empty( $tag->getAttribute( 'id' ) ) ) {
578 - continue;
579 - }
580 - // Set id attribute.
581 - $heading_html = simpletoc_get_heading_html_for_anchor( $html, $block );
582 - $heading_text = trim( wp_strip_all_tags( $heading_html ) );
583 - $anchor = $headline_class_instance->get_headline_anchor( $heading_text );
584 - $tag->setAttribute( 'id', $anchor );
585 - }
586 -
587 - // Save the HTML changes.
588 - $content = $dom->saveHTML( $dom->documentElement ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
589 -
590 - return $content;
673 + return $html;
591 674 }
592 675
593 676 /**
594 677 * Generates a table of contents based on the provided headings and attributes
@@ -679,13 +762,9 @@
679 762 * @param int $this_depth The depth level of the headline.
680 763 * @return bool True if the headline should be excluded, false otherwise.
681 764 */
682 765 function should_exclude_headline( $headline, $attributes, $this_depth ) {
683 - $exclude_headline = false;
684 - preg_match( '/class="([^"]+)"/', $headline, $matches );
685 - if ( ! empty( $matches[1] ) && strpos( $matches[1], 'simpletoc-hidden' ) !== false ) {
686 - $exclude_headline = true;
687 - }
766 + $exclude_headline = simpletoc_heading_has_class( $headline, 'simpletoc-hidden' );
688 767
689 768 return ( $this_depth > $attributes['max_level'] || $exclude_headline || $this_depth < $attributes['min_level'] );
690 769 }
691 770
@@ -700,9 +779,14 @@
700 779 $toc_headings = array();
701 780 $headline_ids = new SimpleTOC_Headline_Ids();
702 781
703 782 foreach ( $headings as $headline ) {
704 - $this_depth = (int) $headline[2];
783 + $this_depth = simpletoc_get_heading_depth( $headline );
784 +
785 + if ( false === $this_depth ) {
786 + continue;
787 + }
788 +
705 789 $title = trim( wp_strip_all_tags( $headline ) );
706 790 $custom_id = extract_id( $headline );
707 791 $link = $custom_id ? $custom_id : $headline_ids->get_headline_anchor( $title );
708 792
@@ -752,10 +836,12 @@
752 836 }
753 837 $list .= "</li>\n<li>";
754 838 }
755 839
756 - $page = get_page_number_from_headline( $toc_heading['headline'] );
757 - $list .= '<a href="' . $absolute_url . $page . '#' . $toc_heading['link'] . '">' . $toc_heading['title'] . '</a>' . PHP_EOL;
840 + $page = get_page_number_from_headline( $toc_heading['headline'] );
841 + $url = $absolute_url . $page . '#' . $toc_heading['link'];
842 + $href = $absolute_url ? esc_url( $url ) : esc_attr( $url );
843 + $list .= '<a href="' . $href . '">' . esc_html( $toc_heading['title'] ) . '</a>' . PHP_EOL;
758 844 }
759 845
760 846 if ( null !== $current_depth ) {
761 847 for ( $current_depth; $current_depth > $min_depth; $current_depth-- ) {
@@ -767,85 +853,8 @@
767 853 return $list;
768 854 }
769 855
770 856 /**
771 - * The open_list function appends a new list item to the global $list variable, adding necessary opening tags if needed to maintain the correct nesting of the list.
772 - *
773 - * @param string &$list_to_append_to The global list variable to append the new list item to.
774 - * @param string $list_type The type of list to be created, either "ul" (unordered list) or "ol" (ordered list).
775 - * @param int &$min_depth The minimum depth of headings that should be included in the table of contents.
776 - * @param int $this_depth The depth of the current heading being processed.
777 - * @return void The function modifies the input $list_to_append_to variable directly.
778 - */
779 -function open_list( &$list_to_append_to, $list_type, &$min_depth, $this_depth ) {
780 - if ( $this_depth === $min_depth ) {
781 - $list_to_append_to .= '<li>';
782 - } else {
783 - for ( $min_depth; $min_depth < $this_depth; $min_depth++ ) {
784 - $list_to_append_to .= "\n<" . $list_type . "><li>\n";
785 - }
786 - }
787 -}
788 -
789 -/**
790 - * Closes an HTML list tag and updates the list string and minimum depth variable as necessary.
791 - *
792 - * @param string $list_to_append_to A reference to the list string being built.
793 - * @param string $list_type The type of list tag being used (ul or ol).
794 - * @param int $min_depth A reference to the minimum depth variable.
795 - * @param int $min_level The minimum depth level of the headings.
796 - * @param int $max_level Maximum depth setting, which is a high number like 6.
797 - * @param int|null $next_depth The depth of the next list item, or null if this is the last item.
798 - * @param int $line The index of the current list item.
799 - * @param int $last_line The index of the last list item.
800 - * @param int $initial_depth The initial depth of the list.
801 - * @param int $this_depth The depth of the current list item.
802 - * @return void
803 - */
804 -function close_list( &$list_to_append_to, $list_type, &$min_depth, $min_level, $max_level, $next_depth, $line, $last_line, $initial_depth, $this_depth ) {
805 - if ( $line !== $last_line ) {
806 - $list_to_append_to .= PHP_EOL;
807 - if ( $next_depth < $this_depth ) {
808 - // Next heading goes back shallower in the ToC!
809 - if ( $next_depth >= $min_level ) {
810 - // Next heading is within min depth bounds and WILL get ToC'd
811 - // Close this item and step back shallower in the ToC.
812 - for ( $min_depth; $min_depth > $next_depth; $min_depth-- ) {
813 - $list_to_append_to .= "</li>\n</" . $list_type . ">\n";
814 - }
815 - } else { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElse
816 - // SKIP CLOSING! Next heading won't be included in the ToC at all.
817 - }
818 - } elseif ( $next_depth === $this_depth ) {
819 - // Next heading is exactly as deep. Not going shallower or deeper in the ToC hierarchy.
820 - // E.g. this is h3, next is h3.
821 - if ( $next_depth < $min_level ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
822 - // E.g. this is h3, next is h3, min is h2
823 - // This heading didn't open a ToC item. Nothing to close.
824 - } else {
825 - // SKIP CLOSING! Next heading will open a new sub-list in the ToC.
826 - $list_to_append_to .= "</li>\n";
827 - }
828 - } else { // phpcs:ignore.
829 - // Next heading is deeper in the ToC.
830 - if ( $next_depth <= $max_level ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
831 - // Next deeper heading is within bounds and will open a new sub-list. Leave this one open.
832 - // E.g. this is h3, next is h4, min is h2, max is h5.
833 - } else { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElse
834 - // Next heading is too deep and will be ignored. We'll close out coming up or finishing the ToC.
835 - // E.g. this is h3, next is h4, max is h3.
836 - }
837 - }
838 - } else {
839 - // This is the last line of the ToC. Close out the whole thing.
840 - // IMPORTANT NOTE: The overall ToC list will be wrapped in a list element and closed out.
841 - for ( $initial_depth; $initial_depth < $this_depth; $initial_depth++ ) {
842 - $list_to_append_to .= "</li>\n</" . $list_type . ">\n";
843 - }
844 - }
845 -}
846 -
847 -/**
848 857 * Adds smooth scrolling styles to the output HTML, if enabled by global option or block attribute.
849 858 *
850 859 * @param string $html The HTML string to which the styles will be added.
851 860 * @param array $attributes An array of block attributes.
@@ -866,9 +875,9 @@
866 875 wp_enqueue_script(
867 876 'simpletoc-accordion',
868 877 plugin_dir_url( __FILE__ ) . 'assets/accordion.js',
869 878 array(),
870 - '6.9.0',
879 + SIMPLETOC_VERSION,
871 880 true
872 881 );
873 882
874 883 wp_enqueue_style(
@@ -874,9 +883,9 @@
874 883 wp_enqueue_style(
875 884 'simpletoc-accordion',
876 885 plugin_dir_url( __FILE__ ) . 'assets/accordion.css',
877 886 array(),
878 - '6.9.0'
887 + SIMPLETOC_VERSION
879 888 );
880 889 }
881 890
882 891 /**
@@ -892,9 +901,9 @@
892 901
893 902 if ( $is_hidden_enabled ) {
894 903 $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : esc_html__( 'Table of Contents', 'simpletoc' );
895 904 $hidden_start = '<details class="simpletoc">
896 - <summary style="cursor: pointer;">' . $title_text . '</summary>';
905 + <summary>' . $title_text . '</summary>';
897 906 $html .= $hidden_start;
898 907 }
899 908
900 909 // If there are no items in the table of contents, return an empty string.
@@ -939,9 +948,9 @@
939 948 // Start and end HTML for accordion, if enabled.
940 949 $accordion_start = '';
941 950 if ( $is_accordion_enabled ) {
942 951 enqueue_accordion_frontend();
943 - $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">';
952 + $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">';
944 953 }
945 954
946 955 // Add the accordion start HTML to the output.
947 956 $html .= $accordion_start;
@@ -989,17 +998,26 @@
989 998 /**
990 999 * Extracts the ID value from the provided heading HTML string.
991 1000 *
992 1001 * @param string $headline The heading HTML string to extract the ID value from.
993 - * @return mixed Returns the extracted ID value, or false if no ID value is found.
1002 + * @return string|false Returns the extracted ID value, or false if no ID value is found.
994 1003 */
995 1004 function extract_id( $headline ) {
996 - $pattern = '/id="([^"]*)"/';
997 - preg_match( $pattern, $headline, $matches );
998 - $id_value = $matches[1] ?? false;
1005 + $processor = simpletoc_get_html_tag_processor( $headline );
999 1006
1000 - if ( false !== $id_value ) {
1001 - return $id_value;
1007 + if ( $processor ) {
1008 +
1009 + while ( $processor->next_tag() ) {
1010 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
1011 + continue;
1012 + }
1013 +
1014 + $id_value = $processor->get_attribute( 'id' );
1015 +
1016 + if ( is_string( $id_value ) ) {
1017 + return $id_value;
1018 + }
1019 + }
1002 1020 }
1003 1021
1004 1022 return false;
1005 1023 }
@@ -1010,22 +1028,25 @@
1010 1028 * @param string $headline The headline string.
1011 1029 * @return string The page number (in the format "X/") if it exists and is greater than 1, or an empty string otherwise.
1012 1030 */
1013 1031 function get_page_number_from_headline( $headline ) {
1014 - $dom = new \DOMDocument();
1032 + $processor = simpletoc_get_html_tag_processor( $headline );
1015 1033
1016 - try {
1017 - $dom->loadHTML( '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $headline, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
1018 - } catch ( \Exception $e ) {
1019 - return '';
1020 - }
1034 + if ( $processor ) {
1021 1035
1022 - $xpath = new \DOMXPath( $dom );
1023 - $nodes = $xpath->query( '//*/@data-page' );
1036 + while ( $processor->next_tag() ) {
1037 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
1038 + continue;
1039 + }
1024 1040
1025 - if ( isset( $nodes[0] ) && $nodes[0]->nodeValue > 1 ) {
1026 - $page_number = $nodes[0]->nodeValue . '/';
1027 - return esc_html( $page_number );
1028 - } else {
1041 + $page_number = (int) $processor->get_attribute( 'data-page' );
1042 +
1043 + if ( $page_number > 1 ) {
1044 + return esc_html( $page_number . '/' );
1045 + }
1046 + }
1047 +
1029 1048 return '';
1030 1049 }
1050 +
1051 + return '';
1031 1052 }