-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Block Bindings: Allow more generic setting of block attributes #9469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7f1f6a0
054756d
b328dee
21e80e2
1c70bc3
8f76759
b7b7ca5
bb7c906
103a5c4
3f2e32b
85b6354
b67890e
6bc4fd5
d943cd8
527c5d8
86e836d
dad7380
88af5ff
a56f978
7c3fc45
66fef38
24a0b0d
2e7df73
e2f0a38
b045050
e739c6c
6a4a3a3
4dbb0e5
a18e435
3410f40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -416,7 +416,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 = self::get_block_bindings_processor( $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. | ||
|
|
@@ -425,53 +425,17 @@ 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( | ||
| array( | ||
| '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->get_updated_html(); | ||
| } else { | ||
| $block_reader->seek( 'iterate-selectors' ); | ||
| } | ||
|
|
@@ -497,6 +461,51 @@ private function replace_html( string $block_content, string $attribute_name, $s | |
| } | ||
| } | ||
|
|
||
| private static function get_block_bindings_processor( string $block_content ) { | ||
| $internal_processor_class = new class('', WP_HTML_Processor::CONSTRUCTOR_UNLOCK_CODE) extends WP_HTML_Processor { | ||
| /** | ||
| * 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() || ! $this->expects_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 `_`. | ||
| $tag_opener = $this->bookmarks['__wp_block_bindings_tag_opener']; | ||
| $start = $tag_opener->start + $tag_opener->length; | ||
| $this->release_bookmark( '_wp_block_bindings_tag_opener' ); | ||
|
|
||
| // Find matching tag closer. | ||
| while ( $this->next_token() && $this->get_current_depth() >= $depth ) { | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reasonable follow-up idea: verify that the processor completed and didn’t pause at an incomplete token or reach unsupported markup.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks good; I left a comment on the new PR. |
||
|
|
||
| $this->set_bookmark( '_wp_block_bindings_tag_closer' ); | ||
| $tag_closer = $this->bookmarks['__wp_block_bindings_tag_closer']; | ||
| $end = $tag_closer->start; | ||
| $this->release_bookmark( '_wp_block_bindings_tag_closer' ); | ||
|
|
||
| $this->lexical_updates[] = new WP_HTML_Text_Replacement( | ||
| $start, | ||
| $end - $start, | ||
| $rich_text | ||
| ); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. applying lexical updates that span token boundaries is not something we have particularly tested well. it’s probably working well enough and hopefully the tests here will catch them, but this is sort of uncharted territory within the HTML Processor. we should make sure that we don’t seek anywhere here to verify that we don’t cross indices. this single issue is one of the main reasons I proposed the single linear pass of the serialization builder, because it structurally prevents the ability of jumping around or referring to indices after changing positions. I’m not particularly confident I would know how this interacts with virtual tokens, for example, whose boundaries are coincident with bookmarks (vs. realized tokens which all have non-overlapping spans) |
||
|
|
||
| return true; | ||
| } | ||
| }; | ||
|
|
||
| return $internal_processor_class::create_fragment( $block_content ); | ||
| } | ||
|
|
||
| /** | ||
| * Generates the render output for the block. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| <?php | ||
| /** | ||
| * Tests for WP_Block::get_block_bindings_processor. | ||
| * | ||
| * @package WordPress | ||
| * @subpackage Blocks | ||
| * @since 6.9.0 | ||
| * | ||
| * @group blocks | ||
| * @group block-bindings | ||
| */ | ||
| class Tests_Blocks_GetBlockBindingsProcessor extends WP_UnitTestCase { | ||
|
|
||
| private static $get_block_bindings_processor_method; | ||
|
|
||
| public static function wpSetupBeforeClass() { | ||
| self::$get_block_bindings_processor_method = new ReflectionMethod( 'WP_Block', 'get_block_bindings_processor' ); | ||
| self::$get_block_bindings_processor_method->setAccessible( true ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 63840 | ||
| */ | ||
| public function test_replace_rich_text() { | ||
| $button_wrapper_opener = '<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">'; | ||
| $button_wrapper_closer = '</a></div>'; | ||
|
|
||
| $processor = self::$get_block_bindings_processor_method->invoke( | ||
| null, | ||
| $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->get_updated_html() | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 63840 | ||
| */ | ||
| public function test_set_attribute_and_replace_rich_text() { | ||
| $figure_opener = '<figure class="wp-block-image">'; | ||
| $img = '<img src="breakfast.jpg" alt="" class="wp-image-1"/>'; | ||
| $figure_closer = '</figure>'; | ||
| $processor = self::$get_block_bindings_processor_method->invoke( | ||
| null, | ||
| $figure_opener . | ||
| $img . | ||
| '<figcaption class="wp-element-caption">Breakfast at a <em>café</em> in Berlin</figcaption>' . | ||
| $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( '<strong>New</strong> image caption' ) ); | ||
| $this->assertEquals( | ||
| '<figure class="wp-block-image size-large">' . | ||
| $img . | ||
| '<figcaption class="wp-element-caption"><strong>New</strong> image caption</figcaption>' . | ||
| $figure_closer, | ||
| $processor->get_updated_html() | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 63840 | ||
| */ | ||
| public function test_replace_rich_text_and_seek() { | ||
| $figure_opener = '<figure class="wp-block-image">'; | ||
| $img = '<img src="breakfast.jpg" alt="" class="wp-image-1"/>'; | ||
| $figure_closer = '</figure>'; | ||
| $processor = self::$get_block_bindings_processor_method->invoke( | ||
| null, | ||
| $figure_opener . | ||
| $img . | ||
| '<figcaption class="wp-element-caption">Breakfast at a <em>café</em> in Berlin</figcaption>' . | ||
| $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( '<strong>New</strong> image caption' ) ); | ||
|
|
||
| $processor->seek( 'image' ); | ||
| $processor->add_class( 'extra-img-class' ); | ||
|
|
||
| $this->assertEquals( | ||
| $figure_opener . | ||
| '<img src="breakfast.jpg" alt="" class="wp-image-1 extra-img-class"/>' . | ||
| '<figcaption class="wp-element-caption"><strong>New</strong> image caption</figcaption>' . | ||
| $figure_closer, | ||
| $processor->get_updated_html() | ||
| ); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is certainly welcome to do, to release the bookmarks, but performance-wise it’s not setting a better example than leaving them dangling.
given that this function contains all of the scanning, it’s even fine to trap the bookmark bounds and re-use one, such as
set_bookmark( 'here' ). I’ve considered exposing something like->current_token()but haven’t yet.part of this is the fact that we expect isolation of bookmarks here and we will dismiss the processor when we’re done.