PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.2.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.2.0
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 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Utils / BlockTemplate.php

BlockTemplate.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.2.0, at includes/Utils/BlockTemplate.php

551 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace WPDeveloper\BetterDocs\Utils;
3
4 /**
5 * Utility methods used for serving block templates from BetterDocs Blocks.
6 * {@internal This class and its methods should only be used within the class-block-template-controller.php and is not intended for public use.}
7 */
8 class BlockTemplate {
9 const ELIGIBLE_FOR_DOC_ARCHIVE_FALLBACK = [ 'taxonomy-doc_category', 'taxonomy-doc_tag' ];
10
11 /**
12 * BetterDocs plugin slug
13 *
14 * This is used to save templates to the DB which are stored against this value in the wp_terms table.
15 *
16 * @var string
17 */
18 const PLUGIN_SLUG = 'betterdocs/betterdocs';
19
20 /**
21 * Gets the directory where templates of a specific template type can be found.
22 *
23 * @param string $template_type wp_template or wp_template_part.
24 *
25 * @return string
26 */
27 public function get_templates_directory( $template_type = 'wp_template' ) {
28 return BETTERDOCS_FSE_TEMPLATES_PATH . DIRECTORY_SEPARATOR;
29 }
30
31 /**
32 * Returns an array containing the references of
33 * the passed blocks and their inner blocks.
34 *
35 * @param array $blocks array of blocks.
36 *
37 * @return array block references to the passed blocks and their inner blocks.
38 */
39 public function flatten_blocks( &$blocks ) {
40 $all_blocks = [];
41 $queue = [];
42 foreach ( $blocks as &$block ) {
43 $queue[] = &$block;
44 }
45 $queue_count = count( $queue );
46
47 while ( $queue_count > 0 ) {
48 $block = &$queue[0];
49 array_shift( $queue );
50 $all_blocks[] = &$block;
51
52 if ( ! empty( $block['innerBlocks'] ) ) {
53 foreach ( $block['innerBlocks'] as &$inner_block ) {
54 $queue[] = &$inner_block;
55 }
56 }
57
58 $queue_count = count( $queue );
59 }
60
61 return $all_blocks;
62 }
63
64 /**
65 * Parses wp_template content and injects the current theme's
66 * stylesheet as a theme attribute into each wp_template_part
67 *
68 * @param string $template_content serialized wp_template content.
69 *
70 * @return string Updated wp_template content.
71 */
72 public function inject_theme_attribute_in_content( $template_content ) {
73 $has_updated_content = false;
74 $new_content = '';
75 $template_blocks = parse_blocks( $template_content );
76
77 $blocks = $this->flatten_blocks( $template_blocks );
78 foreach ( $blocks as &$block ) {
79 if (
80 'core/template-part' === $block['blockName'] &&
81 ! isset( $block['attrs']['theme'] )
82 ) {
83 $block['attrs']['theme'] = wp_get_theme()->get_stylesheet();
84 $has_updated_content = true;
85 }
86 }
87
88 if ( $has_updated_content ) {
89 foreach ( $template_blocks as &$block ) {
90 $new_content .= serialize_block( $block );
91 }
92
93 return $new_content;
94 }
95
96 return $template_content;
97 }
98
99 /**
100 * Build a unified template object based a post Object.
101 * Important: This method is an almost identical duplicate from wp-includes/block-template-utils.php as it was not intended for public use. It has been modified to build templates from plugins rather than themes.
102 *
103 * @param \WP_Post $post Template post.
104 *
105 * @return \WP_Block_Template|\WP_Error Template.
106 */
107 public static function build_template_result_from_post( $post ) {
108 $terms = get_the_terms( $post, 'wp_theme' );
109
110 if ( is_wp_error( $terms ) ) {
111 return $terms;
112 }
113
114 if ( ! $terms ) {
115 return new \WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.', 'betterdocs' ) );
116 }
117
118 $theme = $terms[0]->name;
119 $has_theme_file = true;
120
121 $template = new \WP_Block_Template();
122 $template->wp_id = $post->ID;
123 $template->id = $theme . '//' . $post->post_name;
124 $template->theme = $theme;
125 $template->content = $post->post_content;
126 $template->slug = $post->post_name;
127 $template->source = 'custom';
128 $template->type = $post->post_type;
129 $template->description = $post->post_excerpt;
130 $template->title = $post->post_title;
131 $template->status = $post->post_status;
132 $template->has_theme_file = $has_theme_file;
133 $template->is_custom = false;
134 $template->post_types = array(); // Don't appear in any Edit Post template selector dropdown.
135
136 if ( 'wp_template_part' === $post->post_type ) {
137 $type_terms = get_the_terms( $post, 'wp_template_part_area' );
138 if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) {
139 $template->area = $type_terms[0]->name;
140 }
141 }
142
143 // We are checking 'woocommerce' to maintain classic templates which are saved to the DB,
144 // prior to updating to use the correct slug.
145 // More information found here: https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5423.
146 if ( self::PLUGIN_SLUG === $theme ) {
147 $template->origin = 'plugin';
148 }
149
150 /*
151 * Run the block hooks algorithm introduced in WP 6.4 on the template content.
152 */
153 if ( function_exists( 'inject_ignored_hooked_blocks_metadata_attributes' ) ) {
154 $hooked_blocks = get_hooked_blocks();
155 if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) {
156 $before_block_visitor = make_before_block_visitor( $hooked_blocks, $template );
157 $after_block_visitor = make_after_block_visitor( $hooked_blocks, $template );
158 $blocks = parse_blocks( $template->content );
159 $template->content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
160 }
161 }
162
163 return $template;
164 }
165
166 /**
167 * Build a unified template object based on a theme file.
168 *
169 * @internal Important: This method is an almost identical duplicate from wp-includes/block-template-utils.php as it was not intended for public use. It has been modified to build templates from plugins rather than themes.
170 *
171 * @param array|object $template_file Theme file.
172 * @param string $template_type wp_template or wp_template_part.
173 *
174 * @return \WP_Block_Template Template.
175 */
176 public function build_template_result_from_file( $template_file, $template_type ) {
177 $template_file = (object) $template_file;
178
179 // If the theme has an archive-products.html template but does not have product taxonomy templates
180 // then we will load in the archive-product.html template from the theme to use for product taxonomies on the frontend.
181 $template_is_from_theme = 'theme' === $template_file->source;
182 $theme_name = wp_get_theme()->get( 'TextDomain' );
183
184 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
185 $template_content = file_get_contents( $template_file->path );
186 $template = new \WP_Block_Template();
187 $template->id = $template_is_from_theme ? $theme_name . '//' . $template_file->slug : self::PLUGIN_SLUG . '//' . $template_file->slug;
188 $template->theme = $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG;
189 $template->content = self::inject_theme_attribute_in_content( $template_content );
190 // Remove the term description block from the archive-product template
191 // as the Product Catalog/Shop page doesn't have a description.
192 if ( 'archive-docs' === $template_file->slug ) {
193 $template->content = str_replace( '<!-- wp:term-description {"align":"wide"} /-->', '', $template->content );
194 }
195 // Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909.
196 $template->source = $template_file->source ? $template_file->source : 'plugin';
197 $template->slug = $template_file->slug;
198 $template->type = $template_type;
199 $template->title = ! empty( $template_file->title ) ? $template_file->title : self::get_block_template_title( $template_file->slug );
200 $template->description = ! empty( $template_file->description ) ? $template_file->description : self::get_block_template_description( $template_file->slug );
201 $template->status = 'publish';
202 $template->has_theme_file = true;
203 $template->origin = $template_file->source;
204 $template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are.
205 $template->post_types = array(); // Don't appear in any Edit Post template selector dropdown.
206 $template->area = 'uncategorized';
207
208 /*
209 * Run the block hooks algorithm introduced in WP 6.4 on the template content.
210 */
211 if ( function_exists( 'inject_ignored_hooked_blocks_metadata_attributes' ) ) {
212 $before_block_visitor = '_inject_theme_attribute_in_template_part_block';
213 $after_block_visitor = null;
214 $hooked_blocks = get_hooked_blocks();
215 if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) {
216 $before_block_visitor = make_before_block_visitor( $hooked_blocks, $template );
217 $after_block_visitor = make_after_block_visitor( $hooked_blocks, $template );
218 }
219 $blocks = parse_blocks( $template->content );
220 $template->content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
221 }
222
223 return $template;
224 }
225
226 /**
227 * Build a new template object so that we can make BetterDocs Blocks default templates available in the current theme should they not have any.
228 *
229 * @param string $template_file Block template file path.
230 * @param string $template_type wp_template or wp_template_part.
231 * @param string $template_slug Block template slug e.g. single-docs.
232 * @param bool $template_is_from_theme If the block template file is being loaded from the current theme instead of BetterDocs Blocks.
233 *
234 * @return object Block template object.
235 */
236 public function create_new_block_template_object( $template_file, $template_type, $template_slug, $template_is_from_theme = false ) {
237 $theme_name = wp_get_theme()->get( 'TextDomain' );
238
239 $new_template_item = [
240 'slug' => $template_slug,
241 'id' => $template_is_from_theme ? $theme_name . '//' . $template_slug : self::PLUGIN_SLUG . '//' . $template_slug,
242 'path' => $template_file,
243 'type' => $template_type,
244 'theme' => $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG,
245 // Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909.
246 'source' => $template_is_from_theme ? 'theme' : 'plugin',
247 'title' => $this->convert_slug_to_title( $template_slug ),
248 'description' => '',
249 'post_types' => [] // Don't appear in any Edit Post template selector dropdown.
250 ];
251
252 return (object) $new_template_item;
253 }
254
255 /**
256 * Finds all nested template part file paths in a theme's directory.
257 *
258 * @param string $base_directory The theme's file path.
259 * @return array $path_list A list of paths to all template part files.
260 */
261 public function get_template_paths( $base_directory ) {
262 $path_list = [];
263 if ( file_exists( $base_directory ) ) {
264 $nested_files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $base_directory ) );
265 $nested_html_files = new \RegexIterator( $nested_files, '/^.+\.html$/i', \RecursiveRegexIterator::GET_MATCH );
266 foreach ( $nested_html_files as $path => $file ) {
267 $path_list[] = $path;
268 }
269 }
270 return $path_list;
271 }
272
273 /**
274 * Returns template titles.
275 *
276 * @param string $template_slug The templates slug (e.g. single-docs).
277 * @return string Human friendly title.
278 */
279 public function get_block_template_title( $template_slug ) {
280 $plugin_template_types = $this->get_plugin_block_template_types();
281 if ( isset( $plugin_template_types[ $template_slug ] ) ) {
282 return $plugin_template_types[ $template_slug ]['title'];
283 } else {
284 // Human friendly title converted from the slug.
285 return ucwords( preg_replace( '/[\-_]/', ' ', $template_slug ) );
286 }
287 }
288
289 /**
290 * Returns template descriptions.
291 *
292 * @param string $template_slug The templates slug (e.g. single-docs).
293 * @return string Template description.
294 */
295 public function get_block_template_description( $template_slug ) {
296 $plugin_template_types = $this->get_plugin_block_template_types();
297 if ( isset( $plugin_template_types[ $template_slug ] ) ) {
298 return $plugin_template_types[ $template_slug ]['description'];
299 }
300 return '';
301 }
302
303 public function get_templates_fils_from_betterdocs( $template_type ) {
304 $directory = $this->get_templates_directory( $template_type );
305 $template_files = $this->get_template_paths( $directory );
306 return $template_files;
307 }
308
309 /**
310 * Gets the templates from the BetterDocs blocks directory
311 *
312 * @param string[] $slugs An array of slugs to filter templates by. Templates whose slug does not match will not be returned.
313 * @param array $already_found_templates Templates that have already been found, these are customised templates that are loaded from the database.
314 * @param string $template_type wp_template or wp_template_part.
315 *
316 * @return array Templates from the BetterDocs blocks plugin directory.
317 */
318 public function get_block_templates_from_betterdocs( $slugs, $already_found_templates, $template_type = 'wp_template' ) {
319
320 $template_files = $this->get_templates_fils_from_betterdocs( $template_type );
321 $templates = [];
322
323 // Check if BetterDocs Pro is not installed
324 if ( ! betterdocs()->is_pro_active() || ( betterdocs()->is_pro_active() && betterdocs()->settings->get( 'multiple_kb' ) == false ) ) {
325 // Remove the 'taxonomy-knowledge_base.html' file
326 $template_files = array_filter(
327 $template_files,
328 function ( $file ) {
329 return strpos( $file, 'taxonomy-knowledge_base.html' ) === false;
330 }
331 );
332 }
333
334 foreach ( $template_files as $template_file ) {
335 $template_slug = $this->generate_template_slug_from_path( $template_file );
336
337 // This template does not have a slug we're looking for. Skip it.
338 if ( is_array( $slugs ) && count( $slugs ) > 0 && ! in_array( $template_slug, $slugs, true ) ) {
339 continue;
340 }
341
342 // If the the template is already in the list (i.e. it came from the
343 // database) then we should not overwrite it with the one from the filesystem.
344 if (
345 count(
346 array_filter(
347 $already_found_templates,
348 function ( $template ) use ( $template_slug ) {
349 $template_obj = (object) $template; //phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.Found
350 return $template_obj->slug === $template_slug;
351 }
352 )
353 ) > 0 ) {
354 continue;
355 }
356
357 // At this point the template only exists in the Blocks filesystem and has not been saved in the DB,
358 // or superseded by the theme.
359 $templates[] = $this->create_new_block_template_object( $template_file, $template_type, $template_slug );
360 }
361 return $templates;
362 }
363
364 /**
365 * Returns a filtered list of plugin template types, containing their
366 * localized titles and descriptions.
367 *
368 * @return array The plugin template types.
369 */
370 public function get_plugin_block_template_types() {
371 $plugin_template_types = [
372 'archive-docs' => [
373 'title' => _x( 'Docs Page', 'Template name', 'betterdocs' ),
374 'description' => __( 'Template used to display Docs Page.', 'betterdocs' )
375 ],
376 'taxonomy-doc_category' => [
377 'title' => _x( 'Docs Category', 'Template name', 'betterdocs' ),
378 'description' => __( 'Template used to display docs by category.', 'betterdocs' )
379 ],
380 'taxonomy-doc_tag' => [
381 'title' => _x( 'Docs Tag', 'Template name', 'betterdocs' ),
382 'description' => __( 'Template used to display docs by tag.', 'betterdocs' )
383 ],
384 'single-docs' => [
385 'title' => _x( 'Single Docs', 'Template name', 'betterdocs' ),
386 'description' => __( 'Template used to display the single docs.', 'betterdocs' )
387 ]
388 ];
389
390 return $plugin_template_types;
391 }
392
393 /**
394 * Converts template slugs into readable titles.
395 *
396 * @param string $template_slug The templates slug (e.g. single-docs).
397 * @return string Human friendly title converted from the slug.
398 */
399 public function convert_slug_to_title( $template_slug ) {
400 switch ( $template_slug ) {
401 case 'single-docs':
402 return __( 'Single Docs', 'betterdocs' );
403 case 'archive-docs':
404 return __( 'Docs Page', 'betterdocs' );
405 case 'taxonomy-doc_category':
406 return __( 'Docs Category', 'betterdocs' );
407 case 'taxonomy-doc_tag':
408 return __( 'Docs Tag', 'betterdocs' );
409 default:
410 // Replace all hyphens and underscores with spaces.
411 return ucwords( preg_replace( '/[\-_]/', ' ', $template_slug ) );
412 }
413 }
414
415 /**
416 * Converts template paths into a slug
417 *
418 * @param string $path The template's path.
419 * @return string slug
420 */
421 public function generate_template_slug_from_path( $path ) {
422 $template_extension = '.html';
423
424 return basename( $path, $template_extension );
425 }
426
427 /**
428 * Checks to see if they are using a compatible version of WP, or if not they have a compatible version of the Gutenberg plugin installed.
429 *
430 * @return boolean
431 */
432 public function supports_block_templates() {
433 if (
434 ! betterdocs()->helper->current_theme_is_fse_theme() &&
435 ( ! function_exists( 'gutenberg_supports_block_templates' ) || ! gutenberg_supports_block_templates() )
436 ) {
437 return false;
438 }
439
440 return true;
441 }
442
443 /**
444 * Retrieves a single unified template object using its id.
445 *
446 * @param string $id Template unique identifier (example: theme_slug//template_slug).
447 * @param string $template_type Optional. Template type: `'wp_template'` or '`wp_template_part'`.
448 * Default `'wp_template'`.
449 *
450 * @return \WP_Block_Template|null Template.
451 */
452 public static function get_block_template( $id, $template_type ) {
453 if ( function_exists( 'get_block_template' ) ) {
454 return get_block_template( $id, $template_type );
455 }
456
457 if ( function_exists( 'gutenberg_get_block_template' ) ) {
458 return gutenberg_get_block_template( $id, $template_type );
459 }
460
461 return null;
462 }
463
464 /**
465 * Checks if we can fallback to the `archive-docs` template for a given slug
466 *
467 * `taxonomy-doc_category`, `taxonomy-knowledge_base` and `taxonomy-doc_tag` templates can generally use the
468 * `archive-docs` as a fallback if there are no specific overrides.
469 *
470 * @param string $template_slug Slug to check for fallbacks.
471 * @return boolean
472 */
473 public function template_is_eligible_for_docs_archive_fallback( $template_slug ) {
474 return in_array( $template_slug, self::ELIGIBLE_FOR_DOC_ARCHIVE_FALLBACK, true );
475 }
476
477 /**
478 * Sets the `has_theme_file` to `true` for templates with fallbacks
479 *
480 * There are cases (such as tags and categories) in which fallback templates
481 * can be used; so, while *technically* the theme doesn't have a specific file
482 * for them, it is important that we tell Gutenberg that we do, in fact,
483 * have a theme file (i.e. the fallback one).
484 *
485 * **Note:** this function changes the array that has been passed.
486 *
487 * It returns `true` if anything was changed, `false` otherwise.
488 *
489 * @param array $query_result Array of template objects.
490 * @param object $template A specific template object which could have a fallback.
491 *
492 * @return boolean
493 */
494 public function set_has_theme_file_if_fallback_is_available( $query_result, $template ) {
495 foreach ( $query_result as &$query_result_template ) {
496 if (
497 $query_result_template->slug === $template->slug
498 && $query_result_template->theme === $template->theme
499 ) {
500 if ( $this->template_is_eligible_for_docs_archive_fallback( $template->slug ) ) {
501 $query_result_template->has_theme_file = true;
502 }
503
504 return true;
505 }
506 }
507
508 return false;
509 }
510
511 /**
512 * Removes templates that were added to a theme's block-templates directory, but already had a customised version saved in the database.
513 *
514 * @param \WP_Block_Template[]|\stdClass[] $templates List of templates to run the filter on.
515 *
516 * @return array List of templates with duplicates removed. The customised alternative is preferred over the theme default.
517 */
518 public static function remove_theme_templates_with_custom_alternative( $templates ) {
519
520 // Get the slugs of all templates that have been customised and saved in the database.
521 $customised_template_slugs = array_map(
522 function ( $template ) {
523 return $template->slug;
524 },
525 array_values(
526 array_filter(
527 $templates,
528 function ( $template ) {
529 // This template has been customised and saved as a post.
530 return 'custom' === $template->source;
531 }
532 )
533 )
534 );
535
536 // Remove theme (i.e. filesystem) templates that have the same slug as a customised one. We don't need to check
537 // for `betterdocs` in $template->source here because betterdocs templates won't have been added to $templates
538 // if a saved version was found in the db. This only affects saved templates that were saved BEFORE a theme
539 // template with the same slug was added.
540 return array_values(
541 array_filter(
542 $templates,
543 function ( $template ) use ( $customised_template_slugs ) {
544 // This template has been customised and saved as a post, so return it.
545 return ! ( 'theme' === $template->source && in_array( $template->slug, $customised_template_slugs, true ) );
546 }
547 )
548 );
549 }
550 }
551