Skip to content

Commit 6c21850

Browse files
committed
Block Bindings: Allow generically setting rich-text block attributes.
Replace the existing block-specific, hard-coded, logic in the `WP_Block` class with more generic code that is able to locate and replace a `rich-text` sourced attribute based on the `selector` definition in its `block.json`. This should make it easier to add block bindings support for more block attributes. Props bernhard-reiter, jonsurrell, gziolo, cbravobernal, dmsnell. Fixes #63840. git-svn-id: https://develop.svn.wordpress.org/trunk@60684 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 9b7321c commit 6c21850

File tree

3 files changed

+195
-51
lines changed

3 files changed

+195
-51
lines changed

src/wp-includes/class-wp-block.php

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ private function replace_html( string $block_content, string $attribute_name, $s
416416
switch ( $block_type->attributes[ $attribute_name ]['source'] ) {
417417
case 'html':
418418
case 'rich-text':
419-
$block_reader = new WP_HTML_Tag_Processor( $block_content );
419+
$block_reader = self::get_block_bindings_processor( $block_content );
420420

421421
// TODO: Support for CSS selectors whenever they are ready in the HTML API.
422422
// 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
425425
$block_reader->next_tag();
426426
$block_reader->set_bookmark( 'iterate-selectors' );
427427

428-
// TODO: This shouldn't be needed when the `set_inner_html` function is ready.
429-
// Store the parent tag and its attributes to be able to restore them later in the button.
430-
// The button block has a wrapper while the paragraph and heading blocks don't.
431-
if ( 'core/button' === $this->name ) {
432-
$button_wrapper = $block_reader->get_tag();
433-
$button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
434-
$button_wrapper_attrs = array();
435-
foreach ( $button_wrapper_attribute_names as $name ) {
436-
$button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name );
437-
}
438-
}
439-
440428
foreach ( $selectors as $selector ) {
441429
// If the parent tag, or any of its children, matches the selector, replace the HTML.
442430
if ( strcasecmp( $block_reader->get_tag(), $selector ) === 0 || $block_reader->next_tag(
443431
array(
444432
'tag_name' => $selector,
445433
)
446434
) ) {
435+
// TODO: Use `WP_HTML_Processor::set_inner_html` method once it's available.
447436
$block_reader->release_bookmark( 'iterate-selectors' );
448-
449-
// TODO: Use `set_inner_html` method whenever it's ready in the HTML API.
450-
// Until then, it is hardcoded for the paragraph, heading, and button blocks.
451-
// Store the tag and its attributes to be able to restore them later.
452-
$selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
453-
$selector_attrs = array();
454-
foreach ( $selector_attribute_names as $name ) {
455-
$selector_attrs[ $name ] = $block_reader->get_attribute( $name );
456-
}
457-
$selector_markup = "<$selector>" . wp_kses_post( $source_value ) . "</$selector>";
458-
$amended_content = new WP_HTML_Tag_Processor( $selector_markup );
459-
$amended_content->next_tag();
460-
foreach ( $selector_attrs as $attribute_key => $attribute_value ) {
461-
$amended_content->set_attribute( $attribute_key, $attribute_value );
462-
}
463-
if ( 'core/paragraph' === $this->name || 'core/heading' === $this->name ) {
464-
return $amended_content->get_updated_html();
465-
}
466-
if ( 'core/button' === $this->name ) {
467-
$button_markup = "<$button_wrapper>{$amended_content->get_updated_html()}</$button_wrapper>";
468-
$amended_button = new WP_HTML_Tag_Processor( $button_markup );
469-
$amended_button->next_tag();
470-
foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) {
471-
$amended_button->set_attribute( $attribute_key, $attribute_value );
472-
}
473-
return $amended_button->get_updated_html();
474-
}
437+
$block_reader->replace_rich_text( wp_kses_post( $source_value ) );
438+
return $block_reader->get_updated_html();
475439
} else {
476440
$block_reader->seek( 'iterate-selectors' );
477441
}
@@ -497,6 +461,51 @@ private function replace_html( string $block_content, string $attribute_name, $s
497461
}
498462
}
499463

464+
private static function get_block_bindings_processor( string $block_content ) {
465+
$internal_processor_class = new class('', WP_HTML_Processor::CONSTRUCTOR_UNLOCK_CODE) extends WP_HTML_Processor {
466+
/**
467+
* Replace the rich text content between a tag opener and matching closer.
468+
*
469+
* When stopped on a tag opener, replace the content enclosed by it and its
470+
* matching closer with the provided rich text.
471+
*
472+
* @param string $rich_text The rich text to replace the original content with.
473+
* @return bool True on success.
474+
*/
475+
public function replace_rich_text( $rich_text ) {
476+
if ( $this->is_tag_closer() || ! $this->expects_closer() ) {
477+
return false;
478+
}
479+
480+
$depth = $this->get_current_depth();
481+
482+
$this->set_bookmark( '_wp_block_bindings_tag_opener' );
483+
// The bookmark names are prefixed with `_` so the key below has an extra `_`.
484+
$tag_opener = $this->bookmarks['__wp_block_bindings_tag_opener'];
485+
$start = $tag_opener->start + $tag_opener->length;
486+
$this->release_bookmark( '_wp_block_bindings_tag_opener' );
487+
488+
// Find matching tag closer.
489+
while ( $this->next_token() && $this->get_current_depth() >= $depth ) {
490+
}
491+
492+
$this->set_bookmark( '_wp_block_bindings_tag_closer' );
493+
$tag_closer = $this->bookmarks['__wp_block_bindings_tag_closer'];
494+
$end = $tag_closer->start;
495+
$this->release_bookmark( '_wp_block_bindings_tag_closer' );
496+
497+
$this->lexical_updates[] = new WP_HTML_Text_Replacement(
498+
$start,
499+
$end - $start,
500+
$rich_text
501+
);
502+
503+
return true;
504+
}
505+
};
506+
507+
return $internal_processor_class::create_fragment( $block_content );
508+
}
500509

501510
/**
502511
* Generates the render output for the block.

tests/phpunit/tests/block-bindings/render.php

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,41 @@ public static function wpTearDownAfterClass() {
6161
unregister_block_type( 'test/block' );
6262
}
6363

64+
public function data_update_block_with_value_from_source() {
65+
return array(
66+
'paragraph block' => array(
67+
'content',
68+
<<<HTML
69+
<!-- wp:paragraph -->
70+
<p>This should not appear</p>
71+
<!-- /wp:paragraph -->
72+
HTML
73+
,
74+
'<p>test source value</p>',
75+
),
76+
'button block' => array(
77+
'text',
78+
<<<HTML
79+
<!-- wp:button -->
80+
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">This should not appear</a></div>
81+
<!-- /wp:button -->
82+
HTML
83+
,
84+
'<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">test source value</a></div>',
85+
),
86+
);
87+
}
88+
6489
/**
6590
* Test if the block content is updated with the value returned by the source.
6691
*
6792
* @ticket 60282
6893
*
6994
* @covers ::register_block_bindings_source
95+
*
96+
* @dataProvider data_update_block_with_value_from_source
7097
*/
71-
public function test_update_block_with_value_from_source() {
98+
public function test_update_block_with_value_from_source( $bound_attribute, $block_content, $expected_result ) {
7299
$get_value_callback = function () {
73100
return 'test source value';
74101
};
@@ -81,22 +108,26 @@ public function test_update_block_with_value_from_source() {
81108
)
82109
);
83110

84-
$block_content = <<<HTML
85-
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"test/source"}}}} -->
86-
<p>This should not appear</p>
87-
<!-- /wp:paragraph -->
88-
HTML;
89111
$parsed_blocks = parse_blocks( $block_content );
90-
$block = new WP_Block( $parsed_blocks[0] );
91-
$result = $block->render();
112+
113+
$parsed_blocks[0]['attrs']['metadata'] = array(
114+
'bindings' => array(
115+
$bound_attribute => array(
116+
'source' => self::SOURCE_NAME,
117+
),
118+
),
119+
);
120+
121+
$block = new WP_Block( $parsed_blocks[0] );
122+
$result = $block->render();
92123

93124
$this->assertSame(
94125
'test source value',
95-
$block->attributes['content'],
96-
"The 'content' attribute should be updated with the value returned by the source."
126+
$block->attributes[ $bound_attribute ],
127+
"The '{$bound_attribute}' attribute should be updated with the value returned by the source."
97128
);
98129
$this->assertSame(
99-
'<p>test source value</p>',
130+
$expected_result,
100131
trim( $result ),
101132
'The block content should be updated with the value returned by the source.'
102133
);
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
/**
3+
* Tests for WP_Block::get_block_bindings_processor.
4+
*
5+
* @package WordPress
6+
* @subpackage Blocks
7+
* @since 6.9.0
8+
*
9+
* @group blocks
10+
* @group block-bindings
11+
*/
12+
class Tests_Blocks_GetBlockBindingsProcessor extends WP_UnitTestCase {
13+
14+
private static $get_block_bindings_processor_method;
15+
16+
public static function wpSetupBeforeClass() {
17+
self::$get_block_bindings_processor_method = new ReflectionMethod( 'WP_Block', 'get_block_bindings_processor' );
18+
self::$get_block_bindings_processor_method->setAccessible( true );
19+
}
20+
21+
/**
22+
* @ticket 63840
23+
*/
24+
public function test_replace_rich_text() {
25+
$button_wrapper_opener = '<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">';
26+
$button_wrapper_closer = '</a></div>';
27+
28+
$processor = self::$get_block_bindings_processor_method->invoke(
29+
null,
30+
$button_wrapper_opener . 'This should not appear' . $button_wrapper_closer
31+
);
32+
$processor->next_tag( array( 'tag_name' => 'a' ) );
33+
34+
$this->assertTrue( $processor->replace_rich_text( 'The hardest button to button' ) );
35+
$this->assertEquals(
36+
$button_wrapper_opener . 'The hardest button to button' . $button_wrapper_closer,
37+
$processor->get_updated_html()
38+
);
39+
}
40+
41+
/**
42+
* @ticket 63840
43+
*/
44+
public function test_set_attribute_and_replace_rich_text() {
45+
$figure_opener = '<figure class="wp-block-image">';
46+
$img = '<img src="breakfast.jpg" alt="" class="wp-image-1"/>';
47+
$figure_closer = '</figure>';
48+
$processor = self::$get_block_bindings_processor_method->invoke(
49+
null,
50+
$figure_opener .
51+
$img .
52+
'<figcaption class="wp-element-caption">Breakfast at a <em>café</em> in Berlin</figcaption>' .
53+
$figure_closer
54+
);
55+
56+
$processor->next_tag( array( 'tag_name' => 'figure' ) );
57+
$processor->add_class( 'size-large' );
58+
59+
$processor->next_tag( array( 'tag_name' => 'figcaption' ) );
60+
61+
$this->assertTrue( $processor->replace_rich_text( '<strong>New</strong> image caption' ) );
62+
$this->assertEquals(
63+
'<figure class="wp-block-image size-large">' .
64+
$img .
65+
'<figcaption class="wp-element-caption"><strong>New</strong> image caption</figcaption>' .
66+
$figure_closer,
67+
$processor->get_updated_html()
68+
);
69+
}
70+
71+
/**
72+
* @ticket 63840
73+
*/
74+
public function test_replace_rich_text_and_seek() {
75+
$figure_opener = '<figure class="wp-block-image">';
76+
$img = '<img src="breakfast.jpg" alt="" class="wp-image-1"/>';
77+
$figure_closer = '</figure>';
78+
$processor = self::$get_block_bindings_processor_method->invoke(
79+
null,
80+
$figure_opener .
81+
$img .
82+
'<figcaption class="wp-element-caption">Breakfast at a <em>café</em> in Berlin</figcaption>' .
83+
$figure_closer
84+
);
85+
86+
$processor->next_tag( array( 'tag_name' => 'img' ) );
87+
$processor->set_bookmark( 'image' );
88+
89+
$processor->next_tag( array( 'tag_name' => 'figcaption' ) );
90+
91+
$this->assertTrue( $processor->replace_rich_text( '<strong>New</strong> image caption' ) );
92+
93+
$processor->seek( 'image' );
94+
$processor->add_class( 'extra-img-class' );
95+
96+
$this->assertEquals(
97+
$figure_opener .
98+
'<img src="breakfast.jpg" alt="" class="wp-image-1 extra-img-class"/>' .
99+
'<figcaption class="wp-element-caption"><strong>New</strong> image caption</figcaption>' .
100+
$figure_closer,
101+
$processor->get_updated_html()
102+
);
103+
}
104+
}

0 commit comments

Comments
 (0)