PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 3.1.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v3.1.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 3.1.0, at includes/Utils/BlockTemplate.php

498 lines 20.7 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 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 = []; // 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 return $template;
144 }
145
146 /**
147 * Build a unified template object based on a theme file.
148 * 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.
149 *
150 * @param array|object $template_file Theme file.
151 * @param string $template_type wp_template or wp_template_part.
152 *
153 * @return \WP_Block_Template Template.
154 */
155 public function build_template_result_from_file( $template_file, $template_type ) {
156 $template_file = (object) $template_file;
157
158 // If the theme has an archive-docs.html template but does not have docs taxonomy templates
159 // then we will load in the archive-docs.html template from the theme to use for docs taxonomies on the frontend.
160 $template_is_from_theme = 'theme' === $template_file->source;
161 $theme_name = wp_get_theme()->get( 'TextDomain' );
162
163 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
164 $template_content = file_get_contents( $template_file->path );
165 $template = new \WP_Block_Template();
166 $template->id = $template_is_from_theme ? $theme_name . '//' . $template_file->slug : self::PLUGIN_SLUG . '//' . $template_file->slug;
167 $template->theme = $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG;
168 $template->content = $this->inject_theme_attribute_in_content( $template_content );
169 // 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.
170 $template->source = $template_file->source ? $template_file->source : 'plugin';
171 $template->slug = $template_file->slug;
172 $template->type = $template_type;
173 $template->title = ! empty( $template_file->title ) ? $template_file->title : $this->convert_slug_to_title( $template_file->slug );
174 $template->status = 'publish';
175 $template->has_theme_file = true;
176 $template->origin = $template_file->source;
177 $template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are.
178 $template->post_types = []; // Don't appear in any Edit Post template selector dropdown.
179 $template->area = 'uncategorized';
180 return $template;
181 }
182
183 /**
184 * Build a new template object so that we can make BetterDocs Blocks default templates available in the current theme should they not have any.
185 *
186 * @param string $template_file Block template file path.
187 * @param string $template_type wp_template or wp_template_part.
188 * @param string $template_slug Block template slug e.g. single-docs.
189 * @param bool $template_is_from_theme If the block template file is being loaded from the current theme instead of BetterDocs Blocks.
190 *
191 * @return object Block template object.
192 */
193 public function create_new_block_template_object( $template_file, $template_type, $template_slug, $template_is_from_theme = false ) {
194 $theme_name = wp_get_theme()->get( 'TextDomain' );
195
196 $new_template_item = [
197 'slug' => $template_slug,
198 'id' => $template_is_from_theme ? $theme_name . '//' . $template_slug : self::PLUGIN_SLUG . '//' . $template_slug,
199 'path' => $template_file,
200 'type' => $template_type,
201 'theme' => $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG,
202 // 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.
203 'source' => $template_is_from_theme ? 'theme' : 'plugin',
204 'title' => $this->convert_slug_to_title( $template_slug ),
205 'description' => '',
206 'post_types' => [] // Don't appear in any Edit Post template selector dropdown.
207 ];
208
209 return (object) $new_template_item;
210 }
211
212 /**
213 * Finds all nested template part file paths in a theme's directory.
214 *
215 * @param string $base_directory The theme's file path.
216 * @return array $path_list A list of paths to all template part files.
217 */
218 public function get_template_paths( $base_directory ) {
219 $path_list = [];
220 if ( file_exists( $base_directory ) ) {
221 $nested_files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $base_directory ) );
222 $nested_html_files = new \RegexIterator( $nested_files, '/^.+\.html$/i', \RecursiveRegexIterator::GET_MATCH );
223 foreach ( $nested_html_files as $path => $file ) {
224 $path_list[] = $path;
225 }
226 }
227 return $path_list;
228 }
229
230 /**
231 * Returns template titles.
232 *
233 * @param string $template_slug The templates slug (e.g. single-docs).
234 * @return string Human friendly title.
235 */
236 public function get_block_template_title( $template_slug ) {
237 $plugin_template_types = $this->get_plugin_block_template_types();
238 if ( isset( $plugin_template_types[$template_slug] ) ) {
239 return $plugin_template_types[$template_slug]['title'];
240 } else {
241 // Human friendly title converted from the slug.
242 return ucwords( preg_replace( '/[\-_]/', ' ', $template_slug ) );
243 }
244 }
245
246 /**
247 * Returns template descriptions.
248 *
249 * @param string $template_slug The templates slug (e.g. single-docs).
250 * @return string Template description.
251 */
252 public function get_block_template_description( $template_slug ) {
253 $plugin_template_types = $this->get_plugin_block_template_types();
254 if ( isset( $plugin_template_types[$template_slug] ) ) {
255 return $plugin_template_types[$template_slug]['description'];
256 }
257 return '';
258 }
259
260 public function get_templates_fils_from_betterdocs( $template_type ) {
261 $directory = $this->get_templates_directory( $template_type );
262 $template_files = $this->get_template_paths( $directory );
263 // var_dump($template_files);
264 return $template_files;
265 }
266
267 /**
268 * Gets the templates from the BetterDocs blocks directory
269 *
270 * @param string[] $slugs An array of slugs to filter templates by. Templates whose slug does not match will not be returned.
271 * @param array $already_found_templates Templates that have already been found, these are customised templates that are loaded from the database.
272 * @param string $template_type wp_template or wp_template_part.
273 *
274 * @return array Templates from the BetterDocs blocks plugin directory.
275 */
276 public function get_block_templates_from_betterdocs( $slugs, $already_found_templates, $template_type = 'wp_template' ) {
277
278 $template_files = $this->get_templates_fils_from_betterdocs( $template_type );
279 $templates = [];
280
281 foreach ( $template_files as $template_file ) {
282 $template_slug = $this->generate_template_slug_from_path( $template_file );
283
284 // This template does not have a slug we're looking for. Skip it.
285 if ( is_array( $slugs ) && count( $slugs ) > 0 && ! in_array( $template_slug, $slugs, true ) ) {
286 continue;
287 }
288
289 // If the the template is already in the list (i.e. it came from the
290 // database) then we should not overwrite it with the one from the filesystem.
291 if (
292 count(
293 array_filter(
294 $already_found_templates,
295 function ( $template ) use ( $template_slug ) {
296 $template_obj = (object) $template; //phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.Found
297 return $template_obj->slug === $template_slug;
298 }
299 )
300 ) > 0 ) {
301 continue;
302 }
303
304 // At this point the template only exists in the Blocks filesystem and has not been saved in the DB,
305 // or superseded by the theme.
306 $templates[] = $this->create_new_block_template_object( $template_file, $template_type, $template_slug );
307 }
308 return $templates;
309 }
310
311 /**
312 * Returns a filtered list of plugin template types, containing their
313 * localized titles and descriptions.
314 *
315 * @return array The plugin template types.
316 */
317 public function get_plugin_block_template_types() {
318 $plugin_template_types = [
319 'archive-docs' => [
320 'title' => _x( 'Docs Page', 'Template name', 'betterdocs' ),
321 'description' => __( 'Template used to display Docs Page.', 'betterdocs' )
322 ],
323 'taxonomy-doc_category' => [
324 'title' => _x( 'Docs Category', 'Template name', 'betterdocs' ),
325 'description' => __( 'Template used to display docs by category.', 'betterdocs' )
326 ],
327 'taxonomy-doc_tag' => [
328 'title' => _x( 'Docs Tag', 'Template name', 'betterdocs' ),
329 'description' => __( 'Template used to display docs by tag.', 'betterdocs' )
330 ],
331 'single-docs' => [
332 'title' => _x( 'Single Docs', 'Template name', 'betterdocs' ),
333 'description' => __( 'Template used to display the single docs.', 'betterdocs' )
334 ]
335 ];
336
337 return $plugin_template_types;
338 }
339
340 /**
341 * Converts template slugs into readable titles.
342 *
343 * @param string $template_slug The templates slug (e.g. single-docs).
344 * @return string Human friendly title converted from the slug.
345 */
346 public function convert_slug_to_title( $template_slug ) {
347 switch ( $template_slug ) {
348 case 'single-docs':
349 return __( 'Single Docs', 'betterdocs' );
350 case 'archive-docs':
351 return __( 'Docs Page', 'betterdocs' );
352 case 'taxonomy-doc_category':
353 return __( 'Docs Category', 'betterdocs' );
354 case 'taxonomy-doc_tag':
355 return __( 'Docs Tag', 'betterdocs' );
356 default:
357 // Replace all hyphens and underscores with spaces.
358 return ucwords( preg_replace( '/[\-_]/', ' ', $template_slug ) );
359 }
360 }
361
362 /**
363 * Converts template paths into a slug
364 *
365 * @param string $path The template's path.
366 * @return string slug
367 */
368 public function generate_template_slug_from_path( $path ) {
369 $template_extension = '.html';
370
371 return basename( $path, $template_extension );
372 }
373
374 /**
375 * 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.
376 *
377 * @return boolean
378 */
379 public function supports_block_templates() {
380 if (
381 ! betterdocs()->helper->current_theme_is_fse_theme() &&
382 ( ! function_exists( 'gutenberg_supports_block_templates' ) || ! gutenberg_supports_block_templates() )
383 ) {
384 return false;
385 }
386
387 return true;
388 }
389
390 /**
391 * Retrieves a single unified template object using its id.
392 *
393 * @param string $id Template unique identifier (example: theme_slug//template_slug).
394 * @param string $template_type Optional. Template type: `'wp_template'` or '`wp_template_part'`.
395 * Default `'wp_template'`.
396 *
397 * @return \WP_Block_Template|null Template.
398 */
399 public static function get_block_template( $id, $template_type ) {
400 if ( function_exists( 'get_block_template' ) ) {
401 return get_block_template( $id, $template_type );
402 }
403
404 if ( function_exists( 'gutenberg_get_block_template' ) ) {
405 return gutenberg_get_block_template( $id, $template_type );
406 }
407
408 return null;
409 }
410
411 /**
412 * Checks if we can fallback to the `archive-docs` template for a given slug
413 *
414 * `taxonomy-doc_category`, `taxonomy-knowledge_base` and `taxonomy-doc_tag` templates can generally use the
415 * `archive-docs` as a fallback if there are no specific overrides.
416 *
417 * @param string $template_slug Slug to check for fallbacks.
418 * @return boolean
419 */
420 public function template_is_eligible_for_docs_archive_fallback( $template_slug ) {
421 return in_array( $template_slug, self::ELIGIBLE_FOR_DOC_ARCHIVE_FALLBACK, true );
422 }
423
424 /**
425 * Sets the `has_theme_file` to `true` for templates with fallbacks
426 *
427 * There are cases (such as tags and categories) in which fallback templates
428 * can be used; so, while *technically* the theme doesn't have a specific file
429 * for them, it is important that we tell Gutenberg that we do, in fact,
430 * have a theme file (i.e. the fallback one).
431 *
432 * **Note:** this function changes the array that has been passed.
433 *
434 * It returns `true` if anything was changed, `false` otherwise.
435 *
436 * @param array $query_result Array of template objects.
437 * @param object $template A specific template object which could have a fallback.
438 *
439 * @return boolean
440 */
441 public function set_has_theme_file_if_fallback_is_available( $query_result, $template ) {
442 foreach ( $query_result as &$query_result_template ) {
443 if (
444 $query_result_template->slug === $template->slug
445 && $query_result_template->theme === $template->theme
446 ) {
447 if ( $this->template_is_eligible_for_docs_archive_fallback( $template->slug ) ) {
448 $query_result_template->has_theme_file = true;
449 }
450
451 return true;
452 }
453 }
454
455 return false;
456 }
457
458 /**
459 * Removes templates that were added to a theme's block-templates directory, but already had a customised version saved in the database.
460 *
461 * @param \WP_Block_Template[]|\stdClass[] $templates List of templates to run the filter on.
462 *
463 * @return array List of templates with duplicates removed. The customised alternative is preferred over the theme default.
464 */
465 public static function remove_theme_templates_with_custom_alternative( $templates ) {
466
467 // Get the slugs of all templates that have been customised and saved in the database.
468 $customised_template_slugs = array_map(
469 function ( $template ) {
470 return $template->slug;
471 },
472 array_values(
473 array_filter(
474 $templates,
475 function ( $template ) {
476 // This template has been customised and saved as a post.
477 return 'custom' === $template->source;
478 }
479 )
480 )
481 );
482
483 // Remove theme (i.e. filesystem) templates that have the same slug as a customised one. We don't need to check
484 // for `betterdocs` in $template->source here because betterdocs templates won't have been added to $templates
485 // if a saved version was found in the db. This only affects saved templates that were saved BEFORE a theme
486 // template with the same slug was added.
487 return array_values(
488 array_filter(
489 $templates,
490 function ( $template ) use ( $customised_template_slugs ) {
491 // This template has been customised and saved as a post, so return it.
492 return ! ( 'theme' === $template->source && in_array( $template->slug, $customised_template_slugs, true ) );
493 }
494 )
495 );
496 }
497 }
498