PluginProbe
SimpleTOC – Table of Contents Block / 7.4.0
SimpleTOC – Table of Contents Block v7.4.0
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 5.0.31 All 161 releases
← All changes | plugin.php +221 -194 7.0.77.4.0 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.7
6 + * Version: 7.4.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.4.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.
@@ -54,9 +57,10 @@
54 57 wp_add_inline_script(
55 58 'simpletoc-toc-editor-script',
56 59 'window.simpletocEditorSettings = ' . wp_json_encode(
57 60 array(
58 - 'settingsUrl' => admin_url( 'options-general.php?page=simpletoc' ),
61 + 'settingsUrl' => admin_url( 'options-general.php?page=simpletoc' ),
62 + 'scrollSpyEnabled' => simpletoc_scroll_spy_enabled(),
59 63 )
60 64 ) . ';',
61 65 'before'
62 66 );
@@ -74,8 +78,10 @@
74 78 */
75 79 function add_simpletoc_block_editor_settings( $editor_settings, $editor_context ) {
76 80 $editor_settings['simpletocSettingsUrl'] = admin_url( 'options-general.php?page=simpletoc' );
77 81
82 + $editor_settings['simpletocScrollSpyEnabled'] = simpletoc_scroll_spy_enabled();
83 +
78 84 return $editor_settings;
79 85 }
80 86
81 87 add_filter( 'block_editor_settings_all', __NAMESPACE__ . '\add_simpletoc_block_editor_settings', 10, 2 );
@@ -160,11 +166,11 @@
160 166 2
161 167 );
162 168
163 169 /**
164 - * 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.
165 171 *
166 - * @param array TOC plugins.
172 + * @param array $toc_plugins TOC plugins.
167 173 */
168 174 add_filter(
169 175 'rank_math/researches/toc_plugins',
170 176 function ( $toc_plugins ) {
@@ -191,9 +197,9 @@
191 197
192 198 return $content;
193 199 }
194 200
195 -add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 1 );
201 +add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 1 );
196 202
197 203 /**
198 204 * Recursively adds IDs to the headings of a nested block structure.
199 205 *
@@ -214,9 +220,9 @@
214 220 * @param array $supported_blocks The array of supported blocks.
215 221 */
216 222 $supported_blocks = apply_filters( 'simpletoc_supported_blocks_for_ids', $supported_blocks );
217 223
218 - // Need two separate instances so that IDs aren't double coubnted.
224 + // Need two separate instances so that IDs aren't double counted.
219 225 $inner_html_id_instance = new SimpleTOC_Headline_Ids();
220 226 $inner_content_id_instance = new SimpleTOC_Headline_Ids();
221 227
222 228 foreach ( $blocks as &$block ) {
@@ -222,12 +228,8 @@
222 228 foreach ( $blocks as &$block ) {
223 229 if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $supported_blocks, true ) && isset( $block['innerHTML'] ) && isset( $block['innerContent'] ) && isset( $block['innerContent'][0] ) ) {
224 230 $block['innerHTML'] = add_anchor_attribute( $block['innerHTML'], $inner_html_id_instance, $block );
225 231 $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 232 } elseif ( ! empty( $block['innerBlocks'] ) ) {
231 233 // search in groups.
232 234 $block['innerBlocks'] = add_ids_to_blocks_recursive( $block['innerBlocks'] );
233 235 }
@@ -245,21 +247,30 @@
245 247 function render_callback_simpletoc( $attributes ) {
246 248 $is_backend = defined( 'REST_REQUEST' ) && REST_REQUEST && 'edit' === filter_input( INPUT_GET, 'context' );
247 249 $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : __( 'Table of Contents', 'simpletoc' );
248 250 $alignclass = ! empty( $attributes['align'] ) ? 'align' . $attributes['align'] : '';
249 - $class_name = ! empty( $attributes['className'] ) ? wp_strip_all_tags( $attributes['className'] ) : '';
250 251 $title_level = $attributes['title_level'];
251 252 $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'] );
253 + $legacy_box_style_enabled = ! empty( $attributes['box_style'] );
254 + $typography_enabled = ! empty( $attributes['fontSize'] ) || ! empty( $attributes['style']['typography'] );
253 255 $wrapper_classes = array( 'simpletoc' );
254 256 $wrapper_style = '';
255 257
256 - if ( $box_style_enabled ) {
258 + if ( simpletoc_scroll_spy_enabled() || ! empty( $attributes['scroll_spy'] ) ) {
259 + $wrapper_classes[] = 'has-simpletoc-scroll-spy';
260 + }
261 +
262 + if ( $typography_enabled ) {
263 + $wrapper_classes[] = 'has-simpletoc-typography';
264 + }
265 +
266 + if ( $global_box_style_enabled || $legacy_box_style_enabled ) {
267 + $wrapper_classes[] = 'is-style-boxed';
257 268 $wrapper_classes[] = 'has-simpletoc-box-style';
258 269
259 270 if ( $global_box_style_enabled ) {
260 271 $wrapper_classes[] = 'has-background';
261 - $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
272 + $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
262 273 } elseif ( ! empty( $attributes['box_color'] ) ) {
263 274 $wrapper_classes[] = 'has-background';
264 275 $wrapper_style = safecss_filter_attr( 'background-color:' . $attributes['box_color'] . ';' );
265 276 } else {
@@ -267,9 +278,8 @@
267 278 $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
268 279 }
269 280 }
270 281
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 282 $wrapper_attrs = get_block_wrapper_attributes(
273 283 array(
274 284 'class' => implode( ' ', $wrapper_classes ),
275 285 'style' => $wrapper_style,
@@ -274,11 +284,10 @@
274 284 'class' => implode( ' ', $wrapper_classes ),
275 285 'style' => $wrapper_style,
276 286 )
277 287 );
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>' : '';
288 + $pre_html = '<div role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>';
289 + $post_html = '</div>';
281 290
282 291 $post = get_post();
283 292 $blocks = ! is_null( $post ) && ! is_null( $post->post_content ) ? parse_blocks( $post->post_content ) : '';
284 293
@@ -287,17 +296,17 @@
287 296 $headings_clean = array_map( 'trim', $headings );
288 297 $toc_html = generate_toc( $headings_clean, $attributes );
289 298
290 299 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 );
300 + 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 301 }
293 302
294 303 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 );
304 + 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 305 }
297 306
298 307 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 );
308 + 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 309 }
301 310
302 311 return $pre_html . $toc_html . $post_html;
303 312 }
@@ -311,27 +320,20 @@
311 320 * @param string $alignclass The CSS class for alignment of the Table of Contents block.
312 321 * @param string $title_text The text for the Table of Contents title.
313 322 * @param string $warning_text1 The first part of the warning message to be displayed.
314 323 * @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.
324 + * @param string $wrapper_attrs Block wrapper attributes.
317 325 *
318 326 * @return string The HTML output for the empty blocks message.
319 327 */
320 -function get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, $warning_text1, $warning_text2, $wrapper_attrs = '', $has_wrapper = false ) {
328 +function get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, $warning_text1, $warning_text2, $wrapper_attrs = '' ) {
321 329 $html = '';
322 330
323 331 if ( $is_backend ) {
324 - if ( $has_wrapper ) {
325 - $html .= '<div role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>';
326 - }
327 -
332 + $html .= '<div role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>';
328 333 $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 - }
334 + $html .= sprintf( '<p class="components-notice is-warning %s">%s %s</p>', esc_attr( $alignclass ), esc_html( $warning_text1 ), esc_html( $warning_text2 ) );
335 + $html .= '</div>';
334 336 }
335 337
336 338 return $html;
337 339 }
@@ -359,9 +361,9 @@
359 361 if ( isset( $blocks[ $block ]['blockName'] ) && 'core/heading' === $blocks[ $block ]['blockName'] ) {
360 362 // make sure its a headline.
361 363 foreach ( $headings as $heading => &$inner_heading ) {
362 364 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'] );
365 + $inner_heading = simpletoc_add_page_number_to_headline( $blocks[ $block ]['innerHTML'], $pages );
364 366 }
365 367 }
366 368 }
367 369 }
@@ -403,13 +405,10 @@
403 405 // search in groups.
404 406 $arr = array_merge( filter_headings_recursive( $inner_block ), $arr );
405 407 }
406 408 } 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 - }
409 + if ( isset( $blocks['blockName'] ) && ( 'core/heading' === $blocks['blockName'] ) && 'core/heading' !== $inner_block && simpletoc_is_heading_html( $inner_block ) ) {
410 + $arr[] = $inner_block;
412 411 }
413 412
414 413 $supported_third_party_blocks = array(
415 414 'generateblocks/headline', /* GenerateBlocks 1.x */
@@ -426,14 +425,11 @@
426 425 'simpletoc_supported_third_party_blocks',
427 426 $supported_third_party_blocks
428 427 );
429 428
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 - }
429 + if ( isset( $blocks['blockName'] ) && in_array( $blocks['blockName'], $supported_third_party_blocks, true ) && 'core/heading' !== $inner_block && simpletoc_is_heading_html( $inner_block ) ) {
430 + $inner_block = simpletoc_maybe_replace_generateblocks_dynamic_tags( $inner_block, $blocks );
431 + $arr[] = $inner_block;
436 432 }
437 433 }
438 434 }
439 435
@@ -486,12 +482,11 @@
486 482 $html_wo_nbs = str_replace( '&nbsp;', ' ', $zero_punctuation );
487 483 // remove umlauts and accents.
488 484 $string_without_accents = remove_accents( $html_wo_nbs );
489 485 // Sanitizes a title, replacing whitespace and a few other characters with dashes.
490 - $sanitized_string = sanitize_title_with_dashes( $string_without_accents );
491 - // Encode for use in an url.
492 - $urlencoded = rawurlencode( $sanitized_string );
493 - return $urlencoded;
486 + // Already returns a URL-safe, percent-encoded slug for non-ASCII input, so no
487 + // further rawurlencode() is needed (that would double-encode the string).
488 + return sanitize_title_with_dashes( $string_without_accents );
494 489 }
495 490
496 491 /**
497 492 * Add additional plugin meta links to the SimpleTOC plugin page.
@@ -511,8 +506,134 @@
511 506 return $links;
512 507 }
513 508
514 509 /**
510 + * Loads the WordPress HTML Tag Processor when available.
511 + *
512 + * @return bool True when the HTML Tag Processor can be used.
513 + */
514 +function simpletoc_load_html_tag_processor() {
515 + if ( class_exists( '\WP_HTML_Tag_Processor' ) ) {
516 + return true;
517 + }
518 +
519 + if ( defined( 'ABSPATH' ) && defined( 'WPINC' ) ) {
520 + $html_tag_processor_file = ABSPATH . WPINC . '/html-api/class-wp-html-tag-processor.php';
521 +
522 + if ( file_exists( $html_tag_processor_file ) ) {
523 + require_once $html_tag_processor_file;
524 + }
525 + }
526 +
527 + return class_exists( '\WP_HTML_Tag_Processor' );
528 +}
529 +
530 +/**
531 + * Creates an HTML Tag Processor for a valid HTML fragment.
532 + *
533 + * Parsed block content can contain non-string placeholders for nested blocks.
534 + * The WordPress HTML API accepts strings only.
535 + *
536 + * @param mixed $html The HTML fragment to inspect.
537 + * @return \WP_HTML_Tag_Processor|null The processor, or null when unavailable or invalid.
538 + */
539 +function simpletoc_get_html_tag_processor( $html ) {
540 + if ( ! is_string( $html ) || ! simpletoc_load_html_tag_processor() ) {
541 + return null;
542 + }
543 +
544 + return new \WP_HTML_Tag_Processor( $html );
545 +}
546 +
547 +/**
548 + * Returns true when the provided HTML contains a heading tag.
549 + *
550 + * @param string $html The HTML to inspect.
551 + * @return bool True when the HTML contains a heading tag.
552 + */
553 +function simpletoc_is_heading_html( $html ) {
554 + return false !== simpletoc_get_heading_depth( $html );
555 +}
556 +
557 +/**
558 + * Gets the first heading depth from an HTML fragment.
559 + *
560 + * @param string $html The HTML to inspect.
561 + * @return int|false The heading depth, or false when no heading was found.
562 + */
563 +function simpletoc_get_heading_depth( $html ) {
564 + $processor = simpletoc_get_html_tag_processor( $html );
565 +
566 + if ( $processor ) {
567 +
568 + while ( $processor->next_tag() ) {
569 + $tag_name = $processor->get_tag();
570 +
571 + if ( in_array( $tag_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
572 + return (int) substr( $tag_name, 1 );
573 + }
574 + }
575 + }
576 +
577 + return false;
578 +}
579 +
580 +/**
581 + * Adds a data-page attribute to the first heading in an HTML fragment.
582 + *
583 + * @param string $html The heading HTML.
584 + * @param int $page_number The page number to set.
585 + * @return string The updated HTML.
586 + */
587 +function simpletoc_add_page_number_to_headline( $html, $page_number ) {
588 + $processor = simpletoc_get_html_tag_processor( $html );
589 +
590 + if ( $processor ) {
591 +
592 + while ( $processor->next_tag() ) {
593 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
594 + continue;
595 + }
596 +
597 + $processor->set_attribute( 'data-page', (string) $page_number );
598 + return $processor->get_updated_html();
599 + }
600 + }
601 +
602 + return $html;
603 +}
604 +
605 +/**
606 + * Checks whether the first heading in an HTML fragment has the provided class.
607 + *
608 + * @param string $html The heading HTML.
609 + * @param string $class_name The class name to find.
610 + * @return bool True when the class exists.
611 + */
612 +function simpletoc_heading_has_class( $html, $class_name ) {
613 + $processor = simpletoc_get_html_tag_processor( $html );
614 +
615 + if ( $processor ) {
616 +
617 + while ( $processor->next_tag() ) {
618 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
619 + continue;
620 + }
621 +
622 + $class_attribute = $processor->get_attribute( 'class' );
623 +
624 + if ( ! is_string( $class_attribute ) ) {
625 + return false;
626 + }
627 +
628 + return in_array( $class_name, preg_split( '/\s+/', trim( $class_attribute ) ), true );
629 + }
630 + }
631 +
632 + return false;
633 +}
634 +
635 +/**
515 636 * Adds an ID attribute to all Heading tags in the provided HTML.
516 637 *
517 638 * @param string $html The HTML content to modify.
518 639 * @param SimpleTOC_Headline_Ids $headline_class_instance The instance of the SimpleTOC_Headline_Ids class.
@@ -519,8 +640,11 @@
519 640 * @param array $block The parsed block data.
520 641 * @return string The modified HTML content with ID attributes added to the Heading tags
521 642 */
522 643 function add_anchor_attribute( $html, $headline_class_instance = null, $block = array() ) {
644 + if ( ! is_string( $html ) ) {
645 + return $html;
646 + }
523 647
524 648 // remove non-breaking space entites from input HTML.
525 649 $html_wo_nbs = str_replace( '&nbsp;', ' ', $html );
526 650
@@ -528,19 +652,12 @@
528 652 if ( ! $html_wo_nbs ) {
529 653 return $html;
530 654 }
531 655
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';
656 + $processor = simpletoc_get_html_tag_processor( $html_wo_nbs );
534 657
535 - if ( file_exists( $html_tag_processor_file ) ) {
536 - require_once $html_tag_processor_file;
537 - }
538 - }
658 + if ( $processor ) {
539 659
540 - if ( class_exists( '\WP_HTML_Tag_Processor' ) ) {
541 - $processor = new \WP_HTML_Tag_Processor( $html_wo_nbs );
542 -
543 660 while ( $processor->next_tag() ) {
544 661 if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
545 662 continue;
546 663 }
@@ -558,37 +675,9 @@
558 675
559 676 return $processor->get_updated_html();
560 677 }
561 678
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;
679 + return $html;
591 680 }
592 681
593 682 /**
594 683 * Generates a table of contents based on the provided headings and attributes
@@ -679,13 +768,9 @@
679 768 * @param int $this_depth The depth level of the headline.
680 769 * @return bool True if the headline should be excluded, false otherwise.
681 770 */
682 771 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 - }
772 + $exclude_headline = simpletoc_heading_has_class( $headline, 'simpletoc-hidden' );
688 773
689 774 return ( $this_depth > $attributes['max_level'] || $exclude_headline || $this_depth < $attributes['min_level'] );
690 775 }
691 776
@@ -700,9 +785,14 @@
700 785 $toc_headings = array();
701 786 $headline_ids = new SimpleTOC_Headline_Ids();
702 787
703 788 foreach ( $headings as $headline ) {
704 - $this_depth = (int) $headline[2];
789 + $this_depth = simpletoc_get_heading_depth( $headline );
790 +
791 + if ( false === $this_depth ) {
792 + continue;
793 + }
794 +
705 795 $title = trim( wp_strip_all_tags( $headline ) );
706 796 $custom_id = extract_id( $headline );
707 797 $link = $custom_id ? $custom_id : $headline_ids->get_headline_anchor( $title );
708 798
@@ -752,10 +842,12 @@
752 842 }
753 843 $list .= "</li>\n<li>";
754 844 }
755 845
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;
846 + $page = get_page_number_from_headline( $toc_heading['headline'] );
847 + $url = $absolute_url . $page . '#' . $toc_heading['link'];
848 + $href = $absolute_url ? esc_url( $url ) : esc_attr( $url );
849 + $list .= '<a href="' . $href . '">' . esc_html( $toc_heading['title'] ) . '</a>' . PHP_EOL;
758 850 }
759 851
760 852 if ( null !== $current_depth ) {
761 853 for ( $current_depth; $current_depth > $min_depth; $current_depth-- ) {
@@ -767,85 +859,8 @@
767 859 return $list;
768 860 }
769 861
770 862 /**
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 863 * Adds smooth scrolling styles to the output HTML, if enabled by global option or block attribute.
849 864 *
850 865 * @param string $html The HTML string to which the styles will be added.
851 866 * @param array $attributes An array of block attributes.
@@ -866,9 +881,9 @@
866 881 wp_enqueue_script(
867 882 'simpletoc-accordion',
868 883 plugin_dir_url( __FILE__ ) . 'assets/accordion.js',
869 884 array(),
870 - '6.9.0',
885 + SIMPLETOC_VERSION,
871 886 true
872 887 );
873 888
874 889 wp_enqueue_style(
@@ -874,9 +889,9 @@
874 889 wp_enqueue_style(
875 890 'simpletoc-accordion',
876 891 plugin_dir_url( __FILE__ ) . 'assets/accordion.css',
877 892 array(),
878 - '6.9.0'
893 + SIMPLETOC_VERSION
879 894 );
880 895 }
881 896
882 897 /**
@@ -892,9 +907,9 @@
892 907
893 908 if ( $is_hidden_enabled ) {
894 909 $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : esc_html__( 'Table of Contents', 'simpletoc' );
895 910 $hidden_start = '<details class="simpletoc">
896 - <summary style="cursor: pointer;">' . $title_text . '</summary>';
911 + <summary>' . $title_text . '</summary>';
897 912 $html .= $hidden_start;
898 913 }
899 914
900 915 // If there are no items in the table of contents, return an empty string.
@@ -939,9 +954,9 @@
939 954 // Start and end HTML for accordion, if enabled.
940 955 $accordion_start = '';
941 956 if ( $is_accordion_enabled ) {
942 957 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">';
958 + $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 959 }
945 960
946 961 // Add the accordion start HTML to the output.
947 962 $html .= $accordion_start;
@@ -989,17 +1004,26 @@
989 1004 /**
990 1005 * Extracts the ID value from the provided heading HTML string.
991 1006 *
992 1007 * @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.
1008 + * @return string|false Returns the extracted ID value, or false if no ID value is found.
994 1009 */
995 1010 function extract_id( $headline ) {
996 - $pattern = '/id="([^"]*)"/';
997 - preg_match( $pattern, $headline, $matches );
998 - $id_value = $matches[1] ?? false;
1011 + $processor = simpletoc_get_html_tag_processor( $headline );
999 1012
1000 - if ( false !== $id_value ) {
1001 - return $id_value;
1013 + if ( $processor ) {
1014 +
1015 + while ( $processor->next_tag() ) {
1016 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
1017 + continue;
1018 + }
1019 +
1020 + $id_value = $processor->get_attribute( 'id' );
1021 +
1022 + if ( is_string( $id_value ) ) {
1023 + return $id_value;
1024 + }
1025 + }
1002 1026 }
1003 1027
1004 1028 return false;
1005 1029 }
@@ -1010,22 +1034,25 @@
1010 1034 * @param string $headline The headline string.
1011 1035 * @return string The page number (in the format "X/") if it exists and is greater than 1, or an empty string otherwise.
1012 1036 */
1013 1037 function get_page_number_from_headline( $headline ) {
1014 - $dom = new \DOMDocument();
1038 + $processor = simpletoc_get_html_tag_processor( $headline );
1015 1039
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 - }
1040 + if ( $processor ) {
1021 1041
1022 - $xpath = new \DOMXPath( $dom );
1023 - $nodes = $xpath->query( '//*/@data-page' );
1042 + while ( $processor->next_tag() ) {
1043 + if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
1044 + continue;
1045 + }
1024 1046
1025 - if ( isset( $nodes[0] ) && $nodes[0]->nodeValue > 1 ) {
1026 - $page_number = $nodes[0]->nodeValue . '/';
1027 - return esc_html( $page_number );
1028 - } else {
1047 + $page_number = (int) $processor->get_attribute( 'data-page' );
1048 +
1049 + if ( $page_number > 1 ) {
1050 + return esc_html( $page_number . '/' );
1051 + }
1052 + }
1053 +
1029 1054 return '';
1030 1055 }
1056 +
1057 + return '';
1031 1058 }