PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 2.5.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v2.5.5
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 / BlockTemplateUtils.php

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

443 lines 18.4 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\Utils;
4
5 /**
6 * Utility methods used for serving block templates from BetterDocs Blocks.
7 * {@internal This class and its methods should only be used within the class-block-template-controller.php and is not intended for public use.}
8 */
9 class BlockTemplateUtils {
10 const ELIGIBLE_FOR_DOC_ARCHIVE_FALLBACK = [ 'taxonomy-knowledge_base', 'taxonomy-doc_category', 'taxonomy-doc_tag' ];
11
12 /**
13 * BetterDocs plugin slug
14 *
15 * This is used to save templates to the DB which are stored against this value in the wp_terms table.
16 *
17 * @var string
18 */
19 const PLUGIN_SLUG = 'betterdocs/betterdocs';
20
21 /**
22 * Returns an array containing the references of
23 * the passed blocks and their inner blocks.
24 *
25 * @param array $blocks array of blocks.
26 *
27 * @return array block references to the passed blocks and their inner blocks.
28 */
29 public static function flatten_blocks( &$blocks ) {
30 $all_blocks = [];
31 $queue = [];
32 foreach ( $blocks as &$block ) {
33 $queue[] = &$block;
34 }
35 $queue_count = count( $queue );
36
37 while ( $queue_count > 0 ) {
38 $block = &$queue[0];
39 array_shift( $queue );
40 $all_blocks[] = &$block;
41
42 if ( ! empty( $block['innerBlocks'] ) ) {
43 foreach ( $block['innerBlocks'] as &$inner_block ) {
44 $queue[] = &$inner_block;
45 }
46 }
47
48 $queue_count = count( $queue );
49 }
50
51 return $all_blocks;
52 }
53
54 /**
55 * Parses wp_template content and injects the current theme's
56 * stylesheet as a theme attribute into each wp_template_part
57 *
58 * @param string $template_content serialized wp_template content.
59 *
60 * @return string Updated wp_template content.
61 */
62 public static function inject_theme_attribute_in_content( $template_content ) {
63 $has_updated_content = false;
64 $new_content = '';
65 $template_blocks = parse_blocks( $template_content );
66
67 $blocks = self::flatten_blocks( $template_blocks );
68 foreach ( $blocks as &$block ) {
69 if (
70 'core/template-part' === $block['blockName'] &&
71 ! isset( $block['attrs']['theme'] )
72 ) {
73 $block['attrs']['theme'] = wp_get_theme()->get_stylesheet();
74 $has_updated_content = true;
75 }
76 }
77
78 if ( $has_updated_content ) {
79 foreach ( $template_blocks as &$block ) {
80 $new_content .= serialize_block( $block );
81 }
82
83 return $new_content;
84 }
85
86 return $template_content;
87 }
88
89 /**
90 * Build a unified template object based a post Object.
91 * 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.
92 *
93 * @param \WP_Post $post Template post.
94 *
95 * @return \WP_Block_Template|\WP_Error Template.
96 */
97 public static function build_template_result_from_post( $post ) {
98 $terms = get_the_terms( $post, 'wp_theme' );
99
100 if ( is_wp_error( $terms ) ) {
101 return $terms;
102 }
103
104 if ( ! $terms ) {
105 return new \WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.', 'betterdocs' ) );
106 }
107
108 $theme = $terms[0]->name;
109 $has_theme_file = true;
110
111 $template = new \WP_Block_Template();
112 $template->wp_id = $post->ID;
113 $template->id = $theme . '//' . $post->post_name;
114 $template->theme = $theme;
115 $template->content = $post->post_content;
116 $template->slug = $post->post_name;
117 $template->source = 'custom';
118 $template->type = $post->post_type;
119 $template->description = $post->post_excerpt;
120 $template->title = $post->post_title;
121 $template->status = $post->post_status;
122 $template->has_theme_file = $has_theme_file;
123 $template->is_custom = false;
124 $template->post_types = []; // Don't appear in any Edit Post template selector dropdown.
125
126 if ( 'wp_template_part' === $post->post_type ) {
127 $type_terms = get_the_terms( $post, 'wp_template_part_area' );
128 if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) {
129 $template->area = $type_terms[0]->name;
130 }
131 }
132
133 return $template;
134 }
135
136 /**
137 * Build a unified template object based on a theme file.
138 * 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.
139 *
140 * @param array|object $template_file Theme file.
141 * @param string $template_type wp_template or wp_template_part.
142 *
143 * @return \WP_Block_Template Template.
144 */
145 public static function build_template_result_from_file( $template_file, $template_type ) {
146 $template_file = (object) $template_file;
147
148 // If the theme has an archive-docs.html template but does not have docs taxonomy templates
149 // then we will load in the archive-docs.html template from the theme to use for docs taxonomies on the frontend.
150 $template_is_from_theme = 'theme' === $template_file->source;
151 $theme_name = wp_get_theme()->get( 'TextDomain' );
152
153 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
154 $template_content = file_get_contents( $template_file->path );
155 $template = new \WP_Block_Template();
156 $template->id = $template_is_from_theme ? $theme_name . '//' . $template_file->slug : self::PLUGIN_SLUG . '//' . $template_file->slug;
157 $template->theme = $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG;
158 $template->content = self::inject_theme_attribute_in_content( $template_content );
159 // 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.
160 $template->source = $template_file->source ? $template_file->source : 'plugin';
161 $template->slug = $template_file->slug;
162 $template->type = $template_type;
163 $template->title = ! empty( $template_file->title ) ? $template_file->title : self::convert_slug_to_title( $template_file->slug );
164 $template->status = 'publish';
165 $template->has_theme_file = true;
166 $template->origin = $template_file->source;
167 $template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are.
168 $template->post_types = []; // Don't appear in any Edit Post template selector dropdown.
169 $template->area = 'uncategorized';
170 return $template;
171 }
172
173 /**
174 * Build a new template object so that we can make BetterDocs Blocks default templates available in the current theme should they not have any.
175 *
176 * @param string $template_file Block template file path.
177 * @param string $template_type wp_template or wp_template_part.
178 * @param string $template_slug Block template slug e.g. single-docs.
179 * @param bool $template_is_from_theme If the block template file is being loaded from the current theme instead of BetterDocs Blocks.
180 *
181 * @return object Block template object.
182 */
183 public static function create_new_block_template_object( $template_file, $template_type, $template_slug, $template_is_from_theme = false ) {
184 $theme_name = wp_get_theme()->get( 'TextDomain' );
185
186 $new_template_item = [
187 'slug' => $template_slug,
188 'id' => $template_is_from_theme ? $theme_name . '//' . $template_slug : self::PLUGIN_SLUG . '//' . $template_slug,
189 'path' => $template_file,
190 'type' => $template_type,
191 'theme' => $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG,
192 // 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.
193 'source' => $template_is_from_theme ? 'theme' : 'plugin',
194 'title' => self::convert_slug_to_title( $template_slug ),
195 'description' => '',
196 'post_types' => [] // Don't appear in any Edit Post template selector dropdown.
197 ];
198
199 return (object) $new_template_item;
200 }
201
202 /**
203 * Finds all nested template part file paths in a theme's directory.
204 *
205 * @param string $base_directory The theme's file path.
206 * @return array $path_list A list of paths to all template part files.
207 */
208 public static function get_template_paths( $base_directory ) {
209 $path_list = [];
210 if ( file_exists( $base_directory ) ) {
211 $nested_files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $base_directory ) );
212 $nested_html_files = new \RegexIterator( $nested_files, '/^.+\.html$/i', \RecursiveRegexIterator::GET_MATCH );
213 foreach ( $nested_html_files as $path => $file ) {
214 $path_list[] = $path;
215 }
216 }
217 return $path_list;
218 }
219
220 /**
221 * Returns template titles.
222 *
223 * @param string $template_slug The templates slug (e.g. single-docs).
224 * @return string Human friendly title.
225 */
226 public static function get_block_template_title( $template_slug ) {
227 $plugin_template_types = self::get_plugin_block_template_types();
228 if ( isset( $plugin_template_types[$template_slug] ) ) {
229 return $plugin_template_types[$template_slug]['title'];
230 } else {
231 // Human friendly title converted from the slug.
232 return ucwords( preg_replace( '/[\-_]/', ' ', $template_slug ) );
233 }
234 }
235
236 /**
237 * Returns template descriptions.
238 *
239 * @param string $template_slug The templates slug (e.g. single-docs).
240 * @return string Template description.
241 */
242 public static function get_block_template_description( $template_slug ) {
243 $plugin_template_types = self::get_plugin_block_template_types();
244 if ( isset( $plugin_template_types[$template_slug] ) ) {
245 return $plugin_template_types[$template_slug]['description'];
246 }
247 return '';
248 }
249
250 /**
251 * Returns a filtered list of plugin template types, containing their
252 * localized titles and descriptions.
253 *
254 * @return array The plugin template types.
255 */
256 public static function get_plugin_block_template_types() {
257 $plugin_template_types = [
258 'archive-docs' => [
259 'title' => _x( 'Docs Page', 'Template name', 'betterdocs' ),
260 'description' => __( 'Template used to display docs page.', 'betterdocs' )
261 ],
262 'taxonomy-knowledge_base' => [
263 'title' => _x( 'Knowledge Base', 'Template name', 'betterdocs' ),
264 'description' => __( 'Template used to display Knowledge Bases.', 'betterdocs' )
265 ],
266 'taxonomy-doc_category' => [
267 'title' => _x( 'Doc Category', 'Template name', 'betterdocs' ),
268 'description' => __( 'Template used to display docs by category.', 'betterdocs' )
269 ],
270 'taxonomy-doc_tag' => [
271 'title' => _x( 'Doc Tag', 'Template name', 'betterdocs' ),
272 'description' => __( 'Template used to display docs by tag.', 'betterdocs' )
273 ],
274 'single-docs' => [
275 'title' => _x( 'Single Docs', 'Template name', 'betterdocs' ),
276 'description' => __( 'Template used to display the single docs.', 'betterdocs' )
277 ]
278 ];
279
280 return $plugin_template_types;
281 }
282
283 /**
284 * Converts template slugs into readable titles.
285 *
286 * @param string $template_slug The templates slug (e.g. single-docs).
287 * @return string Human friendly title converted from the slug.
288 */
289 public static function convert_slug_to_title( $template_slug ) {
290 switch ( $template_slug ) {
291 case 'single-docs':
292 return __( 'Single Docs', 'betterdocs' );
293 case 'archive-docs':
294 return __( 'Docs Page', 'betterdocs' );
295 case 'taxonomy-knowledge_base':
296 return __( 'Knowledge Base', 'betterdocs' );
297 case 'taxonomy-doc_category':
298 return __( 'Docs Category', 'betterdocs' );
299 case 'taxonomy-doc_tag':
300 return __( 'Docs Tag', 'betterdocs' );
301 default:
302 // Replace all hyphens and underscores with spaces.
303 return ucwords( preg_replace( '/[\-_]/', ' ', $template_slug ) );
304 }
305 }
306
307 /**
308 * Converts template paths into a slug
309 *
310 * @param string $path The template's path.
311 * @return string slug
312 */
313 public static function generate_template_slug_from_path( $path ) {
314 $template_extension = '.html';
315
316 return basename( $path, $template_extension );
317 }
318
319 /**
320 * 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.
321 *
322 * @return boolean
323 */
324 public static function supports_block_templates() {
325 if (
326 ! betterdocs()->helper->current_theme_is_fse_theme() &&
327 ( ! function_exists( 'gutenberg_supports_block_templates' ) || ! gutenberg_supports_block_templates() )
328 ) {
329 return false;
330 }
331
332 return true;
333 }
334
335 /**
336 * Retrieves a single unified template object using its id.
337 *
338 * @param string $id Template unique identifier (example: theme_slug//template_slug).
339 * @param string $template_type Optional. Template type: `'wp_template'` or '`wp_template_part'`.
340 * Default `'wp_template'`.
341 *
342 * @return \WP_Block_Template|null Template.
343 */
344 public static function get_block_template( $id, $template_type ) {
345 if ( function_exists( 'get_block_template' ) ) {
346 return get_block_template( $id, $template_type );
347 }
348
349 if ( function_exists( 'gutenberg_get_block_template' ) ) {
350 return gutenberg_get_block_template( $id, $template_type );
351 }
352
353 return null;
354 }
355
356 /**
357 * Checks if we can fallback to the `archive-docs` template for a given slug
358 *
359 * `taxonomy-doc_category`, `taxonomy-knowledge_base` and `taxonomy-doc_tag` templates can generally use the
360 * `archive-docs` as a fallback if there are no specific overrides.
361 *
362 * @param string $template_slug Slug to check for fallbacks.
363 * @return boolean
364 */
365 public static function template_is_eligible_for_docs_archive_fallback( $template_slug ) {
366 return in_array( $template_slug, self::ELIGIBLE_FOR_DOC_ARCHIVE_FALLBACK, true );
367 }
368
369 /**
370 * Sets the `has_theme_file` to `true` for templates with fallbacks
371 *
372 * There are cases (such as tags and categories) in which fallback templates
373 * can be used; so, while *technically* the theme doesn't have a specific file
374 * for them, it is important that we tell Gutenberg that we do, in fact,
375 * have a theme file (i.e. the fallback one).
376 *
377 * **Note:** this function changes the array that has been passed.
378 *
379 * It returns `true` if anything was changed, `false` otherwise.
380 *
381 * @param array $query_result Array of template objects.
382 * @param object $template A specific template object which could have a fallback.
383 *
384 * @return boolean
385 */
386 public static function set_has_theme_file_if_fallback_is_available( $query_result, $template ) {
387 foreach ( $query_result as &$query_result_template ) {
388 if (
389 $query_result_template->slug === $template->slug
390 && $query_result_template->theme === $template->theme
391 ) {
392 if ( self::template_is_eligible_for_docs_archive_fallback( $template->slug ) ) {
393 $query_result_template->has_theme_file = true;
394 }
395
396 return true;
397 }
398 }
399
400 return false;
401 }
402
403 /**
404 * Removes templates that were added to a theme's block-templates directory, but already had a customised version saved in the database.
405 *
406 * @param \WP_Block_Template[]|\stdClass[] $templates List of templates to run the filter on.
407 *
408 * @return array List of templates with duplicates removed. The customised alternative is preferred over the theme default.
409 */
410 public static function remove_theme_templates_with_custom_alternative( $templates ) {
411
412 // Get the slugs of all templates that have been customised and saved in the database.
413 $customised_template_slugs = array_map(
414 function ( $template ) {
415 return $template->slug;
416 },
417 array_values(
418 array_filter(
419 $templates,
420 function ( $template ) {
421 // This template has been customised and saved as a post.
422 return 'custom' === $template->source;
423 }
424 )
425 )
426 );
427
428 // Remove theme (i.e. filesystem) templates that have the same slug as a customised one. We don't need to check
429 // for `betterdocs` in $template->source here because betterdocs templates won't have been added to $templates
430 // if a saved version was found in the db. This only affects saved templates that were saved BEFORE a theme
431 // template with the same slug was added.
432 return array_values(
433 array_filter(
434 $templates,
435 function ( $template ) use ( $customised_template_slugs ) {
436 // This template has been customised and saved as a post, so return it.
437 return ! ( 'theme' === $template->source && in_array( $template->slug, $customised_template_slugs, true ) );
438 }
439 )
440 );
441 }
442 }
443