diff --git a/plugins/woocommerce-blocks/src/BlockTemplatesController.php b/plugins/woocommerce-blocks/src/BlockTemplatesController.php index 06a339b779f..0e48bab5527 100644 --- a/plugins/woocommerce-blocks/src/BlockTemplatesController.php +++ b/plugins/woocommerce-blocks/src/BlockTemplatesController.php @@ -2,6 +2,7 @@ namespace Automattic\WooCommerce\Blocks; use Automattic\WooCommerce\Blocks\Domain\Package; +use Automattic\WooCommerce\Blocks\Templates\BlockTemplatesCompatibility; use Automattic\WooCommerce\Blocks\Templates\ProductAttributeTemplate; use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils; @@ -322,6 +323,15 @@ class BlockTemplatesController { if ( ! $template->description ) { $template->description = BlockTemplateUtils::get_block_template_description( $template->slug ); } + + if ( 'single-product' === $template->slug ) { + if ( ! is_admin() ) { + $new_content = BlockTemplatesCompatibility::wrap_single_product_template( $template->content ); + $template->content = $new_content; + } + return $template; + } + return $template; }, $query_result diff --git a/plugins/woocommerce-blocks/src/Templates/BlockTemplatesCompatibility.php b/plugins/woocommerce-blocks/src/Templates/BlockTemplatesCompatibility.php index 8177b8bc7ed..49b6d18b0bb 100644 --- a/plugins/woocommerce-blocks/src/Templates/BlockTemplatesCompatibility.php +++ b/plugins/woocommerce-blocks/src/Templates/BlockTemplatesCompatibility.php @@ -361,4 +361,133 @@ class BlockTemplatesCompatibility { } } + /** + * For compatibility reason, we need to wrap the Single Product template in a div with specific class. + * For more details, see https://github.com/woocommerce/woocommerce-blocks/issues/8314. + * + * @param string $template_content Template Content. + * @return string Wrapped template content inside a div. + */ + public static function wrap_single_product_template( $template_content ) { + $parsed_blocks = parse_blocks( $template_content ); + $grouped_blocks = self::group_blocks( $parsed_blocks ); + + // WIP: The list of blocks is WIP. + $single_product_template_blocks = array( 'woocommerce/product-image-gallery', 'woocommerce/product-details', 'woocommerce/add-to-cart-form' ); + + $wrapped_blocks = array_map( + function( $blocks ) use ( $single_product_template_blocks ) { + if ( 'core/template-part' === $blocks[0]['blockName'] ) { + return $blocks; + } + + $has_single_product_template_blocks = self::has_single_product_template_blocks( $blocks, $single_product_template_blocks ); + + if ( $has_single_product_template_blocks ) { + $wrapped_block = self::create_wrap_block_group( $blocks ); + return array( $wrapped_block[0] ); + } + return $blocks; + }, + $grouped_blocks + ); + + $template = array_reduce( + $wrapped_blocks, + function( $carry, $item ) { + if ( is_array( $item ) ) { + return $carry . serialize_blocks( $item ); + } + return $carry . serialize_block( $item ); + }, + '' + ); + + return $template; + } + + /** + * Wrap all the blocks inside the template in a group block. + * + * @param array $blocks Array of parsed block objects. + * @return array Group block with the blocks inside. + */ + private static function create_wrap_block_group( $blocks ) { + $serialized_blocks = serialize_blocks( $blocks ); + + $new_block = parse_blocks( + sprintf( + ' +
+ %1$s +
+ ', + $serialized_blocks + ) + ); + + $new_block['innerBlocks'] = $blocks; + + return $new_block; + + } + + /** + * Check if the Single Product template has a single product template block: + * woocommerce/product-gallery-image, woocommerce/product-details, woocommerce/add-to-cart-form] + * + * @param array $parsed_blocks Array of parsed block objects. + * @param array $single_product_template_blocks Array of single product template blocks. + * @return bool True if the template has a single product template block, false otherwise. + */ + private static function has_single_product_template_blocks( $parsed_blocks, $single_product_template_blocks ) { + $found = false; + + foreach ( $parsed_blocks as $block ) { + if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $single_product_template_blocks, true ) ) { + $found = true; + break; + } + $found = self::has_single_product_template_blocks( $block['innerBlocks'], $single_product_template_blocks ); + if ( $found ) { + break; + } + } + return $found; + } + + + /** + * Group blocks in this way: + * B1 + TP1 + B2 + B3 + B4 + TP2 + B5 + * (B = Block, TP = Template Part) + * becomes: + * [[B1], [TP1], [B2, B3, B4], [TP2], [B5]] + * + * @param array $parsed_blocks Array of parsed block objects. + * @return array Array of blocks grouped by template part. + */ + private static function group_blocks( $parsed_blocks ) { + return array_reduce( + $parsed_blocks, + function( $carry, $block ) { + if ( 'core/template-part' === $block['blockName'] ) { + array_push( $carry, array( $block ) ); + return $carry; + } + if ( empty( $block['blockName'] ) ) { + return $carry; + } + $last_element_index = count( $carry ) - 1 < 0 ? 0 : count( $carry ) - 1; + if ( isset( $carry[ $last_element_index ][0]['blockName'] ) && 'core/template-part' !== $carry[ $last_element_index ][0]['blockName'] ) { + array_push( $carry[ $last_element_index ], $block ); + return $carry; + } + array_push( $carry, array( $block ) ); + return $carry; + }, + array() + ); + } + } diff --git a/plugins/woocommerce-blocks/tests/php/Templates/BlockTemplatesCompatibilityTests.php b/plugins/woocommerce-blocks/tests/php/Templates/BlockTemplatesCompatibilityTests.php new file mode 100644 index 00000000000..071fd19ae55 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/php/Templates/BlockTemplatesCompatibilityTests.php @@ -0,0 +1,330 @@ + + +
+ +
+ + '; + + $expected_single_product_template = ' + + +
+ +
+ + '; + + $result = BlockTemplatesCompatibility::wrap_single_product_template( $default_single_product_template ); + + $result_without_withespace = preg_replace( '/\s+/', '', $result ); + $expected_single_product_template_without_whitespace = preg_replace( '/\s+/', '', $expected_single_product_template ); + + $this->assertEquals( $result_without_withespace, $expected_single_product_template_without_whitespace, '' ); + } + + /** + * Test that the Single Product Template is wrapped in a div with the correct class if it contains a block related to the Single Product Template. + */ + public function test_wrap_single_product_template_if_contains_single_product_blocks() { + + $default_single_product_template = ' + + +
+ +
+ + '; + + $expected_single_product_template = ' + + +
+ +
+ +
+ +
+ + '; + + $result = BlockTemplatesCompatibility::wrap_single_product_template( $default_single_product_template ); + + $result_without_withespace = preg_replace( '/\s+/', '', $result ); + $expected_single_product_template_without_whitespace = preg_replace( '/\s+/', '', $expected_single_product_template ); + + $this->assertEquals( $result_without_withespace, $expected_single_product_template_without_whitespace, '' ); + } + + /** + * Test that the Single Product Template is wrapped in a div with the correct class if it contains a block related to the Single Product Template in a nested structure. + */ + public function test_wrap_single_product_template_if_contains_nested_single_product_blocks() { + + $default_single_product_template = ' + + +
+ +
+ +
+ + +
+ + + + + + + + + + + + + +

+ + +
+ +
+ + '; + + $expected_single_product_template = ' + + +
+ +
+ +
+ +
+ + +
+ + + + + + + + + + + + + +

+ + +
+ +
+ +
+ + '; + + $result = BlockTemplatesCompatibility::wrap_single_product_template( $default_single_product_template ); + + $result_without_withespace = preg_replace( '/\s+/', '', $result ); + $expected_single_product_template_without_whitespace = preg_replace( '/\s+/', '', $expected_single_product_template ); + + $this->assertEquals( $result_without_withespace, $expected_single_product_template_without_whitespace, '' ); + } + + /** + * Test that the Single Product Template is wrapped in a div with the correct class if it contains a block related to the Single Product Template in a nested structure. + */ + public function test_wrap_single_product_template_without_a_main_wrapper() { + + $default_single_product_template = ' + + + +
+ + + + + + + + + + + + + +

+ + +
+ + '; + + $expected_single_product_template = ' + + +
+ + +
+ + + + + + + + + + + + + +

+ + +
+ +
+ + '; + + $result = BlockTemplatesCompatibility::wrap_single_product_template( $default_single_product_template ); + + $result_without_withespace = preg_replace( '/\s+/', '', $result ); + $expected_single_product_template_without_whitespace = preg_replace( '/\s+/', '', $expected_single_product_template ); + + $this->assertEquals( $result_without_withespace, $expected_single_product_template_without_whitespace, '' ); + } + + /** + * Test that the Single Product Template is wrapped in a div with the correct class if it contains a block related to the Single Product Template. + */ + public function test_wrap_single_product_template_with_multiple_header() { + + $default_single_product_template = ' + + + + '; + + $expected_single_product_template = ' + + + +
+ +
+ + '; + + $result = BlockTemplatesCompatibility::wrap_single_product_template( $default_single_product_template ); + + $result_without_withespace = preg_replace( '/\s+/', '', $result ); + $expected_single_product_template_without_whitespace = preg_replace( '/\s+/', '', $expected_single_product_template ); + + $this->assertEquals( $result_without_withespace, $expected_single_product_template_without_whitespace, '' ); + } + + + /** + * Test that the Single Product Template is wrapped in a div with the correct class if it contains a block related to the Single Product Template. + */ + public function test_wrap_single_product_template_with_multiple_footer() { + + $default_single_product_template = ' + + + + '; + + $expected_single_product_template = ' + + +
+ +
+ + + '; + + $result = BlockTemplatesCompatibility::wrap_single_product_template( $default_single_product_template ); + + $result_without_withespace = preg_replace( '/\s+/', '', $result ); + $expected_single_product_template_without_whitespace = preg_replace( '/\s+/', '', $expected_single_product_template ); + + $this->assertEquals( $result_without_withespace, $expected_single_product_template_without_whitespace, '' ); + } + + /** + * Test that the Single Product Template is wrapped in a div with the correct class if it contains a block related to the Single Product Template. + */ + public function test_wrap_single_product_template_with_multiple_blocks_related_to_the_single_product_template() { + + $default_single_product_template = ' + +

test

+ + + + + + '; + + $expected_single_product_template = ' + +

test

+ + + +
+ +
+ + + +
+ +
+ + '; + + $result = BlockTemplatesCompatibility::wrap_single_product_template( $default_single_product_template ); + + $result_without_withespace = preg_replace( '/\s+/', '', $result ); + $expected_single_product_template_without_whitespace = preg_replace( '/\s+/', '', $expected_single_product_template ); + + $this->assertEquals( $result_without_withespace, $expected_single_product_template_without_whitespace, '' ); + } +}