diff --git a/src/wp-includes/class-wp-block-bindings-processor.php b/src/wp-includes/class-wp-block-bindings-processor.php new file mode 100644 index 0000000000000..f833b9ed07562 --- /dev/null +++ b/src/wp-includes/class-wp-block-bindings-processor.php @@ -0,0 +1,70 @@ +output . substr( $this->get_updated_html(), $this->end_of_flushed ); + } + + /** + * Replace the rich text content between a tag opener and matching closer. + * + * When stopped on a tag opener, replace the content enclosed by it and its + * matching closer with the provided rich text. + * + * @param string $rich_text The rich text to replace the original content with. + * @return bool True on success. + */ + public function replace_rich_text( $rich_text ) { + if ( $this->is_tag_closer() ) { + return false; + } + + $depth = $this->get_current_depth(); + + $this->set_bookmark( '_wp_block_bindings_tag_opener' ); + // The bookmark names are prefixed with `_` so the key below has an extra `_`. + $bm = $this->bookmarks['__wp_block_bindings_tag_opener']; + $this->output .= substr( $this->get_updated_html(), $this->end_of_flushed, $bm->start + $bm->length ); + $this->output .= $rich_text; + $this->release_bookmark( '_wp_block_bindings_tag_opener' ); + + // Find matching tag closer. + while ( $this->next_token() && $this->get_current_depth() >= $depth ) { + } + + $this->set_bookmark( '_wp_block_bindings_tag_closer' ); + $bm = $this->bookmarks['__wp_block_bindings_tag_closer']; + $this->end_of_flushed = $bm->start; + $this->release_bookmark( '_wp_block_bindings_tag_closer' ); + + return true; + } +} diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index 3c854f2cf4f93..173b0dbe80067 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -417,7 +417,7 @@ private function replace_html( string $block_content, string $attribute_name, $s switch ( $block_type->attributes[ $attribute_name ]['source'] ) { case 'html': case 'rich-text': - $block_reader = new WP_HTML_Tag_Processor( $block_content ); + $block_reader = WP_Block_Bindings_Processor::create_fragment( $block_content ); // TODO: Support for CSS selectors whenever they are ready in the HTML API. // In the meantime, support comma-separated selectors by exploding them into an array. @@ -426,18 +426,6 @@ private function replace_html( string $block_content, string $attribute_name, $s $block_reader->next_tag(); $block_reader->set_bookmark( 'iterate-selectors' ); - // TODO: This shouldn't be needed when the `set_inner_html` function is ready. - // Store the parent tag and its attributes to be able to restore them later in the button. - // The button block has a wrapper while the paragraph and heading blocks don't. - if ( 'core/button' === $this->name ) { - $button_wrapper = $block_reader->get_tag(); - $button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); - $button_wrapper_attrs = array(); - foreach ( $button_wrapper_attribute_names as $name ) { - $button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name ); - } - } - foreach ( $selectors as $selector ) { // If the parent tag, or any of its children, matches the selector, replace the HTML. if ( strcasecmp( $block_reader->get_tag(), $selector ) === 0 || $block_reader->next_tag( @@ -445,34 +433,10 @@ private function replace_html( string $block_content, string $attribute_name, $s 'tag_name' => $selector, ) ) ) { + // TODO: Use `WP_HTML_Processor::set_inner_html` method once it's available. $block_reader->release_bookmark( 'iterate-selectors' ); - - // TODO: Use `set_inner_html` method whenever it's ready in the HTML API. - // Until then, it is hardcoded for the paragraph, heading, and button blocks. - // Store the tag and its attributes to be able to restore them later. - $selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); - $selector_attrs = array(); - foreach ( $selector_attribute_names as $name ) { - $selector_attrs[ $name ] = $block_reader->get_attribute( $name ); - } - $selector_markup = "<$selector>" . wp_kses_post( $source_value ) . "$selector>"; - $amended_content = new WP_HTML_Tag_Processor( $selector_markup ); - $amended_content->next_tag(); - foreach ( $selector_attrs as $attribute_key => $attribute_value ) { - $amended_content->set_attribute( $attribute_key, $attribute_value ); - } - if ( 'core/paragraph' === $this->name || 'core/heading' === $this->name ) { - return $amended_content->get_updated_html(); - } - if ( 'core/button' === $this->name ) { - $button_markup = "<$button_wrapper>{$amended_content->get_updated_html()}$button_wrapper>"; - $amended_button = new WP_HTML_Tag_Processor( $button_markup ); - $amended_button->next_tag(); - foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) { - $amended_button->set_attribute( $attribute_key, $attribute_value ); - } - return $amended_button->get_updated_html(); - } + $block_reader->replace_rich_text( wp_kses_post( $source_value ) ); + return $block_reader->build(); } else { $block_reader->seek( 'iterate-selectors' ); } diff --git a/src/wp-settings.php b/src/wp-settings.php index 3892b8cd33f91..6bfa853526ae0 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -346,6 +346,7 @@ require ABSPATH . WPINC . '/sitemaps/providers/class-wp-sitemaps-posts.php'; require ABSPATH . WPINC . '/sitemaps/providers/class-wp-sitemaps-taxonomies.php'; require ABSPATH . WPINC . '/sitemaps/providers/class-wp-sitemaps-users.php'; +require ABSPATH . WPINC . '/class-wp-block-bindings-processor.php'; require ABSPATH . WPINC . '/class-wp-block-bindings-source.php'; require ABSPATH . WPINC . '/class-wp-block-bindings-registry.php'; require ABSPATH . WPINC . '/class-wp-block-editor-context.php'; diff --git a/tests/phpunit/tests/block-bindings/render.php b/tests/phpunit/tests/block-bindings/render.php index 007aeace1f2bf..60b970cf7943e 100644 --- a/tests/phpunit/tests/block-bindings/render.php +++ b/tests/phpunit/tests/block-bindings/render.php @@ -61,14 +61,41 @@ public static function wpTearDownAfterClass() { unregister_block_type( 'test/block' ); } + public function data_update_block_with_value_from_source() { + return array( + 'paragraph block' => array( + 'content', + << +
This should not appear
+ +HTML + , + 'test source value
', + ), + 'button block' => array( + 'text', + << + + +HTML + , + ' ', + ), + ); + } + /** * Test if the block content is updated with the value returned by the source. * * @ticket 60282 * * @covers ::register_block_bindings_source + * + * @dataProvider data_update_block_with_value_from_source */ - public function test_update_block_with_value_from_source() { + public function test_update_block_with_value_from_source( $bound_attribute, $block_content, $expected_result ) { $get_value_callback = function () { return 'test source value'; }; @@ -81,22 +108,26 @@ public function test_update_block_with_value_from_source() { ) ); - $block_content = << -This should not appear
- -HTML; $parsed_blocks = parse_blocks( $block_content ); - $block = new WP_Block( $parsed_blocks[0] ); - $result = $block->render(); + + $parsed_blocks[0]['attrs']['metadata'] = array( + 'bindings' => array( + $bound_attribute => array( + 'source' => self::SOURCE_NAME, + ), + ), + ); + + $block = new WP_Block( $parsed_blocks[0] ); + $result = $block->render(); $this->assertSame( 'test source value', - $block->attributes['content'], - "The 'content' attribute should be updated with the value returned by the source." + $block->attributes[ $bound_attribute ], + "The '{$bound_attribute}' attribute should be updated with the value returned by the source." ); $this->assertSame( - 'test source value
', + $expected_result, trim( $result ), 'The block content should be updated with the value returned by the source.' ); diff --git a/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php b/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php new file mode 100644 index 0000000000000..5a8cf11095847 --- /dev/null +++ b/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php @@ -0,0 +1,91 @@ +'; + $button_wrapper_closer = ''; + $processor = WP_Block_Bindings_Processor::create_fragment( + $button_wrapper_opener . 'This should not appear' . $button_wrapper_closer + ); + $processor->next_tag( array( 'tag_name' => 'a' ) ); + + $this->assertTrue( $processor->replace_rich_text( 'The hardest button to button' ) ); + $this->assertEquals( + $button_wrapper_opener . 'The hardest button to button' . $button_wrapper_closer, + $processor->build() + ); + } + + /** + * @ticket 63840 + */ + public function test_set_attribute_and_replace_rich_text() { + $figure_opener = '