<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[57514] trunk: Editor: Add the Block Bindings API.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/57514">57514</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/57514","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>youknowriad</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-02-01 12:52:54 +0000 (Thu, 01 Feb 2024)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Editor: Add the Block Bindings API.

This introduces the Block Bindings API for WordPress.

The API allows developers to connects block attributes to different sources. In this PR, two such sources are included: "post meta" and "pattern". Attributes connected to sources can have their HTML replaced by values coming from the source in a way defined by the binding.

Props czapla, lgladdy, gziolo, sc0ttkclark, swissspidy, artemiosans, kevin940726, fabiankaegy, santosguillamot, talldanwp, wildworks.
Fixes <a href="https://core.trac.wordpress.org/ticket/60282">#60282</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesblockbindingsphp">trunk/src/wp-includes/block-bindings.php</a></li>
<li><a href="#trunksrcwpincludesclasswpblockbindingsregistryphp">trunk/src/wp-includes/class-wp-block-bindings-registry.php</a></li>
<li><a href="#trunksrcwpincludesclasswpblockphp">trunk/src/wp-includes/class-wp-block.php</a></li>
<li><a href="#trunksrcwpsettingsphp">trunk/src/wp-settings.php</a></li>
<li><a href="#trunktestsphpunitincludesfunctionsphp">trunk/tests/phpunit/includes/functions.php</a></li>
<li><a href="#trunktestsphpunittestsblockbindingsregisterphp">trunk/tests/phpunit/tests/block-bindings/register.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li>trunk/src/wp-includes/block-bindings/</li>
<li>trunk/src/wp-includes/block-bindings/sources/</li>
<li><a href="#trunksrcwpincludesblockbindingssourcespatternphp">trunk/src/wp-includes/block-bindings/sources/pattern.php</a></li>
<li><a href="#trunksrcwpincludesblockbindingssourcespostmetaphp">trunk/src/wp-includes/block-bindings/sources/post-meta.php</a></li>
<li><a href="#trunktestsphpunittestsblockbindingsblockbindingsphp">trunk/tests/phpunit/tests/block-bindings/block-bindings.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesblockbindingssourcespatternphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/block-bindings/sources/pattern.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/block-bindings/sources/pattern.php                          (rev 0)
+++ trunk/src/wp-includes/block-bindings/sources/pattern.php    2024-02-01 12:52:54 UTC (rev 57514)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,34 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * The "pattern" source for the Block Bindings API. This source is used by the
+ * Pattern Overrides.
+ *
+ * @since 6.5.0
+ * @package WordPress
+ */
+function pattern_source_callback( $source_attrs, $block_instance, $attribute_name ) {
+       if ( ! _wp_array_get( $block_instance->attributes, array( 'metadata', 'id' ), false ) ) {
+               return null;
+       }
+       $block_id = $block_instance->attributes['metadata']['id'];
+       return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null );
+}
+
+
+/**
+ * Registers the "pattern" source for the Block Bindings API.
+ *
+ * @access private
+ * @since 6.5.0
+ */
+function _register_block_bindings_pattern_overrides_source() {
+       register_block_bindings_source(
+               'core/pattern-overrides',
+               array(
+                       'label'              => _x( 'Pattern Overrides', 'block bindings source' ),
+                       'get_value_callback' => 'pattern_source_callback',
+               )
+       );
+}
+
+add_action( 'init', '_register_block_bindings_pattern_overrides_source' );
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/block-bindings/sources/pattern.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesblockbindingssourcespostmetaphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/block-bindings/sources/post-meta.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/block-bindings/sources/post-meta.php                                (rev 0)
+++ trunk/src/wp-includes/block-bindings/sources/post-meta.php  2024-02-01 12:52:54 UTC (rev 57514)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,47 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Add the post_meta source to the Block Bindings API.
+ *
+ * @since 6.5.0
+ * @package WordPress
+ */
+function post_meta_source_callback( $source_attrs ) {
+       if ( ! isset( $source_attrs['key'] ) ) {
+               return null;
+       }
+
+       // Use the postId attribute if available
+       if ( isset( $source_attrs['postId'] ) ) {
+               $post_id = $source_attrs['postId'];
+       } else {
+               // $block_instance->context['postId'] is not available in the Image block.
+               $post_id = get_the_ID();
+       }
+
+       // If a post isn't public, we need to prevent
+       // unauthorized users from accessing the post meta.
+       $post = get_post( $post_id );
+       if ( ( ! is_post_publicly_viewable( $post ) && ! current_user_can( 'read_post', $post_id ) ) || post_password_required( $post ) ) {
+               return null;
+       }
+
+       return get_post_meta( $post_id, $source_attrs['key'], true );
+}
+
+/**
+ * Registers the "post_meta" source for the Block Bindings API.
+ *
+ * @access private
+ * @since 6.5.0
+ */
+function _register_block_bindings_post_meta_source() {
+       register_block_bindings_source(
+               'core/post-meta',
+               array(
+                       'label'              => _x( 'Post Meta', 'block bindings source' ),
+                       'get_value_callback' => 'post_meta_source_callback',
+               )
+       );
+}
+
+add_action( 'init', '_register_block_bindings_post_meta_source' );
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/block-bindings/sources/post-meta.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesblockbindingsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/block-bindings.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/block-bindings.php  2024-02-01 11:43:21 UTC (rev 57513)
+++ trunk/src/wp-includes/block-bindings.php    2024-02-01 12:52:54 UTC (rev 57514)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -19,7 +19,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 6.5.0
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @param string   $source_name       The name of the source.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @param string   $source_name       The name of the source. It must be a string containing a namespace prefix, i.e.
+ *                                    `my-plugin/my-custom-source`. It must only contain lowercase alphanumeric
+ *                                    characters, the forward slash `/` and dashes.
</ins><span class="cx" style="display: block; padding: 0 10px">  * @param array    $source_properties {
</span><span class="cx" style="display: block; padding: 0 10px">  *     The array of arguments that are used to register a source.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpblockbindingsregistryphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-block-bindings-registry.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-block-bindings-registry.php        2024-02-01 11:43:21 UTC (rev 57513)
+++ trunk/src/wp-includes/class-wp-block-bindings-registry.php  2024-02-01 12:52:54 UTC (rev 57514)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -42,7 +42,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 6.5.0
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param string   $source_name       The name of the source.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param string   $source_name       The name of the source. It must be a string containing a namespace prefix, i.e.
+        *                                    `my-plugin/my-custom-source`. It must only contain lowercase alphanumeric
+        *                                    characters, the forward slash `/` and dashes.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @param array    $source_properties {
</span><span class="cx" style="display: block; padding: 0 10px">         *     The array of arguments that are used to register a source.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpblockphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-block.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-block.php  2024-02-01 11:43:21 UTC (rev 57513)
+++ trunk/src/wp-includes/class-wp-block.php    2024-02-01 12:52:54 UTC (rev 57514)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -192,6 +192,204 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Processes the block bindings in block's attributes.
+        *
+        * 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 replace the HTML with the value of the binding.
+        * The value is retrieved from the source of the binding.
+        *
+        * ### Example
+        *
+        * The "bindings" property for an Image block might look like this:
+        *
+        * ```json
+        * {
+        *   "metadata": {
+        *     "bindings": {
+        *       "title": {
+        *         "source": "post_meta",
+        *         "args": { "key": "text_custom_field" }
+        *       },
+        *       "url": {
+        *         "source": "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.
+        *
+        * @access private
+        * @since 6.5.0
+        *
+        * @param string   $block_content Block content.
+        * @param array    $block The full block, including name and attributes.
+        */
+       private function process_block_bindings( $block_content ) {
+               $block = $this->parsed_block;
+
+               // Allowed blocks that support block bindings.
+               // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes?
+               $allowed_blocks = array(
+                       'core/paragraph' => array( 'content' ),
+                       'core/heading'   => array( 'content' ),
+                       'core/image'     => array( 'url', 'title', 'alt' ),
+                       'core/button'    => array( 'url', 'text' ),
+               );
+
+               // If the block doesn't have the bindings property, isn't one of the allowed
+               // block types, or the bindings property is not an array, return the block content.
+               if ( ! isset( $block['attrs']['metadata']['bindings'] ) ||
+                               ! is_array( $block['attrs']['metadata']['bindings'] ) ||
+                               ! isset( $allowed_blocks[ $this->name ] )
+                               ) {
+                       return $block_content;
+               }
+
+               $block_bindings_sources = get_all_registered_block_bindings_sources();
+               $modified_block_content = $block_content;
+               foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) {
+                       // If the attribute is not in the list, process next attribute.
+                       if ( ! in_array( $binding_attribute, $allowed_blocks[ $this->name ], true ) ) {
+                               continue;
+                       }
+                       // If no source is provided, or that source is not registered, process next attribute.
+                       if ( ! isset( $binding_source['source'] ) || ! is_string( $binding_source['source'] ) || ! isset( $block_bindings_sources[ $binding_source['source'] ] ) ) {
+                               continue;
+                       }
+
+                       $source_callback = $block_bindings_sources[ $binding_source['source'] ]['get_value_callback'];
+                       // Get the value based on the source.
+                       if ( ! isset( $binding_source['args'] ) ) {
+                               $source_args = array();
+                       } else {
+                               $source_args = $binding_source['args'];
+                       }
+                       $source_value = call_user_func_array( $source_callback, array( $source_args, $this, $binding_attribute ) );
+                       // If the value is null, process next attribute.
+                       if ( is_null( $source_value ) ) {
+                               continue;
+                       }
+
+                       // Process the HTML based on the block and the attribute.
+                       $modified_block_content = $this->replace_html( $modified_block_content, $this->name, $binding_attribute, $source_value );
+               }
+               return $modified_block_content;
+       }
+
+       /**
+        * Depending on the block attributes, replace the HTML based on the value returned by the source.
+        *
+        * @since 6.5.0
+        *
+        * @param string $block_content Block content.
+        * @param string $block_name The name of the block to process.
+        * @param string $block_attr The attribute of the block we want to process.
+        * @param string $source_value The value used to replace the HTML.
+        */
+       private function replace_html( string $block_content, string $block_name, string $block_attr, string $source_value ) {
+               $block_type = $this->block_type;
+               if ( null === $block_type || ! isset( $block_type->attributes[ $block_attr ] ) ) {
+                       return $block_content;
+               }
+
+               // Depending on the attribute source, the processing will be different.
+               switch ( $block_type->attributes[ $block_attr ]['source'] ) {
+                       case 'html':
+                       case 'rich-text':
+                               $block_reader = new WP_HTML_Tag_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[ $block_attr ]['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' );
+
+                               // 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' === $block_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 ), $selector ) === 0 || $block_reader->next_tag(
+                                               array(
+                                                       'tag_name' => $selector,
+                                               )
+                                       ) ) {
+                                               $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' === $block_name || 'core/heading' === $block_name ) {
+                                                       return $amended_content->get_updated_html();
+                                               }
+                                               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();
+                                                       foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) {
+                                                               $amended_button->set_attribute( $attribute_key, $attribute_value );
+                                                       }
+                                                       return $amended_button->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[ $block_attr ]['selector'],
+                                       )
+                               ) ) {
+                                       return $block_content;
+                               }
+                               $amended_content->set_attribute( $block_type->attributes[ $block_attr ]['attribute'], esc_attr( $source_value ) );
+                               return $amended_content->get_updated_html();
+                               break;
+
+                       default:
+                               return $block_content;
+                               break;
+               }
+               return;
+       }
+
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Generates the render output for the block.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.5.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -286,6 +484,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Process the block bindings for this block, if any are registered. This
+               // will replace the block content with the value from a registered binding source.
+               $block_content = $this->process_block_bindings( $block_content );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Filters the content of a single block.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span></span></pre></div>
<a id="trunksrcwpsettingsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-settings.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-settings.php 2024-02-01 11:43:21 UTC (rev 57513)
+++ trunk/src/wp-settings.php   2024-02-01 12:52:54 UTC (rev 57514)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -376,6 +376,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/fonts.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/class-wp-script-modules.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/script-modules.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require ABSPATH . WPINC . '/block-bindings/sources/post-meta.php';
+require ABSPATH . WPINC . '/block-bindings/sources/pattern.php';
</ins><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/interactivity-api.php';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> wp_script_modules()->add_hooks();
</span></span></pre></div>
<a id="trunktestsphpunitincludesfunctionsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/includes/functions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/includes/functions.php        2024-02-01 11:43:21 UTC (rev 57513)
+++ trunk/tests/phpunit/includes/functions.php  2024-02-01 12:52:54 UTC (rev 57514)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -339,10 +339,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 5.0.0
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function _unhook_block_registration() {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        // Block types.
</ins><span class="cx" style="display: block; padding: 0 10px">         require __DIR__ . '/unregister-blocks-hooks.php';
</span><span class="cx" style="display: block; padding: 0 10px">        remove_action( 'init', 'register_core_block_types_from_metadata' );
</span><span class="cx" style="display: block; padding: 0 10px">        remove_action( 'init', 'register_block_core_legacy_widget' );
</span><span class="cx" style="display: block; padding: 0 10px">        remove_action( 'init', 'register_block_core_widget_group' );
</span><span class="cx" style="display: block; padding: 0 10px">        remove_action( 'init', 'register_core_block_types_from_metadata' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       // Block binding sources.
+       remove_action( 'init', '_register_block_bindings_pattern_overrides_source' );
+       remove_action( 'init', '_register_block_bindings_post_meta_source' );
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> tests_add_filter( 'init', '_unhook_block_registration', 1000 );
</span></span></pre></div>
<a id="trunktestsphpunittestsblockbindingsblockbindingsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/block-bindings/block-bindings.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/block-bindings/block-bindings.php                               (rev 0)
+++ trunk/tests/phpunit/tests/block-bindings/block-bindings.php 2024-02-01 12:52:54 UTC (rev 57514)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,106 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering the Block Bindings public API.
+ *
+ * @package WordPress
+ * @subpackage Blocks
+ * @since 6.5.0
+ *
+ * @group blocks
+ * @group block-bindings
+ *
+ * @covers register_block_bindings_source
+ */
+class WP_Block_Bindings_Test extends WP_UnitTestCase {
+
+       const SOURCE_NAME  = 'test/source';
+       const SOURCE_LABEL = array(
+               'label' => 'Test source',
+       );
+
+       /**
+        * Set up before each test.
+        *
+        * @since 6.5.0
+        */
+       public function set_up() {
+               foreach ( get_all_registered_block_bindings_sources() as $source_name => $source_properties ) {
+                       if ( str_starts_with( $source_name, 'test/' ) ) {
+                               unregister_block_bindings_source( $source_name );
+                       }
+               }
+
+               parent::set_up();
+       }
+
+       /**
+        * Test if the block content is updated with the value returned by the source.
+        *
+        * @ticket 60282
+        *
+        * @covers register_block_bindings_source
+        */
+       public function test_update_block_with_value_from_source() {
+               $get_value_callback = function () {
+                       return 'test source value';
+               };
+
+               register_block_bindings_source(
+                       self::SOURCE_NAME,
+                       array(
+                               'label'              => self::SOURCE_LABEL,
+                               'get_value_callback' => $get_value_callback,
+                       )
+               );
+
+               $block_content = <<<HTML
+<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"test/source"}}}} --><p>This should not appear</p><!-- /wp:paragraph -->
+HTML;
+
+               $parsed_blocks = parse_blocks( $block_content );
+               $block         = new WP_Block( $parsed_blocks[0] );
+
+               $expected = '<p>test source value</p>';
+               $result   = $block->render();
+
+               // Check if the block content was updated correctly.
+               $this->assertEquals( $expected, $result, 'The block content should be updated with the value returned by the source.' );
+       }
+
+       /**
+        * Test passing arguments to the source.
+        *
+        * @ticket 60282
+        *
+        * @covers register_block_bindings_source
+        */
+       public function test_passing_arguments_to_source() {
+               $get_value_callback = function ( $source_args, $block_instance, $attribute_name ) {
+                       $value = $source_args['key'];
+                       return "The attribute name is '$attribute_name' and its binding has argument 'key' with value '$value'.";
+               };
+
+               register_block_bindings_source(
+                       self::SOURCE_NAME,
+                       array(
+                               'label'              => self::SOURCE_LABEL,
+                               'get_value_callback' => $get_value_callback,
+                       )
+               );
+
+               $key = 'test';
+
+               $block_content = <<<HTML
+<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"test/source", "args": {"key": "$key"}}}}} --><p>This should not appear</p><!-- /wp:paragraph -->
+HTML;
+
+               $parsed_blocks = parse_blocks( $block_content );
+               $block         = new WP_Block( $parsed_blocks[0] );
+
+               $expected = "<p>The attribute name is 'content' and its binding has argument 'key' with value 'test'.</p>";
+               $result   = $block->render();
+
+               // Check if the block content was updated correctly.
+               $this->assertEquals( $expected, $result, 'The block content should be updated with the value returned by the source.' );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/block-bindings/block-bindings.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsblockbindingsregisterphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/block-bindings/register.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/block-bindings/register.php     2024-02-01 11:43:21 UTC (rev 57513)
+++ trunk/tests/phpunit/tests/block-bindings/register.php       2024-02-01 12:52:54 UTC (rev 57514)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -17,6 +17,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">        );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Set up before each test.
+        *
+        * @since 6.5.0
+        */
+       public function set_up() {
+               foreach ( get_all_registered_block_bindings_sources() as $source_name => $source_properties ) {
+                       unregister_block_bindings_source( $source_name );
+               }
+
+               parent::set_up();
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Tear down after each test.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 6.5.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -23,9 +36,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function tear_down() {
</span><span class="cx" style="display: block; padding: 0 10px">                foreach ( get_all_registered_block_bindings_sources() as $source_name => $source_properties ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( str_starts_with( $source_name, 'test/' ) ) {
-                               unregister_block_bindings_source( $source_name );
-                       }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 unregister_block_bindings_source( $source_name );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                parent::tear_down();
</span></span></pre>
</div>
</div>

</body>
</html>