PluginProbe
SimpleTOC – Table of Contents Block / 5.0.29
SimpleTOC – Table of Contents Block v5.0.29
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 +353 -810 7.0.65.0.29 View file →
@@ -1,87 +1,39 @@
1 1 <?php
2 +
2 3 /**
3 4 * Plugin Name: SimpleTOC - Table of Contents Block
4 5 * Plugin URI: https://marc.tv/simpletoc-wordpress-inhaltsverzeichnis-plugin-gutenberg/
5 - * Description: SEO-friendly Table of Contents Gutenberg block. No JavaScript or CSS by default.
6 - * Version: 7.0.6
6 + * Description: SEO-friendly Table of Contents Gutenberg block. No JavaScript and no CSS means faster loading.
7 + * Version: 5.0.29
7 8 * Author: Marc Tönsing
8 - * Author URI: https://toensing.com
9 + * Author URI: https://marc.tv
9 10 * Text Domain: simpletoc
10 11 * License: GPL v2 or later
11 12 * License URI: http://www.gnu.org/licenses/gpl-2.0.html
12 13 *
13 - * @package simpletoc
14 14 */
15 15
16 -namespace MToensing\SimpleTOC;
17 -
18 -require_once __DIR__ . '/simpletoc-admin-settings.php';
19 -require_once __DIR__ . '/simpletoc-class-headline-ids.php';
20 -
21 -const DEFAULT_BOX_COLOR = '#ebebeb';
22 -
23 16 /**
24 - * Prevents direct execution of the plugin file.
25 - * If a WordPress function does not exist, it means that the file has not been run by WordPress.
26 - */
27 -if ( ! defined( 'ABSPATH' ) || ! function_exists( 'add_filter' ) ) {
28 - header( 'Status: 403 Forbidden' );
29 - header( 'HTTP/1.1 403 Forbidden' );
30 - exit;
31 -}
17 + * Initalise frontend and backend and register block
18 + **/
32 19
33 -/**
34 - * Registers the SimpleTOC block, adds a filter for plugin row meta, and sets script translations.
35 - *
36 - * This function registers the SimpleTOC block by specifying the build directory and render callback function.
37 - * It also sets the script translations for the block editor script and adds a filter for the plugin row meta.
38 - */
39 -function register_simpletoc_block() {
20 +function register_simpletoc_block()
21 +{
40 22
41 - if ( function_exists( 'wp_set_script_translations' ) ) {
42 - wp_set_script_translations( 'simpletoc-toc-editor-script', 'simpletoc' );
43 - }
23 + wp_set_script_translations( 'simpletoc-toc-editor-script', 'simpletoc' );
44 24
45 - add_filter( 'plugin_row_meta', __NAMESPACE__ . '\simpletoc_plugin_meta', 10, 2 );
25 + add_filter('plugin_row_meta', __NAMESPACE__ . '\\simpletoc_plugin_meta', 10, 2);
46 26
47 - register_block_type(
48 - __DIR__ . '/build',
49 - array(
50 - 'render_callback' => __NAMESPACE__ . '\render_callback_simpletoc',
51 - )
52 - );
27 + register_block_type( __DIR__ . '/build', [
28 + 'render_callback' => __NAMESPACE__ . '\\render_callback_simpletoc'
29 + ]);
53 30
54 - wp_add_inline_script(
55 - 'simpletoc-toc-editor-script',
56 - 'window.simpletocEditorSettings = ' . wp_json_encode(
57 - array(
58 - 'settingsUrl' => admin_url( 'options-general.php?page=simpletoc' ),
59 - )
60 - ) . ';',
61 - 'before'
62 - );
63 31 }
64 32
65 -add_action( 'init', __NAMESPACE__ . '\register_simpletoc_block' );
33 +add_action( 'init', 'register_simpletoc_block' );
66 34
67 35 /**
68 - * Adds SimpleTOC-specific block editor settings.
69 - *
70 - * @param array $editor_settings Default editor settings.
71 - * @param \WP_Block_Editor_Context $editor_context Editor context.
72 - *
73 - * @return array
74 - */
75 -function add_simpletoc_block_editor_settings( $editor_settings, $editor_context ) {
76 - $editor_settings['simpletocSettingsUrl'] = admin_url( 'options-general.php?page=simpletoc' );
77 -
78 - return $editor_settings;
79 -}
80 -
81 -add_filter( 'block_editor_settings_all', __NAMESPACE__ . '\add_simpletoc_block_editor_settings', 10, 2 );
82 -
83 -/**
84 36 * Inject potentially missing translations into the block-editor i18n
85 37 * collection.
86 38 *
87 39 * This keeps the plugin backwards compatible, in case the user did not
@@ -93,53 +45,49 @@
93 45 * @param string $domain The text domain.
94 46 *
95 47 * @return string|false|null JSON string
96 48 */
97 -add_filter(
98 - 'load_script_translations',
99 - function ( $translations, $file, $handle, $domain ) {
100 - if ( 'simpletoc' === $domain && $translations ) {
101 - // List of translations that we inject into the block-editor JS.
102 - $dynamic_translations = array(
103 - 'Table of Contents' => __( 'Table of Contents', 'simpletoc' ),
104 - );
49 +add_filter( 'load_script_translations', function($translations, $file, $handle, $domain) {
50 + if ( 'simpletoc' === $domain && $translations ) {
51 + // List of translations that we inject into the block-editor JS.
52 + $dynamic_translations = [
53 + 'Table of Contents' => __( 'Table of Contents', 'simpletoc' ),
54 + ];
105 55
106 - $changed = false;
107 - $obj = json_decode( $translations, true );
56 + $changed = false;
57 + $obj = json_decode( $translations, true );
108 58
109 - // Confirm that the translation JSON is valid.
110 - if ( isset( $obj['locale_data'] ) && isset( $obj['locale_data']['messages'] ) ) {
111 - $messages = $obj['locale_data']['messages'];
59 + // Confirm that the translation JSON is valid.
60 + if ( isset( $obj['locale_data'] ) && isset( $obj['locale_data']['messages'] ) ) {
61 + $messages = $obj['locale_data']['messages'];
112 62
113 - // Inject dynamic translations, when needed.
114 - foreach ( $dynamic_translations as $key => $locale ) {
115 - if ( empty( $messages[ $key ] )
116 - || ! is_array( $messages[ $key ] )
117 - || ! array_key_exists( 0, $messages[ $key ] )
118 - || $locale !== $messages[ $key ][0]
119 - ) {
120 - $messages[ $key ] = array( $locale );
121 - $changed = true;
122 - }
123 - }
63 + // Inject dynamic translations, when needed.
64 + foreach ( $dynamic_translations as $key => $locale ) {
65 + if (
66 + empty( $messages[ $key ] )
67 + || ! is_array( $messages[$key] )
68 + || ! array_key_exists( 0, $messages[ $key ] )
69 + || $locale !== $messages[ $key ][0]
70 + ) {
71 + $messages[ $key ] = [ $locale ];
72 + $changed = true;
73 + }
74 + }
124 75
125 - // Only modify the translations string when locales did change.
126 - if ( $changed ) {
127 - $obj['locale_data']['messages'] = $messages;
128 - $translations = wp_json_encode( $obj );
129 - }
130 - }
131 - }
76 + // Only modify the translations string when locales did change.
77 + if ( $changed ) {
78 + $obj['locale_data']['messages'] = $messages;
79 + $translations = wp_json_encode( $obj );
80 + }
81 + }
82 + }
132 83
133 - return $translations;
134 - },
135 - 10,
136 - 4
137 -);
84 + return $translations;
85 +}, 10, 4 );
138 86
139 87 /**
140 88 * Sets the default value of translatable attributes.
141 - *
89 + *
142 90 * Values inside block.json are static strings that are not translated. This
143 91 * filter inserts relevant translations i
144 92 *
145 93 * @param array $settings Array of determined settings for registering a block type.
@@ -146,20 +94,15 @@
146 94 * @param array $metadata Metadata provided for registering a block type.
147 95 *
148 96 * @return array Modified settings array.
149 97 */
150 -add_filter(
151 - 'block_type_metadata_settings',
152 - function ( $settings, $metadata ) {
153 - if ( 'simpletoc/toc' === $metadata['name'] ) {
154 - $settings['attributes']['title_text']['default'] = __( 'Table of Contents', 'simpletoc' );
155 - }
98 +add_filter( 'block_type_metadata_settings', function( $settings, $metadata ) {
99 + if ( 'simpletoc/toc' === $metadata['name'] ) {
100 + $settings['attributes']['title_text']['default'] = __( 'Table of Contents', 'simpletoc' );
101 + }
156 102
157 - return $settings;
158 - },
159 - 10,
160 - 2
161 -);
103 + return $settings;
104 +}, 10, 2);
162 105
163 106 /**
164 107 * Filter to add plugins to the TOC list for Rank Math plugin
165 108 *
@@ -164,805 +107,405 @@
164 107 * Filter to add plugins to the TOC list for Rank Math plugin
165 108 *
166 109 * @param array TOC plugins.
167 110 */
168 -add_filter(
169 - 'rank_math/researches/toc_plugins',
170 - function ( $toc_plugins ) {
171 - $toc_plugins['simpletoc/plugin.php'] = 'SimpleTOC';
172 - return $toc_plugins;
173 - }
174 -);
175 111
112 +add_filter('rank_math/researches/toc_plugins', function ($toc_plugins) {
113 + $toc_plugins['simpletoc/plugin.php'] = 'SimpleTOC';
114 + return $toc_plugins;
115 +});
176 116
117 +add_filter( 'the_content', 'simpletoc_addIDstoContent', 1 );
177 118
178 -/**
179 - * Adds IDs to the headings of the provided post content using a recursive block structure.
180 - *
181 - * @param string $content The content to add IDs to.
182 - * @return string The content with IDs added to its headings
183 - */
184 -function simpletoc_add_ids_to_content( $content ) {
119 +function simpletoc_addIDstoContent( $content ) {
185 120
186 - $blocks = parse_blocks( $content );
121 + $blocks = parse_blocks( $content );
187 122
188 - $blocks = add_ids_to_blocks_recursive( $blocks );
123 + $blocks = addIDstoBlocks_recursive( $blocks );
189 124
190 - $content = serialize_blocks( $blocks );
125 + $content = serialize_blocks( $blocks );
191 126
192 - return $content;
127 + return $content;
193 128 }
194 129
195 -add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 1 );
130 +function addIDstoBlocks_recursive( $blocks ) {
196 131
197 -/**
198 - * Recursively adds IDs to the headings of a nested block structure.
199 - *
200 - * @param array $blocks The blocks to add IDs to.
201 - * @return array The blocks with IDs added to their headings
202 - */
203 -function add_ids_to_blocks_recursive( $blocks ) {
132 + foreach ( $blocks as &$block ) {
133 + if (isset($block['blockName']) && ( $block['blockName'] === 'core/heading' || $block['blockName'] === 'generateblocks/headline') && isset($block['innerHTML']) && isset($block['innerContent']) && isset($block['innerContent'][0]) ){
134 + $block['innerHTML'] = addAnchorAttribute($block['innerHTML']);
135 + $block['innerContent'][0] = addAnchorAttribute($block['innerContent'][0]);
136 + } else if( isset($block['attrs']['ref']) ){
137 + // search in reusable blocks (this is not finished because I ran out of ideas.)
138 + // $reusable_block_id = $block['attrs']['ref'];
139 + // $reusable_block_content = parse_blocks(get_post($reusable_block_id)->post_content);
140 + } else if ( ! empty( $block['innerBlocks'] ) ) {
141 + // search in groups
142 + $block['innerBlocks'] = addIDstoBlocks_recursive( $block['innerBlocks'] );
143 + }
144 + }
204 145
205 - $supported_blocks = array(
206 - 'core/heading',
207 - 'generateblocks/text',
208 - 'generateblocks/headline',
209 - );
210 -
211 - /**
212 - * Filter to add supported blocks for IDs.
213 - *
214 - * @param array $supported_blocks The array of supported blocks.
215 - */
216 - $supported_blocks = apply_filters( 'simpletoc_supported_blocks_for_ids', $supported_blocks );
217 -
218 - // Need two separate instances so that IDs aren't double coubnted.
219 - $inner_html_id_instance = new SimpleTOC_Headline_Ids();
220 - $inner_content_id_instance = new SimpleTOC_Headline_Ids();
221 -
222 - foreach ( $blocks as &$block ) {
223 - if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $supported_blocks, true ) && isset( $block['innerHTML'] ) && isset( $block['innerContent'] ) && isset( $block['innerContent'][0] ) ) {
224 - $block['innerHTML'] = add_anchor_attribute( $block['innerHTML'], $inner_html_id_instance, $block );
225 - $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 - } elseif ( ! empty( $block['innerBlocks'] ) ) {
231 - // search in groups.
232 - $block['innerBlocks'] = add_ids_to_blocks_recursive( $block['innerBlocks'] );
233 - }
234 - }
235 -
236 - return $blocks;
146 + return $blocks;
237 147 }
238 148
239 149 /**
240 - * Renders a Table of Contents block for a post
150 + * Render block output
241 151 *
242 - * @param array $attributes An array of attributes for the Table of Contents block.
243 - * @return string The HTML output for the Table of Contents block
244 152 */
245 -function render_callback_simpletoc( $attributes ) {
246 - $is_backend = defined( 'REST_REQUEST' ) && REST_REQUEST && 'edit' === filter_input( INPUT_GET, 'context' );
247 - $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : __( 'Table of Contents', 'simpletoc' );
248 - $alignclass = ! empty( $attributes['align'] ) ? 'align' . $attributes['align'] : '';
249 - $class_name = ! empty( $attributes['className'] ) ? wp_strip_all_tags( $attributes['className'] ) : '';
250 - $title_level = $attributes['title_level'];
251 - $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 - $wrapper_classes = array( 'simpletoc' );
254 - $wrapper_style = '';
255 153
256 - if ( $box_style_enabled ) {
257 - $wrapper_classes[] = 'has-simpletoc-box-style';
154 +function render_callback_simpletoc( $attributes )
155 +{
258 156
259 - if ( $global_box_style_enabled ) {
260 - $wrapper_classes[] = 'has-background';
261 - $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
262 - } elseif ( ! empty( $attributes['box_color'] ) ) {
263 - $wrapper_classes[] = 'has-background';
264 - $wrapper_style = safecss_filter_attr( 'background-color:' . $attributes['box_color'] . ';' );
265 - } else {
266 - $wrapper_classes[] = 'has-background';
267 - $wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
268 - }
269 - }
157 + $is_backend = defined('REST_REQUEST') && true === REST_REQUEST && 'edit' === filter_input(INPUT_GET, 'context');
270 158
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 - $wrapper_attrs = get_block_wrapper_attributes(
273 - array(
274 - 'class' => implode( ' ', $wrapper_classes ),
275 - 'style' => $wrapper_style,
276 - )
277 - );
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>' : '';
159 + $title_text = esc_html( trim( $attributes['title_text'] ) );
160 + if ( ! $title_text ) {
161 + $title_text = __('Table of Contents', 'simpletoc');
162 + }
281 163
282 - $post = get_post();
283 - $blocks = ! is_null( $post ) && ! is_null( $post->post_content ) ? parse_blocks( $post->post_content ) : '';
164 + $alignclass = '';
165 + if ( isset ($attributes['align']) ) {
166 + $align = $attributes['align'];
167 + $alignclass = 'align' . $align;
168 + }
284 169
285 - $headings = array_reverse( filter_headings_recursive( $blocks ) );
286 - $headings = simpletoc_add_pagenumber( $blocks, $headings );
287 - $headings_clean = array_map( 'trim', $headings );
288 - $toc_html = generate_toc( $headings_clean, $attributes );
170 + $className = '';
171 + if ( isset( $attributes['className'] ) ) {
172 + $className = strip_tags(htmlspecialchars($attributes['className'] ));
173 + }
289 174
290 - 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 );
292 - }
175 + $pre_html = '';
176 + $post_html = '';
177 + if ( $className != '' ) {
178 + $pre_html = '<div class="simpletoc ' . $className . '">';
179 + $post_html = '</div>';
180 + }
293 181
294 - 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 );
296 - }
182 + $post = get_post();
183 + if ( is_null($post) || is_null($post->post_content) ) {
184 + $blocks = '';
185 + } else {
186 + $blocks = parse_blocks($post->post_content);
187 + }
297 188
298 - 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 );
300 - }
189 + if ( empty($blocks) ) {
190 + $html = '';
191 + if( $is_backend == true ) {
192 + if ($attributes['no_title'] == false) {
193 + $html = '<h2 class="simpletoc-title ' . $alignclass . '">' . $title_text . '</h2>';
194 + }
301 195
302 - return $pre_html . $toc_html . $post_html;
303 -}
196 + $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No blocks found.', 'simpletoc') . ' ' . __('Save or update post first.', 'simpletoc') . '</p>';
197 + }
198 + return $html;
199 + }
304 200
305 -/**
306 - * Generates an HTML message for empty blocks cases in the Table of Contents.
307 - *
308 - * @param bool $is_backend Indicates if the request is from the backend (i.e., the WordPress editor).
309 - * @param array $attributes An array of attributes for the Table of Contents block.
310 - * @param int $title_level The heading level for the Table of Contents title.
311 - * @param string $alignclass The CSS class for alignment of the Table of Contents block.
312 - * @param string $title_text The text for the Table of Contents title.
313 - * @param string $warning_text1 The first part of the warning message to be displayed.
314 - * @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 - *
318 - * @return string The HTML output for the empty blocks message.
319 - */
320 -function get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, $warning_text1, $warning_text2, $wrapper_attrs = '', $has_wrapper = false ) {
321 - $html = '';
201 + $headings = array_reverse(filter_headings_recursive($blocks));
322 202
323 - if ( $is_backend ) {
324 - if ( $has_wrapper ) {
325 - $html .= '<div role="navigation" aria-label="' . esc_attr__( 'Table of Contents', 'simpletoc' ) . '" ' . $wrapper_attrs . '>';
326 - }
203 + // enrich headings with pages as a data-attribute
204 + $headings = simpletoc_add_pagenumber($blocks, $headings);
327 205
328 - $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 );
206 + $headings_clean = array_map('trim', $headings);
330 207
331 - if ( $has_wrapper ) {
332 - $html .= '</div>';
333 - }
334 - }
208 + if ( empty( $headings_clean ) ) {
209 + $html = '';
210 + if( $is_backend == true ) {
335 211
336 - return $html;
337 -}
212 + if ($attributes['no_title'] == false) {
213 + $html = '<h2 class="simpletoc-title ' . $alignclass . '">' . $title_text . '</h2>';
214 + }
338 215
339 -/**
340 - * Adds page numbers to headings in the provided blocks array.
341 - *
342 - * @param array $blocks The array of blocks to process.
343 - * @param array $headings The array of headings to add page numbers to.
344 - * @return array The modified headings array with page numbers added.
345 - */
346 -function simpletoc_add_pagenumber( $blocks, $headings ) {
347 - $pages = 1;
216 + $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No headings found.', 'simpletoc') . ' ' . __('Save or update post first.', 'simpletoc') . '</p>';
217 + }
218 + return $html;
219 + }
348 220
349 - if ( ! is_array( $blocks ) ) {
350 - return $headings;
351 - }
221 + $toclist = generateToc($headings_clean, $attributes);
352 222
353 - foreach ( $blocks as $block => $inner_block ) {
354 - // count nextpage blocks.
355 - if ( isset( $blocks[ $block ]['blockName'] ) && 'core/nextpage' === $blocks[ $block ]['blockName'] ) {
356 - ++$pages;
357 - }
223 + if ( empty( $toclist ) ) {
224 + $html = '';
225 + if( $is_backend == true ) {
358 226
359 - if ( isset( $blocks[ $block ]['blockName'] ) && 'core/heading' === $blocks[ $block ]['blockName'] ) {
360 - // make sure its a headline.
361 - foreach ( $headings as $heading => &$inner_heading ) {
362 - 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'] );
364 - }
365 - }
366 - }
367 - }
368 - return $headings;
369 -}
227 + if ($attributes['no_title'] == false) {
228 + $html = '<h2 class="simpletoc-title ' . $alignclass . '">' . $title_text . '</h2>';
229 + }
370 230
371 -/**
372 - * Return all headings with a recursive walk through all blocks.
373 - * This includes groups and reusable block with groups within reusable blocks.
374 - *
375 - * @param array[] $blocks The blocks to filter headings from.
376 - * @return array[]
377 - */
378 -function filter_headings_recursive( $blocks ) {
379 - $arr = array();
231 + $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No headings found.', 'simpletoc') . ' ' . __('Check minimal and maximum level block settings.', 'simpletoc') . '</p>';
232 + }
233 + return $html;
380 234
381 - if ( ! is_array( $blocks ) ) {
382 - return $arr;
383 - }
235 + }
384 236
385 - // allow developers to ignore specific blocks.
386 - $ignored_blocks = apply_filters( 'simpletoc_excluded_blocks', array() );
237 + $output = $pre_html . $toclist . $post_html;
387 238
388 - foreach ( $blocks as $inner_block ) {
389 - if ( is_array( $inner_block ) ) {
390 - // if block is ignored, skip.
391 - if ( isset( $inner_block['blockName'] ) && in_array( $inner_block['blockName'], $ignored_blocks, true ) ) {
392 - continue;
393 - }
394 -
395 - if ( isset( $inner_block['attrs']['ref'] ) ) {
396 - // search in reusable blocks.
397 - $post = get_post( $inner_block['attrs']['ref'] );
398 - if ( $post ) {
399 - $e_arr = parse_blocks( $post->post_content );
400 - $arr = array_merge( filter_headings_recursive( $e_arr ), $arr );
401 - }
402 - } else {
403 - // search in groups.
404 - $arr = array_merge( filter_headings_recursive( $inner_block ), $arr );
405 - }
406 - } 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 - }
412 - }
413 -
414 - $supported_third_party_blocks = array(
415 - 'generateblocks/headline', /* GenerateBlocks 1.x */
416 - 'generateblocks/text', /* GenerateBlocks 2.0 */
417 - );
418 -
419 - /**
420 - * Filter to add supported third party blocks.
421 - *
422 - * @param array $supported_third_party_blocks The array of supported third party blocks.
423 - * @return array The modified array of supported third party blocks.
424 - */
425 - $supported_third_party_blocks = apply_filters(
426 - 'simpletoc_supported_third_party_blocks',
427 - $supported_third_party_blocks
428 - );
429 -
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 - }
436 - }
437 - }
438 - }
439 -
440 - return $arr;
239 + return $output;
441 240 }
442 241
443 -/**
444 - * Replaces GenerateBlocks dynamic tags in heading HTML before SimpleTOC uses it in the TOC.
445 - *
446 - * @param string $html The heading HTML.
447 - * @param array $block The parsed block data.
448 - * @return string The heading HTML with GenerateBlocks dynamic tags resolved when available.
449 - */
450 -function simpletoc_maybe_replace_generateblocks_dynamic_tags( $html, $block ) {
451 - if ( ! class_exists( '\GenerateBlocks_Register_Dynamic_Tag' ) || false === strpos( $html, '{{' ) ) {
452 - return $html;
453 - }
454 242
455 - return \GenerateBlocks_Register_Dynamic_Tag::replace_tags( $html, $block, null );
456 -}
243 +function simpletoc_add_pagenumber( $blocks, $headings ){
244 + $pages = 1;
457 245
458 -/**
459 - * Gets heading HTML used for anchor generation.
460 - *
461 - * @param string $html The original heading HTML.
462 - * @param array $block The parsed block data.
463 - * @return string The heading HTML to use for anchor generation.
464 - */
465 -function simpletoc_get_heading_html_for_anchor( $html, $block ) {
466 - $heading_html = simpletoc_maybe_replace_generateblocks_dynamic_tags( $html, $block );
246 + foreach ($blocks as $block => $innerBlock) {
467 247
468 - if ( '' === trim( wp_strip_all_tags( $heading_html ) ) ) {
469 - return $html;
470 - }
248 + // count nextpage blocks
249 + if (isset($blocks[$block]['blockName']) && $blocks[$block]['blockName'] === 'core/nextpage' ){
250 + $pages++;
251 + }
471 252
472 - return $heading_html;
253 + if (isset($blocks[$block]['blockName']) && $blocks[$block]["blockName"] === 'core/heading') {
254 + // make sure its a headline.
255 + foreach ($headings as $heading => &$innerHeading){
256 + if($innerHeading == $blocks[$block]["innerHTML"]){
257 + $innerHeading = preg_replace("/(<h1|<h2|<h3|<h4|<h5|<h6)/i", '$1 data-page="' . $pages . '"', $blocks[$block]["innerHTML"]);
258 + }
259 + }
260 + }
261 + }
262 + return $headings;
473 263 }
474 264
475 265 /**
476 - * Sanitizes a string to be used as an anchor attribute in HTML by removing punctuation, non-breaking spaces, umlauts, and accents,
477 - * and replacing whitespace and other characters with dashes.
478 - *
479 - * @param string $string_to_sanitize The input string to be sanitized.
480 - * @return string The sanitized string encoded for use in a URL.
266 + * Return all headings with a recursive walk through all blocks.
267 + * This includes groups and reusable block with groups within reusable blocks.
481 268 */
482 -function simpletoc_sanitize_string( $string_to_sanitize ) {
483 - // remove punctuation.
484 - $zero_punctuation = preg_replace( '/\p{P}/u', '', $string_to_sanitize );
485 - // remove non-breaking spaces.
486 - $html_wo_nbs = str_replace( '&nbsp;', ' ', $zero_punctuation );
487 - // remove umlauts and accents.
488 - $string_without_accents = remove_accents( $html_wo_nbs );
489 - // 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;
494 -}
495 269
496 -/**
497 - * Add additional plugin meta links to the SimpleTOC plugin page.
498 - *
499 - * @param array $links An array of plugin meta links.
500 - * @param string $file The plugin file path.
501 - * @return array The modified array of plugin meta links.
502 - */
503 -function simpletoc_plugin_meta( $links, $file ) {
270 +function filter_headings_recursive( $blocks )
271 +{
272 + $arr = array();
504 273
505 - if ( false !== strpos( $file, 'simpletoc' ) ) {
506 - $links = array_merge( $links, array( '<a href="https://wordpress.org/support/plugin/simpletoc">' . esc_html__( 'Support', 'simpletoc' ) . '</a>' ) );
507 - $links = array_merge( $links, array( '<a href="https://marc.tv/out/donate">' . esc_html__( 'Donate', 'simpletoc' ) . '</a>' ) );
508 - $links = array_merge( $links, array( '<a href="https://wordpress.org/support/plugin/simpletoc/reviews/#new-post">' . esc_html__( 'Write a review', 'simpletoc' ) . '&nbsp;⭐️⭐️⭐️⭐️⭐️</a>' ) );
509 - }
274 + foreach ($blocks as $block => $innerBlock) {
510 275
511 - return $links;
512 -}
276 + if (is_array($innerBlock)) {
513 277
514 -/**
515 - * Adds an ID attribute to all Heading tags in the provided HTML.
516 - *
517 - * @param string $html The HTML content to modify.
518 - * @param SimpleTOC_Headline_Ids $headline_class_instance The instance of the SimpleTOC_Headline_Ids class.
519 - * @param array $block The parsed block data.
520 - * @return string The modified HTML content with ID attributes added to the Heading tags
521 - */
522 -function add_anchor_attribute( $html, $headline_class_instance = null, $block = array() ) {
278 + if (isset($innerBlock['attrs']['ref'])) {
279 + // search in reusable blocks
280 + $e_arr = parse_blocks(get_post($innerBlock['attrs']['ref'])->post_content);
281 + $arr = array_merge(filter_headings_recursive($e_arr), $arr);
282 + } else {
283 + // search in groups
284 + $arr = array_merge(filter_headings_recursive($innerBlock), $arr);
285 + }
286 + } else {
523 287
524 - // remove non-breaking space entites from input HTML.
525 - $html_wo_nbs = str_replace( '&nbsp;', ' ', $html );
288 + if (isset($blocks['blockName']) && ( $blocks['blockName'] === 'core/heading' ) && $innerBlock !== 'core/heading') {
289 + // make sure its a headline.
290 + if (preg_match("/(<h1|<h2|<h3|<h4|<h5|<h6)/i", $innerBlock)) {
291 + $arr[] = $innerBlock;
292 + }
293 + }
526 294
527 - // Thank you Nick Diego.
528 - if ( ! $html_wo_nbs ) {
529 - return $html;
530 - }
295 + if (isset($blocks['blockName']) && ( $blocks['blockName'] === 'generateblocks/headline' ) && $innerBlock !== 'core/heading') {
296 + // make sure its a headline.
297 + if (preg_match("/(<h1|<h2|<h3|<h4|<h5|<h6)/i", $innerBlock)) {
298 + $arr[] = $innerBlock;
299 + }
300 + }
301 + }
302 + }
531 303
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';
304 + return $arr;
534 305
535 - if ( file_exists( $html_tag_processor_file ) ) {
536 - require_once $html_tag_processor_file;
537 - }
538 - }
539 -
540 - if ( class_exists( '\WP_HTML_Tag_Processor' ) ) {
541 - $processor = new \WP_HTML_Tag_Processor( $html_wo_nbs );
542 -
543 - while ( $processor->next_tag() ) {
544 - if ( ! in_array( $processor->get_tag(), array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) {
545 - continue;
546 - }
547 -
548 - // If tag already has an attribute "id" defined, no need for creating a new one.
549 - if ( ! empty( $processor->get_attribute( 'id' ) ) ) {
550 - continue;
551 - }
552 -
553 - $heading_html = simpletoc_get_heading_html_for_anchor( $html, $block );
554 - $heading_text = trim( wp_strip_all_tags( $heading_html ) );
555 - $anchor = $headline_class_instance->get_headline_anchor( $heading_text );
556 - $processor->set_attribute( 'id', $anchor );
557 - }
558 -
559 - return $processor->get_updated_html();
560 - }
561 -
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;
591 306 }
592 307
593 308 /**
594 - * Generates a table of contents based on the provided headings and attributes
595 - *
596 - * @param array $headings An array of headings to include in the table of contents.
597 - * @param array $attributes An array of attributes to customize the output.
598 - * @return string The generated table of contents as HTML
309 + * Remove all problematic characters for toc links
599 310 */
600 -function generate_toc( $headings, $attributes ) {
601 - $list = '';
602 - $html = '';
603 - $min_depth = 6;
604 - $initial_depth = 6;
605 - $align_class = isset( $attributes['align'] ) ? 'align' . $attributes['align'] : '';
606 - $styles = $attributes['remove_indent'] ? 'style="padding-left:0;list-style:none;"' : '';
607 - $list_type = $attributes['use_ol'] ? 'ol' : 'ul';
608 - $global_absolut_urls_enabled = get_option( 'simpletoc_absolute_urls_enabled', false );
609 - $absolute_url = $attributes['use_absolute_urls'] || $global_absolut_urls_enabled ? get_permalink() : '';
610 311
611 - list($min_depth, $initial_depth) = find_min_depth( $headings, $attributes );
612 -
613 - $item_count = 0;
614 - $headline_ids = new SimpleTOC_Headline_Ids();
615 - foreach ( $headings as $line => $headline ) {
616 - $this_depth = (int) $headings[ $line ][2];
617 - $next_depth = isset( $headings[ $line + 1 ][2] ) ? (int) $headings[ $line + 1 ][2] : '';
618 - $exclude_headline = should_exclude_headline( $headline, $attributes, $this_depth );
619 - $title = trim( wp_strip_all_tags( $headline ) );
620 - $custom_id = extract_id( $headline );
621 - $link = $custom_id ? $custom_id : $headline_ids->get_headline_anchor( $title );
622 - if ( ! $exclude_headline ) {
623 - ++$item_count;
624 - open_list( $list, $list_type, $min_depth, $this_depth );
625 - $page = get_page_number_from_headline( $headline );
626 - $list .= '<a href="' . $absolute_url . $page . '#' . $link . '">' . $title . '</a>' . PHP_EOL;
627 -
628 - }
629 - close_list( $list, $list_type, $min_depth, $attributes['min_level'], $attributes['max_level'], $next_depth, $line, count( $headings ) - 1, $initial_depth, $this_depth );
630 -
631 - }
632 -
633 - $html = add_accordion_start( $html, $attributes, $item_count, $align_class );
634 - $html = add_hidden_markup_start( $html, $attributes, $item_count, $align_class );
635 - $html = add_smooth( $html, $attributes );
636 -
637 - // Add the table of contents list to the output if the list is not empty.
638 - if ( ! empty( $list ) ) {
639 - $html_class = 'simpletoc-list';
640 - if ( ! empty( $align_class ) ) {
641 - $html_class .= " $align_class";
642 - }
643 -
644 - $html_style = '';
645 - if ( ! empty( $styles ) ) {
646 - $html_style = " $styles";
647 - }
648 -
649 - $html .= "<$list_type class=\"$html_class\"$html_style>\n$list</li></$list_type>";
650 - }
651 -
652 - $html = add_accordion_end( $html, $attributes );
653 - $html = add_hidden_markup_end( $html, $attributes );
654 -
655 - // return an emtpy string if stripped result is empty.
656 - if ( empty( trim( wp_strip_all_tags( $html ) ) ) ) {
657 - $html = '';
658 - }
659 -
660 - return $html;
312 +function simpletoc_sanitize_string( $string )
313 +{
314 + // remove punctuation
315 + $zero_punctuation = preg_replace("/\p{P}/u", "", $string);
316 + // remove non-breaking spaces
317 + $html_wo_nbs = str_replace("&nbsp;", " ", $zero_punctuation);
318 + // remove umlauts and accents
319 + $string_without_accents = remove_accents($html_wo_nbs);
320 + // Sanitizes a title, replacing whitespace and a few other characters with dashes.
321 + $sanitized_string = sanitize_title_with_dashes($string_without_accents);
322 + // Encode for use in an url
323 + $urlencoded = urlencode($sanitized_string);
324 + return $urlencoded;
661 325 }
662 326
663 -/**
664 - * Finds the minimum depth level of headings in the provided array and adjusts it based on the provided attributes
665 - *
666 - * @param array $headings An array of headings to search through.
667 - * @param array $attributes An array of attributes to adjust the minimum depth level.
668 - * @return array An array containing the minimum depth level and the initial depth level.
669 - */
670 -function find_min_depth( $headings, $attributes ) {
671 - $min_depth = 6;
672 - $initial_depth = 6;
327 +function simpletoc_plugin_meta( $links, $file )
328 +{
673 329
674 - foreach ( $headings as $line => $headline ) {
675 - if ( $min_depth > $headings[ $line ][2] ) {
676 - $min_depth = (int) $headings[ $line ][2];
677 - $initial_depth = $min_depth;
678 - }
679 - }
330 + if (false !== strpos($file, 'simpletoc')) {
331 + $links = array_merge($links, array('<a href="https://wordpress.org/support/plugin/simpletoc">' . __('Support', 'simpletoc') . '</a>'));
332 + $links = array_merge($links, array('<a href="https://marc.tv/out/donate">' . __('Donate', 'simpletoc') . '</a>'));
333 + $links = array_merge($links, array('<a href="https://wordpress.org/support/plugin/simpletoc/reviews/#new-post">' . __('Write a review', 'simpletoc') . '&nbsp;⭐️⭐️⭐️⭐️⭐️</a>'));
334 + }
680 335
681 - if ( $attributes['min_level'] > $min_depth ) {
682 - $min_depth = $attributes['min_level'];
683 - $initial_depth = $min_depth;
684 - }
685 -
686 - return array( $min_depth, $initial_depth );
336 + return $links;
687 337 }
688 338
689 -/**
690 - * Determines if a given headline should be excluded based on the provided attributes.
691 - *
692 - * @param string $headline The headline to check for exclusion.
693 - * @param array $attributes An array of attributes to use for exclusion.
694 - * @param int $this_depth The depth level of the headline.
695 - * @return bool True if the headline should be excluded, false otherwise.
696 - */
697 -function should_exclude_headline( $headline, $attributes, $this_depth ) {
698 - $exclude_headline = false;
699 - preg_match( '/class="([^"]+)"/', $headline, $matches );
700 - if ( ! empty( $matches[1] ) && strpos( $matches[1], 'simpletoc-hidden' ) !== false ) {
701 - $exclude_headline = true;
702 - }
339 +function addAnchorAttribute( $html )
340 +{
703 341
704 - return ( $this_depth > $attributes['max_level'] || $exclude_headline || $this_depth < $attributes['min_level'] );
705 -}
342 + // remove non-breaking space entites from input HTML
343 + $html_wo_nbs = str_replace("&nbsp;", " ", $html);
706 344
707 -/**
708 - * 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.
709 - *
710 - * @param string &$list_to_append_to The global list variable to append the new list item to.
711 - * @param string $list_type The type of list to be created, either "ul" (unordered list) or "ol" (ordered list).
712 - * @param int &$min_depth The minimum depth of headings that should be included in the table of contents.
713 - * @param int $this_depth The depth of the current heading being processed.
714 - * @return void The function modifies the input $list_to_append_to variable directly.
715 - */
716 -function open_list( &$list_to_append_to, $list_type, &$min_depth, $this_depth ) {
717 - if ( $this_depth === $min_depth ) {
718 - $list_to_append_to .= '<li>';
719 - } else {
720 - for ( $min_depth; $min_depth < $this_depth; $min_depth++ ) {
721 - $list_to_append_to .= "\n<" . $list_type . "><li>\n";
722 - }
723 - }
724 -}
345 + // Thank you Nick Diego
346 + if (!$html_wo_nbs) {
347 + return $html;
348 + }
725 349
726 -/**
727 - * Closes an HTML list tag and updates the list string and minimum depth variable as necessary.
728 - *
729 - * @param string $list_to_append_to A reference to the list string being built.
730 - * @param string $list_type The type of list tag being used (ul or ol).
731 - * @param int $min_depth A reference to the minimum depth variable.
732 - * @param int $min_level The minimum depth level of the headings.
733 - * @param int $max_level Maximum depth setting, which is a high number like 6.
734 - * @param int|null $next_depth The depth of the next list item, or null if this is the last item.
735 - * @param int $line The index of the current list item.
736 - * @param int $last_line The index of the last list item.
737 - * @param int $initial_depth The initial depth of the list.
738 - * @param int $this_depth The depth of the current list item.
739 - * @return void
740 - */
741 -function close_list( &$list_to_append_to, $list_type, &$min_depth, $min_level, $max_level, $next_depth, $line, $last_line, $initial_depth, $this_depth ) {
742 - if ( $line !== $last_line ) {
743 - $list_to_append_to .= PHP_EOL;
744 - if ( $next_depth < $this_depth ) {
745 - // Next heading goes back shallower in the ToC!
746 - if ( $next_depth >= $min_level ) {
747 - // Next heading is within min depth bounds and WILL get ToC'd
748 - // Close this item and step back shallower in the ToC.
749 - for ( $min_depth; $min_depth > $next_depth; $min_depth-- ) {
750 - $list_to_append_to .= "</li>\n</" . $list_type . ">\n";
751 - }
752 - } else { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElse
753 - // SKIP CLOSING! Next heading won't be included in the ToC at all.
754 - }
755 - } elseif ( $next_depth === $this_depth ) {
756 - // Next heading is exactly as deep. Not going shallower or deeper in the ToC hierarchy.
757 - // E.g. this is h3, next is h3.
758 - if ( $next_depth < $min_level ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
759 - // E.g. this is h3, next is h3, min is h2
760 - // This heading didn't open a ToC item. Nothing to close.
761 - } else {
762 - // SKIP CLOSING! Next heading will open a new sub-list in the ToC.
763 - $list_to_append_to .= "</li>\n";
764 - }
765 - } else { // phpcs:ignore.
766 - // Next heading is deeper in the ToC.
767 - if ( $next_depth <= $max_level ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
768 - // Next deeper heading is within bounds and will open a new sub-list. Leave this one open.
769 - // E.g. this is h3, next is h4, min is h2, max is h5.
770 - } else { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElse
771 - // Next heading is too deep and will be ignored. We'll close out coming up or finishing the ToC.
772 - // E.g. this is h3, next is h4, max is h3.
773 - }
774 - }
775 - } else {
776 - // This is the last line of the ToC. Close out the whole thing.
777 - // IMPORTANT NOTE: The overall ToC list will be wrapped in a list element and closed out.
778 - for ( $initial_depth; $initial_depth < $this_depth; $initial_depth++ ) {
779 - $list_to_append_to .= "</li>\n</" . $list_type . ">\n";
780 - }
781 - }
782 -}
350 + libxml_use_internal_errors(TRUE);
351 + $dom = new \DOMDocument();
352 + @$dom->loadHTML($html_wo_nbs, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
783 353
784 -/**
785 - * Adds smooth scrolling styles to the output HTML, if enabled by global option or block attribute.
786 - *
787 - * @param string $html The HTML string to which the styles will be added.
788 - * @param array $attributes An array of block attributes.
789 - * @return string The modified HTML string with the added smooth scrolling styles.
790 - */
791 -function add_smooth( $html, $attributes ) {
792 - // Add smooth scrolling styles, if enabled by global option or block attribute.
793 - $is_smooth_enabled = $attributes['add_smooth'] || true === (bool) get_option( 'simpletoc_smooth_enabled', false );
794 - $html .= $is_smooth_enabled ? '<style>html { scroll-behavior: smooth; }</style>' : '';
354 + // use xpath to select the Heading html tags.
355 + $xpath = new \DOMXPath($dom);
356 + $tags = $xpath->evaluate("//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]");
795 357
796 - return $html;
797 -}
358 + // Loop through all the found tags
359 + foreach ($tags as $tag) {
360 + // if tag already has an attribute "id" defined, no need for creating a new one
361 + if (!empty($tag->getAttribute('id'))) {continue;}
362 + // Set id attribute
363 + $heading_text = strip_tags($html);
364 + $anchor = simpletoc_sanitize_string($heading_text);
365 + $tag->setAttribute("id", $anchor);
366 + }
798 367
799 -/**
800 - * Enqueues the necessary CSS and JS files for the accordion functionality on the frontend.
801 - */
802 -function enqueue_accordion_frontend() {
803 - wp_enqueue_script(
804 - 'simpletoc-accordion',
805 - plugin_dir_url( __FILE__ ) . 'assets/accordion.js',
806 - array(),
807 - '6.9.0',
808 - true
809 - );
368 + // Save the HTML changes
369 + $content = utf8_decode($dom->saveHTML($dom->documentElement));
810 370
811 - wp_enqueue_style(
812 - 'simpletoc-accordion',
813 - plugin_dir_url( __FILE__ ) . 'assets/accordion.css',
814 - array(),
815 - '6.9.0'
816 - );
371 + return $content;
817 372 }
818 373
819 -/**
820 - * Adds the opening HTML tag(s) for the hidden markup element and the table of contents title, if applicable.
821 - *
822 - * @param string $html The HTML string to add the opening tag(s) to.
823 - * @param array $attributes The attributes of the table of contents block.
824 - * @param int $itemcount The number of items in the table of contents.
825 - * @param string $alignclass The alignment class for the table of contents block.
826 - */
827 -function add_hidden_markup_start( $html, $attributes, $itemcount, $alignclass ) { // phpcs:ignore.
828 - $is_hidden_enabled = $attributes['hidden'];
374 +function generateToc( $headings, $attributes )
375 +{
829 376
830 - if ( $is_hidden_enabled ) {
831 - $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : esc_html__( 'Table of Contents', 'simpletoc' );
832 - $hidden_start = '<details class="simpletoc">
833 - <summary style="cursor: pointer;">' . $title_text . '</summary>';
834 - $html .= $hidden_start;
835 - }
377 + $list = '';
378 + $html = '';
379 + $min_depth = 6;
380 + $listtype = 'ul';
381 + $absolute_url = '';
382 + $inital_depth = 6;
383 + $link_class = '';
384 + $styles = '';
836 385
837 - // If there are no items in the table of contents, return an empty string.
838 - if ( $itemcount < 1 ) {
839 - return '';
840 - }
386 + $title_text = esc_html( trim( $attributes['title_text'] ) );
387 + if ( ! $title_text ) {
388 + $title_text = __('Table of Contents', 'simpletoc');
389 + }
841 390
842 - return $html;
843 -}
391 + $alignclass = '';
392 + if ( isset ($attributes['align']) ) {
393 + $align = $attributes['align'];
394 + $alignclass = 'align' . $align;
395 + }
844 396
845 -/**
846 - * Adds the closing HTML tag(s) for the hidden markup element if the hidden markup is enabled.
847 - *
848 - * @param string $html The HTML string to add the closing tag(s) to.
849 - * @param array $attributes The attributes of the table of contents block.
850 - * @return string The modified HTML string with the closing tag(s) added.
851 - */
852 -function add_hidden_markup_end( $html, $attributes ) {
853 - $is_hidden_enabled = $attributes['hidden'];
397 + if ($attributes['remove_indent'] == true) {
398 + $styles = 'style="padding-left:0;list-style:none;"';
399 + }
854 400
855 - if ( $is_hidden_enabled ) {
856 - $html .= '</details>';
857 - }
401 + if ($attributes['add_smooth'] == true) {
402 + $link_class = 'class="smooth-scroll"';
403 + }
858 404
859 - return $html;
860 -}
405 + if ($attributes['use_ol'] == true) {
406 + $listtype = 'ol';
407 + }
861 408
862 -/**
863 - * Adds the opening HTML tag(s) for the accordion element and the table of contents title, if applicable.
864 - *
865 - * @param string $html The HTML string to add the opening tag(s) to.
866 - * @param array $attributes The attributes of the table of contents block.
867 - * @param int $itemcount The number of items in the table of contents.
868 - * @param string $alignclass The alignment class for the table of contents block.
869 - */
870 -function add_accordion_start( $html, $attributes, $itemcount, $alignclass ) {
871 - // Check if accordion is enabled either through the function arguments or the options.
872 - $is_accordion_enabled = $attributes['accordion'] || true === (bool) get_option( 'simpletoc_accordion_enabled', false );
873 - $is_hidden_enabled = $attributes['hidden'];
874 - $title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : esc_html__( 'Table of Contents', 'simpletoc' );
409 + if ($attributes['use_absolute_urls'] == true) {
410 + $absolute_url = get_permalink();
411 + }
875 412
876 - // Start and end HTML for accordion, if enabled.
877 - $accordion_start = '';
878 - if ( $is_accordion_enabled ) {
879 - enqueue_accordion_frontend();
880 - $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">';
881 - }
413 + foreach ($headings as $line => $headline) {
414 + if ($min_depth > $headings[$line][2]) {
415 + // search for lowest level
416 + $min_depth = (int) $headings[$line][2];
417 + $inital_depth = $min_depth;
418 + }
419 + }
882 420
883 - // Add the accordion start HTML to the output.
884 - $html .= $accordion_start;
421 + /* If there is a custom min level, make sure it is the baseline. */
422 + if ($attributes['min_level'] > $min_depth) {
423 + $min_depth = $attributes['min_level'];
424 + }
885 425
886 - // Add the table of contents title, if not hidden and not in accordion mode.
887 - $show_title = ! $attributes['no_title'] && ! $is_accordion_enabled && ! $is_hidden_enabled;
888 - if ( $show_title ) {
889 - $title_tag = $attributes['title_level'] > 0 ? "h{$attributes['title_level']}" : 'p';
890 - $title_tag = wp_strip_all_tags( $title_tag );
891 - $html_class = 'simpletoc-title';
426 + $itemcount = 0;
892 427
893 - if ( ! empty( $alignclass ) ) {
894 - $html_class .= " $alignclass";
895 - }
428 + foreach ($headings as $line => $headline) {
896 429
897 - $html = "<$title_tag class=\"$html_class\">$title_text</$title_tag>\n";
898 - }
430 + $title = strip_tags($headline);
431 + $page = '';
432 + $dom = new \DOMDocument();
433 + @$dom->loadHTML($headline, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
434 + $xpath = new \DOMXPath($dom);
435 + $nodes = $xpath->query('//*/@data-page');
899 436
900 - // If there are no items in the table of contents, return an empty string.
901 - if ( $itemcount < 1 ) {
902 - return '';
903 - }
437 + if ( isset($nodes[0] ) && $nodes[0]->nodeValue > 1) {
438 + $page = $nodes[0]->nodeValue . '/';
439 + $absolute_url = get_permalink();
440 + }
904 441
905 - return $html;
906 -}
442 + $link = simpletoc_sanitize_string($title);
443 + if (isset($nodes[0]) && !empty($nodes[0]->ownerElement->getAttribute('id'))) {
444 + // if the node already has an attribute id, use that as anchor
445 + $link = $nodes[0]->ownerElement->getAttribute('id');
446 + }
907 447
908 -/**
909 - * Adds the closing HTML tag(s) for the accordion element if the accordion is enabled.
910 - *
911 - * @param string $html The HTML string to add the closing tag(s) to.
912 - * @param array $attributes The attributes of the table of contents block.
913 - * @return string The modified HTML string with the closing tag(s) added
914 - */
915 -function add_accordion_end( $html, $attributes ) {
916 - // Check if accordion is enabled either through the function arguments or the options.
917 - $is_accordion_enabled = $attributes['accordion'] || true === (bool) get_option( 'simpletoc_accordion_enabled', false );
448 + $this_depth = (int) $headings[$line][2];
449 + if (isset($headings[$line + 1][2])) {
450 + $next_depth = (int) $headings[$line + 1][2];
451 + } else {
452 + $next_depth = '';
453 + }
918 454
919 - if ( $is_accordion_enabled ) {
920 - $html .= '</div>';
921 - }
455 + // skip this heading because a max depth is set.
456 + if ($this_depth > $attributes['max_level'] or strpos($headline, 'class="simpletoc-hidden') > 0) {
457 + goto closelist;
458 + }
922 459
923 - return $html;
924 -}
460 + // skip this heading because a min depth is set.
461 + if( $this_depth < $attributes['min_level'] ){
462 + continue;
463 + }
925 464
926 -/**
927 - * Extracts the ID value from the provided heading HTML string.
928 - *
929 - * @param string $headline The heading HTML string to extract the ID value from.
930 - * @return mixed Returns the extracted ID value, or false if no ID value is found.
931 - */
932 -function extract_id( $headline ) {
933 - $pattern = '/id="([^"]*)"/';
934 - preg_match( $pattern, $headline, $matches );
935 - $id_value = $matches[1] ?? false;
465 + $itemcount++;
936 466
937 - if ( false !== $id_value ) {
938 - return $id_value;
939 - }
467 + // start list
468 + if ($this_depth == $min_depth) {
469 + $list .= "<li>\n";
470 + } else {
471 + // we are not as base level. Start opening levels until base is reached.
472 + for ($min_depth; $min_depth < $this_depth; $min_depth++) {
473 + $list .= "\n\t\t<" . $listtype . "><li>\n";
474 + }
475 + }
940 476
941 - return false;
942 -}
477 + $list .= "<a " . $link_class . " href=\"" . $absolute_url . esc_html($page) . "#" . $link . "\">" . $title . "</a>";
943 478
944 -/**
945 - * Gets the page number from a headline string.
946 - *
947 - * @param string $headline The headline string.
948 - * @return string The page number (in the format "X/") if it exists and is greater than 1, or an empty string otherwise.
949 - */
950 -function get_page_number_from_headline( $headline ) {
951 - $dom = new \DOMDocument();
479 + closelist:
480 + // close lists
481 + // check if this is not the last heading
482 + if ($line != count($headings) - 1) {
483 + // do we need to close the door behind us?
484 + if ($min_depth > $next_depth) {
485 + // If yes, how many times?
486 + for ($min_depth; $min_depth > $next_depth; $min_depth--) {
487 + $list .= "</li></" . $listtype . ">\n";
488 + }
489 + }
490 + if ($min_depth == $next_depth) {
491 + $list .= "</li>";
492 + }
493 + // last heading
494 + } else {
495 + for ($inital_depth; $inital_depth < $this_depth; $inital_depth++) {
496 + $list .= "</li></" . $listtype . ">\n";
497 + }
498 + }
499 + }
952 500
953 - try {
954 - $dom->loadHTML( '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $headline, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
955 - } catch ( \Exception $e ) {
956 - return '';
957 - }
501 + if ($attributes['no_title'] == false) {
502 + $html = "<h2 class=\"simpletoc-title\">" . $title_text . "</h2>";
503 + }
504 + $html .= "<" . $listtype . " class=\"simpletoc-list\" " . $styles ." " . $alignclass .">\n" . $list . "</li></" . $listtype . ">";
958 505
959 - $xpath = new \DOMXPath( $dom );
960 - $nodes = $xpath->query( '//*/@data-page' );
506 + if($itemcount < 1) {
507 + $html = '';
508 + }
961 509
962 - if ( isset( $nodes[0] ) && $nodes[0]->nodeValue > 1 ) {
963 - $page_number = $nodes[0]->nodeValue . '/';
964 - return esc_html( $page_number );
965 - } else {
966 - return '';
967 - }
510 + return $html;
968 511 }