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": [ diff --git a/lib/edit-site-export.php b/lib/edit-site-export.php index 8967788bdf8a1c..2bf82d48df0ce3 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 and template parts need to be synchronized + // before the export. + _gutenberg_synchronize_theme_templates( 'template-part' ); + _gutenberg_synchronize_theme_templates( 'template' ); + // 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_stylesheet(); - $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 parts 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/load.php b/lib/load.php index 05d2cde5cb2c10..7da28e028df285 100644 --- a/lib/load.php +++ b/lib/load.php @@ -93,15 +93,18 @@ 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'; } + require_once dirname( __FILE__ ) . '/widgets-page.php'; require dirname( __FILE__ ) . '/compat.php'; 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 23829d94d565e0..9862c337c46795 100644 --- a/lib/template-loader.php +++ b/lib/template-loader.php @@ -93,13 +93,16 @@ 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 ); + // Create auto-drafts for theme templates. + _gutenberg_synchronize_theme_templates( 'template' ); + + $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,84 +127,7 @@ function gutenberg_override_query_template( $template, $type, array $templates = } /** - * 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. + * Return the correct 'wp_template' to render fot the request template type. * * Accepts an optional $template_hierarchy argument as a hint. * @@ -212,7 +138,7 @@ function create_auto_draft_for_template_part_block( $block ) { * @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; } @@ -230,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_stylesheet(), ) ); + $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; + 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; } - $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; - } - } - - // 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; } /** @@ -396,37 +247,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..503ab89e319790 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_stylesheet() ); + } +} + +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. @@ -145,22 +166,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', ), @@ -177,39 +190,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( @@ -217,52 +197,31 @@ 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; } 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. + * @return mixed Dispatch result. + */ +function gutenberg_filter_rest_wp_template_part_dispatch( $dispatch_result, $request, $route ) { + 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, 3 ); diff --git a/lib/templates-sync.php b/lib/templates-sync.php new file mode 100644 index 00000000000000..934c885d8656cd --- /dev/null +++ b/lib/templates-sync.php @@ -0,0 +1,108 @@ + $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, + ) + ); + } 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_stylesheet(), $content ); + } +} diff --git a/lib/templates.php b/lib/templates.php index 406026bc599050..198804fccb6f54 100644 --- a/lib/templates.php +++ b/lib/templates.php @@ -75,9 +75,41 @@ function gutenberg_register_template_post_type() { ); register_post_type( 'wp_template', $args ); + + $meta_args = array( + 'object_subtype' => 'wp_template', + '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' ); +/** + * 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_stylesheet() ); + } +} + +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. * @@ -203,13 +235,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 +245,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 +257,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 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. + * @return mixed Dispatch result. + */ +function gutenberg_filter_rest_wp_template_dispatch( $dispatch_result, $request, $route ) { + 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, 3 );