Skip to content

Experiment: Apply block bindings to REST API endpoint #8998

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

Draft
wants to merge 11 commits into
base: trunk
Choose a base branch
from
206 changes: 206 additions & 0 deletions src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
@@ -1511,6 +1511,20 @@
return function ( &$block, &$parent_block = null, $prev = null ) use ( $hooked_blocks, $context, $callback ) {
_inject_theme_attribute_in_template_part_block( $block );

// FIXME: We're using the name of the Block Hooks related callback to determine if this is a
// read, write, or read/write operation. This is not ideal, but it works for now.
switch ( $callback ) {
case 'insert_hooked_blocks':
apply_block_bindings_to_block( $block, $parent_block, $context, 'read' );
break;
case 'set_ignored_hooked_blocks_metadata':
apply_block_bindings_to_block( $block, $parent_block, $context, 'write' );
break;
case 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata':
apply_block_bindings_to_block( $block, $parent_block, $context, 'read/write' );
break;
}

$markup = '';

if ( $parent_block && ! $prev ) {
@@ -1582,6 +1596,198 @@
};
}

function apply_block_bindings_to_block( &$parsed_block, $parent_block, $context, $mode ) {
//$parsed_block = $this->parsed_block;
$computed_attributes = array();
$supported_block_attributes = array(
'core/paragraph' => array( 'content' ),
'core/heading' => array( 'content' ),
'core/image' => array( 'id', 'url', 'title', 'alt' ),
'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ),
);

// If the block doesn't have the bindings property, isn't one of the supported
// block types, or the bindings property is not an array, return the block content.
if (
! isset( $supported_block_attributes[ $parsed_block['blockName'] ] ) ||
empty( $parsed_block['attrs']['metadata']['bindings'] ) ||
! is_array( $parsed_block['attrs']['metadata']['bindings'] )
) {
return $computed_attributes;
}

$bindings = $parsed_block['attrs']['metadata']['bindings'];

/*
* If the default binding is set for pattern overrides, replace it
* with a pattern override binding for all supported attributes.
*/
if (
isset( $bindings['__default']['source'] ) &&
'core/pattern-overrides' === $bindings['__default']['source']
) {
$updated_bindings = array();

/*
* Build a binding array of all supported attributes.
* Note that this also omits the `__default` attribute from the
* resulting array.
*/
foreach ( $supported_block_attributes[ $parsed_block['blockName'] ] as $attribute_name ) {
// Retain any non-pattern override bindings that might be present.
$updated_bindings[ $attribute_name ] = isset( $bindings[ $attribute_name ] )
? $bindings[ $attribute_name ]
: array( 'source' => 'core/pattern-overrides' );
}
$bindings = $updated_bindings;
/*
* Update the bindings metadata of the computed attributes.
* This ensures the block receives the expanded __default binding metadata when it renders.
*/
$computed_attributes['metadata'] = array_merge(
$parsed_block['attrs']['metadata'],
array( 'bindings' => $bindings )
);
}

$block_context = array();

if ( $context instanceof WP_Post ) {
$block_context['postId'] = $context->ID;

/*
* The `postType` context is largely unnecessary server-side, since the ID
* is usually sufficient on its own. That being said, since a block's
* manifest is expected to be shared between the server and the client,
* it should be included to consistently fulfill the expectation.
*/
$block_context['postType'] = $context->post_type;
}

/** This filter is documented in wp-includes/blocks.php */
$block_context = apply_filters( 'render_block_context', $block_context, $parsed_block, $parent_block );
$block_instance = new WP_Block( $parsed_block, $block_context );

foreach ( $bindings as $attribute_name => $block_binding ) {
// If the attribute is not in the supported list, process next attribute.
if ( ! in_array( $attribute_name, $supported_block_attributes[ $parsed_block['blockName'] ], true ) ) {
continue;
}
// If no source is provided, or that source is not registered, process next attribute.
if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) {
continue;
}

$block_binding_source = get_block_bindings_source( $block_binding['source'] );
if ( null === $block_binding_source ) {
continue;
}

// FIXME: Adds the necessary context defined by the source.
// if ( ! empty( $block_binding_source->uses_context ) ) {
// foreach ( $block_binding_source->uses_context as $context_name ) {

Check failure on line 1688 in src/wp-includes/blocks.php

GitHub Actions / PHP coding standards / Run coding standards checks

Spaces must be used for mid-line alignment; tabs are not allowed
// if ( array_key_exists( $context_name, $this->available_context ) ) {

Check failure on line 1689 in src/wp-includes/blocks.php

GitHub Actions / PHP coding standards / Run coding standards checks

Spaces must be used for mid-line alignment; tabs are not allowed
// $this->context[ $context_name ] = $this->available_context[ $context_name ];

Check failure on line 1690 in src/wp-includes/blocks.php

GitHub Actions / PHP coding standards / Run coding standards checks

Spaces must be used for mid-line alignment; tabs are not allowed
// }

Check failure on line 1691 in src/wp-includes/blocks.php

GitHub Actions / PHP coding standards / Run coding standards checks

Spaces must be used for mid-line alignment; tabs are not allowed
// }

Check failure on line 1692 in src/wp-includes/blocks.php

GitHub Actions / PHP coding standards / Run coding standards checks

Spaces must be used for mid-line alignment; tabs are not allowed
// }

$source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array();
$source_value = $block_binding_source->get_value( $source_args, $block_instance, $attribute_name );

if ( is_null( $source_value ) ) {
continue;
}

if ( 'write' === $mode || 'read/write' === $mode ) {
// If this is a sourced attribute, we extract its value from the content.
if ( isset( $block_instance->block_type->attributes[ $attribute_name ]['source'] ) ) {
$block_content = $parsed_block['innerContent'][0]; // FIXME
$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.
$selectors = explode( ',', $block_instance->block_type->attributes[ $attribute_name ]['selector'] );
// Add a bookmark to the first tag to be able to iterate over the selectors.
$block_reader->next_tag();
$block_reader->set_bookmark( 'iterate-selectors' );

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,
)
) ) {
if ( 'content' === $block_instance->block_type->attributes[ $attribute_name ]['role'] ) {
$updated_attribute_value = '';

$serialize_token = new ReflectionMethod( 'WP_HTML_Processor', 'serialize_token' );
$serialize_token->setAccessible( true );

while ( $block_reader->next_token() ) {
if ( strtoupper( $selector ) === $block_reader->get_tag() && $block_reader->is_tag_closer() ) {
break;
}
$updated_attribute_value .= $serialize_token->invoke( $block_reader );

call_user_func_array(
'scf_set_value_callback',
array( $source_args, $block_instance, $attribute_name, $updated_attribute_value )
);
}
}

$block_reader->release_bookmark( 'iterate-selectors' );
} else {
$block_reader->seek( 'iterate-selectors' );
}
}
$block_reader->release_bookmark( 'iterate-selectors' );
}
}

// If the value is not null, process the HTML based on the block and the attribute.
$computed_attributes[ $attribute_name ] = $source_value;
}

if ( 'read' === $mode ) {
$parsed_block['attrs'] = array_merge(
$parsed_block['attrs'],
$computed_attributes
);

// Ideally, we'd apply WP_Block::replace_html() to the entire block content (i.e. $parsed_block['innerHTML']),
// but that is overridden by the block's render stage (where it's concatenated from the items in the
// innerContent and innerBlocks arrays). So instead, we apply it here to each item in the innerContent array.
if ( ! empty( $computed_attributes ) ) {
foreach ( $parsed_block['innerContent'] as $index => $content ) {
if ( empty( $content ) ) {
continue;
}
foreach ( $computed_attributes as $attribute_name => $source_value ) {
$content = WP_Block::replace_html( $parsed_block['blockName'], $content, $attribute_name, $source_value );

// If this is a sourced attribute, we remove it from the parsed block's `attrs` array.
if ( isset( $block_instance->block_type->attributes[ $attribute_name ]['source'] ) ) {
unset( $parsed_block['attrs'][ $attribute_name ] );
}
}
$parsed_block['innerContent'][ $index ] = $content;
}
}
}
}

function scf_set_value_callback( array $source_args, $block_instance, string $attribute_name, $value ) {
if ( ! isset( $source_args['key'] ) || ! is_string( $source_args['key'] ) ) {
return;
} else {
return update_field( $source_args['key'], $value );
}

}

Check failure on line 1789 in src/wp-includes/blocks.php

GitHub Actions / PHP coding standards / Run coding standards checks

Function closing brace must go on the next line following the body; found 1 blank lines before brace

/**
* Given an array of attributes, returns a string in the serialized attributes
* format prepared for post content.
22 changes: 12 additions & 10 deletions src/wp-includes/class-wp-block.php
Original file line number Diff line number Diff line change
@@ -373,14 +373,16 @@
* Depending on the block attribute name, replace its value in the HTML based on the value provided.
*
* @since 6.5.0
* @since 6.9.0 Make static, introduce block_name parameter.
*
* @param string $block_name Block type name.
* @param string $block_content Block content.
* @param string $attribute_name The attribute name to replace.
* @param mixed $source_value The value used to replace in the HTML.
* @return string The modified block content.
*/
private function replace_html( string $block_content, string $attribute_name, $source_value ) {
$block_type = $this->block_type;
public static function replace_html( string $block_name, string $block_content, string $attribute_name, $source_value ) {
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name );
if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) {
return $block_content;
}
@@ -401,7 +403,7 @@
// 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 ) {
if ( 'core/button' === $block_name ) {
$button_wrapper = $block_reader->get_tag();
$button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
$button_wrapper_attrs = array();
@@ -433,10 +435,10 @@
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 ) {
if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) {
return $amended_content->get_updated_html();
}
if ( 'core/button' === $this->name ) {
if ( 'core/button' === $block_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();
@@ -570,12 +572,12 @@
}
}
}

Check failure on line 575 in src/wp-includes/class-wp-block.php

GitHub Actions / PHP coding standards / Run coding standards checks

Spaces must be used for mid-line alignment; tabs are not allowed
if ( ! empty( $computed_attributes ) && ! empty( $block_content ) ) {
foreach ( $computed_attributes as $attribute_name => $source_value ) {
$block_content = $this->replace_html( $block_content, $attribute_name, $source_value );
}
}
// if ( ! empty( $computed_attributes ) && ! empty( $block_content ) ) {

Check failure on line 576 in src/wp-includes/class-wp-block.php

GitHub Actions / PHP coding standards / Run coding standards checks

Spaces must be used for mid-line alignment; tabs are not allowed
// foreach ( $computed_attributes as $attribute_name => $source_value ) {

Check failure on line 577 in src/wp-includes/class-wp-block.php

GitHub Actions / PHP coding standards / Run coding standards checks

Spaces must be used for mid-line alignment; tabs are not allowed
// $block_content = $this->replace_html( $block_content, $attribute_name, $source_value );
// }
// }

if ( $is_dynamic ) {
$global_post = $post;
2 changes: 2 additions & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
@@ -788,4 +788,6 @@
add_filter( 'rest_prepare_wp_block', 'insert_hooked_blocks_into_rest_response', 10, 2 );
add_filter( 'rest_prepare_wp_navigation', 'insert_hooked_blocks_into_rest_response', 10, 2 );

add_filter( 'rest_prepare_book', 'insert_hooked_blocks_into_rest_response', 10, 2 );

unset( $filter, $action );
Loading