Make WordPress Core

Changeset 60684


Ignore:
Timestamp:
08/27/2025 03:41:36 PM (12 months ago)
Author:
Bernhard Reiter
Message:

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.

Location:
trunk
Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-block.php

    r60663 r60684  
    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.
     
    425425                                $block_reader->next_tag();
    426426                                $block_reader->set_bookmark( 'iterate-selectors' );
    427 
    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                                 }
    439427
    440428                                foreach ( $selectors as $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' );
     
    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        /**
  • trunk/tests/phpunit/tests/block-bindings/render.php

    r60611 r60684  
    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 -->
     72HTML
     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 -->
     82HTML
     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.
     
    6893         *
    6994         * @covers ::register_block_bindings_source
    70          */
    71         public function test_update_block_with_value_from_source() {
     95         *
     96         * @dataProvider data_update_block_with_value_from_source
     97         */
     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';
     
    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;
    89                 $parsed_blocks = parse_blocks( $block_content );
    90                 $block         = new WP_Block( $parsed_blocks[0] );
    91                 $result        = $block->render();
     111                $parsed_blocks = parse_blocks( $block_content );
     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."
    97                 );
    98                 $this->assertSame(
    99                         '<p>test source value</p>',
     126                        $block->attributes[ $bound_attribute ],
     127                        "The '{$bound_attribute}' attribute should be updated with the value returned by the source."
     128                );
     129                $this->assertSame(
     130                        $expected_result,
    100131                        trim( $result ),
    101132                        'The block content should be updated with the value returned by the source.'
Note: See TracChangeset for help on using the changeset viewer.