PluginProbe
SimpleTOC – Table of Contents Block / 5.0.33
SimpleTOC – Table of Contents Block v5.0.33
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.33, at plugin.php

523 lines 15.7 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.33
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
178 // By default, the wrapper is not enabled because it causes problems on some themes
179 $wrapper_enabled = apply_filters( 'simpletoc_wrapper_enabled', false );
180
181 if ( $className !== ''|| $wrapper_enabled ) {
182 $wrapper_attrs = get_block_wrapper_attributes( [ 'class' => 'simpletoc' ] );
183 $pre_html = "<div $wrapper_attrs>";
184 $post_html = '</div>';
185 }
186
187 $post = get_post();
188 if ( is_null($post) || is_null($post->post_content) ) {
189 $blocks = '';
190 } else {
191 $blocks = parse_blocks($post->post_content);
192 }
193
194 if ( empty($blocks) ) {
195 $html = '';
196 if( $is_backend == true ) {
197 if ($attributes['no_title'] == false) {
198 $html = '<h2 class="simpletoc-title ' . $alignclass . '">' . $title_text . '</h2>';
199 }
200
201 $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No blocks found.', 'simpletoc') . ' ' . __('Save or update post first.', 'simpletoc') . '</p>';
202 }
203 return $html;
204 }
205
206 $headings = array_reverse(filter_headings_recursive($blocks));
207
208 // enrich headings with pages as a data-attribute
209 $headings = simpletoc_add_pagenumber($blocks, $headings);
210
211 $headings_clean = array_map('trim', $headings);
212
213 if ( empty( $headings_clean ) ) {
214 $html = '';
215 if( $is_backend == true ) {
216
217 if ($attributes['no_title'] == false) {
218 $html = '<h2 class="simpletoc-title ' . $alignclass . '">' . $title_text . '</h2>';
219 }
220
221 $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No headings found.', 'simpletoc') . ' ' . __('Save or update post first.', 'simpletoc') . '</p>';
222 }
223 return $html;
224 }
225
226 $toclist = generateToc($headings_clean, $attributes);
227
228 if ( empty( $toclist ) ) {
229 $html = '';
230 if( $is_backend == true ) {
231
232 if ($attributes['no_title'] == false) {
233 $html = '<h2 class="simpletoc-title ' . $alignclass . '">' . $title_text . '</h2>';
234 }
235
236 $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No headings found.', 'simpletoc') . ' ' . __('Check minimal and maximum level block settings.', 'simpletoc') . '</p>';
237 }
238 return $html;
239
240 }
241
242 $output = $pre_html . $toclist . $post_html;
243
244 return $output;
245 }
246
247
248 function simpletoc_add_pagenumber( $blocks, $headings ){
249 $pages = 1;
250
251 foreach ($blocks as $block => $innerBlock) {
252
253 // count nextpage blocks
254 if (isset($blocks[$block]['blockName']) && $blocks[$block]['blockName'] === 'core/nextpage' ){
255 $pages++;
256 }
257
258 if (isset($blocks[$block]['blockName']) && $blocks[$block]["blockName"] === 'core/heading') {
259 // make sure its a headline.
260 foreach ($headings as $heading => &$innerHeading){
261 if($innerHeading == $blocks[$block]["innerHTML"]){
262 $innerHeading = preg_replace("/(<h1|<h2|<h3|<h4|<h5|<h6)/i", '$1 data-page="' . $pages . '"', $blocks[$block]["innerHTML"]);
263 }
264 }
265 }
266 }
267 return $headings;
268 }
269
270 /**
271 * Return all headings with a recursive walk through all blocks.
272 * This includes groups and reusable block with groups within reusable blocks.
273 * @var array[] $blocks
274 * @return array[]
275 */
276 function filter_headings_recursive( array $blocks ): array {
277 $arr = [];
278 // allow developers to ignore specific blocks
279 $ignored_blocks = apply_filters( 'simpletoc_excluded_blocks', [] );
280
281 foreach ( $blocks as $innerBlock ) {
282
283 if ( is_array( $innerBlock ) ) {
284
285 // if block is ignored, skip
286 if ( isset( $innerBlock['blockName'] ) && in_array( $innerBlock['blockName'], $ignored_blocks ) ) {
287 continue;
288 }
289
290 if ( isset( $innerBlock['attrs']['ref'] ) ) {
291 // search in reusable blocks
292 $e_arr = parse_blocks( get_post( $innerBlock['attrs']['ref'] )->post_content );
293 $arr = array_merge( filter_headings_recursive( $e_arr ), $arr );
294 } else {
295 // search in groups
296 $arr = array_merge( filter_headings_recursive( $innerBlock ), $arr );
297 }
298 } else {
299
300 if ( isset( $blocks['blockName'] ) && ( $blocks['blockName'] === 'core/heading' ) && $innerBlock !== 'core/heading' ) {
301 // make sure it's a headline.
302 if ( preg_match( "/(<h1|<h2|<h3|<h4|<h5|<h6)/i", $innerBlock ) ) {
303 $arr[] = $innerBlock;
304 }
305 }
306
307 if ( isset( $blocks['blockName'] ) && ( $blocks['blockName'] === 'generateblocks/headline' ) && $innerBlock !== 'core/heading' ) {
308 // make sure it's a headline.
309 if ( preg_match( "/(<h1|<h2|<h3|<h4|<h5|<h6)/i", $innerBlock ) ) {
310 $arr[] = $innerBlock;
311 }
312 }
313 }
314 }
315
316 return $arr;
317 }
318
319 /**
320 * Remove all problematic characters for toc links
321 */
322
323 function simpletoc_sanitize_string( $string )
324 {
325 // remove punctuation
326 $zero_punctuation = preg_replace("/\p{P}/u", "", $string);
327 // remove non-breaking spaces
328 $html_wo_nbs = str_replace("&nbsp;", " ", $zero_punctuation);
329 // remove umlauts and accents
330 $string_without_accents = remove_accents($html_wo_nbs);
331 // Sanitizes a title, replacing whitespace and a few other characters with dashes.
332 $sanitized_string = sanitize_title_with_dashes($string_without_accents);
333 // Encode for use in an url
334 $urlencoded = urlencode($sanitized_string);
335 return $urlencoded;
336 }
337
338 function simpletoc_plugin_meta( $links, $file )
339 {
340
341 if (false !== strpos($file, 'simpletoc')) {
342 $links = array_merge($links, array('<a href="https://wordpress.org/support/plugin/simpletoc">' . __('Support', 'simpletoc') . '</a>'));
343 $links = array_merge($links, array('<a href="https://marc.tv/out/donate">' . __('Donate', 'simpletoc') . '</a>'));
344 $links = array_merge($links, array('<a href="https://wordpress.org/support/plugin/simpletoc/reviews/#new-post">' . __('Write a review', 'simpletoc') . '&nbsp;⭐️⭐️⭐️⭐️⭐️</a>'));
345 }
346
347 return $links;
348 }
349
350 function addAnchorAttribute( $html )
351 {
352
353 // remove non-breaking space entites from input HTML
354 $html_wo_nbs = str_replace("&nbsp;", " ", $html);
355
356 // Thank you Nick Diego
357 if (!$html_wo_nbs) {
358 return $html;
359 }
360
361 libxml_use_internal_errors(TRUE);
362 $dom = new \DOMDocument();
363 @$dom->loadHTML($html_wo_nbs, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
364
365 // use xpath to select the Heading html tags.
366 $xpath = new \DOMXPath($dom);
367 $tags = $xpath->evaluate("//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]");
368
369 // Loop through all the found tags
370 foreach ($tags as $tag) {
371 // if tag already has an attribute "id" defined, no need for creating a new one
372 if (!empty($tag->getAttribute('id'))) {continue;}
373 // Set id attribute
374 $heading_text = strip_tags($html);
375 $anchor = simpletoc_sanitize_string($heading_text);
376 $tag->setAttribute("id", $anchor);
377 }
378
379 // Save the HTML changes
380 $content = utf8_decode($dom->saveHTML($dom->documentElement));
381
382 return $content;
383 }
384
385 function generateToc( $headings, $attributes )
386 {
387
388 $list = '';
389 $html = '';
390 $min_depth = 6;
391 $listtype = 'ul';
392 $absolute_url = '';
393 $inital_depth = 6;
394 $link_class = '';
395 $styles = '';
396
397 $title_text = esc_html( trim( $attributes['title_text'] ) );
398 if ( ! $title_text ) {
399 $title_text = __('Table of Contents', 'simpletoc');
400 }
401
402 $alignclass = '';
403 if ( isset ($attributes['align']) ) {
404 $align = $attributes['align'];
405 $alignclass = 'align' . $align;
406 }
407
408 if ($attributes['remove_indent'] == true) {
409 $styles = 'style="padding-left:0;list-style:none;"';
410 }
411
412 if ($attributes['add_smooth'] == true) {
413 $link_class = 'class="smooth-scroll"';
414 }
415
416 if ($attributes['use_ol'] == true) {
417 $listtype = 'ol';
418 }
419
420 if ($attributes['use_absolute_urls'] == true) {
421 $absolute_url = get_permalink();
422 }
423
424 foreach ($headings as $line => $headline) {
425 if ($min_depth > $headings[$line][2]) {
426 // search for lowest level
427 $min_depth = (int) $headings[$line][2];
428 $inital_depth = $min_depth;
429 }
430 }
431
432 /* If there is a custom min level, make sure it is the baseline. */
433 if ($attributes['min_level'] > $min_depth) {
434 $min_depth = $attributes['min_level'];
435 }
436
437 $itemcount = 0;
438
439 foreach ($headings as $line => $headline) {
440
441 $title = strip_tags($headline);
442 $page = '';
443 $dom = new \DOMDocument();
444 @$dom->loadHTML($headline, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
445 $xpath = new \DOMXPath($dom);
446 $nodes = $xpath->query('//*/@data-page');
447
448 if ( isset($nodes[0] ) && $nodes[0]->nodeValue > 1) {
449 $page = $nodes[0]->nodeValue . '/';
450 $absolute_url = get_permalink();
451 }
452
453 $link = simpletoc_sanitize_string($title);
454 if (isset($nodes[0]) && !empty($nodes[0]->ownerElement->getAttribute('id'))) {
455 // if the node already has an attribute id, use that as anchor
456 $link = $nodes[0]->ownerElement->getAttribute('id');
457 }
458
459 $this_depth = (int) $headings[$line][2];
460 if (isset($headings[$line + 1][2])) {
461 $next_depth = (int) $headings[$line + 1][2];
462 } else {
463 $next_depth = '';
464 }
465
466 // skip this heading because a max depth is set.
467 if ($this_depth > $attributes['max_level'] or strpos($headline, 'class="simpletoc-hidden') > 0) {
468 goto closelist;
469 }
470
471 // skip this heading because a min depth is set.
472 if( $this_depth < $attributes['min_level'] ){
473 continue;
474 }
475
476 $itemcount++;
477
478 // start list
479 if ($this_depth == $min_depth) {
480 $list .= "<li>\n";
481 } else {
482 // we are not as base level. Start opening levels until base is reached.
483 for ($min_depth; $min_depth < $this_depth; $min_depth++) {
484 $list .= "\n\t\t<" . $listtype . "><li>\n";
485 }
486 }
487
488 $list .= "<a " . $link_class . " href=\"" . $absolute_url . esc_html($page) . "#" . $link . "\">" . $title . "</a>";
489
490 closelist:
491 // close lists
492 // check if this is not the last heading
493 if ($line != count($headings) - 1) {
494 // do we need to close the door behind us?
495 if ($min_depth > $next_depth) {
496 // If yes, how many times?
497 for ($min_depth; $min_depth > $next_depth; $min_depth--) {
498 $list .= "</li></" . $listtype . ">\n";
499 }
500 }
501 if ($min_depth == $next_depth) {
502 $list .= "</li>";
503 }
504 // last heading
505 } else {
506 for ($inital_depth; $inital_depth < $this_depth; $inital_depth++) {
507 $list .= "</li></" . $listtype . ">\n";
508 }
509 }
510 }
511
512 if ($attributes['no_title'] == false) {
513 $html = "<h2 class=\"simpletoc-title\">" . $title_text . "</h2>";
514 }
515 $html .= "<" . $listtype . " class=\"simpletoc-list\" " . $styles ." " . $alignclass .">\n" . $list . "</li></" . $listtype . ">";
516
517 if($itemcount < 1) {
518 $html = '';
519 }
520
521 return $html;
522 }
523