diff --git a/src/BlockTemplatesController.php b/src/BlockTemplatesController.php new file mode 100644 index 00000000000..8225f989acb --- /dev/null +++ b/src/BlockTemplatesController.php @@ -0,0 +1,106 @@ +templates_directory = plugin_dir_path( __DIR__ ) . 'templates/' . self::TEMPLATES_DIR_NAME; + $this->init(); + } + + /** + * Initialization method. + */ + protected function init() { + add_filter( 'get_block_templates', array( $this, 'add_block_templates' ), 10, 3 ); + } + + /** + * Add the block template objects to be used. + * + * @param array $query_result Array of template objects. + * @return array + */ + public function add_block_templates( $query_result ) { + if ( ! gutenberg_supports_block_templates() ) { + return $query_result; + } + + $template_files = $this->get_block_templates(); + + foreach ( $template_files as $template_file ) { + $query_result[] = BlockTemplateUtils::gutenberg_build_template_result_from_file( $template_file, 'wp_template' ); + } + + return $query_result; + } + + /** + * Get and build the block template objects from the block template files. + * + * @return array + */ + public function get_block_templates() { + $template_files = BlockTemplateUtils::gutenberg_get_template_paths( $this->templates_directory ); + $templates = array(); + + foreach ( $template_files as $template_file ) { + $template_slug = substr( + $template_file, + strpos( $template_file, self::TEMPLATES_DIR_NAME . DIRECTORY_SEPARATOR ) + 1 + strlen( self::TEMPLATES_DIR_NAME ), + -5 + ); + + // If the theme already has a template then there is no need to load ours in. + if ( $this->theme_has_template( $template_slug ) ) { + continue; + } + + $new_template_item = array( + 'title' => ucwords( str_replace( '-', ' ', $template_slug ) ), + 'slug' => $template_slug, + 'path' => $template_file, + 'theme' => get_template_directory(), + 'type' => 'wp_template', + ); + $templates[] = $new_template_item; + } + + return $templates; + } + + /** + * Check if the theme has a template. So we know if to load our own in or not. + * + * @param string $template_name name of the template file without .html extension e.g. 'single-product'. + * @return boolean + */ + public function theme_has_template( $template_name ) { + return is_readable( get_template_directory() . '/block-templates/' . $template_name . '.html' ) || + is_readable( get_stylesheet_directory() . '/block-templates/' . $template_name . '.html' ); + } +} diff --git a/src/Domain/Bootstrap.php b/src/Domain/Bootstrap.php index 0e547ad98ba..b7944d36e35 100644 --- a/src/Domain/Bootstrap.php +++ b/src/Domain/Bootstrap.php @@ -5,6 +5,7 @@ use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use Automattic\WooCommerce\Blocks\BlockTypesController; +use Automattic\WooCommerce\Blocks\BlockTemplatesController; use Automattic\WooCommerce\Blocks\InboxNotifications; use Automattic\WooCommerce\Blocks\Installer; use Automattic\WooCommerce\Blocks\Registry\Container; @@ -101,6 +102,9 @@ function() { $this->container->get( RestApi::class ); $this->container->get( GoogleAnalytics::class ); $this->container->get( BlockTypesController::class ); + if ( $this->package->feature()->is_experimental_build() ) { + $this->container->get( BlockTemplatesController::class ); + } if ( $this->package->feature()->is_feature_plugin_build() ) { $this->container->get( PaymentsApi::class ); } @@ -227,6 +231,14 @@ function ( Container $container ) { return new BlockTypesController( $asset_api, $asset_data_registry ); } ); + if ( $this->package->feature()->is_experimental_build() ) { + $this->container->register( + BlockTemplatesController::class, + function ( Container $container ) { + return new BlockTemplatesController(); + } + ); + } $this->container->register( DraftOrders::class, function( Container $container ) { diff --git a/src/Utils/BlockTemplateUtils.php b/src/Utils/BlockTemplateUtils.php new file mode 100644 index 00000000000..a2dffb7b4ec --- /dev/null +++ b/src/Utils/BlockTemplateUtils.php @@ -0,0 +1,131 @@ + 0 ) { + $block = &$queue[0]; + array_shift( $queue ); + $all_blocks[] = &$block; + + if ( ! empty( $block['innerBlocks'] ) ) { + foreach ( $block['innerBlocks'] as &$inner_block ) { + $queue[] = &$inner_block; + } + } + + $queue_count = count( $queue ); + } + + return $all_blocks; + } + + /** + * Parses wp_template content and injects the current theme's + * stylesheet as a theme attribute into each wp_template_part + * + * @param string $template_content serialized wp_template content. + * + * @return string Updated wp_template content. + */ + public static function gutenberg_inject_theme_attribute_in_content( $template_content ) { + $has_updated_content = false; + $new_content = ''; + $template_blocks = parse_blocks( $template_content ); + + $blocks = self::gutenberg_flatten_blocks( $template_blocks ); + foreach ( $blocks as &$block ) { + if ( + 'core/template-part' === $block['blockName'] && + ! isset( $block['attrs']['theme'] ) + ) { + $block['attrs']['theme'] = wp_get_theme()->get_stylesheet(); + $has_updated_content = true; + } + } + + if ( $has_updated_content ) { + foreach ( $template_blocks as &$block ) { + $new_content .= serialize_block( $block ); + } + + return $new_content; + } + + return $template_content; + } + + /** + * Build a unified template object based on a theme file. + * + * @param array $template_file Theme file. + * @param array $template_type wp_template or wp_template_part. + * + * @return WP_Block_Template Template. + */ + public static function gutenberg_build_template_result_from_file( $template_file, $template_type ) { + $default_template_types = gutenberg_get_default_template_types(); + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $template_content = file_get_contents( $template_file['path'] ); + $theme = wp_get_theme()->get_stylesheet(); + + $template = new \WP_Block_Template(); + $template->id = $theme . '//' . $template_file['slug']; + $template->theme = $theme; + $template->content = self::gutenberg_inject_theme_attribute_in_content( $template_content ); + $template->slug = $template_file['slug']; + $template->source = 'theme'; + $template->type = $template_type; + $template->title = ! empty( $template_file['title'] ) ? $template_file['title'] : $template_file['slug']; + $template->status = 'publish'; + $template->has_theme_file = true; + + if ( 'wp_template' === $template_type && isset( $default_template_types[ $template_file['slug'] ] ) ) { + $template->description = $default_template_types[ $template_file['slug'] ]['description']; + $template->title = $default_template_types[ $template_file['slug'] ]['title']; + } + + if ( 'wp_template_part' === $template_type && isset( $template_file['area'] ) ) { + $template->area = $template_file['area']; + } + + return $template; + } + + /** + * 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. + */ + public static 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; + } +}