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 +258 -56 5.0.35.0.29 View file →
@@ -2,16 +2,16 @@
2 2
3 3 /**
4 4 * Plugin Name: SimpleTOC - Table of Contents Block
5 5 * Plugin URI: https://marc.tv/simpletoc-wordpress-inhaltsverzeichnis-plugin-gutenberg/
6 - * Description: Adds a basic "Table of Contents" Gutenberg block.
7 - * Version: 5.0.3
6 + * Description: SEO-friendly Table of Contents Gutenberg block. No JavaScript and no CSS means faster loading.
7 + * Version: 5.0.29
8 8 * Author: Marc Tönsing
9 9 * Author URI: https://marc.tv
10 10 * Text Domain: simpletoc
11 11 * License: GPL v2 or later
12 12 * License URI: http://www.gnu.org/licenses/gpl-2.0.html
13 - *
13 + *
14 14 */
15 15
16 16 /**
17 17 * Initalise frontend and backend and register block
@@ -23,10 +23,10 @@
23 23 wp_set_script_translations( 'simpletoc-toc-editor-script', 'simpletoc' );
24 24
25 25 add_filter('plugin_row_meta', __NAMESPACE__ . '\\simpletoc_plugin_meta', 10, 2);
26 26
27 - register_block_type( __DIR__ . '/build' , [
28 - 'render_callback' => __NAMESPACE__ . '\\render_callback'
27 + register_block_type( __DIR__ . '/build', [
28 + 'render_callback' => __NAMESPACE__ . '\\render_callback_simpletoc'
29 29 ]);
30 30
31 31 }
32 32
@@ -32,8 +32,79 @@
32 32
33 33 add_action( 'init', 'register_simpletoc_block' );
34 34
35 35 /**
36 + * Inject potentially missing translations into the block-editor i18n
37 + * collection.
38 + *
39 + * This keeps the plugin backwards compatible, in case the user did not
40 + * update translations on their website (yet).
41 + *
42 + * @param string|false|null $translations JSON-encoded translation data. Default null.
43 + * @param string|false $file Path to the translation file to load. False if there isn't one.
44 + * @param string $handle Name of the script to register a translation domain to.
45 + * @param string $domain The text domain.
46 + *
47 + * @return string|false|null JSON string
48 + */
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 + ];
55 +
56 + $changed = false;
57 + $obj = json_decode( $translations, true );
58 +
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'];
62 +
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 + }
75 +
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 + }
83 +
84 + return $translations;
85 +}, 10, 4 );
86 +
87 +/**
88 + * Sets the default value of translatable attributes.
89 + *
90 + * Values inside block.json are static strings that are not translated. This
91 + * filter inserts relevant translations i
92 + *
93 + * @param array $settings Array of determined settings for registering a block type.
94 + * @param array $metadata Metadata provided for registering a block type.
95 + *
96 + * @return array Modified settings array.
97 + */
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 + }
102 +
103 + return $settings;
104 +}, 10, 2);
105 +
106 +/**
36 107 * Filter to add plugins to the TOC list for Rank Math plugin
37 108 *
38 109 * @param array TOC plugins.
39 110 */
@@ -42,18 +113,55 @@
42 113 $toc_plugins['simpletoc/plugin.php'] = 'SimpleTOC';
43 114 return $toc_plugins;
44 115 });
45 116
117 +add_filter( 'the_content', 'simpletoc_addIDstoContent', 1 );
118 +
119 +function simpletoc_addIDstoContent( $content ) {
120 +
121 + $blocks = parse_blocks( $content );
122 +
123 + $blocks = addIDstoBlocks_recursive( $blocks );
124 +
125 + $content = serialize_blocks( $blocks );
126 +
127 + return $content;
128 +}
129 +
130 +function addIDstoBlocks_recursive( $blocks ) {
131 +
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 + }
145 +
146 + return $blocks;
147 +}
148 +
46 149 /**
47 - * Render block output
150 + * Render block output
48 151 *
49 152 */
50 153
51 -function render_callback( $attributes )
154 +function render_callback_simpletoc( $attributes )
52 155 {
53 - //add only if block is used in this post.
54 - add_filter('render_block', __NAMESPACE__ . '\\filter_block', 10, 2);
55 156
157 + $is_backend = defined('REST_REQUEST') && true === REST_REQUEST && 'edit' === filter_input(INPUT_GET, 'context');
158 +
159 + $title_text = esc_html( trim( $attributes['title_text'] ) );
160 + if ( ! $title_text ) {
161 + $title_text = __('Table of Contents', 'simpletoc');
162 + }
163 +
56 164 $alignclass = '';
57 165 if ( isset ($attributes['align']) ) {
58 166 $align = $attributes['align'];
59 167 $alignclass = 'align' . $align;
@@ -60,53 +168,108 @@
60 168 }
61 169
62 170 $className = '';
63 171 if ( isset( $attributes['className'] ) ) {
64 - $className = strip_tags(htmlspecialchars($attributes['className']));
172 + $className = strip_tags(htmlspecialchars($attributes['className'] ));
65 173 }
66 174
175 + $pre_html = '';
176 + $post_html = '';
177 + if ( $className != '' ) {
178 + $pre_html = '<div class="simpletoc ' . $className . '">';
179 + $post_html = '</div>';
180 + }
181 +
67 182 $post = get_post();
68 - $blocks = parse_blocks($post->post_content);
183 + if ( is_null($post) || is_null($post->post_content) ) {
184 + $blocks = '';
185 + } else {
186 + $blocks = parse_blocks($post->post_content);
187 + }
69 188
70 - if (empty($blocks)) {
189 + if ( empty($blocks) ) {
71 190 $html = '';
72 - if ($attributes['no_title'] == false) {
73 - $html = '<h2 class="simpletoc-title">' . __('Table of Contents2', 'simpletoc') . '</h2>';
191 + if( $is_backend == true ) {
192 + if ($attributes['no_title'] == false) {
193 + $html = '<h2 class="simpletoc-title ' . $alignclass . '">' . $title_text . '</h2>';
194 + }
195 +
196 + $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No blocks found.', 'simpletoc') . ' ' . __('Save or update post first.', 'simpletoc') . '</p>';
74 197 }
75 - $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No blocks found.', 'simpletoc') . ' ' . __('Save or update post first.', 'simpletoc') . '</p>';
76 198 return $html;
77 199 }
78 200
79 201 $headings = array_reverse(filter_headings_recursive($blocks));
80 202
203 + // enrich headings with pages as a data-attribute
204 + $headings = simpletoc_add_pagenumber($blocks, $headings);
205 +
81 206 $headings_clean = array_map('trim', $headings);
82 207
83 - if (empty($headings_clean)) {
208 + if ( empty( $headings_clean ) ) {
84 209 $html = '';
85 - if ($attributes['no_title'] == false) {
86 - $html = '<h2 class="simpletoc-title">' . __('Table of Contents', 'simpletoc') . '</h2>';
210 + if( $is_backend == true ) {
211 +
212 + if ($attributes['no_title'] == false) {
213 + $html = '<h2 class="simpletoc-title ' . $alignclass . '">' . $title_text . '</h2>';
214 + }
215 +
216 + $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No headings found.', 'simpletoc') . ' ' . __('Save or update post first.', 'simpletoc') . '</p>';
87 217 }
88 - $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No headings found.', 'simpletoc') . ' ' . __('Save or update post first.', 'simpletoc') . '</p>';
89 218 return $html;
90 219 }
91 220
92 - $output = generateToc($headings_clean, $attributes);
221 + $toclist = generateToc($headings_clean, $attributes);
93 222
94 - $pre_html = '<div class="simpletoc ' . $className . ' ' . $alignclass . '">';
95 - $post_html = '</div>';
96 - $output = $pre_html . $output . $post_html;
223 + if ( empty( $toclist ) ) {
224 + $html = '';
225 + if( $is_backend == true ) {
97 226
227 + if ($attributes['no_title'] == false) {
228 + $html = '<h2 class="simpletoc-title ' . $alignclass . '">' . $title_text . '</h2>';
229 + }
230 +
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;
234 +
235 + }
236 +
237 + $output = $pre_html . $toclist . $post_html;
238 +
98 239 return $output;
99 240 }
100 241
242 +
243 +function simpletoc_add_pagenumber( $blocks, $headings ){
244 + $pages = 1;
245 +
246 + foreach ($blocks as $block => $innerBlock) {
247 +
248 + // count nextpage blocks
249 + if (isset($blocks[$block]['blockName']) && $blocks[$block]['blockName'] === 'core/nextpage' ){
250 + $pages++;
251 + }
252 +
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;
263 +}
264 +
101 265 /**
102 - * Return all headings with a recursive walk through all blocks.
103 - * This includes groups and reusable block with groups within reusable blocks.
266 + * Return all headings with a recursive walk through all blocks.
267 + * This includes groups and reusable block with groups within reusable blocks.
104 268 */
105 269
106 -function filter_headings_recursive($blocks)
270 +function filter_headings_recursive( $blocks )
107 271 {
108 -
109 272 $arr = array();
110 273
111 274 foreach ($blocks as $block => $innerBlock) {
112 275
@@ -121,27 +284,34 @@
121 284 $arr = array_merge(filter_headings_recursive($innerBlock), $arr);
122 285 }
123 286 } else {
124 287
125 - if (isset($blocks['blockName']) && $blocks['blockName'] === 'core/heading' && $innerBlock !== 'core/heading') {
288 + if (isset($blocks['blockName']) && ( $blocks['blockName'] === 'core/heading' ) && $innerBlock !== 'core/heading') {
126 289 // make sure its a headline.
127 290 if (preg_match("/(<h1|<h2|<h3|<h4|<h5|<h6)/i", $innerBlock)) {
128 291 $arr[] = $innerBlock;
129 292 }
130 293 }
294 +
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 + }
131 301 }
132 302 }
133 303
134 304 return $arr;
305 +
135 306 }
136 307
137 308 /**
138 - * Remove all problematic characters for toc links
309 + * Remove all problematic characters for toc links
139 310 */
140 311
141 -function simpletoc_sanitize_string($string)
312 +function simpletoc_sanitize_string( $string )
142 313 {
143 -
144 314 // remove punctuation
145 315 $zero_punctuation = preg_replace("/\p{P}/u", "", $string);
146 316 // remove non-breaking spaces
147 317 $html_wo_nbs = str_replace("&nbsp;", " ", $zero_punctuation);
@@ -150,13 +320,12 @@
150 320 // Sanitizes a title, replacing whitespace and a few other characters with dashes.
151 321 $sanitized_string = sanitize_title_with_dashes($string_without_accents);
152 322 // Encode for use in an url
153 323 $urlencoded = urlencode($sanitized_string);
154 -
155 324 return $urlencoded;
156 325 }
157 326
158 -function simpletoc_plugin_meta($links, $file)
327 +function simpletoc_plugin_meta( $links, $file )
159 328 {
160 329
161 330 if (false !== strpos($file, 'simpletoc')) {
162 331 $links = array_merge($links, array('<a href="https://wordpress.org/support/plugin/simpletoc">' . __('Support', 'simpletoc') . '</a>'));
@@ -166,15 +335,15 @@
166 335
167 336 return $links;
168 337 }
169 338
170 -function addAnchorAttribute($html)
339 +function addAnchorAttribute( $html )
171 340 {
172 341
173 342 // remove non-breaking space entites from input HTML
174 343 $html_wo_nbs = str_replace("&nbsp;", " ", $html);
175 344
176 - // Thank you Nick Diego
345 + // Thank you Nick Diego
177 346 if (!$html_wo_nbs) {
178 347 return $html;
179 348 }
180 349
@@ -187,9 +356,10 @@
187 356 $tags = $xpath->evaluate("//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]");
188 357
189 358 // Loop through all the found tags
190 359 foreach ($tags as $tag) {
191 -
360 + // if tag already has an attribute "id" defined, no need for creating a new one
361 + if (!empty($tag->getAttribute('id'))) {continue;}
192 362 // Set id attribute
193 363 $heading_text = strip_tags($html);
194 364 $anchor = simpletoc_sanitize_string($heading_text);
195 365 $tag->setAttribute("id", $anchor);
@@ -200,24 +370,11 @@
200 370
201 371 return $content;
202 372 }
203 373
204 -function filter_block($block_content, $block)
374 +function generateToc( $headings, $attributes )
205 375 {
206 - $className = '';
207 376
208 - if ($block['blockName'] !== 'core/heading') {
209 - return $block_content;
210 - }
211 -
212 - $block_content = addAnchorAttribute($block_content);
213 -
214 - return $block_content;
215 -}
216 -
217 -function generateToc($headings, $attributes)
218 -{
219 -
220 377 $list = '';
221 378 $html = '';
222 379 $min_depth = 6;
223 380 $listtype = 'ul';
@@ -225,12 +382,23 @@
225 382 $inital_depth = 6;
226 383 $link_class = '';
227 384 $styles = '';
228 385
386 + $title_text = esc_html( trim( $attributes['title_text'] ) );
387 + if ( ! $title_text ) {
388 + $title_text = __('Table of Contents', 'simpletoc');
389 + }
390 +
391 + $alignclass = '';
392 + if ( isset ($attributes['align']) ) {
393 + $align = $attributes['align'];
394 + $alignclass = 'align' . $align;
395 + }
396 +
229 397 if ($attributes['remove_indent'] == true) {
230 398 $styles = 'style="padding-left:0;list-style:none;"';
231 399 }
232 -
400 +
233 401 if ($attributes['add_smooth'] == true) {
234 402 $link_class = 'class="smooth-scroll"';
235 403 }
236 404
@@ -249,12 +417,35 @@
249 417 $inital_depth = $min_depth;
250 418 }
251 419 }
252 420
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 + }
425 +
426 + $itemcount = 0;
427 +
253 428 foreach ($headings as $line => $headline) {
254 429
255 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');
436 +
437 + if ( isset($nodes[0] ) && $nodes[0]->nodeValue > 1) {
438 + $page = $nodes[0]->nodeValue . '/';
439 + $absolute_url = get_permalink();
440 + }
441 +
256 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 + }
447 +
257 448 $this_depth = (int) $headings[$line][2];
258 449 if (isset($headings[$line + 1][2])) {
259 450 $next_depth = (int) $headings[$line + 1][2];
260 451 } else {
@@ -265,9 +456,16 @@
265 456 if ($this_depth > $attributes['max_level'] or strpos($headline, 'class="simpletoc-hidden') > 0) {
266 457 goto closelist;
267 458 }
268 459
269 - // start list
460 + // skip this heading because a min depth is set.
461 + if( $this_depth < $attributes['min_level'] ){
462 + continue;
463 + }
464 +
465 + $itemcount++;
466 +
467 + // start list
270 468 if ($this_depth == $min_depth) {
271 469 $list .= "<li>\n";
272 470 } else {
273 471 // we are not as base level. Start opening levels until base is reached.
@@ -275,15 +473,15 @@
275 473 $list .= "\n\t\t<" . $listtype . "><li>\n";
276 474 }
277 475 }
278 476
279 - $list .= "<a " . $link_class . " href=\"" . $absolute_url . "#" . $link . "\">" . $title . "</a>";
477 + $list .= "<a " . $link_class . " href=\"" . $absolute_url . esc_html($page) . "#" . $link . "\">" . $title . "</a>";
280 478
281 479 closelist:
282 480 // close lists
283 481 // check if this is not the last heading
284 482 if ($line != count($headings) - 1) {
285 - // do we need to close the door behind us?
483 + // do we need to close the door behind us?
286 484 if ($min_depth > $next_depth) {
287 485 // If yes, how many times?
288 486 for ($min_depth; $min_depth > $next_depth; $min_depth--) {
289 487 $list .= "</li></" . $listtype . ">\n";
@@ -300,10 +498,14 @@
300 498 }
301 499 }
302 500
303 501 if ($attributes['no_title'] == false) {
304 - $html = "<h2 class=\"simpletoc-title\">" . __("Table of Contents", "simpletoc") . "</h2>";
502 + $html = "<h2 class=\"simpletoc-title\">" . $title_text . "</h2>";
305 503 }
306 - $html .= "<" . $listtype . " class=\"simpletoc-list\" " . $styles .">\n" . $list . "</li></" . $listtype . ">";
504 + $html .= "<" . $listtype . " class=\"simpletoc-list\" " . $styles ." " . $alignclass .">\n" . $list . "</li></" . $listtype . ">";
505 +
506 + if($itemcount < 1) {
507 + $html = '';
508 + }
307 509
308 510 return $html;
309 511 }