get_all_registered() ) as $block_type ) {
$supported_block_attributes = gutenberg_get_block_bindings_supported_attributes( $block_type );
if ( ! empty( $supported_block_attributes ) ) {
$editor_settings['__experimentalBlockBindingsSupportedAttributes'][ $block_type ] = $supported_block_attributes;
}
}
return $editor_settings;
}
);
/**
* Callback function for the render_block filter.
*
* @since 6.9.0
*
* @param string $block_content The block content.
* @param array $block The full block, including name and attributes.
* @param WP_Block $instance The block instance.
*/
function gutenberg_block_bindings_render_block( $block_content, $block, $instance ) {
static $inside_block_bindings_render = false;
if ( $inside_block_bindings_render ) {
return $block_content;
}
// Process the block bindings and get attributes updated with the values from the sources.
$computed_attributes = gutenberg_process_block_bindings( $instance );
if ( empty( $computed_attributes ) ) {
return $block_content;
}
/*
* Merge the computed attributes with the original attributes.
*
* Note that this is not a recursive merge, meaning that nested attributes --
* such as block bindings metadata -- will be completely replaced.
* This is desirable. At this point, Core has already processed any block
* bindings that it supports. What remains to be processed are only the attributes
* for which support was added later (through the `block_bindings_supported_attributes`
* filter). To do so, we'll run `$instance->render()` once more
* so the block can update its content based on those attributes.
*/
$instance->attributes = array_merge( $instance->attributes, $computed_attributes );
/*
* If we're dealing with the Button block, we remove the bindings metadata
* in order to avoid having it reprocessed, which would lead to Core
* capitalizing the wrapper tag (e.g.
).
*/
if ( 'core/button' === $instance->name ) {
unset( $instance->parsed_block['attrs']['metadata']['bindings'] );
}
/**
* This filter (`gutenberg_block_bindings_render_block`) is called from `WP_Block::render()`.
* To avoid infinite recursion, we set a flag that this filter checks when invoked which tells
* it to exit early.
*/
$inside_block_bindings_render = true;
$block_content = $instance->render();
$inside_block_bindings_render = false;
if ( ! empty( $computed_attributes ) && ! empty( $block_content ) ) {
foreach ( $computed_attributes as $attribute_name => $source_value ) {
$block_content = gutenberg_replace_html( $block_content, $attribute_name, $source_value, $instance->block_type );
}
}
return $block_content;
}
add_filter( 'render_block', 'gutenberg_block_bindings_render_block', 10, 3 );
/**
* Retrieves the list of block attributes supported by block bindings.
*
* @since 6.9.0
*
* @param string $block_type The block type whose supported attributes are being retrieved.
* @return array The list of block attributes that are supported by block bindings.
*/
function gutenberg_get_block_bindings_supported_attributes( $block_type ) {
/*
* List of block attributes supported by Block Bindings in WP 6.8.
* DO NOT MODIFY THIS ARRAY. It's a snapshot of what Core supports in 6.8.
* Use the `block_bindings_supported_attributes` filter instead to add support
* for new block attributes.
*/
$block_bindings_supported_attributes_6_8 = array(
'core/paragraph' => array( 'content' ),
'core/heading' => array( 'content' ),
'core/image' => array( 'id', 'url', 'title', 'alt' ),
'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ),
);
$supported_block_attributes =
isset( $block_type, $block_bindings_supported_attributes_6_8[ $block_type ] ) ?
$block_bindings_supported_attributes_6_8[ $block_type ] :
array();
/**
* Filters the supported block attributes for block bindings.
*
* @since 6.9.0
*
* @param string[] $supported_block_attributes The block's attributes that are supported by block bindings.
* @param string $block_type The block type whose attributes are being filtered.
*/
$supported_block_attributes = apply_filters(
'block_bindings_supported_attributes',
$supported_block_attributes,
$block_type
);
/**
* Filters the supported block attributes for block bindings.
*
* The dynamic portion of the hook name, `$block_type`, refers to the block type
* whose attributes are being filtered.
*
* @since 6.9.0
*
* @param string[] $supported_block_attributes The block's attributes that are supported by block bindings.
*/
$supported_block_attributes = apply_filters(
"block_bindings_supported_attributes_{$block_type}",
$supported_block_attributes
);
return $supported_block_attributes;
}
/**
* Processes the block bindings and updates the block attributes with the values from the sources.
*
* A block might contain bindings in its attributes. Bindings are mappings
* between an attribute of the block and a source. A "source" is a function
* registered with `register_block_bindings_source()` that defines how to
* retrieve a value from outside the block, e.g. from post meta.
*
* This function will process those bindings and update the block's attributes
* with the values coming from the bindings.
*
* ### Example
*
* The "bindings" property for an Image block might look like this:
*
* ```json
* {
* "metadata": {
* "bindings": {
* "title": {
* "source": "core/post-meta",
* "args": { "key": "text_custom_field" }
* },
* "url": {
* "source": "core/post-meta",
* "args": { "key": "url_custom_field" }
* }
* }
* }
* }
* ```
*
* The above example will replace the `title` and `url` attributes of the Image
* block with the values of the `text_custom_field` and `url_custom_field` post meta.
*
* @since 6.9.0
*
* @param WP_Block $instance The block instance.
* @return array The computed block attributes for the provided block bindings.
*/
function gutenberg_process_block_bindings( $instance ) {
$block_type = $instance->name;
$parsed_block = $instance->parsed_block;
$computed_attributes = array();
/*
* List of block attributes supported by Block Bindings in WP 6.8.
* DO NOT MODIFY THIS ARRAY. It's a snapshot of what Core supports in 6.8.
* Use the `block_bindings_supported_attributes` filter instead to add support
* for new block attributes.
*/
$block_bindings_supported_attributes_6_8 = array(
'core/paragraph' => array( 'content' ),
'core/heading' => array( 'content' ),
'core/image' => array( 'id', 'url', 'title', 'alt' ),
'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ),
);
$supported_block_attributes = gutenberg_get_block_bindings_supported_attributes( $block_type );
/*
* Remove attributes that we know are processed by WP 6.8 from the list,
* except if we're dealing with the button block, since WP 6.8 capitalizes its
* tag name (e.g.
).
*/
if ( 'core/button' !== $block_type && isset( $block_type, $block_bindings_supported_attributes_6_8[ $block_type ] ) ) {
$supported_block_attributes = array_diff(
$supported_block_attributes,
$block_bindings_supported_attributes_6_8[ $block_type ]
);
}
// 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 (
empty( $supported_block_attributes ) ||
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 as $attribute_name ) {
// Retain any non-pattern override bindings that might be present.
$updated_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 )
);
}
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, 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;
}
if ( ! class_exists( 'WP_Block_Context_Extractor' ) ) {
// phpcs:ignore Gutenberg.Commenting.SinceTag.MissingClassSinceTag
class WP_Block_Context_Extractor extends WP_Block {
/**
* Static methods of subclasses have access to protected properties
* of instances of the parent class.
* In this case, this gives us access to `available_context`.
*/
// phpcs:ignore Gutenberg.Commenting.SinceTag.MissingMethodSinceTag
public static function get_available_context( $instance ) {
return $instance->available_context;
}
}
}
$available_context = WP_Block_Context_Extractor::get_available_context( $instance );
// Adds the necessary context defined by the source.
if ( ! empty( $block_binding_source->uses_context ) ) {
foreach ( $block_binding_source->uses_context as $context_name ) {
if ( array_key_exists( $context_name, $available_context ) ) {
$instance->context[ $context_name ] = $available_context[ $context_name ];
}
}
}
$source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array();
$source_value = $block_binding_source->get_value( $source_args, $instance, $attribute_name );
// If the value is not null, process the HTML based on the block and the attribute.
if ( ! is_null( $source_value ) ) {
$computed_attributes[ $attribute_name ] = $source_value;
}
}
return $computed_attributes;
}
/**
* Depending on the block attribute name, replace its value in the HTML based on the value provided.
*
* @since 6.5.0
*
* @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.
* @param WP_Block_Type $block_type The block type.
* @return string The modified block content.
*/
function gutenberg_replace_html( string $block_content, string $attribute_name, $source_value, WP_Block_Type $block_type ) {
if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) {
return $block_content;
}
// Depending on the attribute source, the processing will be different.
switch ( $block_type->attributes[ $attribute_name ]['source'] ) {
case 'html':
case 'rich-text':
$block_reader = gutenberg_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.
$selectors = explode( ',', $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,
)
) ) {
// TODO: Use `WP_HTML_Processor::set_inner_html` method once it's available.
$block_reader->release_bookmark( 'iterate-selectors' );
$block_reader->replace_rich_text( wp_kses_post( $source_value ) );
return $block_reader->get_updated_html();
} else {
$block_reader->seek( 'iterate-selectors' );
}
}
$block_reader->release_bookmark( 'iterate-selectors' );
return $block_content;
case 'attribute':
$amended_content = new WP_HTML_Tag_Processor( $block_content );
if ( ! $amended_content->next_tag(
array(
// TODO: build the query from CSS selector.
'tag_name' => $block_type->attributes[ $attribute_name ]['selector'],
)
) ) {
return $block_content;
}
$amended_content->set_attribute( $block_type->attributes[ $attribute_name ]['attribute'], $source_value );
return $amended_content->get_updated_html();
default:
return $block_content;
}
}
function gutenberg_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.
*/
// phpcs:ignore Gutenberg.CodeAnalysis.GuardedFunctionAndClassNames.FunctionNotGuardedAgainstRedeclaration
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 ) {
}
$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
);
return true;
}
};
return $internal_processor_class::create_fragment( $block_content );
}