From 2d7b4f3f06beab2859f24afef76774661bff3f6a Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 13 Aug 2025 15:13:13 +0200 Subject: [PATCH 01/17] Introduce data provider to allow extending test coverage --- tests/phpunit/tests/block-bindings/render.php | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/tests/phpunit/tests/block-bindings/render.php b/tests/phpunit/tests/block-bindings/render.php index 007aeace1f2bf..fdae23e3076b3 100644 --- a/tests/phpunit/tests/block-bindings/render.php +++ b/tests/phpunit/tests/block-bindings/render.php @@ -61,14 +61,30 @@ 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

' + ) + ); + } + /** * 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 +97,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.' ); From a1371761bbcbf238f7297f60f8062b1a0d6f3a2d Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 13 Aug 2025 15:27:59 +0200 Subject: [PATCH 02/17] Add test coverage for Button block's text attribute --- src/wp-includes/class-wp-block.php | 2 +- tests/phpunit/tests/block-bindings/render.php | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index 3c854f2cf4f93..fc4e3a0a0fec1 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -430,7 +430,7 @@ private function replace_html( string $block_content, string $attribute_name, $s // 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 = strtolower( $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 ) { diff --git a/tests/phpunit/tests/block-bindings/render.php b/tests/phpunit/tests/block-bindings/render.php index fdae23e3076b3..41d92fd42c93c 100644 --- a/tests/phpunit/tests/block-bindings/render.php +++ b/tests/phpunit/tests/block-bindings/render.php @@ -71,7 +71,16 @@ public function data_update_block_with_value_from_source() { HTML, '

test source value

' - ) + ), + 'button block' => array( + 'text', + << + + +HTML, + '' + ), ); } From 83b3ae13492f6cfd49b31ec9acdfa98b8e5c0762 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 13 Aug 2025 13:24:53 +0200 Subject: [PATCH 03/17] Block Bindings: Simplify replace_html() method --- src/wp-includes/class-wp-block.php | 44 ++++-------------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index fc4e3a0a0fec1..5737ef107ac44 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_HTML_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 = strtolower( $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( @@ -447,32 +435,12 @@ private function replace_html( string $block_content, string $attribute_name, $s ) ) { $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 ) . ""; - $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()}"; - $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(); + // TODO: Use `set_inner_html` method whenever it's ready in the HTML API.) + $block_reader->next_token(); + if ( '#text' === $block_reader->get_token_type() ) { + $block_reader->set_modifiable_text( $source_value ); } + return $block_reader->get_updated_html(); } else { $block_reader->seek( 'iterate-selectors' ); } From 15d619b6fc6ec0209ea4c048fa766b11293288ba Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Mon, 18 Aug 2025 16:16:57 +0200 Subject: [PATCH 04/17] Add WP_Block_Bindings_Processor class --- .../class-wp-block-bindings-processor.php | 34 +++++++++++++++++++ src/wp-settings.php | 1 + 2 files changed, 35 insertions(+) create mode 100644 src/wp-includes/class-wp-block-bindings-processor.php 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..c86ed6c5aa6b8 --- /dev/null +++ b/src/wp-includes/class-wp-block-bindings-processor.php @@ -0,0 +1,34 @@ +output . substr( $this->html, $this->end_of_flushed ); + } + + public function replace_rich_text( $rich_text ) { + if ( $this->is_tag_closer() ) { + return false; + } + + $tag_name = $this->get_tag(); + $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->html, $this->end_of_flushed, $bm->start + $bm->length ); + $this->output .= $rich_text; + $this->release_bookmark( '_wp_block_bindings_tag_opener' ); + + 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' ); + } +} 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'; From 9dc0aba004904d6553ef8a54bab1a088c1fb2eb4 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Mon, 18 Aug 2025 11:33:25 +0200 Subject: [PATCH 05/17] Use WP_Block_Bindings_Processor for block bindings --- src/wp-includes/class-wp-block.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index 5737ef107ac44..f8f39d7524ef8 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 = WP_HTML_Processor::create_fragment( $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. @@ -433,14 +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.) - $block_reader->next_token(); - if ( '#text' === $block_reader->get_token_type() ) { - $block_reader->set_modifiable_text( $source_value ); - } - return $block_reader->get_updated_html(); + $block_reader->replace_rich_text( $source_value ); + return $block_reader->build(); } else { $block_reader->seek( 'iterate-selectors' ); } From d9132f8b4dcdddadc097878fb352309282faaebd Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Mon, 18 Aug 2025 16:21:25 +0200 Subject: [PATCH 06/17] Add kses back :/ --- src/wp-includes/class-wp-block.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index f8f39d7524ef8..173b0dbe80067 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -435,7 +435,7 @@ private function replace_html( string $block_content, string $attribute_name, $s ) ) { // TODO: Use `WP_HTML_Processor::set_inner_html` method once it's available. $block_reader->release_bookmark( 'iterate-selectors' ); - $block_reader->replace_rich_text( $source_value ); + $block_reader->replace_rich_text( wp_kses_post( $source_value ) ); return $block_reader->build(); } else { $block_reader->seek( 'iterate-selectors' ); From c011d254900b14631d2de8d02ca7bf3e636dbfc1 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Mon, 18 Aug 2025 16:22:47 +0200 Subject: [PATCH 07/17] WPCS --- tests/phpunit/tests/block-bindings/render.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/block-bindings/render.php b/tests/phpunit/tests/block-bindings/render.php index 41d92fd42c93c..60b970cf7943e 100644 --- a/tests/phpunit/tests/block-bindings/render.php +++ b/tests/phpunit/tests/block-bindings/render.php @@ -69,17 +69,19 @@ public function data_update_block_with_value_from_source() {

This should not appear

-HTML, - '

test source value

' +HTML + , + '

test source value

', ), - 'button block' => array( + 'button block' => array( 'text', << -HTML, - '' +HTML + , + '', ), ); } @@ -113,7 +115,7 @@ public function test_update_block_with_value_from_source( $bound_attribute, $blo $bound_attribute => array( 'source' => self::SOURCE_NAME, ), - ) + ), ); $block = new WP_Block( $parsed_blocks[0] ); From 9295227f7af241380dc17ec928e614196993d5f1 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Mon, 18 Aug 2025 16:24:49 +0200 Subject: [PATCH 08/17] Remove obsolete var --- src/wp-includes/class-wp-block-bindings-processor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-block-bindings-processor.php b/src/wp-includes/class-wp-block-bindings-processor.php index c86ed6c5aa6b8..7c979cd948ecf 100644 --- a/src/wp-includes/class-wp-block-bindings-processor.php +++ b/src/wp-includes/class-wp-block-bindings-processor.php @@ -13,8 +13,7 @@ public function replace_rich_text( $rich_text ) { return false; } - $tag_name = $this->get_tag(); - $depth = $this->get_current_depth(); + $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 `_`. @@ -23,6 +22,7 @@ public function replace_rich_text( $rich_text ) { $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 ) { } From 3c3f90886984ebd834d8d9036f853ee115e39bff Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Mon, 18 Aug 2025 16:29:16 +0200 Subject: [PATCH 09/17] Indentation --- src/wp-includes/class-wp-block-bindings-processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-block-bindings-processor.php b/src/wp-includes/class-wp-block-bindings-processor.php index 7c979cd948ecf..3309e2daf2407 100644 --- a/src/wp-includes/class-wp-block-bindings-processor.php +++ b/src/wp-includes/class-wp-block-bindings-processor.php @@ -22,7 +22,7 @@ public function replace_rich_text( $rich_text ) { $this->output .= $rich_text; $this->release_bookmark( '_wp_block_bindings_tag_opener' ); - // Find matching tag closer. + // Find matching tag closer. while ( $this->next_token() && $this->get_current_depth() >= $depth ) { } From c46300d24ad703a43ba97b4f14877724936009f2 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Mon, 18 Aug 2025 17:10:01 +0200 Subject: [PATCH 10/17] Return true upon success --- src/wp-includes/class-wp-block-bindings-processor.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wp-includes/class-wp-block-bindings-processor.php b/src/wp-includes/class-wp-block-bindings-processor.php index 3309e2daf2407..a20c0a557de6e 100644 --- a/src/wp-includes/class-wp-block-bindings-processor.php +++ b/src/wp-includes/class-wp-block-bindings-processor.php @@ -30,5 +30,7 @@ public function replace_rich_text( $rich_text ) { $bm = $this->bookmarks['__wp_block_bindings_tag_closer']; $this->end_of_flushed = $bm->start; $this->release_bookmark( '_wp_block_bindings_tag_closer' ); + + return true; } } From f52870270ef2398ae7c0c5eda89e5e12576b4ec0 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Mon, 18 Aug 2025 17:15:47 +0200 Subject: [PATCH 11/17] Add basic PHPDoc --- .../class-wp-block-bindings-processor.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/wp-includes/class-wp-block-bindings-processor.php b/src/wp-includes/class-wp-block-bindings-processor.php index a20c0a557de6e..915c72953eeef 100644 --- a/src/wp-includes/class-wp-block-bindings-processor.php +++ b/src/wp-includes/class-wp-block-bindings-processor.php @@ -1,5 +1,12 @@ output . substr( $this->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; From 73efaa7953f82d8c2787201e9a5db759e5c498e6 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Tue, 19 Aug 2025 13:58:26 +0200 Subject: [PATCH 12/17] Add more PHPDoc --- src/wp-includes/class-wp-block-bindings-processor.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/wp-includes/class-wp-block-bindings-processor.php b/src/wp-includes/class-wp-block-bindings-processor.php index 915c72953eeef..c11bd47f37bdb 100644 --- a/src/wp-includes/class-wp-block-bindings-processor.php +++ b/src/wp-includes/class-wp-block-bindings-processor.php @@ -6,6 +6,12 @@ * This class can be used to perform the sort of structural * changes to an HTML document that are required by * Block Bindings. + * + * @access private + * + * @package WordPress + * @subpackage Block Bindings + * @since 6.9.0 */ class WP_Block_Bindings_Processor extends WP_HTML_Processor { private $output = ''; From d5798c1a5b07064ac06918a0a31da6bc50f8b75b Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Tue, 19 Aug 2025 14:21:48 +0200 Subject: [PATCH 13/17] Add basic test coverage --- .../wpBlockBindingsProcessor.php | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php diff --git a/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php b/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php new file mode 100644 index 0000000000000..218413007db8e --- /dev/null +++ b/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php @@ -0,0 +1,29 @@ +'; + $figure_closer = ''; + $processor = WP_Block_Bindings_Processor::create_fragment( + $figure_opener . + '
Breakfast at a café in Berlin
' . + $figure_closer + ); + $processor->next_tag( array( 'tag_name' => 'figcaption' ) ); + + $this->assertTrue( $processor->replace_rich_text( 'New image caption' ) ); + $this->assertEquals( + $figure_opener . '
New image caption
' . $figure_closer, + $processor->build() + ); + } +} From 26a0a294d429f7596b61edc7884b5dc1e8dedc55 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Tue, 19 Aug 2025 14:56:04 +0200 Subject: [PATCH 14/17] Allow setting attributes --- .../class-wp-block-bindings-processor.php | 4 ++-- .../block-bindings/wpBlockBindingsProcessor.php | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-wp-block-bindings-processor.php b/src/wp-includes/class-wp-block-bindings-processor.php index c11bd47f37bdb..8c3a06c64e8ea 100644 --- a/src/wp-includes/class-wp-block-bindings-processor.php +++ b/src/wp-includes/class-wp-block-bindings-processor.php @@ -18,7 +18,7 @@ class WP_Block_Bindings_Processor extends WP_HTML_Processor { private $end_of_flushed = 0; public function build() { - return $this->output . substr( $this->html, $this->end_of_flushed ); + return $this->output . substr( $this->get_updated_html(), $this->end_of_flushed ); } /** @@ -40,7 +40,7 @@ public function replace_rich_text( $rich_text ) { $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->html, $this->end_of_flushed, $bm->start + $bm->length ); + $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' ); diff --git a/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php b/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php index 218413007db8e..470e9d12387a0 100644 --- a/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php +++ b/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php @@ -10,19 +10,28 @@ * @group block-bindings */ class Tests_Blocks_wpBlockBindingsProcessor extends WP_UnitTestCase { - public function test_replace_rich_text() { - $figure_opener = '
'; + public function test_set_attribute_and_replace_rich_text() { + $figure_opener = '
'; + $img = ''; $figure_closer = '
'; $processor = WP_Block_Bindings_Processor::create_fragment( $figure_opener . + $img . '
Breakfast at a café in Berlin
' . $figure_closer ); + + $processor->next_tag( array( 'tag_name' => 'figure' ) ); + $processor->add_class( 'size-large' ); + $processor->next_tag( array( 'tag_name' => 'figcaption' ) ); - $this->assertTrue( $processor->replace_rich_text( 'New image caption' ) ); + $this->assertTrue( $processor->replace_rich_text( 'New image caption' ) ); $this->assertEquals( - $figure_opener . '
New image caption
' . $figure_closer, + '
' . + $img . + '
New image caption
' . + $figure_closer, $processor->build() ); } From 0dbe2353869473e4d3cb222cd5e8c516e1c77758 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Tue, 19 Aug 2025 16:56:18 +0200 Subject: [PATCH 15/17] Increase test coverage --- .../wpBlockBindingsProcessor.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php b/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php index 470e9d12387a0..c3467556c36dd 100644 --- a/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php +++ b/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php @@ -10,6 +10,27 @@ * @group block-bindings */ class Tests_Blocks_wpBlockBindingsProcessor extends WP_UnitTestCase { + /** + * @ticket 63840 + */ + public function test_replace_rich_text() { + $button_wrapper_opener = ''; + $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 = '
'; $img = ''; From 3b9eac6fec835ad85623697a32ce227e27c48377 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 20 Aug 2025 11:13:26 +0200 Subject: [PATCH 16/17] Increase test coverage --- .../wpBlockBindingsProcessor.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php b/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php index c3467556c36dd..5a8cf11095847 100644 --- a/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php +++ b/tests/phpunit/tests/block-bindings/wpBlockBindingsProcessor.php @@ -56,4 +56,36 @@ public function test_set_attribute_and_replace_rich_text() { $processor->build() ); } + + /** + * @ticket 63840 + */ + public function test_replace_rich_text_and_seek() { + $figure_opener = '
'; + $img = ''; + $figure_closer = '
'; + $processor = WP_Block_Bindings_Processor::create_fragment( + $figure_opener . + $img . + '
Breakfast at a café in Berlin
' . + $figure_closer + ); + + $processor->next_tag( array( 'tag_name' => 'img' ) ); + $processor->set_bookmark( 'image' ); + + $processor->next_tag( array( 'tag_name' => 'figcaption' ) ); + + $this->assertTrue( $processor->replace_rich_text( 'New image caption' ) ); + + $processor->seek( 'image' ); + + $this->assertEquals( + $figure_opener . + $img . + '
New image caption
' . + $figure_closer, + $processor->build() + ); + } } From e98185e27331ac862983e692fbef6ff8233a9427 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 20 Aug 2025 11:13:40 +0200 Subject: [PATCH 17/17] Add more commentary and warnings --- .../class-wp-block-bindings-processor.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-block-bindings-processor.php b/src/wp-includes/class-wp-block-bindings-processor.php index 8c3a06c64e8ea..f833b9ed07562 100644 --- a/src/wp-includes/class-wp-block-bindings-processor.php +++ b/src/wp-includes/class-wp-block-bindings-processor.php @@ -5,7 +5,19 @@ * * This class can be used to perform the sort of structural * changes to an HTML document that are required by - * Block Bindings. + * Block Bindings. Namely, proper nesting structure of HTML is + * maintained, but HTML updates could still leak out of the + * containing parent node. For example, this allows inserting + * an A element inside an open A element, which would close + * the containing A element. + + * Modifications may be requested for a document _once_ after + * matching a token. Due to the way the modifications are + * applied, it's not possible to replace the rich text content + * for a node more than once. Furthermore, if a `replace_rich_text()` + * operation is followed by a `seek()` to a position before the + * updated rich text content, any modification at that earlier + * position will lead to broken output. * * @access private *