PluginProbe
SimpleTOC – Table of Contents Block / 5.0.30
SimpleTOC – Table of Contents Block v5.0.30
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.30, at plugin.php

512 lines 15.2 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.30
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 */
269
270 function filter_headings_recursive( $blocks )
271 {
272 $arr = array();
273
274 foreach ($blocks as $block => $innerBlock) {
275
276 if (is_array($innerBlock)) {
277
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 {
287
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 }
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 }
301 }
302 }
303
304 return $arr;
305
306 }
307
308 /**
309 * Remove all problematic characters for toc links
310 */
311
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;
325 }
326
327 function simpletoc_plugin_meta( $links, $file )
328 {
329
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 }
335
336 return $links;
337 }
338
339 function addAnchorAttribute( $html )
340 {
341
342 // remove non-breaking space entites from input HTML
343 $html_wo_nbs = str_replace("&nbsp;", " ", $html);
344
345 // Thank you Nick Diego
346 if (!$html_wo_nbs) {
347 return $html;
348 }
349
350 libxml_use_internal_errors(TRUE);
351 $dom = new \DOMDocument();
352 @$dom->loadHTML($html_wo_nbs, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
353
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]");
357
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 }
367
368 // Save the HTML changes
369 $content = utf8_decode($dom->saveHTML($dom->documentElement));
370
371 return $content;
372 }
373
374 function generateToc( $headings, $attributes )
375 {
376
377 $list = '';
378 $html = '';
379 $min_depth = 6;
380 $listtype = 'ul';
381 $absolute_url = '';
382 $inital_depth = 6;
383 $link_class = '';
384 $styles = '';
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
397 if ($attributes['remove_indent'] == true) {
398 $styles = 'style="padding-left:0;list-style:none;"';
399 }
400
401 if ($attributes['add_smooth'] == true) {
402 $link_class = 'class="smooth-scroll"';
403 }
404
405 if ($attributes['use_ol'] == true) {
406 $listtype = 'ol';
407 }
408
409 if ($attributes['use_absolute_urls'] == true) {
410 $absolute_url = get_permalink();
411 }
412
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 }
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
428 foreach ($headings as $line => $headline) {
429
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
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
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 }
454
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 }
459
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
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 }
476
477 $list .= "<a " . $link_class . " href=\"" . $absolute_url . esc_html($page) . "#" . $link . "\">" . $title . "</a>";
478
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 }
500
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 . ">";
505
506 if($itemcount < 1) {
507 $html = '';
508 }
509
510 return $html;
511 }
512