-
-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathplugin.php
767 lines (654 loc) · 29.3 KB
/
plugin.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
<?php
/**
* Plugin Name: SimpleTOC - Table of Contents Block
* Plugin URI: https://marc.tv/simpletoc-wordpress-inhaltsverzeichnis-plugin-gutenberg/
* Description: SEO-friendly Table of Contents Gutenberg block. No JavaScript and no CSS means faster loading.
* Version: 6.6.1
* Author: Marc Tönsing
* Author URI: https://toensing.com
* Text Domain: simpletoc
* License: GPL v2 or later
* License URI: http://www.gnu.org/licenses/gpl-2.0.html
*
*/
require_once plugin_dir_path(__FILE__) . 'simpletoc-admin-settings.php';
/**
* Prevents direct execution of the plugin file.
* If a WordPress function does not exist, it means that the file has not been run by WordPress.
*/
if (!function_exists('add_filter')) {
header('Status: 403 Forbidden');
header('HTTP/1.1 403 Forbidden');
exit;
}
/**
* Registers the SimpleTOC block, adds a filter for plugin row meta, and sets script translations.
*
* This function registers the SimpleTOC block by specifying the build directory and render callback function.
* It also sets the script translations for the block editor script and adds a filter for the plugin row meta.
*/
function register_simpletoc_block()
{
if (function_exists('wp_set_script_translations')) {
wp_set_script_translations('simpletoc-toc-editor-script', 'simpletoc');
}
add_filter('plugin_row_meta', __NAMESPACE__ . '\\simpletoc_plugin_meta', 10, 2);
register_block_type(__DIR__ . '/build', [
'render_callback' => __NAMESPACE__ . '\\render_callback_simpletoc'
]);
}
add_action('init', 'register_simpletoc_block');
/**
* Inject potentially missing translations into the block-editor i18n
* collection.
*
* This keeps the plugin backwards compatible, in case the user did not
* update translations on their website (yet).
*
* @param string|false|null $translations JSON-encoded translation data. Default null.
* @param string|false $file Path to the translation file to load. False if there isn't one.
* @param string $handle Name of the script to register a translation domain to.
* @param string $domain The text domain.
*
* @return string|false|null JSON string
*/
add_filter('load_script_translations', function ($translations, $file, $handle, $domain) {
if ('simpletoc' === $domain && $translations) {
// List of translations that we inject into the block-editor JS.
$dynamic_translations = [
'Table of Contents' => __('Table of Contents', 'simpletoc'),
];
$changed = false;
$obj = json_decode($translations, true);
// Confirm that the translation JSON is valid.
if (isset($obj['locale_data']) && isset($obj['locale_data']['messages'])) {
$messages = $obj['locale_data']['messages'];
// Inject dynamic translations, when needed.
foreach ($dynamic_translations as $key => $locale) {
if (empty($messages[$key])
|| !is_array($messages[$key])
|| !array_key_exists(0, $messages[$key])
|| $locale !== $messages[$key][0]
) {
$messages[$key] = [$locale];
$changed = true;
}
}
// Only modify the translations string when locales did change.
if ($changed) {
$obj['locale_data']['messages'] = $messages;
$translations = wp_json_encode($obj);
}
}
}
return $translations;
}, 10, 4);
/**
* Sets the default value of translatable attributes.
*
* Values inside block.json are static strings that are not translated. This
* filter inserts relevant translations i
*
* @param array $settings Array of determined settings for registering a block type.
* @param array $metadata Metadata provided for registering a block type.
*
* @return array Modified settings array.
*/
add_filter('block_type_metadata_settings', function ($settings, $metadata) {
if ('simpletoc/toc' === $metadata['name']) {
$settings['attributes']['title_text']['default'] = __('Table of Contents', 'simpletoc');
}
return $settings;
}, 10, 2);
/**
* Filter to add plugins to the TOC list for Rank Math plugin
*
* @param array TOC plugins.
*/
add_filter('rank_math/researches/toc_plugins', function ($toc_plugins) {
$toc_plugins['simpletoc/plugin.php'] = 'SimpleTOC';
return $toc_plugins;
});
/**
* Adds IDs to the headings of the provided post content using a recursive block structure.
* @param string $content The content to add IDs to
* @return string The content with IDs added to its headings
*/
function simpletoc_add_ids_to_content($content)
{
$blocks = parse_blocks($content);
$blocks = add_ids_to_blocks_recursive($blocks);
$content = serialize_blocks($blocks);
return $content;
}
add_filter('the_content', 'simpletoc_add_ids_to_content', 1);
/**
* Recursively adds IDs to the headings of a nested block structure.
* @param array $blocks The blocks to add IDs to
* @return array The blocks with IDs added to their headings
*/
function add_ids_to_blocks_recursive($blocks)
{
foreach ($blocks as &$block) {
if (isset($block['blockName']) && ($block['blockName'] === 'core/heading' || $block['blockName'] === 'generateblocks/headline') && isset($block['innerHTML']) && isset($block['innerContent']) && isset($block['innerContent'][0])) {
$block['innerHTML'] = add_anchor_attribute($block['innerHTML']);
$block['innerContent'][0] = add_anchor_attribute($block['innerContent'][0]);
} elseif (isset($block['attrs']['ref'])) {
// search in reusable blocks (this is not finished because I ran out of ideas.)
// $reusable_block_id = $block['attrs']['ref'];
// $reusable_block_content = parse_blocks(get_post($reusable_block_id)->post_content);
} elseif (!empty($block['innerBlocks'])) {
// search in groups
$block['innerBlocks'] = add_ids_to_blocks_recursive($block['innerBlocks']);
}
}
return $blocks;
}
/**
* Renders a Table of Contents block for a post
* @param array $attributes An array of attributes for the Table of Contents block
* @return string The HTML output for the Table of Contents block
*/
function render_callback_simpletoc($attributes)
{
$is_backend = defined('REST_REQUEST') && REST_REQUEST && 'edit' === filter_input(INPUT_GET, 'context');
$title_text = $attributes['title_text'] ? esc_html(trim($attributes['title_text'])) : __('Table of Contents', 'simpletoc');
$alignclass = !empty($attributes['align']) ? 'align' . $attributes['align'] : '';
$className = !empty($attributes['className']) ? strip_tags($attributes['className']) : '';
$title_level = $attributes['title_level'];
$wrapper_enabled = apply_filters('simpletoc_wrapper_enabled', false) || get_option('simpletoc_wrapper_enabled') == 1 || get_option('simpletoc_accordion_enabled') == 1;
$wrapper_attrs = get_block_wrapper_attributes(['class' => 'simpletoc']);
$pre_html = (!empty($className) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper']) ? '<div role="navigation" aria-label="' . __('Table of Contents', 'simpletoc') . '" ' . $wrapper_attrs . '>' : '';
$post_html = (!empty($className) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper']) ? '</div>' : '';
$post = get_post();
$blocks = !is_null($post) && !is_null($post->post_content) ? parse_blocks($post->post_content) : '';
$headings = array_reverse(filter_headings_recursive($blocks));
$headings = simpletoc_add_pagenumber($blocks, $headings);
$headings_clean = array_map('trim', $headings);
$toc_html = generate_toc($headings_clean, $attributes);
if (empty($blocks)) {
return get_empty_blocks_message($is_backend, $attributes, $title_level, $alignclass, $title_text, __('No blocks found.', 'simpletoc'), __('Save or update post first.', 'simpletoc'));
}
if (empty($headings_clean)) {
return get_empty_blocks_message($is_backend, $attributes, $title_level, $alignclass, $title_text, __('No headings found.', 'simpletoc'), __('Save or update post first.', 'simpletoc'));
}
if (empty($toc_html)) {
return get_empty_blocks_message($is_backend, $attributes, $title_level, $alignclass, $title_text, __('No headings found.', 'simpletoc'), __('Check minimal and maximum level block settings.', 'simpletoc'));
}
return $pre_html . $toc_html . $post_html;
}
/**
* Generates an HTML message for empty blocks cases in the Table of Contents.
*
* @param bool $is_backend Indicates if the request is from the backend (i.e., the WordPress editor).
* @param array $attributes An array of attributes for the Table of Contents block.
* @param int $title_level The heading level for the Table of Contents title.
* @param string $alignclass The CSS class for alignment of the Table of Contents block.
* @param string $title_text The text for the Table of Contents title.
* @param string $warning_text1 The first part of the warning message to be displayed.
* @param string $warning_text2 The second part of the warning message to be displayed.
*
* @return string The HTML output for the empty blocks message.
*/
function get_empty_blocks_message($is_backend, $attributes, $title_level, $alignclass, $title_text, $warning_text1, $warning_text2)
{
$html = '';
if ($is_backend) {
$html .= sprintf('<h%d class="simpletoc-title %s">%s</h%d>', $title_level, $alignclass, $title_text, $title_level);
$html .= sprintf('<p class="components-notice is-warning %s">%s %s</p>', $alignclass, $warning_text1, $warning_text2);
}
return $html;
}
/**
* Adds page numbers to headings in the provided blocks array.
* @param array $blocks The array of blocks to process.
* @param array $headings The array of headings to add page numbers to.
* @return array The modified headings array with page numbers added.
*/
function simpletoc_add_pagenumber($blocks, $headings)
{
$pages = 1;
if (!is_array($blocks)) {
return $headings;
}
foreach ($blocks as $block => $innerBlock) {
// count nextpage blocks
if (isset($blocks[$block]['blockName']) && $blocks[$block]['blockName'] === 'core/nextpage') {
$pages++;
}
if (isset($blocks[$block]['blockName']) && $blocks[$block]["blockName"] === 'core/heading') {
// make sure its a headline.
foreach ($headings as $heading => &$innerHeading) {
if ($innerHeading == $blocks[$block]["innerHTML"]) {
$innerHeading = preg_replace("/(<h1|<h2|<h3|<h4|<h5|<h6)/i", '$1 data-page="' . $pages . '"', $blocks[$block]["innerHTML"]);
}
}
}
}
return $headings;
}
/**
* Return all headings with a recursive walk through all blocks.
* This includes groups and reusable block with groups within reusable blocks.
* @var array[] $blocks
* @return array[]
*/
function filter_headings_recursive($blocks)
{
$arr = [];
if (!is_array($blocks)) {
return $arr;
}
// allow developers to ignore specific blocks
$ignored_blocks = apply_filters('simpletoc_excluded_blocks', []);
foreach ($blocks as $innerBlock) {
if (is_array($innerBlock)) {
// if block is ignored, skip
if (isset($innerBlock['blockName']) && in_array($innerBlock['blockName'], $ignored_blocks)) {
continue;
}
if (isset($innerBlock['attrs']['ref'])) {
// search in reusable blocks
$post = get_post($innerBlock['attrs']['ref']);
if ($post) {
$e_arr = parse_blocks($post->post_content);
$arr = array_merge(filter_headings_recursive($e_arr), $arr);
}
} else {
// search in groups
$arr = array_merge(filter_headings_recursive($innerBlock), $arr);
}
} else {
if (isset($blocks['blockName']) && ($blocks['blockName'] === 'core/heading') && $innerBlock !== 'core/heading') {
// make sure it's a headline.
if (preg_match("/(<h1|<h2|<h3|<h4|<h5|<h6)/i", $innerBlock)) {
$arr[] = $innerBlock;
}
}
if (isset($blocks['blockName']) && ($blocks['blockName'] === 'generateblocks/headline') && $innerBlock !== 'core/heading') {
// make sure it's a headline.
if (preg_match("/(<h1|<h2|<h3|<h4|<h5|<h6)/i", $innerBlock)) {
$arr[] = $innerBlock;
}
}
}
}
return $arr;
}
/**
* Sanitizes a string to be used as an anchor attribute in HTML by removing punctuation, non-breaking spaces, umlauts, and accents,
* and replacing whitespace and other characters with dashes.
* @param string $string The input string to be sanitized.
* @return string The sanitized string encoded for use in a URL.
*/
function simpletoc_sanitize_string($string)
{
// remove punctuation
$zero_punctuation = preg_replace("/\p{P}/u", "", $string);
// remove non-breaking spaces
$html_wo_nbs = str_replace(" ", " ", $zero_punctuation);
// remove umlauts and accents
$string_without_accents = remove_accents($html_wo_nbs);
// Sanitizes a title, replacing whitespace and a few other characters with dashes.
$sanitized_string = sanitize_title_with_dashes($string_without_accents);
// Encode for use in an url
$urlencoded = urlencode($sanitized_string);
return $urlencoded;
}
/**
* Add additional plugin meta links to the SimpleTOC plugin page.
* @param array $links An array of plugin meta links.
* @param string $file The plugin file path.
* @return array The modified array of plugin meta links.
*/
function simpletoc_plugin_meta($links, $file)
{
if (false !== strpos($file, 'simpletoc')) {
$links = array_merge($links, array('<a href="https://wordpress.org/support/plugin/simpletoc">' . __('Support', 'simpletoc') . '</a>'));
$links = array_merge($links, array('<a href="https://marc.tv/out/donate">' . __('Donate', 'simpletoc') . '</a>'));
$links = array_merge($links, array('<a href="https://wordpress.org/support/plugin/simpletoc/reviews/#new-post">' . __('Write a review', 'simpletoc') . ' ⭐️⭐️⭐️⭐️⭐️</a>'));
}
return $links;
}
/**
* Adds an ID attribute to all Heading tags in the provided HTML.
* @param string $html The HTML content to modify
* @return string The modified HTML content with ID attributes added to the Heading tags
*/
function add_anchor_attribute($html)
{
// remove non-breaking space entites from input HTML
$html_wo_nbs = str_replace(" ", " ", $html);
// Thank you Nick Diego
if (!$html_wo_nbs) {
return $html;
}
libxml_use_internal_errors(true);
$dom = new \DOMDocument();
@$dom->loadHTML('<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $html_wo_nbs, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
// use xpath to select the Heading html tags.
$xpath = new \DOMXPath($dom);
$tags = $xpath->evaluate("//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]");
// Loop through all the found tags
foreach ($tags as $tag) {
// if tag already has an attribute "id" defined, no need for creating a new one
if (!empty($tag->getAttribute('id'))) {
continue;
}
// Set id attribute
$heading_text = trim(strip_tags($html));
$anchor = simpletoc_sanitize_string($heading_text);
$tag->setAttribute("id", $anchor);
}
// Save the HTML changes
$content = $dom->saveHTML($dom->documentElement);
return $content;
}
/**
* Generates a table of contents based on the provided headings and attributes
* @param array $headings An array of headings to include in the table of contents
* @param array $attributes An array of attributes to customize the output
* @return string The generated table of contents as HTML
*/
function generate_toc($headings, $attributes)
{
$list = '';
$html = '';
$min_depth = 6;
$initial_depth = 6;
$align_class = isset($attributes['align']) ? 'align' . $attributes['align'] : '';
$styles = $attributes['remove_indent'] ? 'style="padding-left:0;list-style:none;"' : '';
$list_type = $attributes['use_ol'] ? 'ol' : 'ul';
$global_absolut_urls_enabled = get_option('simpletoc_absolute_urls_enabled', false);
$absolute_url = $attributes['use_absolute_urls'] || $global_absolut_urls_enabled ? get_permalink() : '';
list($min_depth, $initial_depth) = find_min_depth($headings, $attributes);
$item_count = 0;
foreach ($headings as $line => $headline) {
$this_depth = (int)$headings[$line][2];
$next_depth = isset($headings[$line + 1][2]) ? (int)$headings[$line + 1][2] : '';
$exclude_headline = should_exclude_headline($headline, $attributes, $this_depth);
$title = trim(strip_tags($headline));
$customId = extract_id($headline);
$link = $customId ? $customId : simpletoc_sanitize_string($title);
if (!$exclude_headline) {
$item_count++;
open_list($list, $list_type, $min_depth, $this_depth);
$page = get_page_number_from_headline($headline);
$list .= "<a href=\"" . $absolute_url . $page . "#" . $link . "\">" . $title . "</a>" . PHP_EOL;
}
close_list($list, $list_type, $min_depth, $attributes['min_level'], $attributes['max_level'], $next_depth, $line, count($headings) - 1, $initial_depth, $this_depth);
}
$html = add_accordion_start($html, $attributes, $item_count, $align_class);
$html = add_hidden_markup_start($html, $attributes, $item_count, $align_class);
$html = add_smooth($html, $attributes);
// Add the table of contents list to the output if the list is not empty.
if (!empty($list)) {
$html_class = 'simpletoc-list';
if (!empty($align_class)) {
$html_class .= " $align_class";
}
$html_style = '';
if (!empty($styles)) {
$html_style = " $styles";
}
$html .= "<$list_type class=\"$html_class\"$html_style>\n$list</li></$list_type>";
}
$html = add_accordion_end($html, $attributes);
$html = add_hidden_markup_end($html, $attributes);
// return an emtpy string if stripped result is empty
if (empty(trim(strip_tags($html)))) {
$html = '';
}
return $html;
}
/**
* Finds the minimum depth level of headings in the provided array and adjusts it based on the provided attributes
* @param array $headings An array of headings to search through
* @param array $attributes An array of attributes to adjust the minimum depth level
* @return array An array containing the minimum depth level and the initial depth level
*/
function find_min_depth($headings, $attributes)
{
$min_depth = 6;
$initial_depth = 6;
foreach ($headings as $line => $headline) {
if ($min_depth > $headings[$line][2]) {
$min_depth = (int)$headings[$line][2];
$initial_depth = $min_depth;
}
}
if ($attributes['min_level'] > $min_depth) {
$min_depth = $attributes['min_level'];
$initial_depth = $min_depth;
}
return [$min_depth, $initial_depth];
}
/**
* Determines if a given headline should be excluded based on the provided attributes
* @param string $headline The headline to check for exclusion
* @param array $attributes An array of attributes to use for exclusion
* @param int $this_depth The depth level of the headline
* @return bool True if the headline should be excluded, false otherwise
*/
function should_exclude_headline($headline, $attributes, $this_depth)
{
$exclude_headline = false;
preg_match('/class="([^"]+)"/', $headline, $matches);
if (!empty($matches[1]) && strpos($matches[1], 'simpletoc-hidden') !== false) {
$exclude_headline = true;
}
return ($this_depth > $attributes['max_level'] || $exclude_headline || $this_depth < $attributes['min_level']);
}
/**
* The open_list function appends a new list item to the global $list variable, adding necessary opening tags if needed to maintain the correct nesting of the list.
* @param string &$list The global list variable to append the new list item to.
* @param string $list_type The type of list to be created, either "ul" (unordered list) or "ol" (ordered list).
* @param int &$min_depth The minimum depth of headings that should be included in the table of contents.
* @param int $this_depth The depth of the current heading being processed.
* @return void The function modifies the input $list variable directly.
*/
function open_list(&$list, $list_type, &$min_depth, $this_depth)
{
if ($this_depth == $min_depth) {
$list .= "<li>";
} else {
for ($min_depth; $min_depth < $this_depth; $min_depth++) {
$list .= "\n<" . $list_type . "><li>\n";
}
}
}
/**
* Closes an HTML list tag and updates the list string and minimum depth variable as necessary.
* @param string $list A reference to the list string being built.
* @param string $list_type The type of list tag being used (ul or ol).
* @param int $min_depth A reference to the minimum depth variable.
* @param int $min_depth Minimum depth setting, which is a low number like 1.
* @param int $max_depth Maximum depth setting, which is a high number like 6.
* @param int|null $next_depth The depth of the next list item, or null if this is the last item.
* @param int $line The index of the current list item.
* @param int $last_line The index of the last list item.
* @param int $initial_depth The initial depth of the list.
* @param int $this_depth The depth of the current list item.
* @return void
*/
function close_list(&$list, $list_type, &$min_depth, $min_level, $max_level, $next_depth, $line, $last_line, $initial_depth, $this_depth)
{
if ($line !== $last_line) {
$list .= PHP_EOL;
if($next_depth < $this_depth) {
// Next heading goes back shallower in the ToC!
if($next_depth >= $min_level) {
// Next heading is within min depth bounds and WILL get ToC'd
// Close this item and step back shallower in the ToC.
for ($min_depth; $min_depth > $next_depth; $min_depth--) {
$list .= "</li>\n</" . $list_type . ">\n";
}
} else {
// SKIP CLOSING! Next heading won't be included in the ToC at all.
}
} elseif($next_depth === $this_depth) {
// Next heading is exactly as deep. Not going shallower or deeper in the ToC hierarchy.
// E.g. this is h3, next is h3
if ($next_depth < $min_level) {
// E.g. this is h3, next is h3, min is h2
// This heading didn't open a ToC item. Nothing to close.
} else {
// SKIP CLOSING! Next heading will open a new sub-list in the ToC.
$list .= "</li>\n";
}
} else {
// Next heading is deeper in the ToC.
if ($next_depth <= $max_level) {
// Next deeper heading is within bounds and will open a new sub-list. Leave this one open.
// E.g. this is h3, next is h4, min is h2, max is h5
} else {
// Next heading is too deep and will be ignored. We'll close out coming up or finishing the ToC.
// E.g. this is h3, next is h4, max is h3
}
}
} else {
// This is the last line of the ToC. Close out the whole thing.
// IMPORTANT NOTE: The overall ToC list will be wrapped in a list element and closed out.
for ($initial_depth; $initial_depth < $this_depth; $initial_depth++) {
$list .= "</li>\n</" . $list_type . ">\n";
}
}
}
/**
* Adds smooth scrolling styles to the output HTML, if enabled by global option or block attribute.
* @param string $html The HTML string to which the styles will be added.
* @param array $attributes An array of block attributes.
* @return string The modified HTML string with the added smooth scrolling styles.
*/
function add_smooth($html, $attributes)
{
// Add smooth scrolling styles, if enabled by global option or block attribute
$isSmoothEnabled = $attributes['add_smooth'] || get_option('simpletoc_smooth_enabled') == 1;
$html .= $isSmoothEnabled ? '<style>html { scroll-behavior: smooth; }</style>' : '';
return $html;
}
/**
* Enqueues the necessary CSS and JS files for the accordion functionality on the frontend.
*/
function enqueue_accordion_frontend()
{
wp_enqueue_script(
'simpletoc-accordion',
plugin_dir_url(__FILE__) . 'assets/accordion.js',
array(),
'6.6.1',
true
);
wp_enqueue_style(
'simpletoc-accordion',
plugin_dir_url(__FILE__) . 'assets/accordion.css',
array(),
'6.6.1'
);
}
function add_hidden_markup_start($html, $attributes, $itemcount, $alignclass) {
$isHiddenEnabled = $attributes['hidden'];
if ($isHiddenEnabled) {
$titleText = esc_html(trim($attributes['title_text'])) ?: __('Table of Contents', 'simpletoc');
$hiddenStart = '<details id="simpletoc-details" class="simpletoc" aria-labelledby="simpletoc-title">
<summary style="cursor: pointer;">' . $titleText . '</summary>';
$html .= $hiddenStart;
}
// If there are no items in the table of contents, return an empty string
if ($itemcount < 1) {
return '';
}
return $html;
}
function add_hidden_markup_end($html, $attributes)
{
$isHiddenEnabled = $attributes['hidden'];
if ($isHiddenEnabled) {
$html .= '</details>';
}
return $html;
}
/**
* Adds the opening HTML tag(s) for the accordion element and the table of contents title, if applicable.
* @param string $html The HTML string to add the opening tag(s) to
* @param array $attributes The attributes of the table of contents block
* @param int $itemcount The number of items in the table of contents
* @param string $alignclass The alignment class for the table of contents block
*/
function add_accordion_start($html, $attributes, $itemcount, $alignclass)
{
// Check if accordion is enabled either through the function arguments or the options
$isAccordionEnabled = $attributes['accordion'] || get_option('simpletoc_accordion_enabled') == 1;
$isHiddenEnabled = $attributes['hidden'];
// Start and end HTML for accordion, if enabled
$accordionStart = '';
if ($isAccordionEnabled) {
enqueue_accordion_frontend();
$titleText = esc_html(trim($attributes['title_text'])) ?: __('Table of Contents', 'simpletoc');
$accordionStart = '<h2 style="margin: 0;"><button type="button" aria-expanded="false" aria-controls="simpletoc-content-container" class="simpletoc-collapsible">' . $titleText . '<span class="simpletoc-icon" aria-hidden="true"></span></button></h2><div id="simpletoc-content-container" class="simpletoc-content">';
}
// Add the accordion start HTML to the output
$html .= $accordionStart;
// Add the table of contents title, if not hidden and not in accordion mode
$showTitle = !$attributes['no_title'] && !$isAccordionEnabled && !$isHiddenEnabled;
if ($showTitle) {
$titleTag = $attributes['title_level'] > 0 ? "h{$attributes['title_level']}" : 'p';
$html_class = 'simpletoc-title';
if (!empty($alignclass)) {
$html_class .= " $alignclass";
}
$html = "<$titleTag class=\"$html_class\">{$attributes["title_text"]}</$titleTag>\n";
}
// If there are no items in the table of contents, return an empty string
if ($itemcount < 1) {
return '';
}
return $html;
}
/**
* Adds the closing HTML tag(s) for the accordion element if the accordion is enabled.
* @param string $html The HTML string to add the closing tag(s) to
* @param array $attributes The attributes of the table of contents block
* @return string The modified HTML string with the closing tag(s) added
*/
function add_accordion_end($html, $attributes)
{
// Check if accordion is enabled either through the function arguments or the options
$isAccordionEnabled = $attributes['accordion'] || get_option('simpletoc_accordion_enabled') == 1;
if ($isAccordionEnabled) {
$html .= '</div>';
}
return $html;
}
/**
* Extracts the ID value from the provided heading HTML string.
* @param string $headline The heading HTML string to extract the ID value from
* @return mixed Returns the extracted ID value, or false if no ID value is found
*/
function extract_id($headline)
{
$pattern = '/id="([^"]*)"/';
preg_match($pattern, $headline, $matches);
$idValue = $matches[1] ?? false;
if ($idValue != false) {
return $idValue;
}
}
/**
* Gets the page number from a headline string.
* @param string $headline The headline string.
* @return string The page number (in the format "X/") if it exists and is greater than 1, or an empty string otherwise.
*/
function get_page_number_from_headline($headline)
{
$dom = new \DOMDocument();
@$dom->loadHTML('<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $headline, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$xpath = new \DOMXPath($dom);
$nodes = $xpath->query('//*/@data-page');
if (isset($nodes[0]) && $nodes[0]->nodeValue > 1) {
$pageNumber = $nodes[0]->nodeValue . '/';
return esc_html($pageNumber);
} else {
return '';
}
}