PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.4
4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 All 202 releases
betterdocs / includes / Shortcodes / ToC.php

ToC.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.4, at includes/Shortcodes/ToC.php

205 lines 8.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\Shortcodes;
4
5 use WPDeveloper\BetterDocs\Utils\Node;
6 use WPDeveloper\BetterDocs\Core\Shortcode;
7
8 class ToC extends Shortcode {
9 protected $html_attributes = [];
10
11 public function get_name() {
12 return 'betterdocs_toc';
13 }
14
15 public function get_style_depends() {
16 return [ 'betterdocs-toc' ];
17 }
18
19 /**
20 * Summary of default_attributes
21 * @return array
22 */
23 public function default_attributes() {
24 return [
25 'post_type' => 'docs',
26 'post_id' => get_the_ID(),
27 'htags' => '1,2,3,4,5,6',
28 'hierarchy' => '',
29 'list_number' => '',
30 'collapsible_on_mobile' => $this->settings->get( 'collapsible_toc_mobile', false ),
31 'toc_title' => ''
32 ];
33 }
34
35 public function render( $atts, $content = null ) {
36 // Check if post is password protected and user hasn't provided correct password
37 if ( post_password_required( $this->attributes['post_id'] ) ) {
38 // Don't show ToC for password-protected posts until password is provided
39 return '';
40 }
41
42 if ( '1' == $this->attributes['collapsible_on_mobile'] && false === has_action( 'wp_footer', [ __CLASS__, 'print_collapsible_toggle' ] ) ) {
43 add_action( 'wp_footer', [ __CLASS__, 'print_collapsible_toggle' ], 5 );
44 }
45
46 $this->views( 'shortcodes/toc' );
47 }
48
49 /**
50 * "Collapsible on small devices" is toggled by betterdocs.js, which only
51 * docs pages load. On other pages (the shortcode, block or widget placed
52 * on a page) the title did nothing and the list stayed hidden on phones,
53 * so add the same toggle there. The CSS limits it to small screens.
54 */
55 public static function print_collapsible_toggle() {
56 if ( wp_script_is( 'betterdocs', 'enqueued' ) ) {
57 return;
58 }
59
60 $js = <<<'JS'
61 document.addEventListener('click',function(e){var t=e.target.closest?e.target.closest('.betterdocs-toc.collapsible-sm .toc-title'):null;if(!t)return;e.preventDefault();
62 function s(el){el.style.display=getComputedStyle(el).display==='none'?'block':'none';}
63 t.querySelectorAll('.angle-icon').forEach(s);var l=t.nextElementSibling;if(l&&l.classList.contains('toc-list'))s(l);});
64 JS;
65 wp_register_script( 'betterdocs-toc-collapsible', false, [], BETTERDOCS_VERSION, true );
66 wp_add_inline_script( 'betterdocs-toc-collapsible', $js );
67 wp_enqueue_script( 'betterdocs-toc-collapsible' );
68 }
69
70 public function view_params() {
71 return [
72 'post' => get_post( $this->attributes['post_id'] )
73 ];
74 }
75
76 /**
77 * Process content for TOC generation without triggering heavy content filters
78 * This handles special characters while avoiding memory issues from plugins like WPML
79 *
80 * @param string $content Raw post content
81 * @return string Processed content
82 */
83 public function process_content_for_toc( $content ) {
84 // Check if we should use the full content filter (for backward compatibility)
85 $use_full_filter = apply_filters( 'betterdocs_toc_use_full_content_filter', false );
86
87 if ( $use_full_filter ) {
88 // Use the full content filter if explicitly enabled
89 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- intentional WP core filter integration to render content as the platform would.
90 return apply_filters( 'the_content', $content );
91 }
92
93 // Apply only essential content processing filters that handle special characters
94 // without triggering heavy processing from plugins like WPML, page builders, etc.
95
96 // Handle shortcodes first (but don't execute them, just remove them to avoid conflicts)
97 $content = strip_shortcodes( $content );
98
99 // Decode HTML entities to handle special characters properly
100 $content = html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );
101
102 // Convert line breaks to proper HTML if needed
103 $content = wpautop( $content );
104
105 // Apply specific filters that are safe and necessary for TOC generation
106 // These are lightweight filters that handle character encoding and basic formatting
107 $content = apply_filters( 'betterdocs_toc_content_processing', $content );
108
109 // Additional safety: limit content size to prevent memory issues
110 $max_content_length = apply_filters( 'betterdocs_toc_max_content_length', 500000 ); // 500KB default
111 if ( strlen( $content ) > $max_content_length ) {
112 $content = substr( $content, 0, $max_content_length );
113 }
114
115 return $content;
116 }
117
118 /**
119 * This method is responsible for re-arranging the TOC data based on hierarchy or non-hierarchy
120 *
121 * @param string $post_content
122 * @param string $htag_support
123 * @return Node|null
124 */
125 public function format_toc_data( $post_content, $htag_support, $toc_hierarchy ) {
126 $matches = [];
127
128 if ( $htag_support != '' ) {
129 preg_match_all( '/(<h([' . $htag_support . ']{1})[^>]*>).*<\/h\2>/msuU', $post_content, $matches, PREG_SET_ORDER );
130 }
131
132 if ( ! empty( $matches ) ) {
133 /*
134 |--------------------------------------------------------------------------
135 | Backtracking Algorithm Using Iteration | Main Login For Hierarchy TOC
136 |--------------------------------------------------------------------------
137 |
138 | Initially an object with key null and empty item of arrays are inserted into the stack of arrays.
139 | When inside the loop condition for the first time, the last stack value is assigned in a variable $last_data.
140 | And a new node object $new_data which is instantiated and the tag number is inserted for comparison as key, and empty items
141 | as a array are inserted into items property of the new node object. On the 'if' condition it checks, if the current tag number
142 | is smaller or equal to the last stack number. If the condition is true, the it enters into the while loop condition, which also
143 | checks if the current last stack tag number is greater or equal to the current tag number. It pops values from the stack until and unless the
144 | condition becomes false. The while loop condition becomes false only when the last stack value tag number becomes null and the last stack number is not
145 | greater or equal to the current node tag number.
146 |
147 | Backtracking occurs when the current tag_number $number[2] is less than or equal to the stacks last tag_number which is assigned as $last_data
148 |
149 | And then the last value is inserted as the new last_data, and the new_data node is inserted into the last_data node items.
150 | Additionally the $new_data is inserted into the stack to keep track of the used node. Somehow if the if condition becomes false
151 | then the stack last data is taken, and the new_data is inserted into the last_data->items and the new_data is inserted into the stack.
152 |
153 */
154
155 $dynamic_toc_title_switch = $this->settings->get( 'toc_dynamic_title' );
156 $stack = [];
157 $root = new Node();
158 $root->key = null;
159 $root->items = [];
160 $tag_counter = 0;
161
162 array_push( $stack, $root );
163
164 foreach ( $matches as $number ) {
165 $last_data = $stack[ count( $stack ) - 1 ];
166 $current_tag_number = isset( $number[2] ) ? $number[2] : '';
167 $current_title = isset( $number[0] ) ? $number[0] : '';
168 $current_tag = isset( $number[1] ) ? $number[1] : '';
169
170 $heading_name = preg_replace( '/<[^<]+?>/', '', $current_title );
171 $heading_name = ! empty( $heading_name ) ? strtolower( str_replace( ' ', '-', preg_replace( '/[^\p{L}\p{N}\s]/u', '', $heading_name ) ) ) : '';
172 preg_match( '/id="(.+?)"/', $current_title, $matches_id );
173 $heading_id = isset( $matches_id[1] ) ? strtolower( $matches_id[1] ) : '';
174 $tag_number = ! empty( $heading_id ) ? $heading_id : ( ! empty( $heading_name ) && $dynamic_toc_title_switch ? $heading_name : $tag_counter . '-toc-title' );
175
176 $new_data = new Node();
177 $new_data->key = $current_tag_number;
178 $new_data->tag = $current_tag;
179 $new_data->title = $current_title;
180 $new_data->tag_number = $tag_number;
181 $new_data->items = [];
182
183 if ( $last_data->key != null && $current_tag_number <= $last_data->key ) {
184 while ( $stack[ count( $stack ) - 1 ]->key != null && $stack[ count( $stack ) - 1 ]->key >= $current_tag_number ) {
185 array_pop( $stack );
186 }
187 $last_data = $stack[ count( $stack ) - 1 ];
188 }
189
190 array_push( $last_data->items, $new_data );
191
192 if ( $toc_hierarchy != 'off' && $toc_hierarchy != '' ) {
193 array_push( $stack, $new_data );
194 }
195
196 ++$tag_counter;
197 }
198
199 return $root;
200 }
201
202 return null;
203 }
204 }
205