PluginProbe
SimpleTOC – Table of Contents Block / 5.0.31
SimpleTOC – Table of Contents Block v5.0.31
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
simpletoc / plugin.php

plugin.php in SimpleTOC – Table of Contents Block 5.0.31, at plugin.php

518 lines 15.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Plugin Name: SimpleTOC - Table of Contents Block
5 * Plugin URI: https://marc.tv/simpletoc-wordpress-inhaltsverzeichnis-plugin-gutenberg/
6 * Description: SEO-friendly Table of Contents Gutenberg block. No JavaScript and no CSS means faster loading.
7 * Version: 5.0.31
8 * Author: Marc Tönsing
9 * Author URI: https://marc.tv
10 * Text Domain: simpletoc
11 * License: GPL v2 or later
12 * License URI: http://www.gnu.org/licenses/gpl-2.0.html
13 *
14 */
15
16 /**
17 * Initalise frontend and backend and register block
18 **/
19
20 function register_simpletoc_block()
21 {
22
23 wp_set_script_translations( 'simpletoc-toc-editor-script', 'simpletoc' );
24
25 add_filter('plugin_row_meta', __NAMESPACE__ . '\\simpletoc_plugin_meta', 10, 2);
26
27 register_block_type( __DIR__ . '/build', [
28 'render_callback' => __NAMESPACE__ . '\\render_callback_simpletoc'
29 ]);
30
31 }
32
33 add_action( 'init', 'register_simpletoc_block' );
34
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 /**
107 * Filter to add plugins to the TOC list for Rank Math plugin
108 *
109 * @param array TOC plugins.
110 */
111
112 add_filter('rank_math/researches/toc_plugins', function ($toc_plugins) {
113 $toc_plugins['simpletoc/plugin.php'] = 'SimpleTOC';
114 return $toc_plugins;
115 });
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
149 /**
150 * Render block output
151 *
152 */
153
154 function render_callback_simpletoc( $attributes )
155 {
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
164 $alignclass = '';
165 if ( isset ($attributes['align']) ) {
166 $align = $attributes['align'];
167 $alignclass = 'align' . $align;
168 }
169
170 $className = '';
171 if ( isset( $attributes['className'] ) ) {
172 $className = strip_tags(htmlspecialchars($attributes['className'] ));
173 }
174
175 $pre_html = '';
176 $post_html = '';
177 if ( $className != '' ) {
178 $pre_html = '<div class="simpletoc ' . $className . '">';
179 $post_html = '</div>';
180 }
181
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 }
188
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 }
195
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 }
200
201 $headings = array_reverse(filter_headings_recursive($blocks));
202
203 // enrich headings with pages as a data-attribute
204 $headings = simpletoc_add_pagenumber($blocks, $headings);
205
206 $headings_clean = array_map('trim', $headings);
207
208 if ( empty( $headings_clean ) ) {
209 $html = '';
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>';
217 }
218 return $html;
219 }
220
221 $toclist = generateToc($headings_clean, $attributes);
222
223 if ( empty( $toclist ) ) {
224 $html = '';
225 if( $is_backend == true ) {
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
239 return $output;
240 }
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
265 /**
266 * Return all headings with a recursive walk through all blocks.
267 * This includes groups and reusable block with groups within reusable blocks.
268 * @var array[] $blocks
269 * @return array[]
270 */
271 function filter_headings_recursive( array $blocks ): array {
272 $arr = [];
273 // allow developers to ignore specific blocks
274 $ignored_blocks = apply_filters( 'simpletoc_excluded_blocks', [] );
275
276 foreach ( $blocks as $innerBlock ) {
277
278 if ( is_array( $innerBlock ) ) {
279
280 // if block is ignored, skip
281 if ( isset( $innerBlock['blockName'] ) && in_array( $innerBlock['blockName'], $ignored_blocks ) ) {
282 continue;
283 }
284
285 if ( isset( $innerBlock['attrs']['ref'] ) ) {
286 // search in reusable blocks
287 $e_arr = parse_blocks( get_post( $innerBlock['attrs']['ref'] )->post_content );
288 $arr = array_merge( filter_headings_recursive( $e_arr ), $arr );
289 } else {
290 // search in groups
291 $arr = array_merge( filter_headings_recursive( $innerBlock ), $arr );
292 }
293 } else {
294
295 if ( isset( $blocks['blockName'] ) && ( $blocks['blockName'] === 'core/heading' ) && $innerBlock !== 'core/heading' ) {
296 // make sure it's a headline.
297 if ( preg_match( "/(<h1|<h2|<h3|<h4|<h5|<h6)/i", $innerBlock ) ) {
298 $arr[] = $innerBlock;
299 }
300 }
301
302 if ( isset( $blocks['blockName'] ) && ( $blocks['blockName'] === 'generateblocks/headline' ) && $innerBlock !== 'core/heading' ) {
303 // make sure it's a headline.
304 if ( preg_match( "/(<h1|<h2|<h3|<h4|<h5|<h6)/i", $innerBlock ) ) {
305 $arr[] = $innerBlock;
306 }
307 }
308 }
309 }
310
311 return $arr;
312 }
313
314 /**
315 * Remove all problematic characters for toc links
316 */
317
318 function simpletoc_sanitize_string( $string )
319 {
320 // remove punctuation
321 $zero_punctuation = preg_replace("/\p{P}/u", "", $string);
322 // remove non-breaking spaces
323 $html_wo_nbs = str_replace("&nbsp;", " ", $zero_punctuation);
324 // remove umlauts and accents
325 $string_without_accents = remove_accents($html_wo_nbs);
326 // Sanitizes a title, replacing whitespace and a few other characters with dashes.
327 $sanitized_string = sanitize_title_with_dashes($string_without_accents);
328 // Encode for use in an url
329 $urlencoded = urlencode($sanitized_string);
330 return $urlencoded;
331 }
332
333 function simpletoc_plugin_meta( $links, $file )
334 {
335
336 if (false !== strpos($file, 'simpletoc')) {
337 $links = array_merge($links, array('<a href="https://wordpress.org/support/plugin/simpletoc">' . __('Support', 'simpletoc') . '</a>'));
338 $links = array_merge($links, array('<a href="https://marc.tv/out/donate">' . __('Donate', 'simpletoc') . '</a>'));
339 $links = array_merge($links, array('<a href="https://wordpress.org/support/plugin/simpletoc/reviews/#new-post">' . __('Write a review', 'simpletoc') . '&nbsp;⭐️⭐️⭐️⭐️⭐️</a>'));
340 }
341
342 return $links;
343 }
344
345 function addAnchorAttribute( $html )
346 {
347
348 // remove non-breaking space entites from input HTML
349 $html_wo_nbs = str_replace("&nbsp;", " ", $html);
350
351 // Thank you Nick Diego
352 if (!$html_wo_nbs) {
353 return $html;
354 }
355
356 libxml_use_internal_errors(TRUE);
357 $dom = new \DOMDocument();
358 @$dom->loadHTML($html_wo_nbs, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
359
360 // use xpath to select the Heading html tags.
361 $xpath = new \DOMXPath($dom);
362 $tags = $xpath->evaluate("//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]");
363
364 // Loop through all the found tags
365 foreach ($tags as $tag) {
366 // if tag already has an attribute "id" defined, no need for creating a new one
367 if (!empty($tag->getAttribute('id'))) {continue;}
368 // Set id attribute
369 $heading_text = strip_tags($html);
370 $anchor = simpletoc_sanitize_string($heading_text);
371 $tag->setAttribute("id", $anchor);
372 }
373
374 // Save the HTML changes
375 $content = utf8_decode($dom->saveHTML($dom->documentElement));
376
377 return $content;
378 }
379
380 function generateToc( $headings, $attributes )
381 {
382
383 $list = '';
384 $html = '';
385 $min_depth = 6;
386 $listtype = 'ul';
387 $absolute_url = '';
388 $inital_depth = 6;
389 $link_class = '';
390 $styles = '';
391
392 $title_text = esc_html( trim( $attributes['title_text'] ) );
393 if ( ! $title_text ) {
394 $title_text = __('Table of Contents', 'simpletoc');
395 }
396
397 $alignclass = '';
398 if ( isset ($attributes['align']) ) {
399 $align = $attributes['align'];
400 $alignclass = 'align' . $align;
401 }
402
403 if ($attributes['remove_indent'] == true) {
404 $styles = 'style="padding-left:0;list-style:none;"';
405 }
406
407 if ($attributes['add_smooth'] == true) {
408 $link_class = 'class="smooth-scroll"';
409 }
410
411 if ($attributes['use_ol'] == true) {
412 $listtype = 'ol';
413 }
414
415 if ($attributes['use_absolute_urls'] == true) {
416 $absolute_url = get_permalink();
417 }
418
419 foreach ($headings as $line => $headline) {
420 if ($min_depth > $headings[$line][2]) {
421 // search for lowest level
422 $min_depth = (int) $headings[$line][2];
423 $inital_depth = $min_depth;
424 }
425 }
426
427 /* If there is a custom min level, make sure it is the baseline. */
428 if ($attributes['min_level'] > $min_depth) {
429 $min_depth = $attributes['min_level'];
430 }
431
432 $itemcount = 0;
433
434 foreach ($headings as $line => $headline) {
435
436 $title = strip_tags($headline);
437 $page = '';
438 $dom = new \DOMDocument();
439 @$dom->loadHTML($headline, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
440 $xpath = new \DOMXPath($dom);
441 $nodes = $xpath->query('//*/@data-page');
442
443 if ( isset($nodes[0] ) && $nodes[0]->nodeValue > 1) {
444 $page = $nodes[0]->nodeValue . '/';
445 $absolute_url = get_permalink();
446 }
447
448 $link = simpletoc_sanitize_string($title);
449 if (isset($nodes[0]) && !empty($nodes[0]->ownerElement->getAttribute('id'))) {
450 // if the node already has an attribute id, use that as anchor
451 $link = $nodes[0]->ownerElement->getAttribute('id');
452 }
453
454 $this_depth = (int) $headings[$line][2];
455 if (isset($headings[$line + 1][2])) {
456 $next_depth = (int) $headings[$line + 1][2];
457 } else {
458 $next_depth = '';
459 }
460
461 // skip this heading because a max depth is set.
462 if ($this_depth > $attributes['max_level'] or strpos($headline, 'class="simpletoc-hidden') > 0) {
463 goto closelist;
464 }
465
466 // skip this heading because a min depth is set.
467 if( $this_depth < $attributes['min_level'] ){
468 continue;
469 }
470
471 $itemcount++;
472
473 // start list
474 if ($this_depth == $min_depth) {
475 $list .= "<li>\n";
476 } else {
477 // we are not as base level. Start opening levels until base is reached.
478 for ($min_depth; $min_depth < $this_depth; $min_depth++) {
479 $list .= "\n\t\t<" . $listtype . "><li>\n";
480 }
481 }
482
483 $list .= "<a " . $link_class . " href=\"" . $absolute_url . esc_html($page) . "#" . $link . "\">" . $title . "</a>";
484
485 closelist:
486 // close lists
487 // check if this is not the last heading
488 if ($line != count($headings) - 1) {
489 // do we need to close the door behind us?
490 if ($min_depth > $next_depth) {
491 // If yes, how many times?
492 for ($min_depth; $min_depth > $next_depth; $min_depth--) {
493 $list .= "</li></" . $listtype . ">\n";
494 }
495 }
496 if ($min_depth == $next_depth) {
497 $list .= "</li>";
498 }
499 // last heading
500 } else {
501 for ($inital_depth; $inital_depth < $this_depth; $inital_depth++) {
502 $list .= "</li></" . $listtype . ">\n";
503 }
504 }
505 }
506
507 if ($attributes['no_title'] == false) {
508 $html = "<h2 class=\"simpletoc-title\">" . $title_text . "</h2>";
509 }
510 $html .= "<" . $listtype . " class=\"simpletoc-list\" " . $styles ." " . $alignclass .">\n" . $list . "</li></" . $listtype . ">";
511
512 if($itemcount < 1) {
513 $html = '';
514 }
515
516 return $html;
517 }
518