PluginProbe
SimpleTOC – Table of Contents Block / 5.0.24
SimpleTOC – Table of Contents Block v5.0.24
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.24, at plugin.php

431 lines 12.6 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 CSS and JavaScript is loaded and does not slow down your website.
7 * Version: 5.0.24
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'
29 ]);
30
31 }
32
33 add_action( 'init', 'register_simpletoc_block' );
34
35 /**
36 * Filter to add plugins to the TOC list for Rank Math plugin
37 *
38 * @param array TOC plugins.
39 */
40
41 add_filter('rank_math/researches/toc_plugins', function ($toc_plugins) {
42 $toc_plugins['simpletoc/plugin.php'] = 'SimpleTOC';
43 return $toc_plugins;
44 });
45
46 add_filter( 'the_content', 'simpletoc_addIDstoContent', 1 );
47
48 function simpletoc_addIDstoContent( $content ) {
49
50 $blocks = parse_blocks( $content );
51
52 $blocks = addIDstoBlocks_recursive( $blocks );
53
54 $content = serialize_blocks( $blocks );
55
56 return $content;
57 }
58
59 function addIDstoBlocks_recursive( $blocks ) {
60
61 foreach ( $blocks as &$block ) {
62 if (isset($block['blockName']) && ( $block['blockName'] === 'core/heading' || $block['blockName'] === 'generateblocks/headline') && isset($block['innerHTML']) && isset($block['innerContent']) && isset($block['innerContent'][0]) ){
63 $block['innerHTML'] = addAnchorAttribute($block['innerHTML']);
64 $block['innerContent'][0] = addAnchorAttribute($block['innerContent'][0]);
65 } else if( isset($block['attrs']['ref']) ){
66 // search in reusable blocks (this is not finished because I ran out of ideas.)
67 // $reusable_block_id = $block['attrs']['ref'];
68 // $reusable_block_content = parse_blocks(get_post($reusable_block_id)->post_content);
69 } else if ( ! empty( $block['innerBlocks'] ) ) {
70 // search in groups
71 $block['innerBlocks'] = addIDstoBlocks_recursive( $block['innerBlocks'] );
72 }
73 }
74
75 return $blocks;
76 }
77
78 /**
79 * Render block output
80 *
81 */
82
83 function render_callback( $attributes )
84 {
85
86 $is_backend = defined('REST_REQUEST') && true === REST_REQUEST && 'edit' === filter_input(INPUT_GET, 'context');
87
88 $alignclass = '';
89 if ( isset ($attributes['align']) ) {
90 $align = $attributes['align'];
91 $alignclass = 'align' . $align;
92 }
93
94 $className = '';
95 if ( isset( $attributes['className'] ) ) {
96 $className = strip_tags(htmlspecialchars($attributes['className'] ));
97 }
98
99 $pre_html = '';
100 $post_html = '';
101 if ( $className != '' ) {
102 $pre_html = '<div class="simpletoc ' . $className . '">';
103 $post_html = '</div>';
104 }
105
106 $post = get_post();
107 if ( is_null($post) || is_null($post->post_content) ) {
108 $blocks = '';
109 } else {
110 $blocks = parse_blocks($post->post_content);
111 }
112
113 if ( empty($blocks) ) {
114 $html = '';
115 if( $is_backend == true ) {
116 if ($attributes['no_title'] == false) {
117 $html = '<h2 class="simpletoc-title ' . $alignclass . '">' . __('Table of Contents', 'simpletoc') . '</h2>';
118 }
119
120 $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No blocks found.', 'simpletoc') . ' ' . __('Save or update post first.', 'simpletoc') . '</p>';
121 }
122 return $html;
123 }
124
125 $headings = array_reverse(filter_headings_recursive($blocks));
126
127 // enrich headings with pages as a data-attribute
128 $headings = simpletoc_add_pagenumber($blocks, $headings);
129
130 $headings_clean = array_map('trim', $headings);
131
132 if ( empty( $headings_clean ) ) {
133 $html = '';
134 if( $is_backend == true ) {
135
136 if ($attributes['no_title'] == false) {
137 $html = '<h2 class="simpletoc-title ' . $alignclass . '">' . __('Table of Contents', 'simpletoc') . '</h2>';
138 }
139
140 $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No headings found.', 'simpletoc') . ' ' . __('Save or update post first.', 'simpletoc') . '</p>';
141 }
142 return $html;
143 }
144
145 $toclist = generateToc($headings_clean, $attributes);
146
147 if ( empty( $toclist ) ) {
148 $html = '';
149 if( $is_backend == true ) {
150
151 if ($attributes['no_title'] == false) {
152 $html = '<h2 class="simpletoc-title ' . $alignclass . '">' . __('Table of Contents', 'simpletoc') . '</h2>';
153 }
154
155 $html .= '<p class="components-notice is-warning ' . $alignclass . '">' . __('No headings found.', 'simpletoc') . ' ' . __('Check minimal and maximum level block settings.', 'simpletoc') . '</p>';
156 }
157 return $html;
158
159 }
160
161 $output = $pre_html . $toclist . $post_html;
162
163 return $output;
164 }
165
166
167 function simpletoc_add_pagenumber( $blocks, $headings ){
168 $pages = 1;
169
170 foreach ($blocks as $block => $innerBlock) {
171
172 // count nextpage blocks
173 if (isset($blocks[$block]['blockName']) && $blocks[$block]['blockName'] === 'core/nextpage' ){
174 $pages++;
175 }
176
177 if (isset($blocks[$block]['blockName']) && $blocks[$block]["blockName"] === 'core/heading') {
178 // make sure its a headline.
179 foreach ($headings as $heading => &$innerHeading){
180 if($innerHeading == $blocks[$block]["innerHTML"]){
181 $innerHeading = preg_replace("/(<h1|<h2|<h3|<h4|<h5|<h6)/i", '$1 data-page="' . $pages . '"', $blocks[$block]["innerHTML"]);
182 }
183 }
184 }
185 }
186 return $headings;
187 }
188
189 /**
190 * Return all headings with a recursive walk through all blocks.
191 * This includes groups and reusable block with groups within reusable blocks.
192 */
193
194 function filter_headings_recursive( $blocks )
195 {
196 $arr = array();
197
198 foreach ($blocks as $block => $innerBlock) {
199
200 if (is_array($innerBlock)) {
201
202 if (isset($innerBlock['attrs']['ref'])) {
203 // search in reusable blocks
204 $e_arr = parse_blocks(get_post($innerBlock['attrs']['ref'])->post_content);
205 $arr = array_merge(filter_headings_recursive($e_arr), $arr);
206 } else {
207 // search in groups
208 $arr = array_merge(filter_headings_recursive($innerBlock), $arr);
209 }
210 } else {
211
212 if (isset($blocks['blockName']) && ( $blocks['blockName'] === 'core/heading' ) && $innerBlock !== 'core/heading') {
213 // make sure its a headline.
214 if (preg_match("/(<h1|<h2|<h3|<h4|<h5|<h6)/i", $innerBlock)) {
215 $arr[] = $innerBlock;
216 }
217 }
218
219 if (isset($blocks['blockName']) && ( $blocks['blockName'] === 'generateblocks/headline' ) && $innerBlock !== 'core/heading') {
220 // make sure its a headline.
221 if (preg_match("/(<h1|<h2|<h3|<h4|<h5|<h6)/i", $innerBlock)) {
222 $arr[] = $innerBlock;
223 }
224 }
225 }
226 }
227
228 return $arr;
229
230 }
231
232 /**
233 * Remove all problematic characters for toc links
234 */
235
236 function simpletoc_sanitize_string( $string )
237 {
238 // remove punctuation
239 $zero_punctuation = preg_replace("/\p{P}/u", "", $string);
240 // remove non-breaking spaces
241 $html_wo_nbs = str_replace("&nbsp;", " ", $zero_punctuation);
242 // remove umlauts and accents
243 $string_without_accents = remove_accents($html_wo_nbs);
244 // Sanitizes a title, replacing whitespace and a few other characters with dashes.
245 $sanitized_string = sanitize_title_with_dashes($string_without_accents);
246 // Encode for use in an url
247 $urlencoded = urlencode($sanitized_string);
248 return $urlencoded;
249 }
250
251 function simpletoc_plugin_meta( $links, $file )
252 {
253
254 if (false !== strpos($file, 'simpletoc')) {
255 $links = array_merge($links, array('<a href="https://wordpress.org/support/plugin/simpletoc">' . __('Support', 'simpletoc') . '</a>'));
256 $links = array_merge($links, array('<a href="https://marc.tv/out/donate">' . __('Donate', 'simpletoc') . '</a>'));
257 $links = array_merge($links, array('<a href="https://wordpress.org/support/plugin/simpletoc/reviews/#new-post">' . __('Write a review', 'simpletoc') . '&nbsp;⭐️⭐️⭐️⭐️⭐️</a>'));
258 }
259
260 return $links;
261 }
262
263 function addAnchorAttribute( $html )
264 {
265
266 // remove non-breaking space entites from input HTML
267 $html_wo_nbs = str_replace("&nbsp;", " ", $html);
268
269 // Thank you Nick Diego
270 if (!$html_wo_nbs) {
271 return $html;
272 }
273
274 libxml_use_internal_errors(TRUE);
275 $dom = new \DOMDocument();
276 @$dom->loadHTML($html_wo_nbs, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
277
278 // use xpath to select the Heading html tags.
279 $xpath = new \DOMXPath($dom);
280 $tags = $xpath->evaluate("//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]");
281
282 // Loop through all the found tags
283 foreach ($tags as $tag) {
284 // if tag already has an attribute "id" defined, no need for creating a new one
285 if (!empty($tag->getAttribute('id'))) {continue;}
286 // Set id attribute
287 $heading_text = strip_tags($html);
288 $anchor = simpletoc_sanitize_string($heading_text);
289 $tag->setAttribute("id", $anchor);
290 }
291
292 // Save the HTML changes
293 $content = utf8_decode($dom->saveHTML($dom->documentElement));
294
295 return $content;
296 }
297
298 function generateToc( $headings, $attributes )
299 {
300
301 $list = '';
302 $html = '';
303 $min_depth = 6;
304 $listtype = 'ul';
305 $absolute_url = '';
306 $inital_depth = 6;
307 $link_class = '';
308 $styles = '';
309
310 $alignclass = '';
311 if ( isset ($attributes['align']) ) {
312 $align = $attributes['align'];
313 $alignclass = 'align' . $align;
314 }
315
316 if ($attributes['remove_indent'] == true) {
317 $styles = 'style="padding-left:0;list-style:none;"';
318 }
319
320 if ($attributes['add_smooth'] == true) {
321 $link_class = 'class="smooth-scroll"';
322 }
323
324 if ($attributes['use_ol'] == true) {
325 $listtype = 'ol';
326 }
327
328 if ($attributes['use_absolute_urls'] == true) {
329 $absolute_url = get_permalink();
330 }
331
332 foreach ($headings as $line => $headline) {
333 if ($min_depth > $headings[$line][2]) {
334 // search for lowest level
335 $min_depth = (int) $headings[$line][2];
336 $inital_depth = $min_depth;
337 }
338 }
339
340 /* If there is a custom min level, make sure it is the baseline. */
341 if ($attributes['min_level'] > $min_depth) {
342 $min_depth = $attributes['min_level'];
343 }
344
345 $itemcount = 0;
346
347 foreach ($headings as $line => $headline) {
348
349 $title = strip_tags($headline);
350 $page = '';
351 $dom = new \DOMDocument();
352 @$dom->loadHTML($headline, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
353 $xpath = new \DOMXPath($dom);
354 $nodes = $xpath->query('//*/@data-page');
355
356 if ( isset($nodes[0] ) && $nodes[0]->nodeValue > 1) {
357 $page = $nodes[0]->nodeValue . '/';
358 $absolute_url = get_permalink();
359 }
360
361 $link = simpletoc_sanitize_string($title);
362 if (isset($nodes[0]) && !empty($nodes[0]->ownerElement->getAttribute('id'))) {
363 // if the node already has an attribute id, use that as anchor
364 $link = $nodes[0]->ownerElement->getAttribute('id');
365 }
366
367 $this_depth = (int) $headings[$line][2];
368 if (isset($headings[$line + 1][2])) {
369 $next_depth = (int) $headings[$line + 1][2];
370 } else {
371 $next_depth = '';
372 }
373
374 // skip this heading because a max depth is set.
375 if ($this_depth > $attributes['max_level'] or strpos($headline, 'class="simpletoc-hidden') > 0) {
376 goto closelist;
377 }
378
379 // skip this heading because a min depth is set.
380 if( $this_depth < $attributes['min_level'] ){
381 continue;
382 }
383
384 $itemcount++;
385
386 // start list
387 if ($this_depth == $min_depth) {
388 $list .= "<li>\n";
389 } else {
390 // we are not as base level. Start opening levels until base is reached.
391 for ($min_depth; $min_depth < $this_depth; $min_depth++) {
392 $list .= "\n\t\t<" . $listtype . "><li>\n";
393 }
394 }
395
396 $list .= "<a " . $link_class . " href=\"" . $absolute_url . esc_html($page) . "#" . $link . "\">" . $title . "</a>";
397
398 closelist:
399 // close lists
400 // check if this is not the last heading
401 if ($line != count($headings) - 1) {
402 // do we need to close the door behind us?
403 if ($min_depth > $next_depth) {
404 // If yes, how many times?
405 for ($min_depth; $min_depth > $next_depth; $min_depth--) {
406 $list .= "</li></" . $listtype . ">\n";
407 }
408 }
409 if ($min_depth == $next_depth) {
410 $list .= "</li>";
411 }
412 // last heading
413 } else {
414 for ($inital_depth; $inital_depth < $this_depth; $inital_depth++) {
415 $list .= "</li></" . $listtype . ">\n";
416 }
417 }
418 }
419
420 if ($attributes['no_title'] == false) {
421 $html = "<h2 class=\"simpletoc-title\">" . __("Table of Contents", "simpletoc") . "</h2>";
422 }
423 $html .= "<" . $listtype . " class=\"simpletoc-list\" " . $styles ." " . $alignclass .">\n" . $list . "</li></" . $listtype . ">";
424
425 if($itemcount < 1) {
426 $html = '';
427 }
428
429 return $html;
430 }
431