Skip to content

Block Bindings: Allow more generic setting of block attributes #9469

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

Open
wants to merge 17 commits into
base: trunk
Choose a base branch
from

Conversation

ockham
Copy link
Contributor

@ockham ockham commented Aug 13, 2025

Currently, Block Bindings support for block attributes such as core/paragraph's content or core/button's text depends on hard-coded logic to locate and replace the respective attributes. Since that logic is not filterable, it means that extending support for additional Core or third-party blocks requires hand-writing similar code in the block's PHP. This is limiting the scalability of Block Bindings.

Thus, the existing block-specific custom logic should be replaced by more generic code that is able to locate and replace an attribute based on the selector definition in its block.json.

Ultimately, this will require a set_inner_html() method from the HTML API (which doesn't exist yet); but we can already provide a decent solution based on what's currently available from the HTML API.


This is a preparatory step for making it easier to have block bindings support more blocks and block attributes, see WordPress/gutenberg#70642 (comment).

For example, try applying the following patch, which enables Block Bindings support for the Image block's caption attribute, and test coverage to verify the latter. Check that tests pass (npm run test:php -- --group=block-bindings):

Patch
diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php
index 173b0dbe80..2a72d09ddb 100644
--- a/src/wp-includes/class-wp-block.php
+++ b/src/wp-includes/class-wp-block.php
@@ -109,7 +109,7 @@ class WP_Block {
 	private const BLOCK_BINDINGS_SUPPORTED_ATTRIBUTES = array(
 		'core/paragraph' => array( 'content' ),
 		'core/heading'   => array( 'content' ),
-		'core/image'     => array( 'id', 'url', 'title', 'alt' ),
+		'core/image'     => array( 'id', 'url', 'title', 'alt', 'caption' ),
 		'core/button'    => array( 'url', 'text', 'linkTarget', 'rel' ),
 		'core/post-date' => array( 'datetime' ),
 	);
diff --git a/tests/phpunit/tests/block-bindings/render.php b/tests/phpunit/tests/block-bindings/render.php
index 60b970cf79..178c5c17ba 100644
--- a/tests/phpunit/tests/block-bindings/render.php
+++ b/tests/phpunit/tests/block-bindings/render.php
@@ -83,6 +83,16 @@ HTML
 				,
 				'<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">test source value</a></div>',
 			),
+			'image block' => array(
+				'caption',
+				<<<HTML
+<!-- wp:image {"id":66,"sizeSlug":"large","linkDestination":"none"} -->
+<figure class="wp-block-image size-large"><img src="breakfast.jpg" alt="" class="wp-image-1"/><figcaption class="wp-element-caption">Breakfast at a <em>café</em> in Wrocław.</figcaption></figure>
+<!-- /wp:image -->
+HTML
+			,
+			'<figure class="wp-block-image size-large"><img src="breakfast.jpg" alt="" class="wp-image-1"/><figcaption class="wp-element-caption">test source value</figcaption></figure>'
+			)
 		);
 	}
 

Trac ticket: https://core.trac.wordpress.org/ticket/63840


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@ockham ockham self-assigned this Aug 13, 2025
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@ockham ockham force-pushed the try/simplify-block-bindings-replace-html branch from e390302 to 83b3ae1 Compare August 13, 2025 13:29
@ockham ockham changed the title Block Bindings: Simplify replace_html() method Block Bindings: Allow more generic setting of block attributes Aug 19, 2025
@ockham ockham marked this pull request as ready for review August 20, 2025 09:18
@ockham ockham requested review from gziolo and cbravobernal August 20, 2025 09:18
Copy link

github-actions bot commented Aug 20, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props bernhard-reiter, jonsurrell, gziolo, cbravobernal.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@ockham
Copy link
Contributor Author

ockham commented Aug 20, 2025

Here's a little experiment that builds on this PR: ockham#5

@gziolo
Copy link
Member

gziolo commented Aug 20, 2025

This is awesome! Can we replicate the same logic in the Gutenberg plugin or does it require any code from HTML API in trunk?

I would be happy to explore how this could be integrated with WordPress/gutenberg#70975.

Copy link
Member

@sirreal sirreal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left some early feedback and questions.

* @param string $rich_text The rich text to replace the original content with.
* @return bool True on success.
*/
public function replace_rich_text( $rich_text ) {
Copy link
Member

@sirreal sirreal Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should rely on WP_HTML_Text_Replacement and lexical_updates, is there a reason it does not?

The algorithm should be something like:

  • Ensure the processor is stopped on an open tag that is not atomic (like SCRIPT), void (like BR), nor foreign content with a self-closing flag (like G in (<svg><g /></svg>).
  • It may be necessary to flush updates with get_updated_html, I'm not sure off the top of my head.
  • Find the open tag, use a bookmark to get its closing position.
  • Find the matching close tag, use a bookmark to find the byte length of the replacement.
  • Push a lexical update replacement like this:
$this->lexical_updates[] = new WP_HTML_Text_Replacement(
	$start,
	$length,
	$replacement_html
);

I'd like if the function mentioned that the replacement is not checked for proper HTML nesting, it's unsafe by nature. The method provides raw HTML replacement between tags. There's no guarantee that the HTML will nest as expected after the replacement is applied.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should rely on WP_HTML_Text_Replacement and lexical_updates, is there a reason it does not?

The algorithm should be something like: [...]

I think that would amount to a basic version of set_inner_html(), no? The idea for WP_Block_Bindings_Processor was that it would give weaker guarantees -- e.g. you can't currently replace_rich_text(), then seek() to an earlier position, and make another change. This simpler and less safe implementation should however be sufficient for the problem domain -- i.e. block bindings -- where we can make a few assumptions on what kind of operations are needed.

The current approach is based on a recommendation and an earlier experiment by @dmsnell, which is similar in spirit (with a string buffer that's totally separate from the one that's kept by WP_HTML_Processor).

That said, I can spin up another PR to try out the lexical_updates-based approach.


I'd like if the function mentioned that the replacement is not checked for proper HTML nesting, it's unsafe by nature. The method provides raw HTML replacement between tags. There's no guarantee that the HTML will nest as expected after the replacement is applied.

Good point. Something like that is mentioned in the PHPDoc of the class, but it's a good idea to also include it in the method's.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said, I can spin up another PR to try out the lexical_updates-based approach.

ockham#6

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would amount to a basic version of set_inner_html(), no?

Yes, but an unsafe version of set_inner_html(). Similar to what's implemented here.

The idea for WP_Block_Bindings_Processor was that it would give weaker guarantees -- e.g. you can't currently replace_rich_text(), then seek() to an earlier position, and make another change. This simpler and less safe implementation should however be sufficient for the problem domain -- i.e. block bindings -- where we can make a few assumptions on what kind of operations are needed.

The current approach is based on a recommendation and an earlier experiment by @dmsnell, which is similar in spirit (with a string buffer that's totally separate from the one that's kept by WP_HTML_Processor).

It's unclear to me why this would be safer. The unsafe operation is intending to put HTML inside a tag that potentially "leaks" to change external structure. Once we do that here, it feels like the unsafe part has happened. Taking the result of this and putting it back into another processor easily circumvents the prevention on seeks and such.

<?php

/**
* WP_Block_Bindings_Processor class.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has @access private, but it may be worth noting early and clearly in this description that the class is only for use by WordPress core and usage by extenders is not supported and likely to break.

Is there any way to avoid adding the file and class? The HTML API should publicly provide the necessary functionality someday.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has @access private, but it may be worth noting early and clearly in this description that the class is only for use by WordPress core and usage by extenders is not supported and likely to break.

I'm not familiar with such a narrative in WordPress core. Everything is rather done in the spirit that it can be used by 3rd party devs, so public API needs to be continued indefinitely. At least it should not fatally affect the existing code if major changes are necessary.

It's perfectly fine to use a specialized class for Block Bindings even if it one day becomes only a facade. It's similar to how Interactivity API directives processing is structured:

final class WP_Interactivity_API_Directives_Processor extends WP_HTML_Tag_Processor {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only caveat of having a final class is that we won't be able to extend it from Gutenberg in future developments. Meaning we will have to always combine a WordPress 'develop' version with Gutenberg in case we need to work both with Core functions and editor features.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has @access private, but it may be worth noting early and clearly in this description that the class is only for use by WordPress core and usage by extenders is not supported and likely to break.

Yeah, happy to warn more verbosely against that 👍

Is there any way to avoid adding the file and class? The HTML API should publicly provide the necessary functionality someday.

It's a problem of balancing the desire for the "right" implementation (that will take longer) with the need for a "good enough" one (that we can use sooner). Block Bindings currently suffer from being not very scalable, which is to a large degree due to the way that its attribute replacing logic is hard-coded on a per-block basis. We're pretty adamant that we need to replace this with a more generic way in order to see wider adoption.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible implement this with an anonymous class in the function that processes block bindings?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible implement this with an anonymous class in the function that processes block bindings?

Probably 🤔 I can give that a try.

@ockham
Copy link
Contributor Author

ockham commented Aug 20, 2025

This is awesome! Can we replicate the same logic in the Gutenberg plugin or does it require any code from HTML API in trunk?

I don't think it relies on any new additions to the HTML API that are only present in trunk, so it should be possible to backport this to GB. The main annoyance is probably that the block bindings logic is rather opaquely located in the WP_Block class, i.e. there are no "easy" filters to extend it for other blocks and attributes; so instead, we might have to duplicate a bunch of code in the block's render() method.

I would be happy to explore how this could be integrated with WordPress/gutenberg#70975.

👍

FWIW, non-conditional binding (i.e. replacement of an existing citation) works pretty much OOTB (with this PR), simply by adding the attribute to BLOCK_BINDINGS_SUPPORTED_ATTRIBUTES:

Patch
diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php
index 173b0dbe80..902093fe3b 100644
--- a/src/wp-includes/class-wp-block.php
+++ b/src/wp-includes/class-wp-block.php
@@ -112,6 +112,7 @@ class WP_Block {
 		'core/image'     => array( 'id', 'url', 'title', 'alt' ),
 		'core/button'    => array( 'url', 'text', 'linkTarget', 'rel' ),
 		'core/post-date' => array( 'datetime' ),
+		'core/pullquote' => array( 'citation' ),
 	);
 
 	/**
diff --git a/tests/phpunit/tests/block-bindings/render.php b/tests/phpunit/tests/block-bindings/render.php
index 60b970cf79..d2c38b7951 100644
--- a/tests/phpunit/tests/block-bindings/render.php
+++ b/tests/phpunit/tests/block-bindings/render.php
@@ -83,6 +83,17 @@ HTML
 				,
 				'<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">test source value</a></div>',
 			),
+			'pullquote block'   => array(
+				'citation',
+				<<<HTML
+<!-- wp:pullquote -->
+<figure class="wp-block-pullquote"><blockquote><p>This should not appear</p><cite>Quote McQuoteface</cite></blockquote></figure>
+<!-- /wp:pullquote -->
+HTML
+				,
+				'<figure class="wp-block-pullquote"><blockquote><p>This should not appear</p><cite>test source value</cite></blockquote></figure>',
+			)
+
 		);
 	}
 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants