From ed05605e9b5834b85108e660533ec23fd6eb2b67 Mon Sep 17 00:00:00 2001 From: Ronald Huereca Date: Thu, 18 Dec 2025 16:21:18 -0600 Subject: [PATCH 01/11] Adding direct file limits. --- simpletoc-admin-settings.php | 5 +++++ simpletoc-class-headline-ids.php | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/simpletoc-admin-settings.php b/simpletoc-admin-settings.php index 692b61e..a59fb8a 100644 --- a/simpletoc-admin-settings.php +++ b/simpletoc-admin-settings.php @@ -7,6 +7,11 @@ namespace MToensing\SimpleTOC; +if ( ! defined( 'ABSPATH' ) ) { + header( 'Status: 403 Forbidden' ); + header( 'HTTP/1.1 403 Forbidden' ); + exit; +} /** * Add SimpleTOC global settings page. */ diff --git a/simpletoc-class-headline-ids.php b/simpletoc-class-headline-ids.php index 85b95e9..e4b9de5 100644 --- a/simpletoc-class-headline-ids.php +++ b/simpletoc-class-headline-ids.php @@ -9,6 +9,12 @@ namespace MToensing\SimpleTOC; +if ( ! defined( 'ABSPATH' ) ) { + header( 'Status: 403 Forbidden' ); + header( 'HTTP/1.1 403 Forbidden' ); + exit; +} + /** * Class to manage headline IDs. * From 9079fe180f7b44d90ca569096b7a753d51b5548a Mon Sep 17 00:00:00 2001 From: Ronald Huereca Date: Thu, 18 Dec 2025 16:46:10 -0600 Subject: [PATCH 02/11] Fixing recursion for Table of Content IDs. --- plugin.php | 9 ++-- simpletoc-class-headline-ids-wrapper.php | 57 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 simpletoc-class-headline-ids-wrapper.php diff --git a/plugin.php b/plugin.php index 3d364ce..4d889c3 100644 --- a/plugin.php +++ b/plugin.php @@ -17,6 +17,7 @@ require_once __DIR__ . '/simpletoc-admin-settings.php'; require_once __DIR__ . '/simpletoc-class-headline-ids.php'; +require_once __DIR__ . '/simpletoc-class-headline-ids-wrapper.php'; /** * Prevents direct execution of the plugin file. @@ -166,6 +167,8 @@ function simpletoc_add_ids_to_content( $content ) { add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 1 ); + + /** * Recursively adds IDs to the headings of a nested block structure. * @@ -187,9 +190,9 @@ function add_ids_to_blocks_recursive( $blocks ) { */ $supported_blocks = apply_filters( 'simpletoc_supported_blocks_for_ids', $supported_blocks ); - // Need two separate instances so that IDs aren't double coubnted. - $inner_html_id_instance = new SimpleTOC_Headline_Ids(); - $inner_content_id_instance = new SimpleTOC_Headline_Ids(); + // Need two separate instances so that IDs aren't double counted. + $inner_html_id_instance = SimpleTOC_Headline_Ids_Wrapper::get_inner_html_id_instance(); + $inner_content_id_instance = SimpleTOC_Headline_Ids_Wrapper::get_inner_content_id_instance(); foreach ( $blocks as &$block ) { if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $supported_blocks, true ) && isset( $block['innerHTML'] ) && isset( $block['innerContent'] ) && isset( $block['innerContent'][0] ) ) { diff --git a/simpletoc-class-headline-ids-wrapper.php b/simpletoc-class-headline-ids-wrapper.php new file mode 100644 index 0000000..37e5676 --- /dev/null +++ b/simpletoc-class-headline-ids-wrapper.php @@ -0,0 +1,57 @@ + Date: Tue, 13 Jan 2026 17:41:27 -0600 Subject: [PATCH 03/11] Getting block to render conditionally in the editor late. --- plugin.php | 104 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 33 deletions(-) diff --git a/plugin.php b/plugin.php index eee2692..a61f1e5 100644 --- a/plugin.php +++ b/plugin.php @@ -165,9 +165,74 @@ function simpletoc_add_ids_to_content( $content ) { return $content; } -add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 1 ); +add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 100 ); +add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_render_toc', 101 ); +/** + * Renders the Table of Contents block. + * + * @param string $content The content to render the Table of Contents block for. + * @return string The rendered Table of Contents block. + */ +function simpletoc_render_toc( $content ) { + // Try to get the [simpletoc] block attributes. + $maybe_shortcode_result = preg_match( '/\[simpletoc ([^\]]*)\]/m', $content, $matches ); + + if ( ! $maybe_shortcode_result ) { + return $content; + } + + // Decode HTML entities and convert curly quotes to straight quotes for valid JSON. + $json_string = html_entity_decode( $matches[1], ENT_QUOTES | ENT_HTML5, 'UTF-8' ); + // Convert curly quotes (left and right) back to straight quotes for valid JSON. + $json_string = str_replace( array( "\u{201C}", "\u{201D}", "\u{2018}", "\u{2019}" ), array( '"', '"', "'", "'" ), $json_string ); + + $attributes = json_decode( $json_string, true ); + + if ( null === $attributes ) { + return $content; + } + + $is_backend = defined( 'REST_REQUEST' ) && REST_REQUEST && 'edit' === filter_input( INPUT_GET, 'context' ); + $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : __( 'Table of Contents', 'simpletoc' ); + $alignclass = ! empty( $attributes['align'] ) ? 'align' . $attributes['align'] : ''; + $class_name = ! empty( $attributes['className'] ) ? wp_strip_all_tags( $attributes['className'] ) : ''; + $title_level = $attributes['title_level']; + + $wrapper_enabled = apply_filters( 'simpletoc_wrapper_enabled', false ) || true === (bool) get_option( 'simpletoc_wrapper_enabled', false ) || true === (bool) get_option( 'simpletoc_accordion_enabled', false ); + + $wrapper_attrs = get_block_wrapper_attributes( array( 'class' => 'simpletoc' ) ); + $pre_html = ( ! empty( $class_name ) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper'] ) ? '
' : ''; + $post_html = ( ! empty( $class_name ) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper'] ) ? '
' : ''; + + $post = get_post(); + $blocks = ! is_null( $post ) && ! is_null( $post->post_content ) ? parse_blocks( $post->post_content ) : ''; + + $headings = array_reverse( filter_headings_recursive( $blocks ) ); + $headings = simpletoc_add_pagenumber( $blocks, $headings ); + $headings_clean = array_map( 'trim', $headings ); + $toc_html = generate_toc( $headings_clean, $attributes ); + + if ( empty( $blocks ) ) { + $toc_html .= get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No blocks found.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ) ); + } + + if ( empty( $headings_clean ) ) { + $toc_html .= get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No headings found.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ) ); + } + + if ( empty( $toc_html ) ) { + $toc_html .= get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No headings found.', 'simpletoc' ), __( 'Check minimal and maximum level block settings.', 'simpletoc' ) ); + } + + $toc_html = $pre_html . $toc_html . $post_html; + + // Replace the [simpletoc] block with the rendered Table of Contents block. + $content = str_replace( $matches[0], $toc_html, $content ); + + return $content; +} /** * Recursively adds IDs to the headings of a nested block structure. @@ -218,39 +283,12 @@ function add_ids_to_blocks_recursive( $blocks ) { * @return string The HTML output for the Table of Contents block */ function render_callback_simpletoc( $attributes ) { - $is_backend = defined( 'REST_REQUEST' ) && REST_REQUEST && 'edit' === filter_input( INPUT_GET, 'context' ); - $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : __( 'Table of Contents', 'simpletoc' ); - $alignclass = ! empty( $attributes['align'] ) ? 'align' . $attributes['align'] : ''; - $class_name = ! empty( $attributes['className'] ) ? wp_strip_all_tags( $attributes['className'] ) : ''; - $title_level = $attributes['title_level']; - - $wrapper_enabled = apply_filters( 'simpletoc_wrapper_enabled', false ) || true === (bool) get_option( 'simpletoc_wrapper_enabled', false ) || true === (bool) get_option( 'simpletoc_accordion_enabled', false ); - - $wrapper_attrs = get_block_wrapper_attributes( array( 'class' => 'simpletoc' ) ); - $pre_html = ( ! empty( $class_name ) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper'] ) ? '
' : ''; - $post_html = ( ! empty( $class_name ) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper'] ) ? '
' : ''; - - $post = get_post(); - $blocks = ! is_null( $post ) && ! is_null( $post->post_content ) ? parse_blocks( $post->post_content ) : ''; - - $headings = array_reverse( filter_headings_recursive( $blocks ) ); - $headings = simpletoc_add_pagenumber( $blocks, $headings ); - $headings_clean = array_map( 'trim', $headings ); - $toc_html = generate_toc( $headings_clean, $attributes ); - - if ( empty( $blocks ) ) { - return get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No blocks found.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ) ); - } - - if ( empty( $headings_clean ) ) { - return get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No headings found.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ) ); + $return = sprintf( '[simpletoc %s]', wp_json_encode( $attributes ) ); + if ( defined( 'REST_REQUEST' ) && REST_REQUEST && 'edit' === filter_input( INPUT_GET, 'context' ) ) { + // This ensures simple TOC is rendered in the editor. + $return = apply_filters( 'the_content', $return ); } - - if ( empty( $toc_html ) ) { - 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' ) ); - } - - return $pre_html . $toc_html . $post_html; + return $return; } /** From cef7dc49793092b215cd649341b9c810267f555a Mon Sep 17 00:00:00 2001 From: Ronald Huereca Date: Tue, 13 Jan 2026 19:14:02 -0600 Subject: [PATCH 04/11] Adding late dom parsing for Headlines and shortcode replaceemnt. --- plugin.php | 119 +++++++++++++++++++++++++---------------------------- 1 file changed, 55 insertions(+), 64 deletions(-) diff --git a/plugin.php b/plugin.php index a61f1e5..1bb53e4 100644 --- a/plugin.php +++ b/plugin.php @@ -15,6 +15,8 @@ namespace MToensing\SimpleTOC; +use WP_HTML_Token; + require_once __DIR__ . '/simpletoc-admin-settings.php'; require_once __DIR__ . '/simpletoc-class-headline-ids.php'; require_once __DIR__ . '/simpletoc-class-headline-ids-wrapper.php'; @@ -206,20 +208,18 @@ function simpletoc_render_toc( $content ) { $pre_html = ( ! empty( $class_name ) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper'] ) ? '
' : ''; $post_html = ( ! empty( $class_name ) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper'] ) ? '
' : ''; - $post = get_post(); - $blocks = ! is_null( $post ) && ! is_null( $post->post_content ) ? parse_blocks( $post->post_content ) : ''; - - $headings = array_reverse( filter_headings_recursive( $blocks ) ); - $headings = simpletoc_add_pagenumber( $blocks, $headings ); - $headings_clean = array_map( 'trim', $headings ); - $toc_html = generate_toc( $headings_clean, $attributes ); - - if ( empty( $blocks ) ) { - $toc_html .= get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No blocks found.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ) ); + $post = get_post(); + if ( ! $post ) { + return $content; } - if ( empty( $headings_clean ) ) { - $toc_html .= get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No headings found.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ) ); + $post_content = $post->post_content; + $headings = filter_headings( $post_content ); + // $headings = simpletoc_add_pagenumber( $blocks, $headings ); + $toc_html = generate_toc( $headings, $attributes ); + + if ( empty( $headings ) ) { + $toc_html .= get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No headings found. Please save your post and ensure headings are present.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ) ); } if ( empty( $toc_html ) ) { @@ -286,7 +286,7 @@ function render_callback_simpletoc( $attributes ) { $return = sprintf( '[simpletoc %s]', wp_json_encode( $attributes ) ); if ( defined( 'REST_REQUEST' ) && REST_REQUEST && 'edit' === filter_input( INPUT_GET, 'context' ) ) { // This ensures simple TOC is rendered in the editor. - $return = apply_filters( 'the_content', $return ); + $return = simpletoc_render_toc( $return ); } return $return; } @@ -348,71 +348,62 @@ function simpletoc_add_pagenumber( $blocks, $headings ) { } /** - * Return all headings with a recursive walk through all blocks. - * This includes groups and reusable block with groups within reusable blocks. + * Filter through all headings. Only return headings that have IDs, do not have SimpleTOC excluded class. Optional filter for ignoring headings inside container blocks.. * - * @param array[] $blocks The blocks to filter headings from. - * @return array[] + * @param string $content The content to filter headings from. + * @return array[] - Array of headings with HTML tags. */ -function filter_headings_recursive( $blocks ) { +function filter_headings( $content ) { + $arr = array(); - if ( ! is_array( $blocks ) ) { + if ( empty( $content ) ) { return $arr; } - // allow developers to ignore specific blocks. - $ignored_blocks = apply_filters( 'simpletoc_excluded_blocks', array() ); - - foreach ( $blocks as $inner_block ) { - if ( is_array( $inner_block ) ) { - // if block is ignored, skip. - if ( isset( $inner_block['blockName'] ) && in_array( $inner_block['blockName'], $ignored_blocks, true ) ) { - continue; - } + $dom = new \DOMDocument(); + try { + $dom->loadHTML( '' . "\n" . $content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); + } catch ( \Exception $e ) { + return $arr; + } - if ( isset( $inner_block['attrs']['ref'] ) ) { - // search in reusable blocks. - $post = get_post( $inner_block['attrs']['ref'] ); - if ( $post ) { - $e_arr = parse_blocks( $post->post_content ); - $arr = array_merge( filter_headings_recursive( $e_arr ), $arr ); - } - } else { - // search in groups. - $arr = array_merge( filter_headings_recursive( $inner_block ), $arr ); - } - } else { - if ( isset( $blocks['blockName'] ) && ( 'core/heading' === $blocks['blockName'] ) && 'core/heading' !== $inner_block ) { - // make sure it's a headline. - if ( preg_match( '/(evaluate( '//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]' ); - $supported_third_party_blocks = array( - 'generateblocks/headline', /* GenerateBlocks 1.x */ - 'generateblocks/text', /* GenerateBlocks 2.0 */ - ); + foreach ( $tags as $tag ) { + $tag_id = 'simple-toc'; // $tag->getAttribute( 'id' ); + if ( ! $tag_id ) { + continue; + } - /** - * Filter to add supported third party blocks. - * - * @param array $supported_third_party_blocks The array of supported third party blocks. - * @return array The modified array of supported third party blocks. - */ - $supported_third_party_blocks = apply_filters( - 'simpletoc_supported_third_party_blocks', - $supported_third_party_blocks - ); + // check if the heading has the SimpleTOC excluded class. + $tag_classes = $tag->getAttribute( 'class' ); + if ( $tag_classes ) { + if ( strpos( $tag_classes, 'simpletoc-excluded' ) !== false ) { + continue; + } + } - if ( isset( $blocks['blockName'] ) && in_array( $blocks['blockName'], $supported_third_party_blocks, true ) && 'core/heading' !== $inner_block ) { - // make sure it's a headline. - if ( preg_match( '/(parentNode; + if ( $parent_tag && isset( $parent_tag->tagName ) ) { + if ( in_array( strtolower( $parent_tag->tag_name ), array( 'div', 'section', 'article', 'main', 'header', 'footer' ), true ) ) { + continue; } } } + + $arr[] = $tag->ownerDocument->saveHTML( $tag ); // This gets the full HTML of the heading tag. } return $arr; From d062912937ecbff72e22d19c7992ac9154cbe74e Mon Sep 17 00:00:00 2001 From: Ronald Huereca Date: Tue, 13 Jan 2026 21:55:15 -0600 Subject: [PATCH 05/11] Fixing rendering in the block editor. --- plugin.php | 154 +++++++++++++---------- simpletoc-class-headline-ids-wrapper.php | 57 --------- simpletoc-class-headline-ids.php | 35 +++--- 3 files changed, 109 insertions(+), 137 deletions(-) delete mode 100644 simpletoc-class-headline-ids-wrapper.php diff --git a/plugin.php b/plugin.php index 1bb53e4..8796f27 100644 --- a/plugin.php +++ b/plugin.php @@ -19,7 +19,6 @@ require_once __DIR__ . '/simpletoc-admin-settings.php'; require_once __DIR__ . '/simpletoc-class-headline-ids.php'; -require_once __DIR__ . '/simpletoc-class-headline-ids-wrapper.php'; /** * Prevents direct execution of the plugin file. @@ -158,15 +157,12 @@ function ( $toc_plugins ) { */ function simpletoc_add_ids_to_content( $content ) { - $blocks = parse_blocks( $content ); - - $blocks = add_ids_to_blocks_recursive( $blocks ); - - $content = serialize_blocks( $blocks ); + $content = add_ids_to_blocks( $content ); return $content; } +// Run late, but before toc is rendered as to be able to track and add IDs to the headings. add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 100 ); add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_render_toc', 101 ); @@ -175,24 +171,25 @@ function simpletoc_add_ids_to_content( $content ) { * Renders the Table of Contents block. * * @param string $content The content to render the Table of Contents block for. + * @param bool $return_toc_html Whether to return the TOC HTML only, without the content included. + * @param array $attributes The attributes of the Table of Contents block. * @return string The rendered Table of Contents block. */ -function simpletoc_render_toc( $content ) { - // Try to get the [simpletoc] block attributes. - $maybe_shortcode_result = preg_match( '/\[simpletoc ([^\]]*)\]/m', $content, $matches ); +function simpletoc_render_toc( $content, $return_toc_html = false, $attributes = array() ) { + if ( ! $return_toc_html ) { + $maybe_shortcode_result = preg_match( '/\[simpletoc ([^\]]*)\]/m', $content, $matches ); - if ( ! $maybe_shortcode_result ) { - return $content; - } + // Decode HTML entities and convert curly quotes to straight quotes for valid JSON. + $json_string = html_entity_decode( $matches[1], ENT_QUOTES | ENT_HTML5, 'UTF-8' ); + // Convert curly quotes (left and right) back to straight quotes for valid JSON. + $json_string = str_replace( array( "\u{201C}", "\u{201D}", "\u{2018}", "\u{2019}" ), array( '"', '"', "'", "'" ), $json_string ); - // Decode HTML entities and convert curly quotes to straight quotes for valid JSON. - $json_string = html_entity_decode( $matches[1], ENT_QUOTES | ENT_HTML5, 'UTF-8' ); - // Convert curly quotes (left and right) back to straight quotes for valid JSON. - $json_string = str_replace( array( "\u{201C}", "\u{201D}", "\u{2018}", "\u{2019}" ), array( '"', '"', "'", "'" ), $json_string ); - - $attributes = json_decode( $json_string, true ); + $attributes = json_decode( $json_string, true ); + } else { + $attributes = $attributes; + } - if ( null === $attributes ) { + if ( empty( $attributes ) ) { return $content; } @@ -208,13 +205,7 @@ function simpletoc_render_toc( $content ) { $pre_html = ( ! empty( $class_name ) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper'] ) ? '
' : ''; $post_html = ( ! empty( $class_name ) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper'] ) ? '
' : ''; - $post = get_post(); - if ( ! $post ) { - return $content; - } - - $post_content = $post->post_content; - $headings = filter_headings( $post_content ); + $headings = filter_headings( $content ); // $headings = simpletoc_add_pagenumber( $blocks, $headings ); $toc_html = generate_toc( $headings, $attributes ); @@ -228,6 +219,10 @@ function simpletoc_render_toc( $content ) { $toc_html = $pre_html . $toc_html . $post_html; + if ( $return_toc_html ) { + return $toc_html; + } + // Replace the [simpletoc] block with the rendered Table of Contents block. $content = str_replace( $matches[0], $toc_html, $content ); @@ -237,43 +232,57 @@ function simpletoc_render_toc( $content ) { /** * Recursively adds IDs to the headings of a nested block structure. * - * @param array $blocks The blocks to add IDs to. + * @param string $content The content to add IDs to. * @return array The blocks with IDs added to their headings */ -function add_ids_to_blocks_recursive( $blocks ) { +function add_ids_to_blocks( $content ) { - $supported_blocks = array( - 'core/heading', - 'generateblocks/text', - 'generateblocks/headline', - ); + $dom = new \DOMDocument(); + try { + @$dom->loadHTML( '' . "\n" . $content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); + } catch ( \Exception $e ) { + return $content; + } - /** - * Filter to add supported blocks for IDs. - * - * @param array $supported_blocks The array of supported blocks. - */ - $supported_blocks = apply_filters( 'simpletoc_supported_blocks_for_ids', $supported_blocks ); - - // Need two separate instances so that IDs aren't double counted. - $inner_html_id_instance = SimpleTOC_Headline_Ids_Wrapper::get_inner_html_id_instance(); - $inner_content_id_instance = SimpleTOC_Headline_Ids_Wrapper::get_inner_content_id_instance(); - - foreach ( $blocks as &$block ) { - if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $supported_blocks, true ) && isset( $block['innerHTML'] ) && isset( $block['innerContent'] ) && isset( $block['innerContent'][0] ) ) { - $block['innerHTML'] = add_anchor_attribute( $block['innerHTML'], $inner_html_id_instance ); - $block['innerContent'][0] = add_anchor_attribute( $block['innerContent'][0], $inner_content_id_instance ); - } elseif ( isset( $block['attrs']['ref'] ) ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElseif - // search in reusable blocks (this is not finished because I ran out of ideas.) - // $reusable_block_id = $block['attrs']['ref']; - // $reusable_block_content = parse_blocks(get_post($reusable_block_id)->post_content);. - } elseif ( ! empty( $block['innerBlocks'] ) ) { - // search in groups. - $block['innerBlocks'] = add_ids_to_blocks_recursive( $block['innerBlocks'] ); + // use xpath to select the Heading html tags. + $xpath = new \DOMXPath( $dom ); + $tags = $xpath->evaluate( '//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]' ); + + foreach ( $tags as $tag ) { + // check if the heading has the SimpleTOC excluded class. + $tag_classes = $tag->getAttribute( 'class' ); + if ( $tag_classes ) { + if ( strpos( $tag_classes, 'simpletoc-excluded' ) !== false ) { + continue; + } + } + + /** + * Filter to skip headings inside container blocks. + * + * @param bool $skip_in_wrapper Whether to skip headings inside container blocks. + * @return bool The filtered value. + */ + $skip_in_wrapper = apply_filters( 'simpletoc_skip_in_wrapper', true ); + if ( $skip_in_wrapper ) { + // Try to get parent tag. + $parent_tag = $tag->parentNode; + if ( $parent_tag && isset( $parent_tag->tagName ) ) { + if ( in_array( strtolower( strtolower( $parent_tag->tagName ) ), array( 'div', 'section', 'article', 'main', 'header', 'footer' ), true ) ) { + continue; + } + } + } + + // Set the ID attribute of the headline anchor if it doesn't exist. + $tag_id = $tag->getAttribute( 'id' ); + $headline_anchor = SimpleTOC_Headline_Ids::get_headline_anchor( $tag->ownerDocument->saveHTML( $tag ), true ); + if ( empty( $tag_id ) ) { + $tag->setAttribute( 'id', $headline_anchor ); } } - return $blocks; + return $dom->saveHTML(); } /** @@ -285,8 +294,23 @@ function add_ids_to_blocks_recursive( $blocks ) { function render_callback_simpletoc( $attributes ) { $return = sprintf( '[simpletoc %s]', wp_json_encode( $attributes ) ); if ( defined( 'REST_REQUEST' ) && REST_REQUEST && 'edit' === filter_input( INPUT_GET, 'context' ) ) { - // This ensures simple TOC is rendered in the editor. - $return = simpletoc_render_toc( $return ); + // Strip out the simple toc block from the content. + $post = get_post(); + if ( ! $post ) { + return $return; + } + $post_content = $post->post_content; + + //