From c6d9adc54a4a3176c2385d84f3bdc7bf2aaff837 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 3 Nov 2020 07:30:47 +0100 Subject: [PATCH 01/13] Fix REST API auto-draft creation --- ...lass-wp-rest-template-parts-controller.php | 130 ++++++++++++++++++ lib/load.php | 5 + lib/template-loader.php | 112 +-------------- lib/template-parts.php | 66 ++------- 4 files changed, 149 insertions(+), 164 deletions(-) create mode 100644 lib/class-wp-rest-template-parts-controller.php diff --git a/lib/class-wp-rest-template-parts-controller.php b/lib/class-wp-rest-template-parts-controller.php new file mode 100644 index 00000000000000..3c604e95dd2d2c --- /dev/null +++ b/lib/class-wp-rest-template-parts-controller.php @@ -0,0 +1,130 @@ + 'wp_template_part', + 'post_status' => array( 'publish', 'auto-draft' ), + 'title' => $slug, + 'meta_key' => 'theme', + 'meta_value' => $theme, + 'posts_per_page' => 1, + 'no_found_rows' => true, + ) + ); + $template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null; + if ( ! $template_part_post ) { + wp_insert_post( + array( + 'post_content' => $content, + 'post_title' => $slug, + 'post_status' => 'auto-draft', + 'post_type' => 'wp_template_part', + 'post_name' => $slug, + 'meta_input' => array( + 'theme' => $theme, + ), + ) + ); + } else { + // Potentially we could decide to update the content if different. + } +} + +/** + * Create the template parts auto-drafts for the current theme. + * + * @access private + */ +function create_theme_template_parts() { + /** + * Finds all nested template part file paths in a theme's directory. + * + * @param string $base_directory The theme's file path. + * @return array $path_list A list of paths to all template part files. + */ + function get_template_part_paths( $base_directory ) { + $path_list = array(); + if ( file_exists( $base_directory . '/block-template-parts' ) ) { + $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory . '/block-template-parts' ) ); + $nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH ); + foreach ( $nested_html_files as $path => $file ) { + $path_list[] = $path; + } + } + return $path_list; + } + + // Get file paths for all theme supplied template parts. + $template_part_files = get_template_part_paths( get_stylesheet_directory() ); + if ( is_child_theme() ) { + $template_part_files = array_merge( $template_part_files, get_template_part_paths( get_template_directory() ) ); + } + // Build and save each template part. + foreach ( $template_part_files as $template_part_file ) { + $content = file_get_contents( $template_part_file ); + $slug = substr( + $template_part_file, + // Starting position of slug. + strpos( $template_part_file, 'block-template-parts/' ) + 21, + // Subtract ending '.html'. + -5 + ); + create_auto_draft_for_template_part_block( $slug, wp_get_theme()->get( 'TextDomain' ), $content ); + } +} + +/** + * Core class used to access menu templte parts via the REST API. + * + * @see WP_REST_Controller + */ +class WP_REST_Template_Parts_Controller extends WP_REST_Posts_Controller { + + /** + * Retrieves a list of template parts. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + create_theme_template_parts(); + + return parent::get_items( $request ); + } + + /** + * Retrieves a single template parat. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + create_theme_template_parts(); + + return parent::get_items( $request ); + } +} diff --git a/lib/load.php b/lib/load.php index 05d2cde5cb2c10..e4f18bc8afa01e 100644 --- a/lib/load.php +++ b/lib/load.php @@ -93,9 +93,14 @@ function gutenberg_is_experiment_enabled( $name ) { if ( ! class_exists( 'WP_Block_List' ) ) { require dirname( __FILE__ ) . '/class-wp-block-list.php'; } + if ( ! class_exists( 'WP_Widget_Block' ) ) { require_once dirname( __FILE__ ) . '/class-wp-widget-block.php'; } + +if ( ! class_exists( 'WP_REST_Template_Parts_Controller' ) ) { + require_once dirname( __FILE__ ) . '/class-wp-rest-template-parts-controller.php'; +} require_once dirname( __FILE__ ) . '/widgets-page.php'; require dirname( __FILE__ ) . '/compat.php'; diff --git a/lib/template-loader.php b/lib/template-loader.php index 23829d94d565e0..e16251e65a4572 100644 --- a/lib/template-loader.php +++ b/lib/template-loader.php @@ -123,83 +123,6 @@ function gutenberg_override_query_template( $template, $type, array $templates = return gutenberg_dir_path() . 'lib/template-canvas.php'; } -/** - * Recursively traverses a block tree, creating auto drafts - * for any encountered template parts without a fixed post. - * - * @access private - * - * @param array $block The root block to start traversing from. - * @return int[] A list of template parts IDs for the given block. - */ -function create_auto_draft_for_template_part_block( $block ) { - $template_part_ids = array(); - - if ( 'core/template-part' === $block['blockName'] && isset( $block['attrs']['slug'] ) ) { - if ( isset( $block['attrs']['postId'] ) ) { - // Template part is customized. - $template_part_id = $block['attrs']['postId']; - } else { - // A published post might already exist if this template part - // was customized elsewhere or if it's part of a customized - // template. We also check if an auto-draft was already created - // because preloading can make this run twice, so, different code - // paths can end up with different posts for the same template part. - // E.g. The server could send back post ID 1 to the client, preload, - // and create another auto-draft. So, if the client tries to resolve the - // post ID from the slug and theme, it won't match with what the server sent. - $template_part_query = new WP_Query( - array( - 'post_type' => 'wp_template_part', - 'post_status' => array( 'publish', 'auto-draft' ), - 'title' => $block['attrs']['slug'], - 'meta_key' => 'theme', - 'meta_value' => $block['attrs']['theme'], - 'posts_per_page' => 1, - 'no_found_rows' => true, - ) - ); - $template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null; - if ( $template_part_post && 'auto-draft' !== $template_part_post->post_status ) { - $template_part_id = $template_part_post->ID; - } else { - // Template part is not customized, get it from a file and make an auto-draft for it, unless one already exists - // and the underlying file hasn't changed. - $template_part_file_path = get_stylesheet_directory() . '/block-template-parts/' . $block['attrs']['slug'] . '.html'; - if ( ! file_exists( $template_part_file_path ) ) { - $template_part_file_path = false; - } - - if ( $template_part_file_path ) { - $file_contents = file_get_contents( $template_part_file_path ); - if ( $template_part_post && $template_part_post->post_content === $file_contents ) { - $template_part_id = $template_part_post->ID; - } else { - $template_part_id = wp_insert_post( - array( - 'post_content' => $file_contents, - 'post_title' => $block['attrs']['slug'], - 'post_status' => 'auto-draft', - 'post_type' => 'wp_template_part', - 'post_name' => $block['attrs']['slug'], - 'meta_input' => array( - 'theme' => $block['attrs']['theme'], - ), - ) - ); - } - } - } - } - $template_part_ids[ $block['attrs']['slug'] ] = $template_part_id; - } - - foreach ( $block['innerBlocks'] as $inner_block ) { - $template_part_ids = array_merge( $template_part_ids, create_auto_draft_for_template_part_block( $inner_block ) ); - } - return $template_part_ids; -} - /** * Return the correct 'wp_template' post and template part IDs for the current template. * @@ -323,11 +246,11 @@ function gutenberg_find_template_post_and_parts( $template_type, $template_hiera if ( $current_template_post ) { $template_part_ids = array(); - if ( is_admin() || defined( 'REST_REQUEST' ) ) { + /* if ( is_admin() || defined( 'REST_REQUEST' ) ) { foreach ( parse_blocks( $current_template_post->post_content ) as $block ) { $template_part_ids = array_merge( $template_part_ids, create_auto_draft_for_template_part_block( $block ) ); } - } + } */ return array( 'template_post' => $current_template_post, 'template_part_ids' => $template_part_ids, @@ -396,37 +319,6 @@ function gutenberg_strip_php_suffix( $template_file ) { return preg_replace( '/\.php$/', '', $template_file ); } -/** - * Extends default editor settings to enable template and template part editing. - * - * @param array $settings Default editor settings. - * - * @return array Filtered editor settings. - */ -function gutenberg_template_loader_filter_block_editor_settings( $settings ) { - global $post; - - if ( ! $post ) { - return $settings; - } - - // If this is the Site Editor, auto-drafts for template parts have already been generated - // through `filter_rest_wp_template_part_query`, when called via the REST API. - if ( isset( $settings['editSiteInitialState'] ) ) { - return $settings; - } - - // Otherwise, create template part auto-drafts for the edited post. - $post = get_post(); - foreach ( parse_blocks( $post->post_content ) as $block ) { - create_auto_draft_for_template_part_block( $block ); - } - - // TODO: Set editing mode and current template ID for editing modes support. - return $settings; -} -add_filter( 'block_editor_settings', 'gutenberg_template_loader_filter_block_editor_settings' ); - /** * Removes post details from block context when rendering a block template. * diff --git a/lib/template-parts.php b/lib/template-parts.php index 7795eb1fb9295f..9b5069226f0e05 100644 --- a/lib/template-parts.php +++ b/lib/template-parts.php @@ -36,17 +36,18 @@ function gutenberg_register_template_part_post_type() { ); $args = array( - 'labels' => $labels, - 'description' => __( 'Template parts to include in your templates.', 'gutenberg' ), - 'public' => false, - 'has_archive' => false, - 'show_ui' => true, - 'show_in_menu' => 'themes.php', - 'show_in_admin_bar' => false, - 'show_in_rest' => true, - 'rest_base' => 'template-parts', - 'map_meta_cap' => true, - 'supports' => array( + 'labels' => $labels, + 'description' => __( 'Template parts to include in your templates.', 'gutenberg' ), + 'public' => false, + 'has_archive' => false, + 'show_ui' => true, + 'show_in_menu' => 'themes.php', + 'show_in_admin_bar' => false, + 'show_in_rest' => true, + 'rest_base' => 'template-parts', + 'rest_controller_class' => 'WP_REST_Template_Parts_Controller', + 'map_meta_cap' => true, + 'supports' => array( 'title', 'slug', 'editor', @@ -217,49 +218,6 @@ function filter_rest_wp_template_part_query( $args, $request ) { 'value' => $request['theme'], ); - // Ensure auto-drafts of all theme supplied template parts are created. - if ( wp_get_theme()->stylesheet === $request['theme'] ) { - /** - * Finds all nested template part file paths in a theme's directory. - * - * @param string $base_directory The theme's file path. - * @return array $path_list A list of paths to all template part files. - */ - function get_template_part_paths( $base_directory ) { - $path_list = array(); - if ( file_exists( $base_directory . '/block-template-parts' ) ) { - $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory . '/block-template-parts' ) ); - $nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH ); - foreach ( $nested_html_files as $path => $file ) { - $path_list[] = $path; - } - } - return $path_list; - } - - // Get file paths for all theme supplied template parts. - $template_part_files = get_template_part_paths( get_stylesheet_directory() ); - if ( is_child_theme() ) { - $template_part_files = array_merge( $template_part_files, get_template_part_paths( get_template_directory() ) ); - } - // Build and save each template part. - foreach ( $template_part_files as $template_part_file ) { - $content = file_get_contents( $template_part_file ); - // Infer slug from filepath. - $slug = substr( - $template_part_file, - // Starting position of slug. - strpos( $template_part_file, 'block-template-parts/' ) + 21, - // Subtract ending '.html'. - -5 - ); - // Wrap content with the template part block, parse, and create auto-draft. - $template_part_string = '' . $content . ''; - $template_part_block = parse_blocks( $template_part_string )[0]; - create_auto_draft_for_template_part_block( $template_part_block ); - } - }; - $args['meta_query'] = $meta_query; } From 8e2c664c791b3cd2c53e284b876d31a0048efcc7 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 3 Nov 2020 08:01:28 +0100 Subject: [PATCH 02/13] Remove useless query filters --- lib/template-parts.php | 45 ++---------------------------------------- 1 file changed, 2 insertions(+), 43 deletions(-) diff --git a/lib/template-parts.php b/lib/template-parts.php index 9b5069226f0e05..3b11eb17e5f16e 100644 --- a/lib/template-parts.php +++ b/lib/template-parts.php @@ -146,22 +146,14 @@ function gutenberg_render_template_part_list_table_column( $column_name, $post_i /** - * Filter for adding a `resolved`, a `template`, and a `theme` parameter to `wp_template_part` queries. + * Filter for adding and a `theme` parameter to `wp_template_part` queries. * * @param array $query_params The query parameters. * @return array Filtered $query_params. */ function filter_rest_wp_template_part_collection_params( $query_params ) { $query_params += array( - 'resolved' => array( - 'description' => __( 'Whether to filter for resolved template parts.', 'gutenberg' ), - 'type' => 'boolean', - ), - 'template' => array( - 'description' => __( 'The template slug for the template that the template part is used by.', 'gutenberg' ), - 'type' => 'string', - ), - 'theme' => array( + 'theme' => array( 'description' => __( 'The theme slug for the theme that created the template part.', 'gutenberg' ), 'type' => 'string', ), @@ -178,39 +170,6 @@ function filter_rest_wp_template_part_collection_params( $query_params ) { * @return array Filtered $args. */ function filter_rest_wp_template_part_query( $args, $request ) { - /** - * Unlike `filter_rest_wp_template_query`, we resolve queries also if there's only a `template` argument set. - * The difference is that in the case of templates, we can use the `slug` field that already exists (as part - * of the entities endpoint, wheras for template parts, we have to register the extra `template` argument), - * so we need the `resolved` flag to convey the different semantics (only return 'resolved' templates that match - * the `slug` vs return _all_ templates that match it (e.g. including all auto-drafts)). - * - * A template parts query with a `template` arg but not a `resolved` one is conceivable, but probably wouldn't be - * very useful: It'd be all template parts for all templates matching that `template` slug (including auto-drafts etc). - * - * @see filter_rest_wp_template_query - * @see filter_rest_wp_template_part_collection_params - * @see https://github.com/WordPress/gutenberg/pull/21878#discussion_r436961706 - */ - if ( $request['resolved'] || $request['template'] ) { - $template_part_ids = array( 0 ); // Return nothing by default (the 0 is needed for `post__in`). - $template_types = $request['template'] ? array( $request['template'] ) : get_template_types(); - - foreach ( $template_types as $template_type ) { - // Skip 'embed' for now because it is not a regular template type. - if ( in_array( $template_type, array( 'embed' ), true ) ) { - continue; - } - - $current_template = gutenberg_find_template_post_and_parts( $template_type ); - if ( isset( $current_template ) ) { - $template_part_ids = $template_part_ids + $current_template['template_part_ids']; - } - } - $args['post__in'] = $template_part_ids; - $args['post_status'] = array( 'publish', 'auto-draft' ); - } - if ( $request['theme'] ) { $meta_query = isset( $args['meta_query'] ) ? $args['meta_query'] : array(); $meta_query[] = array( From 8692d470ef6d9bb564141ec177343c62f7d86ba1 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 5 Nov 2020 10:19:45 +0100 Subject: [PATCH 03/13] Small tweaks --- ...lass-wp-rest-template-parts-controller.php | 20 ++++++++----------- lib/load.php | 6 +++--- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/class-wp-rest-template-parts-controller.php b/lib/class-wp-rest-template-parts-controller.php index 3c604e95dd2d2c..48fe7ff89ad513 100644 --- a/lib/class-wp-rest-template-parts-controller.php +++ b/lib/class-wp-rest-template-parts-controller.php @@ -16,18 +16,14 @@ * @param string $content Template part content. */ function create_auto_draft_for_template_part_block( $slug, $theme, $content ) { - // A published post might already exist if this template part - // was customized elsewhere or if it's part of a customized - // template. We also check if an auto-draft was already created - // because preloading can make this run twice, so, different code - // paths can end up with different posts for the same template part. - // E.g. The server could send back post ID 1 to the client, preload, - // and create another auto-draft. So, if the client tries to resolve the - // post ID from the slug and theme, it won't match with what the server sent. + // We check if an auto-draft was already created, + // before running the REST API calls + // because the site editor needs an existing auto-draft + // for each theme template part to work properly. $template_part_query = new WP_Query( array( 'post_type' => 'wp_template_part', - 'post_status' => array( 'publish', 'auto-draft' ), + 'post_status' => array( 'auto-draft' ), 'title' => $slug, 'meta_key' => 'theme', 'meta_value' => $theme, @@ -59,7 +55,7 @@ function create_auto_draft_for_template_part_block( $slug, $theme, $content ) { * * @access private */ -function create_theme_template_parts() { +function synchronize_theme_template_parts() { /** * Finds all nested template part file paths in a theme's directory. * @@ -111,7 +107,7 @@ class WP_REST_Template_Parts_Controller extends WP_REST_Posts_Controller { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { - create_theme_template_parts(); + synchronize_theme_template_parts(); return parent::get_items( $request ); } @@ -123,7 +119,7 @@ public function get_items( $request ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_item( $request ) { - create_theme_template_parts(); + synchronize_theme_template_parts(); return parent::get_items( $request ); } diff --git a/lib/load.php b/lib/load.php index e4f18bc8afa01e..346e3a7826cc6b 100644 --- a/lib/load.php +++ b/lib/load.php @@ -71,6 +71,9 @@ function gutenberg_is_experiment_enabled( $name ) { if ( ! class_exists( 'WP_REST_Batch_Controller' ) ) { require_once dirname( __FILE__ ) . '/class-wp-rest-batch-controller.php'; } + if ( ! class_exists( 'WP_REST_Template_Parts_Controller' ) ) { + require_once dirname( __FILE__ ) . '/class-wp-rest-template-parts-controller.php'; + } /** * End: Include for phase 2 */ @@ -98,9 +101,6 @@ function gutenberg_is_experiment_enabled( $name ) { require_once dirname( __FILE__ ) . '/class-wp-widget-block.php'; } -if ( ! class_exists( 'WP_REST_Template_Parts_Controller' ) ) { - require_once dirname( __FILE__ ) . '/class-wp-rest-template-parts-controller.php'; -} require_once dirname( __FILE__ ) . '/widgets-page.php'; require dirname( __FILE__ ) . '/compat.php'; From 66c09776b5787832ae09a39a48748bde9e284674 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 5 Nov 2020 12:04:11 +0100 Subject: [PATCH 04/13] Simplify template resolution --- ...lass-wp-rest-template-parts-controller.php | 87 ------------- lib/edit-site-export.php | 60 ++++++--- lib/template-loader.php | 118 ++++-------------- lib/template-parts.php | 87 +++++++++++++ lib/templates.php | 105 ++++++++++++++-- 5 files changed, 247 insertions(+), 210 deletions(-) diff --git a/lib/class-wp-rest-template-parts-controller.php b/lib/class-wp-rest-template-parts-controller.php index 48fe7ff89ad513..4d950835a473a5 100644 --- a/lib/class-wp-rest-template-parts-controller.php +++ b/lib/class-wp-rest-template-parts-controller.php @@ -6,93 +6,6 @@ * @package WordPress */ -/** - * Creates a template part auto-draft if it doesn't exist yet. - * - * @access private - * - * @param string $slug Template part slug. - * @param string $theme Template part theme. - * @param string $content Template part content. - */ -function create_auto_draft_for_template_part_block( $slug, $theme, $content ) { - // We check if an auto-draft was already created, - // before running the REST API calls - // because the site editor needs an existing auto-draft - // for each theme template part to work properly. - $template_part_query = new WP_Query( - array( - 'post_type' => 'wp_template_part', - 'post_status' => array( 'auto-draft' ), - 'title' => $slug, - 'meta_key' => 'theme', - 'meta_value' => $theme, - 'posts_per_page' => 1, - 'no_found_rows' => true, - ) - ); - $template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null; - if ( ! $template_part_post ) { - wp_insert_post( - array( - 'post_content' => $content, - 'post_title' => $slug, - 'post_status' => 'auto-draft', - 'post_type' => 'wp_template_part', - 'post_name' => $slug, - 'meta_input' => array( - 'theme' => $theme, - ), - ) - ); - } else { - // Potentially we could decide to update the content if different. - } -} - -/** - * Create the template parts auto-drafts for the current theme. - * - * @access private - */ -function synchronize_theme_template_parts() { - /** - * Finds all nested template part file paths in a theme's directory. - * - * @param string $base_directory The theme's file path. - * @return array $path_list A list of paths to all template part files. - */ - function get_template_part_paths( $base_directory ) { - $path_list = array(); - if ( file_exists( $base_directory . '/block-template-parts' ) ) { - $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory . '/block-template-parts' ) ); - $nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH ); - foreach ( $nested_html_files as $path => $file ) { - $path_list[] = $path; - } - } - return $path_list; - } - - // Get file paths for all theme supplied template parts. - $template_part_files = get_template_part_paths( get_stylesheet_directory() ); - if ( is_child_theme() ) { - $template_part_files = array_merge( $template_part_files, get_template_part_paths( get_template_directory() ) ); - } - // Build and save each template part. - foreach ( $template_part_files as $template_part_file ) { - $content = file_get_contents( $template_part_file ); - $slug = substr( - $template_part_file, - // Starting position of slug. - strpos( $template_part_file, 'block-template-parts/' ) + 21, - // Subtract ending '.html'. - -5 - ); - create_auto_draft_for_template_part_block( $slug, wp_get_theme()->get( 'TextDomain' ), $content ); - } -} - /** * Core class used to access menu templte parts via the REST API. * diff --git a/lib/edit-site-export.php b/lib/edit-site-export.php index 8967788bdf8a1c..a373d0146c5e82 100644 --- a/lib/edit-site-export.php +++ b/lib/edit-site-export.php @@ -10,6 +10,11 @@ * and template parts from the site editor, and close the connection. */ function gutenberg_edit_site_export() { + // Theme templates andd template parts need to be synchronized + // before the export. + synchronize_theme_template_parts(); + synchronize_theme_templates(); + // Create ZIP file and directories. $filename = tempnam( get_temp_dir(), 'edit-site-export' ); $zip = new ZipArchive(); @@ -18,26 +23,44 @@ function gutenberg_edit_site_export() { $zip->addEmptyDir( 'theme/block-templates' ); $zip->addEmptyDir( 'theme/block-template-parts' ); - // Load files into ZIP file. - foreach ( get_template_types() as $template_type ) { - // Skip 'embed' for now because it is not a regular template type. - // Skip 'index' because it's a fallback that we handle differently. - if ( in_array( $template_type, array( 'embed', 'index' ), true ) ) { - continue; - } + $theme = wp_get_theme()->get( 'TextDomain' ); - $current_template = gutenberg_find_template_post_and_parts( $template_type ); - if ( isset( $current_template ) ) { - $zip->addFromString( - 'theme/block-templates/' . $current_template['template_post']->post_name . '.html', - gutenberg_strip_post_ids_from_template_part_blocks( $current_template['template_post']->post_content ) - ); + // Load templates into the zip file. + $template_query = new WP_Query( + array( + 'post_type' => 'wp_template', + 'post_status' => array( 'publish', 'auto-draft' ), + 'meta_key' => 'theme', + 'meta_value' => $theme, + 'posts_per_page' => -1, + 'no_found_rows' => true, + ) + ); + while ( $template_query->have_posts() ) { + $template = $template_query->next_post(); + $zip->addFromString( + 'theme/block-templates/' . $template->post_name . '.html', + gutenberg_strip_post_ids_from_template_part_blocks( $template->post_content ) + ); + } - foreach ( $current_template['template_part_ids'] as $template_part_id ) { - $template_part = get_post( $template_part_id ); - $zip->addFromString( 'theme/block-template-parts/' . $template_part->post_name . '.html', $template_part->post_content ); - } - } + // Load template partss into the zip file. + $template_part_query = new WP_Query( + array( + 'post_type' => 'wp_template_part', + 'post_status' => array( 'publish', 'auto-draft' ), + 'meta_key' => 'theme', + 'meta_value' => $theme, + 'posts_per_page' => -1, + 'no_found_rows' => true, + ) + ); + while ( $template_part_query->have_posts() ) { + $template_part = $template_part_query->next_post(); + $zip->addFromString( + 'theme/block-template-parts/' . $template_part->post_name . '.html', + gutenberg_strip_post_ids_from_template_part_blocks( $template_part->post_content ) + ); } // Send back the ZIP file. @@ -49,6 +72,7 @@ function gutenberg_edit_site_export() { echo readfile( $filename ); die(); } + add_action( 'rest_api_init', function () { diff --git a/lib/template-loader.php b/lib/template-loader.php index e16251e65a4572..7eb16c1ddad965 100644 --- a/lib/template-loader.php +++ b/lib/template-loader.php @@ -93,13 +93,13 @@ function get_template_hierarchy( $template_type ) { function gutenberg_override_query_template( $template, $type, array $templates = array() ) { global $_wp_current_template_content; - $current_template = gutenberg_find_template_post_and_parts( $type, $templates ); + $current_template = gutenberg_resolve_template( $type, $templates ); if ( $current_template ) { - $_wp_current_template_content = empty( $current_template['template_post']->post_content ) ? __( 'Empty template.', 'gutenberg' ) : $current_template['template_post']->post_content; + $_wp_current_template_content = empty( $current_template->post_content ) ? __( 'Empty template.', 'gutenberg' ) : $current_template->post_content; if ( isset( $_GET['_wp-find-template'] ) ) { - wp_send_json_success( $current_template['template_post'] ); + wp_send_json_success( $current_template ); } } else { if ( 'index' === $type ) { @@ -124,7 +124,7 @@ function gutenberg_override_query_template( $template, $type, array $templates = } /** - * Return the correct 'wp_template' post and template part IDs for the current template. + * Return the correct 'wp_template' to render fot the request template type. * * Accepts an optional $template_hierarchy argument as a hint. * @@ -135,11 +135,14 @@ function gutenberg_override_query_template( $template, $type, array $templates = * @type int[] A list of template parts IDs for the template. * } */ -function gutenberg_find_template_post_and_parts( $template_type, $template_hierarchy = array() ) { +function gutenberg_resolve_template( $template_type, $template_hierarchy = array() ) { if ( ! $template_type ) { return null; } + // Create auto-drafts for each theme template files. + synchronize_theme_templates(); + if ( empty( $template_hierarchy ) ) { if ( 'index' === $template_type ) { $template_hierarchy = get_template_hierarchy( 'index' ); @@ -153,110 +156,35 @@ function gutenberg_find_template_post_and_parts( $template_type, $template_hiera $template_hierarchy ); - // Find most specific 'wp_template' post matching the hierarchy. + // Find all potential templates 'wp_template' post matching the hierarchy. $template_query = new WP_Query( array( 'post_type' => 'wp_template', - 'post_status' => 'publish', + 'post_status' => array( 'publish', 'auto-draft' ), 'post_name__in' => $slugs, 'orderby' => 'post_name__in', - 'posts_per_page' => 1, + 'posts_per_page' => -1, 'no_found_rows' => true, + 'meta_key' => 'theme', + 'meta_value' => wp_get_theme()->get( 'TextDomain' ), ) ); + $templates = $template_query->get_posts(); - $current_template_post = $template_query->have_posts() ? $template_query->next_post() : null; - + // Order these templates per slug priority. // Build map of template slugs to their priority in the current hierarchy. $slug_priorities = array_flip( $slugs ); - // See if there is a theme block template with higher priority than the resolved template post. - $higher_priority_block_template_path = null; - $higher_priority_block_template_priority = PHP_INT_MAX; - $block_template_files = gutenberg_get_template_paths(); - foreach ( $block_template_files as $path ) { - if ( ! isset( $slug_priorities[ basename( $path, '.html' ) ] ) ) { - continue; - } - $theme_block_template_priority = $slug_priorities[ basename( $path, '.html' ) ]; - if ( - $theme_block_template_priority < $higher_priority_block_template_priority && - ( empty( $current_template_post ) || $theme_block_template_priority < $slug_priorities[ $current_template_post->post_name ] ) - ) { - $higher_priority_block_template_path = $path; - $higher_priority_block_template_priority = $theme_block_template_priority; + usort( + $templates, + function ( $template_a, $template_b ) use ( $slug_priorities ) { + $priority_a = $slug_priorities[ $template_a->post_name ] * 2 + ( 'publish' === $template_a->post_status ? 1 : 0 ); + $priority_b = $slug_priorities[ $template_b->post_name ] * 2 + ( 'publish' === $template_b->post_status ? 1 : 0 ); + return $priority_b - $priority_a; } - } - - // If there is, use it instead. - if ( isset( $higher_priority_block_template_path ) ) { - $post_name = basename( $higher_priority_block_template_path, '.html' ); - $file_contents = file_get_contents( $higher_priority_block_template_path ); - $current_template_post = array( - 'post_content' => $file_contents, - 'post_title' => $post_name, - 'post_status' => 'auto-draft', - 'post_type' => 'wp_template', - 'post_name' => $post_name, - ); - if ( is_admin() || defined( 'REST_REQUEST' ) ) { - $template_query = new WP_Query( - array( - 'post_type' => 'wp_template', - 'post_status' => 'auto-draft', - 'name' => $post_name, - 'posts_per_page' => 1, - 'no_found_rows' => true, - ) - ); - $current_template_post = $template_query->have_posts() ? $template_query->next_post() : $current_template_post; - - // Only create auto-draft of block template for editing - // in admin screens, when necessary, because the underlying - // file has changed. - if ( is_array( $current_template_post ) || $current_template_post->post_content !== $file_contents ) { - if ( ! is_array( $current_template_post ) ) { - $current_template_post->post_content = $file_contents; - } - $current_template_post = get_post( - wp_insert_post( $current_template_post ) - ); - } - } else { - $current_template_post = new WP_Post( - (object) $current_template_post - ); - } - } - - // If we haven't found any template post by here, it means that this theme doesn't even come with a fallback - // `index.html` block template. We create one so that people that are trying to access the editor are greeted - // with a blank page rather than an error. - if ( ! $current_template_post && ( is_admin() || defined( 'REST_REQUEST' ) ) ) { - $current_template_post = array( - 'post_title' => 'index', - 'post_status' => 'auto-draft', - 'post_type' => 'wp_template', - 'post_name' => 'index', - ); - $current_template_post = get_post( - wp_insert_post( $current_template_post ) - ); - } + ); - if ( $current_template_post ) { - $template_part_ids = array(); - /* if ( is_admin() || defined( 'REST_REQUEST' ) ) { - foreach ( parse_blocks( $current_template_post->post_content ) as $block ) { - $template_part_ids = array_merge( $template_part_ids, create_auto_draft_for_template_part_block( $block ) ); - } - } */ - return array( - 'template_post' => $current_template_post, - 'template_part_ids' => $template_part_ids, - ); - } - return null; + return count( $templates ) ? $templates[0] : null; } /** diff --git a/lib/template-parts.php b/lib/template-parts.php index 3b11eb17e5f16e..56eaa12236a137 100644 --- a/lib/template-parts.php +++ b/lib/template-parts.php @@ -183,3 +183,90 @@ function filter_rest_wp_template_part_query( $args, $request ) { return $args; } add_filter( 'rest_wp_template_part_query', 'filter_rest_wp_template_part_query', 99, 2 ); + +/** + * Creates a template part auto-draft if it doesn't exist yet. + * + * @access private + * + * @param string $slug Template part slug. + * @param string $theme Template part theme. + * @param string $content Template part content. + */ +function create_auto_draft_for_template_part( $slug, $theme, $content ) { + // We check if an auto-draft was already created, + // before running the REST API calls + // because the site editor needs an existing auto-draft + // for each theme template part to work properly. + $template_part_query = new WP_Query( + array( + 'post_type' => 'wp_template_part', + 'post_status' => array( 'publish', 'auto-draft' ), + 'title' => $slug, + 'meta_key' => 'theme', + 'meta_value' => $theme, + 'posts_per_page' => 1, + 'no_found_rows' => true, + ) + ); + $template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null; + if ( ! $template_part_post ) { + wp_insert_post( + array( + 'post_content' => $content, + 'post_title' => $slug, + 'post_status' => 'auto-draft', + 'post_type' => 'wp_template_part', + 'post_name' => $slug, + 'meta_input' => array( + 'theme' => $theme, + ), + ) + ); + } else { + // Potentially we could decide to update the content if different. + } +} + +/** + * Create the template parts auto-drafts for the current theme. + * + * @access private + */ +function synchronize_theme_template_parts() { + /** + * Finds all nested template part file paths in a theme's directory. + * + * @param string $base_directory The theme's file path. + * @return array $path_list A list of paths to all template part files. + */ + function get_template_part_paths( $base_directory ) { + $path_list = array(); + if ( file_exists( $base_directory . '/block-template-parts' ) ) { + $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory . '/block-template-parts' ) ); + $nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH ); + foreach ( $nested_html_files as $path => $file ) { + $path_list[] = $path; + } + } + return $path_list; + } + + // Get file paths for all theme supplied template parts. + $template_part_files = get_template_part_paths( get_stylesheet_directory() ); + if ( is_child_theme() ) { + $template_part_files = array_merge( $template_part_files, get_template_part_paths( get_template_directory() ) ); + } + // Build and save each template part. + foreach ( $template_part_files as $template_part_file ) { + $content = file_get_contents( $template_part_file ); + $slug = substr( + $template_part_file, + // Starting position of slug. + strpos( $template_part_file, 'block-template-parts/' ) + 21, + // Subtract ending '.html'. + -5 + ); + create_auto_draft_for_template_part( $slug, wp_get_theme()->get( 'TextDomain' ), $content ); + } +} diff --git a/lib/templates.php b/lib/templates.php index 406026bc599050..0fc99d0b444e86 100644 --- a/lib/templates.php +++ b/lib/templates.php @@ -203,13 +203,6 @@ function filter_rest_wp_template_collection_params( $query_params ) { * @return array Filtered $args. */ function filter_rest_wp_template_query( $args, $request ) { - // Create auto-drafts for each theme template files. - $block_template_files = gutenberg_get_template_paths(); - foreach ( $block_template_files as $path ) { - $template_type = basename( $path, '.html' ); - gutenberg_find_template_post_and_parts( $template_type, array( $template_type ) ); - } - if ( $request['resolved'] ) { $template_ids = array( 0 ); // Return nothing by default (the 0 is needed for `post__in`). $template_types = $request['slug'] ? $request['slug'] : get_template_types(); @@ -220,9 +213,9 @@ function filter_rest_wp_template_query( $args, $request ) { continue; } - $current_template = gutenberg_find_template_post_and_parts( $template_type ); - if ( isset( $current_template ) ) { - $template_ids[] = $current_template['template_post']->ID; + $current_template = gutenberg_resolve_template( $template_type ); + if ( $current_template ) { + $template_ids[] = $current_template->ID; } } $args['post__in'] = $template_ids; @@ -232,3 +225,95 @@ function filter_rest_wp_template_query( $args, $request ) { return $args; } add_filter( 'rest_wp_template_query', 'filter_rest_wp_template_query', 99, 2 ); + +/** + * Creates a template auto-draft if it doesn't exist yet. + * + * @access private + * + * @param string $slug Template slug. + * @param string $theme Template theme. + * @param string $content Template content. + */ +function create_auto_draft_for_template( $slug, $theme, $content ) { + // We check if an auto-draft was already created, + // before running the REST API calls + // because the site editor needs an existing auto-draft + // for each theme template to work properly. + $template_query = new WP_Query( + array( + 'post_type' => 'wp_template', + 'post_status' => array( 'publish', 'auto-draft' ), + 'title' => $slug, + 'meta_key' => 'theme', + 'meta_value' => $theme, + 'posts_per_page' => 1, + 'no_found_rows' => true, + ) + ); + $template_post = $template_query->have_posts() ? $template_query->next_post() : null; + if ( ! $template_post ) { + wp_insert_post( + array( + 'post_content' => $content, + 'post_title' => $slug, + 'post_status' => 'auto-draft', + 'post_type' => 'wp_template', + 'post_name' => $slug, + 'meta_input' => array( + 'theme' => $theme, + ), + ) + ); + } else { + // Potentially we could decide to update the content if different. + } +} + +/** + * Create the template auto-drafts for the current theme. + * + * @access private + */ +function synchronize_theme_templates() { + /** + * Finds all nested template part file paths in a theme's directory. + * + * @param string $base_directory The theme's file path. + * @return array $path_list A list of paths to all template part files. + */ + function get_template_paths( $base_directory ) { + $path_list = array(); + if ( file_exists( $base_directory . '/block-templates' ) ) { + $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory . '/block-templates' ) ); + $nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH ); + foreach ( $nested_html_files as $path => $file ) { + $path_list[] = $path; + } + } + return $path_list; + } + + // Get file paths for all theme supplied template parts. + $template_files = get_template_paths( get_stylesheet_directory() ); + if ( is_child_theme() ) { + $template_files = array_merge( $template_files, get_template_paths( get_template_directory() ) ); + } + // Build and save each template part. + foreach ( $template_files as $template_file ) { + $content = file_get_contents( $template_file ); + $slug = substr( + $template_file, + // Starting position of slug. + strpos( $template_file, 'block-templates/' ) + 16, + // Subtract ending '.html'. + -5 + ); + create_auto_draft_for_template( $slug, wp_get_theme()->get( 'TextDomain' ), $content ); + } + + // If we haven't found any block-template by default, create a fallback one. + if ( count( $template_files ) === 0 ) { + create_auto_draft_for_template( 'index', wp_get_theme()->get( 'TextDomain' ), '' ); + } +} From 0c3e16441659342e1761d124a3c042088a7c9d0c Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 6 Nov 2020 08:32:17 +0100 Subject: [PATCH 05/13] Fix template parts --- lib/edit-site-export.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/edit-site-export.php b/lib/edit-site-export.php index a373d0146c5e82..1e810cbcdfb906 100644 --- a/lib/edit-site-export.php +++ b/lib/edit-site-export.php @@ -10,7 +10,7 @@ * and template parts from the site editor, and close the connection. */ function gutenberg_edit_site_export() { - // Theme templates andd template parts need to be synchronized + // Theme templates and template parts need to be synchronized // before the export. synchronize_theme_template_parts(); synchronize_theme_templates(); @@ -44,7 +44,7 @@ function gutenberg_edit_site_export() { ); } - // Load template partss into the zip file. + // Load template parts into the zip file. $template_part_query = new WP_Query( array( 'post_type' => 'wp_template_part', From b67f49844c9774190fdb3c08e6779cbbd68bc964 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 6 Nov 2020 08:35:49 +0100 Subject: [PATCH 06/13] Register theme meta for templates --- lib/templates.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/templates.php b/lib/templates.php index 0fc99d0b444e86..dc770b228e7317 100644 --- a/lib/templates.php +++ b/lib/templates.php @@ -75,6 +75,17 @@ function gutenberg_register_template_post_type() { ); register_post_type( 'wp_template', $args ); + + $meta_args = array( + 'object_subtype' => 'wp_part', + 'type' => 'string', + 'description' => 'The theme that provided the template, if any.', + 'single' => true, + 'show_in_rest' => true, + ); + + register_post_type( 'wp_template', $args ); + register_meta( 'post', 'theme', $meta_args ); } add_action( 'init', 'gutenberg_register_template_post_type' ); From 942aac142161418cc1617506750195e2769b8770 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 6 Nov 2020 08:53:09 +0100 Subject: [PATCH 07/13] Factorize template sync --- ...lass-wp-rest-template-parts-controller.php | 4 +- lib/edit-site-export.php | 4 +- lib/load.php | 1 + lib/template-loader.php | 2 +- lib/template-parts.php | 87 -------------- lib/templates-sync.php | 111 ++++++++++++++++++ lib/templates.php | 92 --------------- 7 files changed, 117 insertions(+), 184 deletions(-) create mode 100644 lib/templates-sync.php diff --git a/lib/class-wp-rest-template-parts-controller.php b/lib/class-wp-rest-template-parts-controller.php index 4d950835a473a5..8a48706a4e2f30 100644 --- a/lib/class-wp-rest-template-parts-controller.php +++ b/lib/class-wp-rest-template-parts-controller.php @@ -20,7 +20,7 @@ class WP_REST_Template_Parts_Controller extends WP_REST_Posts_Controller { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { - synchronize_theme_template_parts(); + _gutenberg_synchronize_theme_templates( 'template-part' ); return parent::get_items( $request ); } @@ -32,7 +32,7 @@ public function get_items( $request ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_item( $request ) { - synchronize_theme_template_parts(); + _gutenberg_synchronize_theme_templates( 'template-part' ); return parent::get_items( $request ); } diff --git a/lib/edit-site-export.php b/lib/edit-site-export.php index 1e810cbcdfb906..12751ed2b6796c 100644 --- a/lib/edit-site-export.php +++ b/lib/edit-site-export.php @@ -12,8 +12,8 @@ function gutenberg_edit_site_export() { // Theme templates and template parts need to be synchronized // before the export. - synchronize_theme_template_parts(); - synchronize_theme_templates(); + _gutenberg_synchronize_theme_templates( 'template-part' ); + _gutenberg_synchronize_theme_templates( 'template' ); // Create ZIP file and directories. $filename = tempnam( get_temp_dir(), 'edit-site-export' ); diff --git a/lib/load.php b/lib/load.php index 346e3a7826cc6b..078adbe261f5cf 100644 --- a/lib/load.php +++ b/lib/load.php @@ -107,6 +107,7 @@ function gutenberg_is_experiment_enabled( $name ) { require dirname( __FILE__ ) . '/utils.php'; require dirname( __FILE__ ) . '/full-site-editing.php'; +require dirname( __FILE__ ) . '/templates-sync.php'; require dirname( __FILE__ ) . '/templates.php'; require dirname( __FILE__ ) . '/template-parts.php'; require dirname( __FILE__ ) . '/template-loader.php'; diff --git a/lib/template-loader.php b/lib/template-loader.php index 7eb16c1ddad965..73851e561a8b82 100644 --- a/lib/template-loader.php +++ b/lib/template-loader.php @@ -141,7 +141,7 @@ function gutenberg_resolve_template( $template_type, $template_hierarchy = array } // Create auto-drafts for each theme template files. - synchronize_theme_templates(); + _gutenberg_synchronize_theme_templates( 'template' ); if ( empty( $template_hierarchy ) ) { if ( 'index' === $template_type ) { diff --git a/lib/template-parts.php b/lib/template-parts.php index 56eaa12236a137..3b11eb17e5f16e 100644 --- a/lib/template-parts.php +++ b/lib/template-parts.php @@ -183,90 +183,3 @@ function filter_rest_wp_template_part_query( $args, $request ) { return $args; } add_filter( 'rest_wp_template_part_query', 'filter_rest_wp_template_part_query', 99, 2 ); - -/** - * Creates a template part auto-draft if it doesn't exist yet. - * - * @access private - * - * @param string $slug Template part slug. - * @param string $theme Template part theme. - * @param string $content Template part content. - */ -function create_auto_draft_for_template_part( $slug, $theme, $content ) { - // We check if an auto-draft was already created, - // before running the REST API calls - // because the site editor needs an existing auto-draft - // for each theme template part to work properly. - $template_part_query = new WP_Query( - array( - 'post_type' => 'wp_template_part', - 'post_status' => array( 'publish', 'auto-draft' ), - 'title' => $slug, - 'meta_key' => 'theme', - 'meta_value' => $theme, - 'posts_per_page' => 1, - 'no_found_rows' => true, - ) - ); - $template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null; - if ( ! $template_part_post ) { - wp_insert_post( - array( - 'post_content' => $content, - 'post_title' => $slug, - 'post_status' => 'auto-draft', - 'post_type' => 'wp_template_part', - 'post_name' => $slug, - 'meta_input' => array( - 'theme' => $theme, - ), - ) - ); - } else { - // Potentially we could decide to update the content if different. - } -} - -/** - * Create the template parts auto-drafts for the current theme. - * - * @access private - */ -function synchronize_theme_template_parts() { - /** - * Finds all nested template part file paths in a theme's directory. - * - * @param string $base_directory The theme's file path. - * @return array $path_list A list of paths to all template part files. - */ - function get_template_part_paths( $base_directory ) { - $path_list = array(); - if ( file_exists( $base_directory . '/block-template-parts' ) ) { - $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory . '/block-template-parts' ) ); - $nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH ); - foreach ( $nested_html_files as $path => $file ) { - $path_list[] = $path; - } - } - return $path_list; - } - - // Get file paths for all theme supplied template parts. - $template_part_files = get_template_part_paths( get_stylesheet_directory() ); - if ( is_child_theme() ) { - $template_part_files = array_merge( $template_part_files, get_template_part_paths( get_template_directory() ) ); - } - // Build and save each template part. - foreach ( $template_part_files as $template_part_file ) { - $content = file_get_contents( $template_part_file ); - $slug = substr( - $template_part_file, - // Starting position of slug. - strpos( $template_part_file, 'block-template-parts/' ) + 21, - // Subtract ending '.html'. - -5 - ); - create_auto_draft_for_template_part( $slug, wp_get_theme()->get( 'TextDomain' ), $content ); - } -} diff --git a/lib/templates-sync.php b/lib/templates-sync.php new file mode 100644 index 00000000000000..ab909d7f2baaf0 --- /dev/null +++ b/lib/templates-sync.php @@ -0,0 +1,111 @@ + $post_type, + 'post_status' => array( 'publish', 'auto-draft' ), + 'title' => $slug, + 'meta_key' => 'theme', + 'meta_value' => $theme, + 'posts_per_page' => 1, + 'no_found_rows' => true, + ) + ); + $post = $template_query->have_posts() ? $template_query->next_post() : null; + if ( ! $post ) { + wp_insert_post( + array( + 'post_content' => $content, + 'post_title' => $slug, + 'post_status' => 'auto-draft', + 'post_type' => $post_type, + 'post_name' => $slug, + 'meta_input' => array( + 'theme' => $theme, + ), + ) + ); + } else { + // Potentially we could decide to update the content if different. + } +} + +/** + * Finds all nested template part file paths in a theme's directory. + * + * @access private + * + * @param string $base_directory The theme's file path. + * @return array $path_list A list of paths to all template part files. + */ +function _gutenberg_get_template_paths( $base_directory ) { + $path_list = array(); + if ( file_exists( $base_directory ) ) { + $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory ) ); + $nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH ); + foreach ( $nested_html_files as $path => $file ) { + $path_list[] = $path; + } + } + return $path_list; +} + +/** + * Create the template parts auto-drafts for the current theme. + * + * @access private + * @internal + * + * @param string $template_type The template type (template or template-part). + */ +function _gutenberg_synchronize_theme_templates( $template_type ) { + $template_post_types = array( + 'template' => 'wp_template', + 'template-part' => 'wp_template_part', + ); + $template_base_paths = array( + 'template' => 'block-templates', + 'template-part' => 'block-template-parts', + ); + + // Get file paths for all theme supplied template. + $template_files = _gutenberg_get_template_paths( get_stylesheet_directory() . '/' . $template_base_paths[ $template_type ] ); + if ( is_child_theme() ) { + $template_files = array_merge( $template_files, _gutenberg_get_template_paths( get_template_directory() . '/' . $template_base_paths[ $template_type ] ) ); + } + + // Build and save each template part. + foreach ( $template_files as $template_file ) { + $content = file_get_contents( $template_file ); + $slug = substr( + $template_file, + // Starting position of slug. + strpos( $template_file, $template_base_paths[ $template_type ] . '/' ) + 1 + strlen( $template_base_paths[ $template_type ] ), + // Subtract ending '.html'. + -5 + ); + _gutenberg_create_auto_draft_for_template( $template_post_types[ $template_type ], $slug, wp_get_theme()->get( 'TextDomain' ), $content ); + } +} diff --git a/lib/templates.php b/lib/templates.php index dc770b228e7317..a062f67eb2687c 100644 --- a/lib/templates.php +++ b/lib/templates.php @@ -236,95 +236,3 @@ function filter_rest_wp_template_query( $args, $request ) { return $args; } add_filter( 'rest_wp_template_query', 'filter_rest_wp_template_query', 99, 2 ); - -/** - * Creates a template auto-draft if it doesn't exist yet. - * - * @access private - * - * @param string $slug Template slug. - * @param string $theme Template theme. - * @param string $content Template content. - */ -function create_auto_draft_for_template( $slug, $theme, $content ) { - // We check if an auto-draft was already created, - // before running the REST API calls - // because the site editor needs an existing auto-draft - // for each theme template to work properly. - $template_query = new WP_Query( - array( - 'post_type' => 'wp_template', - 'post_status' => array( 'publish', 'auto-draft' ), - 'title' => $slug, - 'meta_key' => 'theme', - 'meta_value' => $theme, - 'posts_per_page' => 1, - 'no_found_rows' => true, - ) - ); - $template_post = $template_query->have_posts() ? $template_query->next_post() : null; - if ( ! $template_post ) { - wp_insert_post( - array( - 'post_content' => $content, - 'post_title' => $slug, - 'post_status' => 'auto-draft', - 'post_type' => 'wp_template', - 'post_name' => $slug, - 'meta_input' => array( - 'theme' => $theme, - ), - ) - ); - } else { - // Potentially we could decide to update the content if different. - } -} - -/** - * Create the template auto-drafts for the current theme. - * - * @access private - */ -function synchronize_theme_templates() { - /** - * Finds all nested template part file paths in a theme's directory. - * - * @param string $base_directory The theme's file path. - * @return array $path_list A list of paths to all template part files. - */ - function get_template_paths( $base_directory ) { - $path_list = array(); - if ( file_exists( $base_directory . '/block-templates' ) ) { - $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory . '/block-templates' ) ); - $nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH ); - foreach ( $nested_html_files as $path => $file ) { - $path_list[] = $path; - } - } - return $path_list; - } - - // Get file paths for all theme supplied template parts. - $template_files = get_template_paths( get_stylesheet_directory() ); - if ( is_child_theme() ) { - $template_files = array_merge( $template_files, get_template_paths( get_template_directory() ) ); - } - // Build and save each template part. - foreach ( $template_files as $template_file ) { - $content = file_get_contents( $template_file ); - $slug = substr( - $template_file, - // Starting position of slug. - strpos( $template_file, 'block-templates/' ) + 16, - // Subtract ending '.html'. - -5 - ); - create_auto_draft_for_template( $slug, wp_get_theme()->get( 'TextDomain' ), $content ); - } - - // If we haven't found any block-template by default, create a fallback one. - if ( count( $template_files ) === 0 ) { - create_auto_draft_for_template( 'index', wp_get_theme()->get( 'TextDomain' ), '' ); - } -} From b1d5dfa9631e1d8c542e1dd67f8dfeaecd9ef3aa Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 6 Nov 2020 11:39:36 +0100 Subject: [PATCH 08/13] Change how REST API for templates are filtered --- ...lass-wp-rest-template-parts-controller.php | 39 ------------------- lib/load.php | 3 -- lib/template-loader.php | 6 +-- lib/template-parts.php | 23 ++++++++++- lib/templates.php | 22 +++++++++++ 5 files changed, 47 insertions(+), 46 deletions(-) delete mode 100644 lib/class-wp-rest-template-parts-controller.php diff --git a/lib/class-wp-rest-template-parts-controller.php b/lib/class-wp-rest-template-parts-controller.php deleted file mode 100644 index 8a48706a4e2f30..00000000000000 --- a/lib/class-wp-rest-template-parts-controller.php +++ /dev/null @@ -1,39 +0,0 @@ - false, 'show_in_rest' => true, 'rest_base' => 'template-parts', - 'rest_controller_class' => 'WP_REST_Template_Parts_Controller', 'map_meta_cap' => true, 'supports' => array( 'title', @@ -183,3 +182,25 @@ function filter_rest_wp_template_part_query( $args, $request ) { return $args; } add_filter( 'rest_wp_template_part_query', 'filter_rest_wp_template_part_query', 99, 2 ); + +/** + * Run synchrnonization for template part API requests + * + * @param mixed $dispatch_result Dispatch result, will be used if not empty. + * @param WP_REST_Request $request Request used to generate the response. + * @param string $route Route matched for the request. + * @param array $handler Route handler used for the request. + */ +function gutenberg_filter_rest_wp_template_part_dispatch( $dispatch_result, $request, $route, $handler ) { + if ( null !== $dispatch_result ) { + return $dispatch_result; + } + + if ( 0 === strpos( $route, '/wp/v2/template-parts' ) && 'GET' === $request->get_method() ) { + _gutenberg_synchronize_theme_templates( 'template-part' ); + } + + return null; +} + +add_filter( 'rest_dispatch_request', 'gutenberg_filter_rest_wp_template_part_dispatch', 10, 4 ); diff --git a/lib/templates.php b/lib/templates.php index a062f67eb2687c..858511a022f014 100644 --- a/lib/templates.php +++ b/lib/templates.php @@ -236,3 +236,25 @@ function filter_rest_wp_template_query( $args, $request ) { return $args; } add_filter( 'rest_wp_template_query', 'filter_rest_wp_template_query', 99, 2 ); + +/** + * Run synchrnonization for template part API requests + * + * @param mixed $dispatch_result Dispatch result, will be used if not empty. + * @param WP_REST_Request $request Request used to generate the response. + * @param string $route Route matched for the request. + * @param array $handler Route handler used for the request. + */ +function gutenberg_filter_rest_wp_template_dispatch( $dispatch_result, $request, $route, $handler ) { + if ( null !== $dispatch_result ) { + return $dispatch_result; + } + + if ( 0 === strpos( $route, '/wp/v2/templates' ) && 'GET' === $request->get_method() ) { + _gutenberg_synchronize_theme_templates( 'template' ); + } + + return null; +} + +add_filter( 'rest_dispatch_request', 'gutenberg_filter_rest_wp_template_dispatch', 10, 4 ); From 3201a1416c3663d58141ac218c32d97df7a74d6d Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 6 Nov 2020 15:56:37 +0100 Subject: [PATCH 09/13] Fix typos --- lib/templates.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/templates.php b/lib/templates.php index 858511a022f014..0f4700399fc26e 100644 --- a/lib/templates.php +++ b/lib/templates.php @@ -77,7 +77,7 @@ function gutenberg_register_template_post_type() { register_post_type( 'wp_template', $args ); $meta_args = array( - 'object_subtype' => 'wp_part', + 'object_subtype' => 'wp_template', 'type' => 'string', 'description' => 'The theme that provided the template, if any.', 'single' => true, @@ -238,7 +238,7 @@ function filter_rest_wp_template_query( $args, $request ) { add_filter( 'rest_wp_template_query', 'filter_rest_wp_template_query', 99, 2 ); /** - * Run synchrnonization for template part API requests + * Run synchrnonization for template API requests * * @param mixed $dispatch_result Dispatch result, will be used if not empty. * @param WP_REST_Request $request Request used to generate the response. From 47b1256845b97df214421ad5297567624fb499ea Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 6 Nov 2020 16:49:35 +0100 Subject: [PATCH 10/13] Ensure theme meta is set properly --- lib/template-parts.php | 21 +++++++++++++++++++++ lib/templates-sync.php | 3 --- lib/templates.php | 21 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/lib/template-parts.php b/lib/template-parts.php index ad95922c8dd4fb..41ee94f8dea96e 100644 --- a/lib/template-parts.php +++ b/lib/template-parts.php @@ -68,6 +68,27 @@ function gutenberg_register_template_part_post_type() { } add_action( 'init', 'gutenberg_register_template_part_post_type' ); +/** + * Automatically set the theme meta for template parts. + * + * @param array $post_id Template Part ID. + * @param array $post Template Part Post. + * @param bool $update Is update. + */ +function gutenberg_set_template_part_post_theme( $post_id, $post, $update ) { + if ( 'wp_template_part' !== $post->post_type || $update || 'trash' === $post->post_status ) { + return; + } + + $theme = get_post_meta( $post_id, 'theme', true ); + + if ( ! $theme ) { + update_post_meta( $post_id, 'theme', wp_get_theme()->get( 'TextDomain' ) ); + } +} + +add_action( 'save_post', 'gutenberg_set_template_part_post_theme', 10, 3 ); + /** * Filters `wp_template_part` posts slug resolution to bypass deduplication logic as * template part slugs should be unique. diff --git a/lib/templates-sync.php b/lib/templates-sync.php index ab909d7f2baaf0..237aa442f0ce02 100644 --- a/lib/templates-sync.php +++ b/lib/templates-sync.php @@ -42,9 +42,6 @@ function _gutenberg_create_auto_draft_for_template( $post_type, $slug, $theme, $ 'post_status' => 'auto-draft', 'post_type' => $post_type, 'post_name' => $slug, - 'meta_input' => array( - 'theme' => $theme, - ), ) ); } else { diff --git a/lib/templates.php b/lib/templates.php index 0f4700399fc26e..bfdb18265d70de 100644 --- a/lib/templates.php +++ b/lib/templates.php @@ -89,6 +89,27 @@ function gutenberg_register_template_post_type() { } add_action( 'init', 'gutenberg_register_template_post_type' ); +/** + * Automatically set the theme meta for templates. + * + * @param array $post_id Template ID. + * @param array $post Template Post. + * @param bool $update Is update. + */ +function gutenberg_set_template_post_theme( $post_id, $post, $update ) { + if ( 'wp_template' !== $post->post_type || $update || 'trash' === $post->post_status ) { + return; + } + + $theme = get_post_meta( $post_id, 'theme', true ); + + if ( ! $theme ) { + update_post_meta( $post_id, 'theme', wp_get_theme()->get( 'TextDomain' ) ); + } +} + +add_action( 'save_post', 'gutenberg_set_template_post_theme', 10, 3 ); + /** * Filters the capabilities of a user to conditionally grant them capabilities for managing 'wp_template' posts. * From 5ef00828fb807344de66cf6cc151685958ce6eb8 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 6 Nov 2020 16:59:41 +0100 Subject: [PATCH 11/13] Small fixes --- lib/template-parts.php | 28 ++++++++++++++-------------- lib/templates.php | 6 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/template-parts.php b/lib/template-parts.php index 41ee94f8dea96e..b51a35bea6f1c2 100644 --- a/lib/template-parts.php +++ b/lib/template-parts.php @@ -36,17 +36,17 @@ function gutenberg_register_template_part_post_type() { ); $args = array( - 'labels' => $labels, - 'description' => __( 'Template parts to include in your templates.', 'gutenberg' ), - 'public' => false, - 'has_archive' => false, - 'show_ui' => true, - 'show_in_menu' => 'themes.php', - 'show_in_admin_bar' => false, - 'show_in_rest' => true, - 'rest_base' => 'template-parts', - 'map_meta_cap' => true, - 'supports' => array( + 'labels' => $labels, + 'description' => __( 'Template parts to include in your templates.', 'gutenberg' ), + 'public' => false, + 'has_archive' => false, + 'show_ui' => true, + 'show_in_menu' => 'themes.php', + 'show_in_admin_bar' => false, + 'show_in_rest' => true, + 'rest_base' => 'template-parts', + 'map_meta_cap' => true, + 'supports' => array( 'title', 'slug', 'editor', @@ -210,9 +210,9 @@ function filter_rest_wp_template_part_query( $args, $request ) { * @param mixed $dispatch_result Dispatch result, will be used if not empty. * @param WP_REST_Request $request Request used to generate the response. * @param string $route Route matched for the request. - * @param array $handler Route handler used for the request. + * @return mixed Dispatch result. */ -function gutenberg_filter_rest_wp_template_part_dispatch( $dispatch_result, $request, $route, $handler ) { +function gutenberg_filter_rest_wp_template_part_dispatch( $dispatch_result, $request, $route ) { if ( null !== $dispatch_result ) { return $dispatch_result; } @@ -224,4 +224,4 @@ function gutenberg_filter_rest_wp_template_part_dispatch( $dispatch_result, $req return null; } -add_filter( 'rest_dispatch_request', 'gutenberg_filter_rest_wp_template_part_dispatch', 10, 4 ); +add_filter( 'rest_dispatch_request', 'gutenberg_filter_rest_wp_template_part_dispatch', 10, 3 ); diff --git a/lib/templates.php b/lib/templates.php index bfdb18265d70de..f12f3dd4309086 100644 --- a/lib/templates.php +++ b/lib/templates.php @@ -264,9 +264,9 @@ function filter_rest_wp_template_query( $args, $request ) { * @param mixed $dispatch_result Dispatch result, will be used if not empty. * @param WP_REST_Request $request Request used to generate the response. * @param string $route Route matched for the request. - * @param array $handler Route handler used for the request. + * @return mixed Dispatch result. */ -function gutenberg_filter_rest_wp_template_dispatch( $dispatch_result, $request, $route, $handler ) { +function gutenberg_filter_rest_wp_template_dispatch( $dispatch_result, $request, $route ) { if ( null !== $dispatch_result ) { return $dispatch_result; } @@ -278,4 +278,4 @@ function gutenberg_filter_rest_wp_template_dispatch( $dispatch_result, $request, return null; } -add_filter( 'rest_dispatch_request', 'gutenberg_filter_rest_wp_template_dispatch', 10, 4 ); +add_filter( 'rest_dispatch_request', 'gutenberg_filter_rest_wp_template_dispatch', 10, 3 ); From fb426c2594f5c039032641a39b4a85559cc20c17 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 6 Nov 2020 17:02:19 +0100 Subject: [PATCH 12/13] use get_stylesheet to get theme identifier --- lib/edit-site-export.php | 2 +- lib/template-loader.php | 2 +- lib/template-parts.php | 2 +- lib/templates-sync.php | 2 +- lib/templates.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/edit-site-export.php b/lib/edit-site-export.php index 12751ed2b6796c..2bf82d48df0ce3 100644 --- a/lib/edit-site-export.php +++ b/lib/edit-site-export.php @@ -23,7 +23,7 @@ function gutenberg_edit_site_export() { $zip->addEmptyDir( 'theme/block-templates' ); $zip->addEmptyDir( 'theme/block-template-parts' ); - $theme = wp_get_theme()->get( 'TextDomain' ); + $theme = wp_get_theme()->get_stylesheet(); // Load templates into the zip file. $template_query = new WP_Query( diff --git a/lib/template-loader.php b/lib/template-loader.php index c8d330d6a26524..9862c337c46795 100644 --- a/lib/template-loader.php +++ b/lib/template-loader.php @@ -166,7 +166,7 @@ function gutenberg_resolve_template( $template_type, $template_hierarchy = array 'posts_per_page' => -1, 'no_found_rows' => true, 'meta_key' => 'theme', - 'meta_value' => wp_get_theme()->get( 'TextDomain' ), + 'meta_value' => wp_get_theme()->get_stylesheet(), ) ); $templates = $template_query->get_posts(); diff --git a/lib/template-parts.php b/lib/template-parts.php index b51a35bea6f1c2..503ab89e319790 100644 --- a/lib/template-parts.php +++ b/lib/template-parts.php @@ -83,7 +83,7 @@ function gutenberg_set_template_part_post_theme( $post_id, $post, $update ) { $theme = get_post_meta( $post_id, 'theme', true ); if ( ! $theme ) { - update_post_meta( $post_id, 'theme', wp_get_theme()->get( 'TextDomain' ) ); + update_post_meta( $post_id, 'theme', wp_get_theme()->get_stylesheet() ); } } diff --git a/lib/templates-sync.php b/lib/templates-sync.php index 237aa442f0ce02..934c885d8656cd 100644 --- a/lib/templates-sync.php +++ b/lib/templates-sync.php @@ -103,6 +103,6 @@ function _gutenberg_synchronize_theme_templates( $template_type ) { // Subtract ending '.html'. -5 ); - _gutenberg_create_auto_draft_for_template( $template_post_types[ $template_type ], $slug, wp_get_theme()->get( 'TextDomain' ), $content ); + _gutenberg_create_auto_draft_for_template( $template_post_types[ $template_type ], $slug, wp_get_theme()->get_stylesheet(), $content ); } } diff --git a/lib/templates.php b/lib/templates.php index f12f3dd4309086..198804fccb6f54 100644 --- a/lib/templates.php +++ b/lib/templates.php @@ -104,7 +104,7 @@ function gutenberg_set_template_post_theme( $post_id, $post, $update ) { $theme = get_post_meta( $post_id, 'theme', true ); if ( ! $theme ) { - update_post_meta( $post_id, 'theme', wp_get_theme()->get( 'TextDomain' ) ); + update_post_meta( $post_id, 'theme', wp_get_theme()->get_stylesheet() ); } } From 582f60d8102f58ee8bdbd4fbbd28eafa68138c3d Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 9 Nov 2020 09:08:39 +0100 Subject: [PATCH 13/13] Add templates and template parts documentation --- docs/architecture/fse-templates.md | 44 +++++++++++++++++++ docs/architecture/readme.md | 1 + .../developers/themes/block-based-themes.md | 2 +- docs/manifest.json | 6 +++ docs/toc.json | 3 +- 5 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 docs/architecture/fse-templates.md diff --git a/docs/architecture/fse-templates.md b/docs/architecture/fse-templates.md new file mode 100644 index 00000000000000..0bf79b012670da --- /dev/null +++ b/docs/architecture/fse-templates.md @@ -0,0 +1,44 @@ +### Template and template parts flows + +> This is the documentation for the current implementation of the block-based templates and template parts themes. This is part of the Full Site Editing project. These features are still experimental in the plugin. “Experimental” means this is just an early implementation that is subject to potential drastic and breaking changes in iterations based on feedback from users, contributors and theme authors. + +This document will explain the internals of how templates and templates parts are rendered in the frontend and edited in the backend. For an introduction about block-based themes and Full site editing templates, refer to the [block-based themes documentation](/docs/designers-developers/developers/themes/block-based-themes.md). + +## Storage + +Just like the regular templates, the block-based templates live initially as files in the theme folder but the main difference is that the user can edit these templates in the UI in the Site Editor. + +When a user edit a template (or template-part), the initial theme template file is kept as is but a forked version of the template is saved to the `wp_template` custom post type (or `wp_template_part` for template parts). + +These capabilities mean that any point of time, a mix of template files (from the theme) and CPT templates (the edited templates) are used to render the frontend of the site. + +## Synchronization + +In order to simplify the algorithm used to edit and render the templates from two different places, we performed an operation called "template synchronization". + +The synchronization consists of duplicating the theme templates in the `wp_template` (and `wp_template_part`) custom templates with an `auto-draft` status. When a user edits these templates, the status is updated to `publish`. + +This means: + + - The rendering/fetching of templates only need to consider the custom post type templates. It is not necessary to fetch the template files from the theme folder directly. The synchronization will ensure these are duplicated in the CPT. + - Untouched theme templates have the `auto-draft` status. + - Edited theme templates have the `publish` status. + +The synchronization is important for two different flows: + + - When editing the template and template parts, the site editor frontend fetches the edited and available templates throught the REST API. This means that for all `GET` API requests performed to the `wp-templates` and `wp-template-parts` end point, the synchronization is required. + - When rendering a template (sometimes referred to as "resolving a template"): this is the algorithm that WordPress follows to traverse the template hierarchy and find the right template to render for the current page being loaded. + - When exporting a block-based theme, we need to export all its templates back as files. The synchronizaion is required in order to simplify the operation and only export the CPT templates. + +## Switching themes + +Since block-based themes make use of templates that can refer to each other and that can be saved to a custom post type, it becomes possible to mix templates and template parts from different themes. For example: + + - A user might like the "header" template part of a theme A and would like to use it in theme B. + - A user might like the "contact" template from theme A and would like to use it in theme B. + +Enabling these flows will require well thought UIs and experience. For the current phase of Full-site editing, we're starting by forbidding these possibilities and making template and template-parts theme specific. + +That said, it is still important to keep track of where the template and template part come from initially. From which theme, it's based. We do so by saving a `theme` post meta containing the theme identifier for each template and template part CPT entry. + +In the future, we might consider allowing the user to mix template and template parts with different `theme` post meta values. diff --git a/docs/architecture/readme.md b/docs/architecture/readme.md index 20bb2f1a340697..c0b214d6147fed 100644 --- a/docs/architecture/readme.md +++ b/docs/architecture/readme.md @@ -10,3 +10,4 @@ Let’s look at the big picture and the architectural and UX principles of the b - What are the decision decisions behind the Data Module? - [Why is Puppeteer the tool of choice for end-to-end tests?](/docs/architecture/automated-testing.md) - [What's the difference between the different editor packages? What's the purpose of each package?](/docs/architecture/modularity.md#whats-the-difference-between-the-different-editor-packages-whats-the-purpose-of-each-package) +- [Template and template parts flows](/docs/architecture/fse-templates.md) diff --git a/docs/designers-developers/developers/themes/block-based-themes.md b/docs/designers-developers/developers/themes/block-based-themes.md index 03f4f278b9a256..6f91f7ab7092f8 100644 --- a/docs/designers-developers/developers/themes/block-based-themes.md +++ b/docs/designers-developers/developers/themes/block-based-themes.md @@ -4,7 +4,7 @@ > Documentation has been shared early to surface what’s being worked on and invite feedback from those experimenting with the APIs. You can provide feedback in the weekly #core-editor chats where the latest progress of this effort will be shared and discussed, or async via Github issues. -**Note:** In order to use these features, make sure to enable the "Full Site Editing" flag from the **Experiments** page of the Gutenberg plugin. +**Note:** In order to use these features, make sure to use a block-based theme. ## What is a block-based theme? diff --git a/docs/manifest.json b/docs/manifest.json index 5cc645a66cad8e..dee68b8b1e8d36 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -77,6 +77,12 @@ "markdown_source": "../docs/architecture/automated-testing.md", "parent": "architecture" }, + { + "title": "FseTemplates", + "slug": "fse-templates", + "markdown_source": "../docs/architecture/fse-templates.md", + "parent": "architecture" + }, { "title": "Developer Documentation", "slug": "developers", diff --git a/docs/toc.json b/docs/toc.json index 7013a98ccc2ca6..ff54916e323e32 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -12,7 +12,8 @@ { "docs/architecture/folder-structure.md": [] }, { "docs/architecture/modularity.md": [] }, { "docs/architecture/performance.md": [] }, - { "docs/architecture/automated-testing.md": [] } + { "docs/architecture/automated-testing.md": [] }, + { "docs/architecture/fse-templates.md": [] } ] }, { "docs/designers-developers/developers/README.md": [ { "docs/designers-developers/developers/block-api/README.md": [