From 6939eb55ddf0cd4c097e9316d6dd87c246612b35 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:18:43 +0200 Subject: [PATCH 01/39] Add support in the JS hook --- .../src/hooks/use-bindings-attributes.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index a63f59b69ca21c..ab7fc2180e0c49 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -25,7 +25,17 @@ import { unlock } from '../lock-unlock'; const BLOCK_BINDINGS_ALLOWED_BLOCKS = { 'core/paragraph': [ 'content' ], 'core/heading': [ 'content' ], - 'core/image': [ 'id', 'url', 'title', 'alt' ], + 'core/image': [ + 'id', + 'url', + 'title', + 'alt', + 'caption', + 'href', + 'rel', + 'linkClass', + 'linkTarget', + ], 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ], }; From 33ebfd89cc966567d92742104b012e55046a1a07 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:18:43 +0200 Subject: [PATCH 02/39] Add support in the PHP array --- lib/compat/wordpress-6.5/blocks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index b91e89103faa02..743d0d39bcf946 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -160,7 +160,7 @@ function gutenberg_process_block_bindings( $block_content, $parsed_block, $block $supported_block_attrs = array( 'core/paragraph' => array( 'content' ), 'core/heading' => array( 'content' ), - 'core/image' => array( 'id', 'url', 'title', 'alt' ), + 'core/image' => array( 'id', 'url', 'title', 'alt', 'caption', 'href', 'rel', 'linkClass', 'linkTarget' ), 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), ); From 1539bb59480b5d47dbb51f530d36dd4e88a07e0f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:18:43 +0200 Subject: [PATCH 03/39] Transform figure > a selector into a --- lib/compat/wordpress-6.5/blocks.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index 743d0d39bcf946..eaf98d3d3064a3 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -132,10 +132,15 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str case 'attribute': $amended_content = new WP_HTML_Tag_Processor( $block_content ); + $selector = $block_type->attributes[ $attribute_name ]['selector']; + // TODO: build the query from CSS selector when the HTML API supports it. + if ( 'figure > a' === $selector ) { + $selector = 'a'; + } if ( ! $amended_content->next_tag( array( // TODO: build the query from CSS selector. - 'tag_name' => $block_type->attributes[ $attribute_name ]['selector'], + 'tag_name' => $selector, ) ) ) { return $block_content; From 47bac8caeb6675cca3ae91b99ceea7c24bce589a Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:18:43 +0200 Subject: [PATCH 04/39] Modify the image rendering if bindings are set --- packages/block-library/src/image/index.php | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 7fe05b2209c812..1a15fdc63b0f81 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -24,6 +24,9 @@ function render_block_core_image( $attributes, $content, $block ) { $p = new WP_HTML_Tag_Processor( $content ); + if ( $p->next_tag( 'figure' ) ) { + $p->set_bookmark( 'figure' ); + } if ( ! $p->next_tag( 'img' ) || null === $p->get_attribute( 'src' ) ) { return ''; } @@ -36,6 +39,42 @@ function render_block_core_image( $attributes, $content, $block ) { $p->set_attribute( 'data-id', $attributes['data-id'] ); } + // Wrap the image with an anchor tag if it's not already wrapped and href is defined. + // This could happen when using block bindings. + $p->seek( 'figure' ); + if ( ! $p->next_tag( 'a' ) && ! empty( $attributes['href'] ) ) { + // Build the anchor tag manually until the HTML API can handle it. + $anchor_tag = ']+>)/', $anchor_tag . '$1', $content ); + $p = new WP_HTML_Tag_Processor( $content ); + } + + // Add the caption if it doesn't exist and caption is defined. + // This could happen when using block bindings. + $p->seek( 'figure' ); + if ( ! $p->next_tag( 'figcaption' ) && ! empty( $attributes['caption'] ) ) { + $caption_tag = '
' . $attributes['caption'] . '
'; + $content = str_replace( '', $caption_tag . '', $content ); + $p = new WP_HTML_Tag_Processor( $content ); + } + + $p->release_bookmark( 'figure' ); + $link_destination = isset( $attributes['linkDestination'] ) ? $attributes['linkDestination'] : 'none'; $lightbox_settings = block_core_image_get_lightbox_settings( $block->parsed_block ); From 841165d9de2ac1069c58b8ddadc0496a7d4d08ba Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:18:43 +0200 Subject: [PATCH 05/39] Add support for image caption --- lib/compat/wordpress-6.5/blocks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index eaf98d3d3064a3..405ac7898cdc20 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -111,7 +111,7 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str 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 ) { + if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name || ( 'core/image' === $block_name && 'caption' === $attribute_name ) ) { return $amended_content->get_updated_html(); } if ( 'core/button' === $block_name ) { From 31a78b8cd5100da681cce10446dee64061cc9849 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:18:43 +0200 Subject: [PATCH 06/39] Adapt Gutenberg compatibility filters --- lib/compat/wordpress-6.5/blocks.php | 124 +++++++++++++++++++--------- 1 file changed, 87 insertions(+), 37 deletions(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index 405ac7898cdc20..eeda2d5df535e2 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -154,14 +154,43 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str } /** - * Process the block bindings attribute. + * Check if the parsed block is supported by block bindings and it includes the bindings attribute. * - * @param string $block_content Block Content. - * @param array $parsed_block The full block, including name and attributes. - * @param WP_Block $block_instance The block instance. - * @return string Block content with the bind applied. + * @param array $parsed_block The full block, including name and attributes. */ - function gutenberg_process_block_bindings( $block_content, $parsed_block, $block_instance ) { + function gutenberg_is_valid_block_for_block_bindings( $parsed_block ) { + $supported_blocks = array( + 'core/paragraph', + 'core/heading', + 'core/image', + 'core/button', + ); + + // Check if the block is supported. + if ( + ! in_array( $parsed_block['blockName'], $supported_blocks, true ) || + empty( $parsed_block['attrs']['metadata']['bindings'] ) || + ! is_array( $parsed_block['attrs']['metadata']['bindings'] ) + ) { + return false; + } + + return true; + } + + /** + * Check if the binding created is valid. + * + * @param array $parsed_block The full block, including name and attributes. + * @param string $attribute_name The attribute name being processed. + * @param array $block_binding The block binding configuration. + */ + function gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $block_binding ) { + // Check if it is a valid block. + if ( ! gutenberg_is_valid_block_for_block_bindings( $parsed_block ) ) { + return false; + } + $supported_block_attrs = array( 'core/paragraph' => array( 'content' ), 'core/heading' => array( 'content' ), @@ -169,48 +198,69 @@ function gutenberg_process_block_bindings( $block_content, $parsed_block, $block 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), ); - // If the block doesn't have the bindings property or isn't one of the supported block types, return. - if ( - ! isset( $supported_block_attrs[ $block_instance->name ] ) || - empty( $parsed_block['attrs']['metadata']['bindings'] ) || - ! is_array( $parsed_block['attrs']['metadata']['bindings'] ) - ) { - return $block_content; + // Check if the attribute is not in the supported list. + if ( ! in_array( $attribute_name, $supported_block_attrs[ $parsed_block['blockName'] ], true ) ) { + return false; + } + // Check if no source is provided, or that source is not registered. + if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) || null === get_block_bindings_source( $block_binding['source'] ) ) { + return false; } - /* - * Assuming the following format for the bindings property of the "metadata" attribute: + return true; + } + + /** + * Replace the block attributes and the HTML with the values from block bindings. + * These filters are temporary for backward-compatibility. It is handled properly without filters in core. + * + */ + add_filter( + 'render_block_data', + /** + * Filter to modify the block attributes with a placeholder value for each attribute that has a block binding. * - * "bindings": { - * "title": { - * "source": "core/post-meta", - * "args": { "key": "text_custom_field" } - * }, - * "url": { - * "source": "core/post-meta", - * "args": { "key": "url_custom_field" } - * } - * } + * @param array $parsed_block The full block, including name and attributes. */ + function ( $parsed_block ) { + if ( ! gutenberg_is_valid_block_for_block_bindings( $parsed_block ) ) { + return $parsed_block; + } + foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) { + if ( ! gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $block_binding ) ) { + continue; + } + // Adds a placeholder value that will get replaced by the replace_html in the render_block filter. + $parsed_block['attrs'][ $attribute_name ] = 'placeholder'; + } + return $parsed_block; + }, + 20, + 1 + ); + + /** + * Process the block bindings attribute. + * + * @param string $block_content Block Content. + * @param array $parsed_block The full block, including name and attributes. + * @param WP_Block $block_instance The block instance. + * @return string Block content with the bind applied. + */ + function gutenberg_process_block_bindings( $block_content, $parsed_block, $block_instance ) { + if ( ! gutenberg_is_valid_block_for_block_bindings( $parsed_block ) ) { + return $block_content; + } $modified_block_content = $block_content; foreach ( $parsed_block['attrs']['metadata']['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_attrs[ $block_instance->name ], 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'] ) ) { + if ( ! gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $block_binding ) ) { continue; } $block_binding_source = get_block_bindings_source( $block_binding['source'] ); - if ( null === $block_binding_source ) { - continue; - } - - $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 ); + $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 the value is not null, process the HTML based on the block and the attribute. if ( ! is_null( $source_value ) ) { From 2abb0017cbbf746230b88fcda5a9bd0522ee5e94 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:18:43 +0200 Subject: [PATCH 07/39] Use regex (temporary) --- lib/compat/wordpress-6.5/blocks.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index eeda2d5df535e2..a151bcbedb6fae 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -111,7 +111,7 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str 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 || ( 'core/image' === $block_name && 'caption' === $attribute_name ) ) { + if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) { return $amended_content->get_updated_html(); } if ( 'core/button' === $block_name ) { @@ -123,6 +123,10 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str } return $amended_button->get_updated_html(); } + if ( 'core/image' === $block_name && 'caption' === $attribute_name ) { + // TODO: Don't use regex. + return preg_replace( '/(]*>)(.*?)(<\/figcaption>)/i', $amended_content->get_updated_html(), $block_content ); + } } else { $block_reader->seek( 'iterate-selectors' ); } From 49f327c7ddf8b33ebee7cebc04214938c300acfe Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:18:43 +0200 Subject: [PATCH 08/39] Fix regex --- lib/compat/wordpress-6.5/blocks.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index a151bcbedb6fae..8aae938597a12f 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -125,7 +125,13 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str } if ( 'core/image' === $block_name && 'caption' === $attribute_name ) { // TODO: Don't use regex. - return preg_replace( '/(]*>)(.*?)(<\/figcaption>)/i', $amended_content->get_updated_html(), $block_content ); + return preg_replace_callback( + '/]*>.*?<\/figcaption>/is', + function () use ( $amended_content ) { + return $amended_content->get_updated_html(); + }, + $block_content + ); } } else { $block_reader->seek( 'iterate-selectors' ); From c85de9898a10e58ce6465ff47d94cf272d29c466 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:18:44 +0200 Subject: [PATCH 09/39] Another approach: Remove tags in render --- packages/block-library/src/image/index.php | 76 +++++++++++----------- packages/block-library/src/image/save.js | 35 +++++----- 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 1a15fdc63b0f81..3963cc7d4ae26c 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -39,42 +39,6 @@ function render_block_core_image( $attributes, $content, $block ) { $p->set_attribute( 'data-id', $attributes['data-id'] ); } - // Wrap the image with an anchor tag if it's not already wrapped and href is defined. - // This could happen when using block bindings. - $p->seek( 'figure' ); - if ( ! $p->next_tag( 'a' ) && ! empty( $attributes['href'] ) ) { - // Build the anchor tag manually until the HTML API can handle it. - $anchor_tag = ']+>)/', $anchor_tag . '$1', $content ); - $p = new WP_HTML_Tag_Processor( $content ); - } - - // Add the caption if it doesn't exist and caption is defined. - // This could happen when using block bindings. - $p->seek( 'figure' ); - if ( ! $p->next_tag( 'figcaption' ) && ! empty( $attributes['caption'] ) ) { - $caption_tag = '
' . $attributes['caption'] . '
'; - $content = str_replace( '', $caption_tag . '', $content ); - $p = new WP_HTML_Tag_Processor( $content ); - } - - $p->release_bookmark( 'figure' ); - $link_destination = isset( $attributes['linkDestination'] ) ? $attributes['linkDestination'] : 'none'; $lightbox_settings = block_core_image_get_lightbox_settings( $block->parsed_block ); @@ -118,7 +82,45 @@ function render_block_core_image( $attributes, $content, $block ) { remove_filter( 'render_block_core/image', 'block_core_image_render_lightbox', 15 ); } - return $p->get_updated_html(); + // TODO: Replace the logic to remove the `a` tag wrapper and `figcaption` when HTML API provides its own methods. + $new_content = $p->get_updated_html(); + // Remove `` tag wrapper if it hasn't `href` attribute. + $p->seek( 'figure' ); + if ( $p->next_tag( 'a' ) && empty( $p->get_attribute( 'href' ) ) ) { + $new_content = preg_replace_callback( + '/]*>(.*?)<\/a>/is', + function ( $matches ) { + return $matches[1]; + }, + $new_content + ); + } + + // Remove `
` if caption is empty. + $p->seek( 'figure' ); + if ( $p->next_tag( 'figcaption' ) ) { + // If the next token is the closing tag, the caption is empty. + $is_empty = true; + $tag = $p->get_tag(); + while ( $p->next_token() && $tag !== $p->get_token_name() && $is_empty ) { + if ( '#comment' !== $p->get_token_type() ) { + /** + * Anything else implies this is not empty. + * This might include any text content (including a space), + * inline images or other HTML. + */ + $is_empty = false; + } + } + + if ( $is_empty ) { + $new_content = preg_replace( '/]*>.*?<\/figcaption>/is', '', $new_content ); + } + } + + $p->release_bookmark( 'figure' ); + + return $new_content; } /** diff --git a/packages/block-library/src/image/save.js b/packages/block-library/src/image/save.js index 69c26cde52a9cd..086e7cae05bf49 100644 --- a/packages/block-library/src/image/save.js +++ b/packages/block-library/src/image/save.js @@ -70,27 +70,24 @@ export default function save( { attributes } ) { /> ); + // Always wrap the image in a link, and always add the figcaption. + // They will be removed in the `render.php` if needed. const figure = ( <> - { href ? ( - - { image } - - ) : ( - image - ) } - { ! RichText.isEmpty( caption ) && ( - - ) } + + { image } + + + ); From f69662dbfa12d7b0e8021c3906f4cbfbfa3d0b88 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 1 Jul 2024 16:18:44 +0200 Subject: [PATCH 10/39] Update supported binding attribtues in another file --- lib/compat/wordpress-6.6/blocks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index 0d8805a489d9cb..c9307b87e9c897 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -17,7 +17,7 @@ function gutenberg_replace_pattern_override_default_binding( $parsed_block ) { $supported_block_attrs = array( 'core/paragraph' => array( 'content' ), 'core/heading' => array( 'content' ), - 'core/image' => array( 'id', 'url', 'title', 'alt' ), + 'core/image' => array( 'id', 'url', 'title', 'alt', 'caption', 'href', 'rel', 'linkClass', 'linkTarget' ), 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), ); From d1734c5820b03050f0e937ddfe9c234080873bbc Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 1 Jul 2024 16:20:28 +0200 Subject: [PATCH 11/39] Update client side pattern override supported attributes --- packages/patterns/src/constants.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/patterns/src/constants.js b/packages/patterns/src/constants.js index 99563a1a16787f..747c935cdccdd3 100644 --- a/packages/patterns/src/constants.js +++ b/packages/patterns/src/constants.js @@ -20,7 +20,17 @@ export const PARTIAL_SYNCING_SUPPORTED_BLOCKS = { 'core/paragraph': [ 'content' ], 'core/heading': [ 'content' ], 'core/button': [ 'text', 'url', 'linkTarget', 'rel' ], - 'core/image': [ 'id', 'url', 'title', 'alt' ], + 'core/image': [ + 'id', + 'url', + 'title', + 'alt', + 'caption', + 'href', + 'rel', + 'linkClass', + 'linkTarget', + ], }; export const PATTERN_OVERRIDES_BINDING_SOURCE = 'core/pattern-overrides'; From 953eea50221956e2b05def8d43c538712dd5c6bb Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:20:28 +0200 Subject: [PATCH 12/39] Add `set_inner_text` private method instead of using regex --- lib/compat/wordpress-6.5/blocks.php | 48 ++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index 8aae938597a12f..9f66c534edbdf4 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -67,7 +67,43 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str switch ( $block_type->attributes[ $attribute_name ]['source'] ) { case 'html': case 'rich-text': - $block_reader = new WP_HTML_Tag_Processor( $block_content ); + // Create private anonymous class because the HTML API doesn't support `set_inner_html` method yet. + $block_reader = new class($block_content) extends WP_HTML_Tag_Processor{ + public function gutenberg_set_inner_text( $new_content ) { + $tag_name = $this->get_tag(); + // Get position of the opener tag. + $this->set_bookmark( 'opener_tag' ); + $opener_tag_bookmark = $this->bookmarks['opener_tag']; + + // Visit the closing tag. + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + $this->release_bookmark( 'opener_tag' ); + return null; + } + + // Get position of the closer tag. + $closer_tag_bookmark = $this->set_bookmark( 'closer_tag' ); + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + + // Appends the new content. + $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; + $inner_content_length = $closer_tag_bookmark->start - $after_opener_tag; + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_opener_tag, $inner_content_length, $new_content ); + $this->release_bookmark( 'opener_tag' ); + $this->release_bookmark( 'closer_tag' ); + } + }; + + if ( 'core/image' === $block_name && 'caption' === $attribute_name ) { + $block_reader->next_tag( 'figcaption' ); + $block_reader->gutenberg_set_inner_text( wp_kses_post( $source_value ) ); + return $block_reader->get_updated_html(); + } // 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. @@ -123,16 +159,6 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str } return $amended_button->get_updated_html(); } - if ( 'core/image' === $block_name && 'caption' === $attribute_name ) { - // TODO: Don't use regex. - return preg_replace_callback( - '/]*>.*?<\/figcaption>/is', - function () use ( $amended_content ) { - return $amended_content->get_updated_html(); - }, - $block_content - ); - } } else { $block_reader->seek( 'iterate-selectors' ); } From 7f00489e6f33753cba48fb58425cb63ee25bb4a7 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:20:28 +0200 Subject: [PATCH 13/39] Support only caption --- lib/compat/wordpress-6.5/blocks.php | 9 ++----- lib/compat/wordpress-6.6/blocks.php | 2 +- .../src/hooks/use-bindings-attributes.js | 12 +-------- packages/block-library/src/image/index.php | 13 +--------- packages/block-library/src/image/save.js | 25 +++++++++++-------- packages/patterns/src/constants.js | 12 +-------- 6 files changed, 20 insertions(+), 53 deletions(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index 9f66c534edbdf4..c8d77c0e973ba1 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -168,15 +168,10 @@ public function gutenberg_set_inner_text( $new_content ) { case 'attribute': $amended_content = new WP_HTML_Tag_Processor( $block_content ); - $selector = $block_type->attributes[ $attribute_name ]['selector']; - // TODO: build the query from CSS selector when the HTML API supports it. - if ( 'figure > a' === $selector ) { - $selector = 'a'; - } if ( ! $amended_content->next_tag( array( // TODO: build the query from CSS selector. - 'tag_name' => $selector, + 'tag_name' => $block_type->attributes[ $attribute_name ]['selector'], ) ) ) { return $block_content; @@ -230,7 +225,7 @@ function gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $bloc $supported_block_attrs = array( 'core/paragraph' => array( 'content' ), 'core/heading' => array( 'content' ), - 'core/image' => array( 'id', 'url', 'title', 'alt', 'caption', 'href', 'rel', 'linkClass', 'linkTarget' ), + 'core/image' => array( 'id', 'url', 'title', 'alt', 'caption' ), 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), ); diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index c9307b87e9c897..c846fcb5e2a4c6 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -17,7 +17,7 @@ function gutenberg_replace_pattern_override_default_binding( $parsed_block ) { $supported_block_attrs = array( 'core/paragraph' => array( 'content' ), 'core/heading' => array( 'content' ), - 'core/image' => array( 'id', 'url', 'title', 'alt', 'caption', 'href', 'rel', 'linkClass', 'linkTarget' ), + 'core/image' => array( 'id', 'url', 'title', 'alt', 'caption' ), 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), ); diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index ab7fc2180e0c49..a7ff6818e74a9c 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -25,17 +25,7 @@ import { unlock } from '../lock-unlock'; const BLOCK_BINDINGS_ALLOWED_BLOCKS = { 'core/paragraph': [ 'content' ], 'core/heading': [ 'content' ], - 'core/image': [ - 'id', - 'url', - 'title', - 'alt', - 'caption', - 'href', - 'rel', - 'linkClass', - 'linkTarget', - ], + 'core/image': [ 'id', 'url', 'title', 'alt', 'caption' ], 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ], }; diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 3963cc7d4ae26c..ac0aa6745ee4a7 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -82,19 +82,8 @@ function render_block_core_image( $attributes, $content, $block ) { remove_filter( 'render_block_core/image', 'block_core_image_render_lightbox', 15 ); } - // TODO: Replace the logic to remove the `a` tag wrapper and `figcaption` when HTML API provides its own methods. + // TODO: Replace the logic to remove the `figcaption` when HTML API provides its own methods. $new_content = $p->get_updated_html(); - // Remove `` tag wrapper if it hasn't `href` attribute. - $p->seek( 'figure' ); - if ( $p->next_tag( 'a' ) && empty( $p->get_attribute( 'href' ) ) ) { - $new_content = preg_replace_callback( - '/]*>(.*?)<\/a>/is', - function ( $matches ) { - return $matches[1]; - }, - $new_content - ); - } // Remove `
` if caption is empty. $p->seek( 'figure' ); diff --git a/packages/block-library/src/image/save.js b/packages/block-library/src/image/save.js index 086e7cae05bf49..25b02147884e95 100644 --- a/packages/block-library/src/image/save.js +++ b/packages/block-library/src/image/save.js @@ -70,19 +70,22 @@ export default function save( { attributes } ) { /> ); - // Always wrap the image in a link, and always add the figcaption. - // They will be removed in the `render.php` if needed. + // Always add the figcaption element. + // It will be removed in the `render.php` if needed. const figure = ( <> - - { image } - - + { href ? ( + + { image } + + ) : ( + image + ) } Date: Mon, 1 Jul 2024 16:20:28 +0200 Subject: [PATCH 14/39] Only run `set_inner_text` when figcaption exists --- lib/compat/wordpress-6.5/blocks.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index c8d77c0e973ba1..6d68f7af904f30 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -100,8 +100,10 @@ public function gutenberg_set_inner_text( $new_content ) { }; if ( 'core/image' === $block_name && 'caption' === $attribute_name ) { - $block_reader->next_tag( 'figcaption' ); - $block_reader->gutenberg_set_inner_text( wp_kses_post( $source_value ) ); + if ( $block_reader->next_tag( 'figcaption' ) + ) { + $block_reader->gutenberg_set_inner_text( wp_kses_post( $source_value ) ); + } return $block_reader->get_updated_html(); } From fd58bcfcab48dfc28d86d317d3d56309bde54986 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:20:28 +0200 Subject: [PATCH 15/39] Go back to existing `save.js` --- packages/block-library/src/image/index.php | 96 +++++++++++++++++----- packages/block-library/src/image/save.js | 12 +-- 2 files changed, 81 insertions(+), 27 deletions(-) diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index ac0aa6745ee4a7..5ec67dc0d3d8c0 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -22,7 +22,66 @@ function render_block_core_image( $attributes, $content, $block ) { return ''; } - $p = new WP_HTML_Tag_Processor( $content ); + /* + * Use a private anonymous class until the HTML API provides similar methods. + * TODO: Replace the logic to remove the `figcaption` when HTML API provides its own methods. + * + * @phpcs:disable Gutenberg.NamingConventions.ValidBlockLibraryFunctionName.FunctionNameInvalid, Gutenberg.Commenting.SinceTag.MissingMethodSinceTag + */ + $p = new class( $content ) extends WP_HTML_Tag_Processor { + public function append_element_after_tag( $new_element ) { + $tag_name = $this->get_tag(); + $this->set_bookmark( 'current_tag' ); + // Visit the closing tag if exists. + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + $this->seek( 'current_tag' ); + $this->release_bookmark( 'current_tag' ); + } + + // Get position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + + // Append the new element. + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $closer_tag_bookmark->start + $closer_tag_bookmark->length, 0, $new_element ); + $this->release_bookmark( 'closer_tag' ); + } + + public function remove_current_tag_element() { + // Get position of the opener tag. + $this->set_bookmark( 'opener_tag' ); + $opener_tag_bookmark = $this->bookmarks['opener_tag']; + + // Visit the closing tag. + $tag_name = $this->get_tag(); + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + $this->release_bookmark( 'opener_tag' ); + return null; + } + + // Get position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + + // Remove the current tag. + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); + $this->release_bookmark( 'opener_tag' ); + $this->release_bookmark( 'closer_tag' ); + } + }; + // @phpcs:enable if ( $p->next_tag( 'figure' ) ) { $p->set_bookmark( 'figure' ); @@ -82,34 +141,27 @@ function render_block_core_image( $attributes, $content, $block ) { remove_filter( 'render_block_core/image', 'block_core_image_render_lightbox', 15 ); } - // TODO: Replace the logic to remove the `figcaption` when HTML API provides its own methods. - $new_content = $p->get_updated_html(); - - // Remove `
` if caption is empty. $p->seek( 'figure' ); if ( $p->next_tag( 'figcaption' ) ) { - // If the next token is the closing tag, the caption is empty. - $is_empty = true; - $tag = $p->get_tag(); - while ( $p->next_token() && $tag !== $p->get_token_name() && $is_empty ) { - if ( '#comment' !== $p->get_token_type() ) { - /** - * Anything else implies this is not empty. - * This might include any text content (including a space), - * inline images or other HTML. - */ - $is_empty = false; - } + // Remove `
` if exists and caption attribute exists but it is empty. + if ( isset( $attributes['caption'] ) && strlen( $attributes['caption'] ) === 0 ) { + $p->remove_current_tag_element(); } - - if ( $is_empty ) { - $new_content = preg_replace( '/]*>.*?<\/figcaption>/is', '', $new_content ); + } else { + // Add caption if it doesn't exist and the caption is not empty. + if ( ! empty( $attributes['caption'] ) ) { + $p->seek( 'figure' ); + // Append caption after link or image. + if ( ! $p->next_tag( 'a' ) ) { + $p->seek( 'figure' ); + $p->next_tag( 'img' ); + } + $p->append_element_after_tag( '
' . $attributes['caption'] . '
' ); } } - $p->release_bookmark( 'figure' ); - return $new_content; + return $p->get_updated_html(); } /** diff --git a/packages/block-library/src/image/save.js b/packages/block-library/src/image/save.js index 25b02147884e95..89977607c352a9 100644 --- a/packages/block-library/src/image/save.js +++ b/packages/block-library/src/image/save.js @@ -86,11 +86,13 @@ export default function save( { attributes } ) { ) : ( image ) } - + { ! RichText.isEmpty( caption ) && ( + + ) } ); From c5abad9f752a9cf90f3ddb4ba73550a3a3d508cd Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:20:28 +0200 Subject: [PATCH 16/39] Fix typo --- lib/compat/wordpress-6.5/blocks.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index 6d68f7af904f30..c476329afefcc1 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -100,8 +100,7 @@ public function gutenberg_set_inner_text( $new_content ) { }; if ( 'core/image' === $block_name && 'caption' === $attribute_name ) { - if ( $block_reader->next_tag( 'figcaption' ) - ) { + if ( $block_reader->next_tag( 'figcaption' ) ) { $block_reader->gutenberg_set_inner_text( wp_kses_post( $source_value ) ); } return $block_reader->get_updated_html(); From 3a9c6e8215085d4d95a894259864203d13d2f801 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:20:28 +0200 Subject: [PATCH 17/39] Remove comment --- packages/block-library/src/image/save.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/block-library/src/image/save.js b/packages/block-library/src/image/save.js index 89977607c352a9..69c26cde52a9cd 100644 --- a/packages/block-library/src/image/save.js +++ b/packages/block-library/src/image/save.js @@ -70,8 +70,6 @@ export default function save( { attributes } ) { /> ); - // Always add the figcaption element. - // It will be removed in the `render.php` if needed. const figure = ( <> { href ? ( From 01ca7ee435d517c3cda7a69eaefb34c8b3d019ea Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:22:02 +0200 Subject: [PATCH 18/39] Hide caption controls if the caption is binded --- packages/block-library/src/image/image.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 17a860fa5f47ce..e926d34ced014e 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -455,6 +455,7 @@ export default function Image( { lockTitleControls = false, lockTitleControlsMessage, lockCaption = false, + hideCaptionControls = false, } = useSelect( ( select ) => { if ( ! isSingleSelected ) { @@ -465,6 +466,7 @@ export default function Image( { url: urlBinding, alt: altBinding, title: titleBinding, + caption: captionBinding, } = metadata?.bindings || {}; const hasParentPattern = !! context[ 'pattern/overrides' ]; const urlBindingSource = getBlockBindingsSource( @@ -476,6 +478,9 @@ export default function Image( { const titleBindingSource = getBlockBindingsSource( titleBinding?.source ); + const captionBindingSource = getBlockBindingsSource( + captionBinding?.source + ); return { lockUrlControls: !! urlBinding && @@ -520,6 +525,13 @@ export default function Image( { titleBindingSource.label ) : __( 'Connected to dynamic data' ), + hideCaptionControls: + captionBinding || + ! captionBindingSource?.canUserEditValue( { + select, + context, + args: captionBinding?.args, + } ), }; }, [ From 2967e88b01a02e6a8ec56f1e323704e0243456d5 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:22:02 +0200 Subject: [PATCH 19/39] Remove user permissions check --- packages/block-library/src/image/image.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index e926d34ced014e..3e5dace6d23dbc 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -478,9 +478,7 @@ export default function Image( { const titleBindingSource = getBlockBindingsSource( titleBinding?.source ); - const captionBindingSource = getBlockBindingsSource( - captionBinding?.source - ); + return { lockUrlControls: !! urlBinding && @@ -525,13 +523,7 @@ export default function Image( { titleBindingSource.label ) : __( 'Connected to dynamic data' ), - hideCaptionControls: - captionBinding || - ! captionBindingSource?.canUserEditValue( { - select, - context, - args: captionBinding?.args, - } ), + hideCaptionControls: !! captionBinding, }; }, [ From ec0e52cb765d745bc359b0dda1528da99719fdad Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:02 +0200 Subject: [PATCH 20/39] Add caption e2e tests --- packages/e2e-tests/plugins/block-bindings.php | 10 + .../editor/various/block-bindings.spec.js | 200 ++++++++++++++++++ 2 files changed, 210 insertions(+) diff --git a/packages/e2e-tests/plugins/block-bindings.php b/packages/e2e-tests/plugins/block-bindings.php index 74aec2adb500fb..a9cada3795f49d 100644 --- a/packages/e2e-tests/plugins/block-bindings.php +++ b/packages/e2e-tests/plugins/block-bindings.php @@ -50,5 +50,15 @@ function gutenberg_test_block_bindings_register_custom_fields() { 'default' => 'show_in_rest false field value', ) ); + register_meta( + 'post', + 'empty_custom_field', + array( + 'show_in_rest' => true, + 'type' => 'string', + 'single' => true, + 'default' => '', + ) + ); } add_action( 'init', 'gutenberg_test_block_bindings_register_custom_fields' ); diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index 222004c7c1bccc..563f2024680ac5 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -1083,6 +1083,48 @@ test.describe( 'Block bindings', () => { expect( titleValue ).toBe( 'default title value' ); } ); + test( 'should hide caption control and lock editing when it is bound', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + caption: 'default caption value', + metadata: { + bindings: { + caption: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + + // Control to remove caption doesn't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByLabel( 'Remove caption' ) + ).toBeHidden(); + + // Caption shows the custom field key. + const caption = imageBlock.getByLabel( 'Image caption text' ); + await expect( caption ).toHaveText( 'text_custom_field' ); + + // Caption is not editable. + await expect( caption ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); + test( 'Multiple bindings should lock the appropriate controls', async ( { editor, page, @@ -1853,6 +1895,122 @@ test.describe( 'Block bindings', () => { ); } ); + test( 'should show value of the custom field in the caption rich text when caption is bound', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + anchor: 'image-caption-binding', + url: imagePlaceholderSrc, + caption: 'default caption value', + metadata: { + bindings: { + caption: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + + // Control to remove caption doesn't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByLabel( 'Remove caption' ) + ).toBeHidden(); + + // Caption shows the custom field value. + const caption = imageBlock.getByLabel( 'Image caption text' ); + await expect( caption ).toHaveText( + 'Value of the text_custom_field' + ); + + // The value of the custom field is shown in the frontend. + const previewPage = await editor.openPreviewPage(); + const figureDom = previewPage.locator( + '#image-caption-binding figcaption' + ); + await expect( figureDom ).toHaveText( + 'Value of the text_custom_field' + ); + } ); + + test( 'should add figcaption element when original caption is empty but it is bound to a value', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + anchor: 'image-caption-binding', + url: imagePlaceholderSrc, + metadata: { + bindings: { + caption: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + + // Caption shows the custom field value in the editor. + const caption = imageBlock.getByLabel( 'Image caption text' ); + await expect( caption ).toHaveText( + 'Value of the text_custom_field' + ); + + // The value of the custom field is shown in the figcaption element. + const previewPage = await editor.openPreviewPage(); + const figureDom = previewPage.locator( + '#image-caption-binding figcaption' + ); + await expect( figureDom ).toHaveText( + 'Value of the text_custom_field' + ); + } ); + + test( 'should remove figcaption element when bound to an empty value', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + anchor: 'image-caption-binding', + url: imagePlaceholderSrc, + caption: 'default caption value', + metadata: { + bindings: { + caption: { + source: 'core/post-meta', + args: { key: 'empty_custom_field' }, + }, + }, + }, + }, + } ); + + // Wait until the image is loaded. + const previewPage = await editor.openPreviewPage(); + await previewPage.locator( '#image-caption-binding' ).waitFor(); + // Check the figcaption doesn't exist. + await expect( + previewPage.locator( '#image-caption-binding figcaption' ) + ).toHaveCount( 0 ); + } ); + test( 'Multiple bindings should show the value of the custom fields', async ( { editor, page, @@ -2170,6 +2328,48 @@ test.describe( 'Block bindings', () => { previewPage.locator( '#image-alt-binding img' ) ).toHaveAttribute( 'alt', 'new value' ); } ); + + test( 'should be possible to edit the value of the text custom field from the image caption', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + anchor: 'image-caption-binding', + url: imagePlaceholderSrc, + caption: 'default caption value', + metadata: { + bindings: { + caption: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + + // Edit the custom field value in the caption input text. + const captionInput = + imageBlock.getByLabel( 'Image caption text' ); + await expect( captionInput ).not.toHaveAttribute( 'readonly' ); + await captionInput.fill( 'new value' ); + + // Check that the image caption attribute didn't change. + const [ imageBlockObject ] = await editor.getBlocks(); + expect( imageBlockObject.attributes.caption ).toBe( + 'default caption value' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#image-caption-binding figcaption' ) + ).toHaveText( 'new value' ); + } ); } ); } ); } ); From fa013642a42fc24c4d1d36925a0031fa317d0da4 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:02 +0200 Subject: [PATCH 21/39] Copy latest changes from core --- lib/compat/wordpress-6.5/blocks.php | 88 +++++++++++++++++++---------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index c476329afefcc1..ba2ae19393b5c1 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -67,45 +67,73 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str switch ( $block_type->attributes[ $attribute_name ]['source'] ) { case 'html': case 'rich-text': - // Create private anonymous class because the HTML API doesn't support `set_inner_html` method yet. - $block_reader = new class($block_content) extends WP_HTML_Tag_Processor{ - public function gutenberg_set_inner_text( $new_content ) { - $tag_name = $this->get_tag(); - // Get position of the opener tag. - $this->set_bookmark( 'opener_tag' ); - $opener_tag_bookmark = $this->bookmarks['opener_tag']; - - // Visit the closing tag. - if ( ! $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) || ! $this->is_tag_closer() ) { - $this->release_bookmark( 'opener_tag' ); - return null; - } + if ( 'core/image' === $block_name && 'caption' === $attribute_name ) { + // Create private anonymous class until the HTML API provides `set_inner_html` method. + $block_reader = new class($block_content) extends WP_HTML_Tag_Processor{ + /** + * Replace the inner text of an HTML with the passed content. + * + * THIS IS A TEMPORARY SOLUTION IN CORE NOT TO BE EMULATED. + * IT IS A TEMPORARY SOLUTION THAT JUST WORKS FOR THIS SPECIFIC + * USE CASE UNTIL THE HTML PROCESSOR PROVIDES ITS OWN METHOD. + * + * @param string $new_content New text to insert in the HTML element. + * @return bool Whether the inner text was properly replaced. + */ + public function gutenberg_set_inner_text( $new_content ) { + return; + /* + * THIS IS A STOP-GAP MEASURE NOT TO BE EMULATED. + * + * Check that the processor is paused on an opener tag. + * + */ + if ( + WP_HTML_Tag_Processor::STATE_MATCHED_TAG !== $this->parser_state || + $this->is_tag_closer() + ) { + return false; + } - // Get position of the closer tag. - $closer_tag_bookmark = $this->set_bookmark( 'closer_tag' ); - $closer_tag_bookmark = $this->bookmarks['closer_tag']; + // Set position of the opener tag. + $this->set_bookmark( 'opener_tag' ); + + /* + * This is a best-effort guess to visit the closer tag and check it exists. + * In the future, this code should rely on the HTML Processor for this kind of operation. + */ + $tag_name = $this->get_tag(); + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + return false; + } - // Appends the new content. - $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; - $inner_content_length = $closer_tag_bookmark->start - $after_opener_tag; - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_opener_tag, $inner_content_length, $new_content ); - $this->release_bookmark( 'opener_tag' ); - $this->release_bookmark( 'closer_tag' ); - } - }; + // Set position of the closer tag. + $this->set_bookmark( 'closer_tag' ); - if ( 'core/image' === $block_name && 'caption' === $attribute_name ) { + // Get opener and closer tag bookmarks. + $opener_tag_bookmark = $this->bookmarks['_opener_tag']; + $closer_tag_bookmark = $this->bookmarks['_closer_tag']; + + // Appends the new content. + $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; + $inner_content_length = $closer_tag_bookmark->start - $after_opener_tag; + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_opener_tag, $inner_content_length, $new_content ); + return true; + } + }; if ( $block_reader->next_tag( 'figcaption' ) ) { $block_reader->gutenberg_set_inner_text( wp_kses_post( $source_value ) ); } return $block_reader->get_updated_html(); } + $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[ $attribute_name ]['selector'] ); From f39fa0be00ec4288e3d3e25a29adc9efabd6058b Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:02 +0200 Subject: [PATCH 22/39] Remove extra return --- lib/compat/wordpress-6.5/blocks.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index ba2ae19393b5c1..7fd1586a2ebd7d 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -81,7 +81,6 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str * @return bool Whether the inner text was properly replaced. */ public function gutenberg_set_inner_text( $new_content ) { - return; /* * THIS IS A STOP-GAP MEASURE NOT TO BE EMULATED. * From 45cd9e46e054d590caebe9546f9bb194a456532d Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:02 +0200 Subject: [PATCH 23/39] Fix bookmarks --- lib/compat/wordpress-6.5/blocks.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index 7fd1586a2ebd7d..8298ebf8cd4d8c 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -115,8 +115,8 @@ public function gutenberg_set_inner_text( $new_content ) { $this->set_bookmark( 'closer_tag' ); // Get opener and closer tag bookmarks. - $opener_tag_bookmark = $this->bookmarks['_opener_tag']; - $closer_tag_bookmark = $this->bookmarks['_closer_tag']; + $opener_tag_bookmark = $this->bookmarks['opener_tag']; + $closer_tag_bookmark = $this->bookmarks['closer_tag']; // Appends the new content. $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; From da0886d7717b4557230f6b7420da998de669395d Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:02 +0200 Subject: [PATCH 24/39] Fix compatibility with older versions --- lib/compat/wordpress-6.5/blocks.php | 153 ++++++++++++++++----- packages/block-library/src/image/index.php | 36 ++++- 2 files changed, 151 insertions(+), 38 deletions(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index 8298ebf8cd4d8c..ba0549ff8d5290 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -71,11 +71,13 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str // Create private anonymous class until the HTML API provides `set_inner_html` method. $block_reader = new class($block_content) extends WP_HTML_Tag_Processor{ /** - * Replace the inner text of an HTML with the passed content. - * - * THIS IS A TEMPORARY SOLUTION IN CORE NOT TO BE EMULATED. + * THESE METHODS ARE A TEMPORARY SOLUTION IN CORE NOT TO BE EMULATED. * IT IS A TEMPORARY SOLUTION THAT JUST WORKS FOR THIS SPECIFIC * USE CASE UNTIL THE HTML PROCESSOR PROVIDES ITS OWN METHOD. + */ + + /** + * Replace the inner text of an HTML with the passed content. * * @param string $new_content New text to insert in the HTML element. * @return bool Whether the inner text was properly replaced. @@ -119,14 +121,125 @@ public function gutenberg_set_inner_text( $new_content ) { $closer_tag_bookmark = $this->bookmarks['closer_tag']; // Appends the new content. - $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; + $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_opener_tag ] ) { + ++$after_opener_tag; + } $inner_content_length = $closer_tag_bookmark->start - $after_opener_tag; $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_opener_tag, $inner_content_length, $new_content ); return true; } + + /** + * Add a new HTML element after the current tag. + * + * @param string $new_element New HTML element to append after the current tag. + */ + public function gutenberg_append_element_after_tag( $new_element ) { + $tag_name = $this->get_tag(); + $this->set_bookmark( 'current_tag' ); + // Visit the closing tag if exists. + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + $this->seek( 'current_tag' ); + $this->release_bookmark( 'current_tag' ); + } + + // Get position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_closer_tag ] ) { + ++$after_closer_tag; + } + + // Append the new element. + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closer_tag, 0, $new_element ); + $this->release_bookmark( 'closer_tag' ); + } + + /** + * Remove the current tag element. + * + * @return bool Whether the element was properly removed. + */ + public function gutenberg_remove_current_tag_element() { + // Get position of the opener tag. + $this->set_bookmark( 'opener_tag' ); + $opener_tag_bookmark = $this->bookmarks['opener_tag']; + + // Visit the closing tag. + $tag_name = $this->get_tag(); + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + $this->release_bookmark( 'opener_tag' ); + return false; + } + + // Get position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + + // Remove the current tag. + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_closer_tag ] ) { + ++$after_closer_tag; + } + $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); + $this->release_bookmark( 'opener_tag' ); + $this->release_bookmark( 'closer_tag' ); + return true; + } }; + + /* + * For backward compatibility, the logic from the image render needs to be replicated. + * This is because the block attributes can't be modified with the binding value with `render_block_data` filter, + * as it doesn't have access to the block instance. + */ + if ( $block_reader->next_tag( 'figure' ) ) { + $block_reader->set_bookmark( 'figure' ); + } + + $new_value = wp_kses_post( $source_value ); + if ( $block_reader->next_tag( 'figcaption' ) ) { - $block_reader->gutenberg_set_inner_text( wp_kses_post( $source_value ) ); + if ( empty( $new_value ) ) { + $block_reader->gutenberg_remove_current_tag_element(); + } else { + $block_reader->gutenberg_set_inner_text( $new_value ); + } + } else { + $block_reader->seek( 'figure' ); + if ( ! $block_reader->next_tag( 'a' ) ) { + $block_reader->seek( 'figure' ); + $block_reader->next_tag( 'img' ); + } + $block_reader->gutenberg_append_element_after_tag( '
' . $new_value . '
' ); } return $block_reader->get_updated_html(); } @@ -269,36 +382,6 @@ function gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $bloc return true; } - /** - * Replace the block attributes and the HTML with the values from block bindings. - * These filters are temporary for backward-compatibility. It is handled properly without filters in core. - * - */ - add_filter( - 'render_block_data', - /** - * Filter to modify the block attributes with a placeholder value for each attribute that has a block binding. - * - * @param array $parsed_block The full block, including name and attributes. - */ - function ( $parsed_block ) { - if ( ! gutenberg_is_valid_block_for_block_bindings( $parsed_block ) ) { - return $parsed_block; - } - - foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) { - if ( ! gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $block_binding ) ) { - continue; - } - // Adds a placeholder value that will get replaced by the replace_html in the render_block filter. - $parsed_block['attrs'][ $attribute_name ] = 'placeholder'; - } - return $parsed_block; - }, - 20, - 1 - ); - /** * Process the block bindings attribute. * diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 5ec67dc0d3d8c0..2ff009e826e8d5 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -29,6 +29,11 @@ function render_block_core_image( $attributes, $content, $block ) { * @phpcs:disable Gutenberg.NamingConventions.ValidBlockLibraryFunctionName.FunctionNameInvalid, Gutenberg.Commenting.SinceTag.MissingMethodSinceTag */ $p = new class( $content ) extends WP_HTML_Tag_Processor { + /** + * Add a new HTML element after the current tag. + * + * @param string $new_element New HTML element to append after the current tag. + */ public function append_element_after_tag( $new_element ) { $tag_name = $this->get_tag(); $this->set_bookmark( 'current_tag' ); @@ -46,12 +51,27 @@ public function append_element_after_tag( $new_element ) { // Get position of the closer tag. $this->set_bookmark( 'closer_tag' ); $closer_tag_bookmark = $this->bookmarks['closer_tag']; + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * It can be removed once 6.5 is not supported anymore. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_closer_tag ] ) { + ++$after_closer_tag; + } // Append the new element. - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $closer_tag_bookmark->start + $closer_tag_bookmark->length, 0, $new_element ); + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closer_tag, 0, $new_element ); $this->release_bookmark( 'closer_tag' ); } + /** + * Remove the current tag element. + * + * @return bool Whether the element was properly removed. + */ public function remove_current_tag_element() { // Get position of the opener tag. $this->set_bookmark( 'opener_tag' ); @@ -66,7 +86,7 @@ public function remove_current_tag_element() { ) ) || ! $this->is_tag_closer() ) { $this->release_bookmark( 'opener_tag' ); - return null; + return false; } // Get position of the closer tag. @@ -74,11 +94,21 @@ public function remove_current_tag_element() { $closer_tag_bookmark = $this->bookmarks['closer_tag']; // Remove the current tag. - $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * It can be removed once 6.5 is not supported anymore. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_closer_tag ] ) { + ++$after_closer_tag; + } $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); $this->release_bookmark( 'opener_tag' ); $this->release_bookmark( 'closer_tag' ); + return true; } }; // @phpcs:enable From 75fc91707ab99d131a3460b7a8bb4365b5e7137f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 25/39] Move logic to 6.6 compat folder --- lib/compat/wordpress-6.5/blocks.php | 277 +++------------------ lib/compat/wordpress-6.6/blocks.php | 370 ++++++++++++++++++++++++++++ 2 files changed, 410 insertions(+), 237 deletions(-) diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index ba0549ff8d5290..b91e89103faa02 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -67,183 +67,6 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str switch ( $block_type->attributes[ $attribute_name ]['source'] ) { case 'html': case 'rich-text': - if ( 'core/image' === $block_name && 'caption' === $attribute_name ) { - // Create private anonymous class until the HTML API provides `set_inner_html` method. - $block_reader = new class($block_content) extends WP_HTML_Tag_Processor{ - /** - * THESE METHODS ARE A TEMPORARY SOLUTION IN CORE NOT TO BE EMULATED. - * IT IS A TEMPORARY SOLUTION THAT JUST WORKS FOR THIS SPECIFIC - * USE CASE UNTIL THE HTML PROCESSOR PROVIDES ITS OWN METHOD. - */ - - /** - * Replace the inner text of an HTML with the passed content. - * - * @param string $new_content New text to insert in the HTML element. - * @return bool Whether the inner text was properly replaced. - */ - public function gutenberg_set_inner_text( $new_content ) { - /* - * THIS IS A STOP-GAP MEASURE NOT TO BE EMULATED. - * - * Check that the processor is paused on an opener tag. - * - */ - if ( - WP_HTML_Tag_Processor::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_tag_closer() - ) { - return false; - } - - // Set position of the opener tag. - $this->set_bookmark( 'opener_tag' ); - - /* - * This is a best-effort guess to visit the closer tag and check it exists. - * In the future, this code should rely on the HTML Processor for this kind of operation. - */ - $tag_name = $this->get_tag(); - if ( ! $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) || ! $this->is_tag_closer() ) { - return false; - } - - // Set position of the closer tag. - $this->set_bookmark( 'closer_tag' ); - - // Get opener and closer tag bookmarks. - $opener_tag_bookmark = $this->bookmarks['opener_tag']; - $closer_tag_bookmark = $this->bookmarks['closer_tag']; - - // Appends the new content. - $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; - /* - * There was a bug in the HTML Processor token length fixed after 6.5. - * This check is needed to add compatibility for that. - * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 - */ - if ( '>' === $this->html[ $after_opener_tag ] ) { - ++$after_opener_tag; - } - $inner_content_length = $closer_tag_bookmark->start - $after_opener_tag; - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_opener_tag, $inner_content_length, $new_content ); - return true; - } - - /** - * Add a new HTML element after the current tag. - * - * @param string $new_element New HTML element to append after the current tag. - */ - public function gutenberg_append_element_after_tag( $new_element ) { - $tag_name = $this->get_tag(); - $this->set_bookmark( 'current_tag' ); - // Visit the closing tag if exists. - if ( ! $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) || ! $this->is_tag_closer() ) { - $this->seek( 'current_tag' ); - $this->release_bookmark( 'current_tag' ); - } - - // Get position of the closer tag. - $this->set_bookmark( 'closer_tag' ); - $closer_tag_bookmark = $this->bookmarks['closer_tag']; - $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; - /* - * There was a bug in the HTML Processor token length fixed after 6.5. - * This check is needed to add compatibility for that. - * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 - */ - if ( '>' === $this->html[ $after_closer_tag ] ) { - ++$after_closer_tag; - } - - // Append the new element. - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closer_tag, 0, $new_element ); - $this->release_bookmark( 'closer_tag' ); - } - - /** - * Remove the current tag element. - * - * @return bool Whether the element was properly removed. - */ - public function gutenberg_remove_current_tag_element() { - // Get position of the opener tag. - $this->set_bookmark( 'opener_tag' ); - $opener_tag_bookmark = $this->bookmarks['opener_tag']; - - // Visit the closing tag. - $tag_name = $this->get_tag(); - if ( ! $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) || ! $this->is_tag_closer() ) { - $this->release_bookmark( 'opener_tag' ); - return false; - } - - // Get position of the closer tag. - $this->set_bookmark( 'closer_tag' ); - $closer_tag_bookmark = $this->bookmarks['closer_tag']; - - // Remove the current tag. - $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; - /* - * There was a bug in the HTML Processor token length fixed after 6.5. - * This check is needed to add compatibility for that. - * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 - */ - if ( '>' === $this->html[ $after_closer_tag ] ) { - ++$after_closer_tag; - } - $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); - $this->release_bookmark( 'opener_tag' ); - $this->release_bookmark( 'closer_tag' ); - return true; - } - }; - - /* - * For backward compatibility, the logic from the image render needs to be replicated. - * This is because the block attributes can't be modified with the binding value with `render_block_data` filter, - * as it doesn't have access to the block instance. - */ - if ( $block_reader->next_tag( 'figure' ) ) { - $block_reader->set_bookmark( 'figure' ); - } - - $new_value = wp_kses_post( $source_value ); - - if ( $block_reader->next_tag( 'figcaption' ) ) { - if ( empty( $new_value ) ) { - $block_reader->gutenberg_remove_current_tag_element(); - } else { - $block_reader->gutenberg_set_inner_text( $new_value ); - } - } else { - $block_reader->seek( 'figure' ); - if ( ! $block_reader->next_tag( 'a' ) ) { - $block_reader->seek( 'figure' ); - $block_reader->next_tag( 'img' ); - } - $block_reader->gutenberg_append_element_after_tag( '
' . $new_value . '
' ); - } - return $block_reader->get_updated_html(); - } - $block_reader = new WP_HTML_Tag_Processor( $block_content ); // TODO: Support for CSS selectors whenever they are ready in the HTML API. @@ -326,83 +149,63 @@ public function gutenberg_remove_current_tag_element() { } /** - * Check if the parsed block is supported by block bindings and it includes the bindings attribute. - * - * @param array $parsed_block The full block, including name and attributes. - */ - function gutenberg_is_valid_block_for_block_bindings( $parsed_block ) { - $supported_blocks = array( - 'core/paragraph', - 'core/heading', - 'core/image', - 'core/button', - ); - - // Check if the block is supported. - if ( - ! in_array( $parsed_block['blockName'], $supported_blocks, true ) || - empty( $parsed_block['attrs']['metadata']['bindings'] ) || - ! is_array( $parsed_block['attrs']['metadata']['bindings'] ) - ) { - return false; - } - - return true; - } - - /** - * Check if the binding created is valid. + * Process the block bindings attribute. * - * @param array $parsed_block The full block, including name and attributes. - * @param string $attribute_name The attribute name being processed. - * @param array $block_binding The block binding configuration. + * @param string $block_content Block Content. + * @param array $parsed_block The full block, including name and attributes. + * @param WP_Block $block_instance The block instance. + * @return string Block content with the bind applied. */ - function gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $block_binding ) { - // Check if it is a valid block. - if ( ! gutenberg_is_valid_block_for_block_bindings( $parsed_block ) ) { - return false; - } - + function gutenberg_process_block_bindings( $block_content, $parsed_block, $block_instance ) { $supported_block_attrs = array( 'core/paragraph' => array( 'content' ), 'core/heading' => array( 'content' ), - 'core/image' => array( 'id', 'url', 'title', 'alt', 'caption' ), + 'core/image' => array( 'id', 'url', 'title', 'alt' ), 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), ); - // Check if the attribute is not in the supported list. - if ( ! in_array( $attribute_name, $supported_block_attrs[ $parsed_block['blockName'] ], true ) ) { - return false; - } - // Check if no source is provided, or that source is not registered. - if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) || null === get_block_bindings_source( $block_binding['source'] ) ) { - return false; + // If the block doesn't have the bindings property or isn't one of the supported block types, return. + if ( + ! isset( $supported_block_attrs[ $block_instance->name ] ) || + empty( $parsed_block['attrs']['metadata']['bindings'] ) || + ! is_array( $parsed_block['attrs']['metadata']['bindings'] ) + ) { + return $block_content; } - return true; - } + /* + * Assuming the following format for the bindings property of the "metadata" attribute: + * + * "bindings": { + * "title": { + * "source": "core/post-meta", + * "args": { "key": "text_custom_field" } + * }, + * "url": { + * "source": "core/post-meta", + * "args": { "key": "url_custom_field" } + * } + * } + */ - /** - * Process the block bindings attribute. - * - * @param string $block_content Block Content. - * @param array $parsed_block The full block, including name and attributes. - * @param WP_Block $block_instance The block instance. - * @return string Block content with the bind applied. - */ - function gutenberg_process_block_bindings( $block_content, $parsed_block, $block_instance ) { - if ( ! gutenberg_is_valid_block_for_block_bindings( $parsed_block ) ) { - return $block_content; - } $modified_block_content = $block_content; foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) { - if ( ! gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $block_binding ) ) { + // If the attribute is not in the supported list, process next attribute. + if ( ! in_array( $attribute_name, $supported_block_attrs[ $block_instance->name ], 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'] ); - $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 ( null === $block_binding_source ) { + continue; + } + + $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 the value is not null, process the HTML based on the block and the attribute. if ( ! is_null( $source_value ) ) { diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index c846fcb5e2a4c6..c89041d4b25b74 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -44,3 +44,373 @@ function gutenberg_replace_pattern_override_default_binding( $parsed_block ) { } add_filter( 'render_block_data', 'gutenberg_replace_pattern_override_default_binding', 10, 1 ); + +// Only process block bindings if they are not processed in core. +if ( ! is_wp_version_compatible( '6.6' ) ) { + /** + * Depending on the block attribute name, replace its value in the HTML based on the value provided. + * + * @param string $block_content Block Content. + * @param string $block_name The name of the block to process. + * @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. + */ + function gutenberg_block_bindings_replace_html( $block_content, $block_name, 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; + } + + // Depending on the attribute source, the processing will be different. + switch ( $block_type->attributes[ $attribute_name ]['source'] ) { + case 'html': + case 'rich-text': + if ( 'core/image' === $block_name && 'caption' === $attribute_name ) { + // Create private anonymous class until the HTML API provides `set_inner_html` method. + $block_reader = new class($block_content) extends WP_HTML_Tag_Processor{ + /** + * THESE METHODS ARE A TEMPORARY SOLUTION IN CORE NOT TO BE EMULATED. + * IT IS A TEMPORARY SOLUTION THAT JUST WORKS FOR THIS SPECIFIC + * USE CASE UNTIL THE HTML PROCESSOR PROVIDES ITS OWN METHOD. + */ + + /** + * Replace the inner text of an HTML with the passed content. + * + * @param string $new_content New text to insert in the HTML element. + * @return bool Whether the inner text was properly replaced. + */ + public function gutenberg_set_inner_text( $new_content ) { + /* + * THIS IS A STOP-GAP MEASURE NOT TO BE EMULATED. + * + * Check that the processor is paused on an opener tag. + * + */ + if ( + WP_HTML_Tag_Processor::STATE_MATCHED_TAG !== $this->parser_state || + $this->is_tag_closer() + ) { + return false; + } + + // Set position of the opener tag. + $this->set_bookmark( 'opener_tag' ); + + /* + * This is a best-effort guess to visit the closer tag and check it exists. + * In the future, this code should rely on the HTML Processor for this kind of operation. + */ + $tag_name = $this->get_tag(); + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + return false; + } + + // Set position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + + // Get opener and closer tag bookmarks. + $opener_tag_bookmark = $this->bookmarks['opener_tag']; + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + + // Appends the new content. + $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_opener_tag ] ) { + ++$after_opener_tag; + } + $inner_content_length = $closer_tag_bookmark->start - $after_opener_tag; + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_opener_tag, $inner_content_length, $new_content ); + return true; + } + + /** + * Add a new HTML element after the current tag. + * + * @param string $new_element New HTML element to append after the current tag. + */ + public function gutenberg_append_element_after_tag( $new_element ) { + $tag_name = $this->get_tag(); + $this->set_bookmark( 'current_tag' ); + // Visit the closing tag if exists. + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + $this->seek( 'current_tag' ); + $this->release_bookmark( 'current_tag' ); + } + + // Get position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_closer_tag ] ) { + ++$after_closer_tag; + } + + // Append the new element. + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closer_tag, 0, $new_element ); + $this->release_bookmark( 'closer_tag' ); + } + + /** + * Remove the current tag element. + * + * @return bool Whether the element was properly removed. + */ + public function gutenberg_remove_current_tag_element() { + // Get position of the opener tag. + $this->set_bookmark( 'opener_tag' ); + $opener_tag_bookmark = $this->bookmarks['opener_tag']; + + // Visit the closing tag. + $tag_name = $this->get_tag(); + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + $this->release_bookmark( 'opener_tag' ); + return false; + } + + // Get position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + + // Remove the current tag. + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_closer_tag ] ) { + ++$after_closer_tag; + } + $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); + $this->release_bookmark( 'opener_tag' ); + $this->release_bookmark( 'closer_tag' ); + return true; + } + }; + + /* + * For backward compatibility, the logic from the image render needs to be replicated. + * This is because the block attributes can't be modified with the binding value with `render_block_data` filter, + * as it doesn't have access to the block instance. + */ + if ( $block_reader->next_tag( 'figure' ) ) { + $block_reader->set_bookmark( 'figure' ); + } + + $new_value = wp_kses_post( $source_value ); + + if ( $block_reader->next_tag( 'figcaption' ) ) { + if ( empty( $new_value ) ) { + $block_reader->gutenberg_remove_current_tag_element(); + } else { + $block_reader->gutenberg_set_inner_text( $new_value ); + } + } else { + $block_reader->seek( 'figure' ); + if ( ! $block_reader->next_tag( 'a' ) ) { + $block_reader->seek( 'figure' ); + $block_reader->next_tag( 'img' ); + } + $block_reader->gutenberg_append_element_after_tag( '
' . $new_value . '
' ); + } + return $block_reader->get_updated_html(); + } + + $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[ $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' ); + + // 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 ) . ""; + $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()}"; + $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[ $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; + } + } + + /** + * Check if the parsed block is supported by block bindings and it includes the bindings attribute. + * + * @param array $parsed_block The full block, including name and attributes. + */ + function gutenberg_is_valid_block_for_block_bindings( $parsed_block ) { + $supported_blocks = array( + 'core/paragraph', + 'core/heading', + 'core/image', + 'core/button', + ); + + // Check if the block is supported. + if ( + ! in_array( $parsed_block['blockName'], $supported_blocks, true ) || + empty( $parsed_block['attrs']['metadata']['bindings'] ) || + ! is_array( $parsed_block['attrs']['metadata']['bindings'] ) + ) { + return false; + } + + return true; + } + + /** + * Check if the binding created is valid. + * + * @param array $parsed_block The full block, including name and attributes. + * @param string $attribute_name The attribute name being processed. + * @param array $block_binding The block binding configuration. + */ + function gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $block_binding ) { + // Check if it is a valid block. + if ( ! gutenberg_is_valid_block_for_block_bindings( $parsed_block ) ) { + return false; + } + + $supported_block_attrs = array( + 'core/paragraph' => array( 'content' ), + 'core/heading' => array( 'content' ), + 'core/image' => array( 'id', 'url', 'title', 'alt', 'caption' ), + 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), + ); + + // Check if the attribute is not in the supported list. + if ( ! in_array( $attribute_name, $supported_block_attrs[ $parsed_block['blockName'] ], true ) ) { + return false; + } + // Check if no source is provided, or that source is not registered. + if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) || null === get_block_bindings_source( $block_binding['source'] ) ) { + return false; + } + + return true; + } + + /** + * Process the block bindings attribute. + * + * @param string $block_content Block Content. + * @param array $parsed_block The full block, including name and attributes. + * @param WP_Block $block_instance The block instance. + * @return string Block content with the bind applied. + */ + function gutenberg_process_block_bindings( $block_content, $parsed_block, $block_instance ) { + if ( ! gutenberg_is_valid_block_for_block_bindings( $parsed_block ) ) { + return $block_content; + } + $modified_block_content = $block_content; + foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) { + if ( ! gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $block_binding ) ) { + continue; + } + + $block_binding_source = get_block_bindings_source( $block_binding['source'] ); + $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 the value is not null, process the HTML based on the block and the attribute. + if ( ! is_null( $source_value ) ) { + $modified_block_content = gutenberg_block_bindings_replace_html( $modified_block_content, $block_instance->name, $attribute_name, $source_value ); + } + } + + return $modified_block_content; + } + + add_filter( 'render_block', 'gutenberg_process_block_bindings', 20, 3 ); +} From 0fe9b31243b0095decc75470d679480a3ce908fc Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 26/39] Only process image caption in 6.6 compat --- lib/compat/wordpress-6.6/blocks.php | 474 ++++++++++------------------ 1 file changed, 162 insertions(+), 312 deletions(-) diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index c89041d4b25b74..cd4f783064a566 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -48,7 +48,7 @@ function gutenberg_replace_pattern_override_default_binding( $parsed_block ) { // Only process block bindings if they are not processed in core. if ( ! is_wp_version_compatible( '6.6' ) ) { /** - * Depending on the block attribute name, replace its value in the HTML based on the value provided. + * Replace the caption value in the HTML based on the binding value. * * @param string $block_content Block Content. * @param string $block_name The name of the block to process. @@ -56,329 +56,181 @@ function gutenberg_replace_pattern_override_default_binding( $parsed_block ) { * @param mixed $source_value The value used to replace in the HTML. * @return string The modified block content. */ - function gutenberg_block_bindings_replace_html( $block_content, $block_name, 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; - } - - // Depending on the attribute source, the processing will be different. - switch ( $block_type->attributes[ $attribute_name ]['source'] ) { - case 'html': - case 'rich-text': - if ( 'core/image' === $block_name && 'caption' === $attribute_name ) { - // Create private anonymous class until the HTML API provides `set_inner_html` method. - $block_reader = new class($block_content) extends WP_HTML_Tag_Processor{ - /** - * THESE METHODS ARE A TEMPORARY SOLUTION IN CORE NOT TO BE EMULATED. - * IT IS A TEMPORARY SOLUTION THAT JUST WORKS FOR THIS SPECIFIC - * USE CASE UNTIL THE HTML PROCESSOR PROVIDES ITS OWN METHOD. - */ - - /** - * Replace the inner text of an HTML with the passed content. - * - * @param string $new_content New text to insert in the HTML element. - * @return bool Whether the inner text was properly replaced. - */ - public function gutenberg_set_inner_text( $new_content ) { - /* - * THIS IS A STOP-GAP MEASURE NOT TO BE EMULATED. - * - * Check that the processor is paused on an opener tag. - * - */ - if ( - WP_HTML_Tag_Processor::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_tag_closer() - ) { - return false; - } - - // Set position of the opener tag. - $this->set_bookmark( 'opener_tag' ); - - /* - * This is a best-effort guess to visit the closer tag and check it exists. - * In the future, this code should rely on the HTML Processor for this kind of operation. - */ - $tag_name = $this->get_tag(); - if ( ! $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) || ! $this->is_tag_closer() ) { - return false; - } - - // Set position of the closer tag. - $this->set_bookmark( 'closer_tag' ); - - // Get opener and closer tag bookmarks. - $opener_tag_bookmark = $this->bookmarks['opener_tag']; - $closer_tag_bookmark = $this->bookmarks['closer_tag']; - - // Appends the new content. - $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; - /* - * There was a bug in the HTML Processor token length fixed after 6.5. - * This check is needed to add compatibility for that. - * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 - */ - if ( '>' === $this->html[ $after_opener_tag ] ) { - ++$after_opener_tag; - } - $inner_content_length = $closer_tag_bookmark->start - $after_opener_tag; - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_opener_tag, $inner_content_length, $new_content ); - return true; - } - - /** - * Add a new HTML element after the current tag. - * - * @param string $new_element New HTML element to append after the current tag. - */ - public function gutenberg_append_element_after_tag( $new_element ) { - $tag_name = $this->get_tag(); - $this->set_bookmark( 'current_tag' ); - // Visit the closing tag if exists. - if ( ! $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) || ! $this->is_tag_closer() ) { - $this->seek( 'current_tag' ); - $this->release_bookmark( 'current_tag' ); - } - - // Get position of the closer tag. - $this->set_bookmark( 'closer_tag' ); - $closer_tag_bookmark = $this->bookmarks['closer_tag']; - $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; - /* - * There was a bug in the HTML Processor token length fixed after 6.5. - * This check is needed to add compatibility for that. - * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 - */ - if ( '>' === $this->html[ $after_closer_tag ] ) { - ++$after_closer_tag; - } - - // Append the new element. - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closer_tag, 0, $new_element ); - $this->release_bookmark( 'closer_tag' ); - } - - /** - * Remove the current tag element. - * - * @return bool Whether the element was properly removed. - */ - public function gutenberg_remove_current_tag_element() { - // Get position of the opener tag. - $this->set_bookmark( 'opener_tag' ); - $opener_tag_bookmark = $this->bookmarks['opener_tag']; - - // Visit the closing tag. - $tag_name = $this->get_tag(); - if ( ! $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) || ! $this->is_tag_closer() ) { - $this->release_bookmark( 'opener_tag' ); - return false; - } - - // Get position of the closer tag. - $this->set_bookmark( 'closer_tag' ); - $closer_tag_bookmark = $this->bookmarks['closer_tag']; - - // Remove the current tag. - $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; - /* - * There was a bug in the HTML Processor token length fixed after 6.5. - * This check is needed to add compatibility for that. - * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 - */ - if ( '>' === $this->html[ $after_closer_tag ] ) { - ++$after_closer_tag; - } - $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); - $this->release_bookmark( 'opener_tag' ); - $this->release_bookmark( 'closer_tag' ); - return true; - } - }; - - /* - * For backward compatibility, the logic from the image render needs to be replicated. - * This is because the block attributes can't be modified with the binding value with `render_block_data` filter, - * as it doesn't have access to the block instance. - */ - if ( $block_reader->next_tag( 'figure' ) ) { - $block_reader->set_bookmark( 'figure' ); - } - - $new_value = wp_kses_post( $source_value ); - - if ( $block_reader->next_tag( 'figcaption' ) ) { - if ( empty( $new_value ) ) { - $block_reader->gutenberg_remove_current_tag_element(); - } else { - $block_reader->gutenberg_set_inner_text( $new_value ); - } - } else { - $block_reader->seek( 'figure' ); - if ( ! $block_reader->next_tag( 'a' ) ) { - $block_reader->seek( 'figure' ); - $block_reader->next_tag( 'img' ); - } - $block_reader->gutenberg_append_element_after_tag( '
' . $new_value . '
' ); - } - return $block_reader->get_updated_html(); + function gutenberg_block_bindings_replace_caption( $block_content, $block_name, string $attribute_name, $source_value ) { + // Create private anonymous class until the HTML API provides `set_inner_html` method. + $block_reader = new class($block_content) extends WP_HTML_Tag_Processor{ + /** + * THESE METHODS ARE A TEMPORARY SOLUTION NOT TO BE EMULATED. + * IT IS A TEMPORARY SOLUTION THAT JUST WORKS FOR THIS SPECIFIC + * USE CASE UNTIL THE HTML PROCESSOR PROVIDES ITS OWN METHOD. + */ + + /** + * Replace the inner text of an HTML with the passed content. + * + * @param string $new_content New text to insert in the HTML element. + * @return bool Whether the inner text was properly replaced. + */ + public function gutenberg_set_inner_text( $new_content ) { + /* + * THIS IS A STOP-GAP MEASURE NOT TO BE EMULATED. + * + * Check that the processor is paused on an opener tag. + * + */ + if ( + WP_HTML_Tag_Processor::STATE_MATCHED_TAG !== $this->parser_state || + $this->is_tag_closer() + ) { + return false; } - $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[ $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' ); + // Set position of the opener tag. + $this->set_bookmark( 'opener_tag' ); - // 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 ); - } + /* + * This is a best-effort guess to visit the closer tag and check it exists. + * In the future, this code should rely on the HTML Processor for this kind of operation. + */ + $tag_name = $this->get_tag(); + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + return false; } - 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 ) . ""; - $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()}"; - $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' ); - } + // Set position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + + // Get opener and closer tag bookmarks. + $opener_tag_bookmark = $this->bookmarks['opener_tag']; + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + + // Appends the new content. + $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_opener_tag ] ) { + ++$after_opener_tag; } - $block_reader->release_bookmark( 'iterate-selectors' ); - return $block_content; + $inner_content_length = $closer_tag_bookmark->start - $after_opener_tag; + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_opener_tag, $inner_content_length, $new_content ); + return true; + } - case 'attribute': - $amended_content = new WP_HTML_Tag_Processor( $block_content ); - if ( ! $amended_content->next_tag( + /** + * Add a new HTML element after the current tag. + * + * @param string $new_element New HTML element to append after the current tag. + */ + public function gutenberg_append_element_after_tag( $new_element ) { + $tag_name = $this->get_tag(); + $this->set_bookmark( 'current_tag' ); + // Visit the closing tag if exists. + if ( ! $this->next_tag( array( - // TODO: build the query from CSS selector. - 'tag_name' => $block_type->attributes[ $attribute_name ]['selector'], + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', ) - ) ) { - return $block_content; + ) || ! $this->is_tag_closer() ) { + $this->seek( 'current_tag' ); + $this->release_bookmark( 'current_tag' ); } - $amended_content->set_attribute( $block_type->attributes[ $attribute_name ]['attribute'], $source_value ); - return $amended_content->get_updated_html(); - default: - return $block_content; - } - } - - /** - * Check if the parsed block is supported by block bindings and it includes the bindings attribute. - * - * @param array $parsed_block The full block, including name and attributes. - */ - function gutenberg_is_valid_block_for_block_bindings( $parsed_block ) { - $supported_blocks = array( - 'core/paragraph', - 'core/heading', - 'core/image', - 'core/button', - ); + // Get position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_closer_tag ] ) { + ++$after_closer_tag; + } - // Check if the block is supported. - if ( - ! in_array( $parsed_block['blockName'], $supported_blocks, true ) || - empty( $parsed_block['attrs']['metadata']['bindings'] ) || - ! is_array( $parsed_block['attrs']['metadata']['bindings'] ) - ) { - return false; - } + // Append the new element. + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closer_tag, 0, $new_element ); + $this->release_bookmark( 'closer_tag' ); + } - return true; - } + /** + * Remove the current tag element. + * + * @return bool Whether the element was properly removed. + */ + public function gutenberg_remove_current_tag_element() { + // Get position of the opener tag. + $this->set_bookmark( 'opener_tag' ); + $opener_tag_bookmark = $this->bookmarks['opener_tag']; + + // Visit the closing tag. + $tag_name = $this->get_tag(); + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + $this->release_bookmark( 'opener_tag' ); + return false; + } - /** - * Check if the binding created is valid. - * - * @param array $parsed_block The full block, including name and attributes. - * @param string $attribute_name The attribute name being processed. - * @param array $block_binding The block binding configuration. - */ - function gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $block_binding ) { - // Check if it is a valid block. - if ( ! gutenberg_is_valid_block_for_block_bindings( $parsed_block ) ) { - return false; + // Get position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + + // Remove the current tag. + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_closer_tag ] ) { + ++$after_closer_tag; + } + $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); + $this->release_bookmark( 'opener_tag' ); + $this->release_bookmark( 'closer_tag' ); + return true; + } + }; + + /* + * For backward compatibility, the logic from the image render needs to be replicated. + * This is because the block attributes can't be modified with the binding value with `render_block_data` filter, + * as it doesn't have access to the block instance. + */ + if ( $block_reader->next_tag( 'figure' ) ) { + $block_reader->set_bookmark( 'figure' ); } - $supported_block_attrs = array( - 'core/paragraph' => array( 'content' ), - 'core/heading' => array( 'content' ), - 'core/image' => array( 'id', 'url', 'title', 'alt', 'caption' ), - 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), - ); + $new_value = wp_kses_post( $source_value ); - // Check if the attribute is not in the supported list. - if ( ! in_array( $attribute_name, $supported_block_attrs[ $parsed_block['blockName'] ], true ) ) { - return false; - } - // Check if no source is provided, or that source is not registered. - if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) || null === get_block_bindings_source( $block_binding['source'] ) ) { - return false; + if ( $block_reader->next_tag( 'figcaption' ) ) { + if ( empty( $new_value ) ) { + $block_reader->gutenberg_remove_current_tag_element(); + } else { + $block_reader->gutenberg_set_inner_text( $new_value ); + } + } else { + $block_reader->seek( 'figure' ); + if ( ! $block_reader->next_tag( 'a' ) ) { + $block_reader->seek( 'figure' ); + $block_reader->next_tag( 'img' ); + } + $block_reader->gutenberg_append_element_after_tag( '
' . $new_value . '
' ); } - - return true; + return $block_reader->get_updated_html(); } /** @@ -390,12 +242,10 @@ function gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $bloc * @return string Block content with the bind applied. */ function gutenberg_process_block_bindings( $block_content, $parsed_block, $block_instance ) { - if ( ! gutenberg_is_valid_block_for_block_bindings( $parsed_block ) ) { - return $block_content; - } $modified_block_content = $block_content; foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) { - if ( ! gutenberg_is_valid_block_binding( $parsed_block, $attribute_name, $block_binding ) ) { + // Only process the caption for compatibility. + if ( 'caption' !== $attribute_name ) { continue; } @@ -405,12 +255,12 @@ function gutenberg_process_block_bindings( $block_content, $parsed_block, $block // If the value is not null, process the HTML based on the block and the attribute. if ( ! is_null( $source_value ) ) { - $modified_block_content = gutenberg_block_bindings_replace_html( $modified_block_content, $block_instance->name, $attribute_name, $source_value ); + $modified_block_content = gutenberg_block_bindings_replace_caption( $modified_block_content, $block_instance->name, $attribute_name, $source_value ); } } return $modified_block_content; } - add_filter( 'render_block', 'gutenberg_process_block_bindings', 20, 3 ); + add_filter( 'render_block_core/image', 'gutenberg_process_block_bindings', 20, 3 ); } From 435058e557850519c4c25f5e7dfcce3d0369fe0e Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 27/39] Remove extra spaces --- lib/compat/wordpress-6.6/blocks.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index cd4f783064a566..99873f7b05cd4c 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -214,7 +214,7 @@ public function gutenberg_remove_current_tag_element() { $block_reader->set_bookmark( 'figure' ); } - $new_value = wp_kses_post( $source_value ); + $new_value = wp_kses_post( $source_value ); if ( $block_reader->next_tag( 'figcaption' ) ) { if ( empty( $new_value ) ) { @@ -230,7 +230,8 @@ public function gutenberg_remove_current_tag_element() { } $block_reader->gutenberg_append_element_after_tag( '
' . $new_value . '
' ); } - return $block_reader->get_updated_html(); + + return $block_reader->get_updated_html(); } /** From 895110a8b95abbcf90be8055a434561cfc41245e Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 28/39] Don't grab bookmarks until the end --- lib/compat/wordpress-6.6/blocks.php | 13 ++++++++----- packages/block-library/src/image/index.php | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index 99873f7b05cd4c..9093ce39914675 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -167,9 +167,8 @@ public function gutenberg_append_element_after_tag( $new_element ) { * @return bool Whether the element was properly removed. */ public function gutenberg_remove_current_tag_element() { - // Get position of the opener tag. + // Set position of the opener tag. $this->set_bookmark( 'opener_tag' ); - $opener_tag_bookmark = $this->bookmarks['opener_tag']; // Visit the closing tag. $tag_name = $this->get_tag(); @@ -183,11 +182,13 @@ public function gutenberg_remove_current_tag_element() { return false; } - // Get position of the closer tag. + // Set position of the closer tag. $this->set_bookmark( 'closer_tag' ); + + // Get position of the tags. + $opener_tag_bookmark = $this->bookmarks['opener_tag']; $closer_tag_bookmark = $this->bookmarks['closer_tag']; - // Remove the current tag. $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; /* * There was a bug in the HTML Processor token length fixed after 6.5. @@ -197,7 +198,9 @@ public function gutenberg_remove_current_tag_element() { if ( '>' === $this->html[ $after_closer_tag ] ) { ++$after_closer_tag; } - $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; + $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; + + // Remove the current tag. $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); $this->release_bookmark( 'opener_tag' ); $this->release_bookmark( 'closer_tag' ); diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 2ff009e826e8d5..7949f9bc85a6eb 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -73,9 +73,8 @@ public function append_element_after_tag( $new_element ) { * @return bool Whether the element was properly removed. */ public function remove_current_tag_element() { - // Get position of the opener tag. + // Set position of the opener tag. $this->set_bookmark( 'opener_tag' ); - $opener_tag_bookmark = $this->bookmarks['opener_tag']; // Visit the closing tag. $tag_name = $this->get_tag(); @@ -89,11 +88,13 @@ public function remove_current_tag_element() { return false; } - // Get position of the closer tag. + // Set position of the closer tag. $this->set_bookmark( 'closer_tag' ); + + // Get tag positions. + $opener_tag_bookmark = $this->bookmarks['opener_tag']; $closer_tag_bookmark = $this->bookmarks['closer_tag']; - // Remove the current tag. $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; /* * There was a bug in the HTML Processor token length fixed after 6.5. @@ -104,7 +105,9 @@ public function remove_current_tag_element() { if ( '>' === $this->html[ $after_closer_tag ] ) { ++$after_closer_tag; } - $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; + $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; + + // Remove the current tag. $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); $this->release_bookmark( 'opener_tag' ); $this->release_bookmark( 'closer_tag' ); From 3e183b7cc684da11404396cd1328cd197df0167d Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 29/39] Remove release_bookmark --- lib/compat/wordpress-6.6/blocks.php | 4 ---- packages/block-library/src/image/index.php | 4 ---- 2 files changed, 8 deletions(-) diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index 9093ce39914675..a7367543426fc0 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -158,7 +158,6 @@ public function gutenberg_append_element_after_tag( $new_element ) { // Append the new element. $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closer_tag, 0, $new_element ); - $this->release_bookmark( 'closer_tag' ); } /** @@ -178,7 +177,6 @@ public function gutenberg_remove_current_tag_element() { 'tag_closers' => 'visit', ) ) || ! $this->is_tag_closer() ) { - $this->release_bookmark( 'opener_tag' ); return false; } @@ -202,8 +200,6 @@ public function gutenberg_remove_current_tag_element() { // Remove the current tag. $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); - $this->release_bookmark( 'opener_tag' ); - $this->release_bookmark( 'closer_tag' ); return true; } }; diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 7949f9bc85a6eb..22290d8ce61e03 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -64,7 +64,6 @@ public function append_element_after_tag( $new_element ) { // Append the new element. $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closer_tag, 0, $new_element ); - $this->release_bookmark( 'closer_tag' ); } /** @@ -84,7 +83,6 @@ public function remove_current_tag_element() { 'tag_closers' => 'visit', ) ) || ! $this->is_tag_closer() ) { - $this->release_bookmark( 'opener_tag' ); return false; } @@ -109,8 +107,6 @@ public function remove_current_tag_element() { // Remove the current tag. $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); - $this->release_bookmark( 'opener_tag' ); - $this->release_bookmark( 'closer_tag' ); return true; } }; From a2b825cb6b9e2266867ee6674df79f8b36dd35a3 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 30/39] Add warning --- packages/block-library/src/image/index.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 22290d8ce61e03..2362de788e16d4 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -29,6 +29,12 @@ function render_block_core_image( $attributes, $content, $block ) { * @phpcs:disable Gutenberg.NamingConventions.ValidBlockLibraryFunctionName.FunctionNameInvalid, Gutenberg.Commenting.SinceTag.MissingMethodSinceTag */ $p = new class( $content ) extends WP_HTML_Tag_Processor { + /** + * THESE METHODS ARE A TEMPORARY SOLUTION NOT TO BE EMULATED. + * IT IS A TEMPORARY SOLUTION THAT JUST WORKS FOR THIS SPECIFIC + * USE CASE UNTIL THE HTML PROCESSOR PROVIDES ITS OWN METHOD. + */ + /** * Add a new HTML element after the current tag. * From 77d71163c603a34054b7cd96db065cd72e16076f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 31/39] Make filter caption specific --- lib/compat/wordpress-6.6/blocks.php | 30 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index a7367543426fc0..072f933859e475 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -241,26 +241,24 @@ public function gutenberg_remove_current_tag_element() { * @param WP_Block $block_instance The block instance. * @return string Block content with the bind applied. */ - function gutenberg_process_block_bindings( $block_content, $parsed_block, $block_instance ) { - $modified_block_content = $block_content; - foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) { - // Only process the caption for compatibility. - if ( 'caption' !== $attribute_name ) { - continue; - } + function gutenberg_process_image_caption_binding( $block_content, $parsed_block, $block_instance ) { + if ( ! isset( $parsed_block['attrs']['metadata']['bindings']['caption'] ) ) { + return $block_content; + } - $block_binding_source = get_block_bindings_source( $block_binding['source'] ); - $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 ); + $caption_binding = $parsed_block['attrs']['metadata']['bindings']['caption']; - // If the value is not null, process the HTML based on the block and the attribute. - if ( ! is_null( $source_value ) ) { - $modified_block_content = gutenberg_block_bindings_replace_caption( $modified_block_content, $block_instance->name, $attribute_name, $source_value ); - } + $caption_binding_source = get_block_bindings_source( $caption_binding['source'] ); + $source_args = ! empty( $caption_binding['args'] ) && is_array( $caption_binding['args'] ) ? $caption_binding['args'] : array(); + $source_value = $caption_binding_source->get_value( $source_args, $block_instance, 'caption' ); + + // If the value is not null, process the HTML based on the block and the attribute. + if ( is_null( $source_value ) ) { + return $block_content; } - return $modified_block_content; + return gutenberg_block_bindings_replace_caption( $block_content, $block_instance->name, 'caption', $source_value ); } - add_filter( 'render_block_core/image', 'gutenberg_process_block_bindings', 20, 3 ); + add_filter( 'render_block_core/image', 'gutenberg_process_image_caption_binding', 20, 3 ); } From dc568a96f8c4a56b69158196d9a84f50c63bcc3e Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 32/39] Only process caption in WP 6.5 --- lib/compat/wordpress-6.6/blocks.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index 072f933859e475..47beeb7f896bc3 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -45,8 +45,8 @@ function gutenberg_replace_pattern_override_default_binding( $parsed_block ) { add_filter( 'render_block_data', 'gutenberg_replace_pattern_override_default_binding', 10, 1 ); -// Only process block bindings if they are not processed in core. -if ( ! is_wp_version_compatible( '6.6' ) ) { +// Only process caption in WordPress 6.5. +if ( ! is_wp_version_compatible( '6.6' ) && is_wp_version_compatible( '6.5' ) ) { /** * Replace the caption value in the HTML based on the binding value. * From 6eeb7c2cf61dff2a421c25d5396832f1ab3ffbc4 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 33/39] Change conditional --- lib/compat/wordpress-6.6/blocks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index 47beeb7f896bc3..82d961dccc5bae 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -116,7 +116,7 @@ public function gutenberg_set_inner_text( $new_content ) { * This check is needed to add compatibility for that. * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 */ - if ( '>' === $this->html[ $after_opener_tag ] ) { + if ( '>' !== $this->html[ $after_opener_tag - 1 ] ) { ++$after_opener_tag; } $inner_content_length = $closer_tag_bookmark->start - $after_opener_tag; From 0e3b174cfe24400f75720162e63f1487f5f56b0d Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 34/39] Add support for 6.4 --- lib/compat/wordpress-6.6/blocks.php | 39 ++++++++++++++++++----------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index 82d961dccc5bae..3763b92c1b2555 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -45,8 +45,8 @@ function gutenberg_replace_pattern_override_default_binding( $parsed_block ) { add_filter( 'render_block_data', 'gutenberg_replace_pattern_override_default_binding', 10, 1 ); -// Only process caption in WordPress 6.5. -if ( ! is_wp_version_compatible( '6.6' ) && is_wp_version_compatible( '6.5' ) ) { +// Only process caption for compat versions of WordPress. +if ( ! is_wp_version_compatible( '6.6' ) ) { /** * Replace the caption value in the HTML based on the binding value. * @@ -72,16 +72,8 @@ function gutenberg_block_bindings_replace_caption( $block_content, $block_name, * @return bool Whether the inner text was properly replaced. */ public function gutenberg_set_inner_text( $new_content ) { - /* - * THIS IS A STOP-GAP MEASURE NOT TO BE EMULATED. - * - * Check that the processor is paused on an opener tag. - * - */ - if ( - WP_HTML_Tag_Processor::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_tag_closer() - ) { + // Check that the processor is paused on an opener tag. + if ( $this->is_tag_closer() ) { return false; } @@ -110,13 +102,19 @@ public function gutenberg_set_inner_text( $new_content ) { $closer_tag_bookmark = $this->bookmarks['closer_tag']; // Appends the new content. + // Compat for 6.4, where bookmarks and WP_HTML_Text_Replacement are different. + if ( ! empty( $opener_tag_bookmark->end ) ) { + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->end + 1, $closer_tag_bookmark->start, $new_content ); + return true; + } + $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; /* * There was a bug in the HTML Processor token length fixed after 6.5. * This check is needed to add compatibility for that. * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 */ - if ( '>' !== $this->html[ $after_opener_tag - 1 ] ) { + if ( '>' === $this->html[ $after_opener_tag ] ) { ++$after_opener_tag; } $inner_content_length = $closer_tag_bookmark->start - $after_opener_tag; @@ -128,6 +126,7 @@ public function gutenberg_set_inner_text( $new_content ) { * Add a new HTML element after the current tag. * * @param string $new_element New HTML element to append after the current tag. + * @return bool Whether the element was properly appended. */ public function gutenberg_append_element_after_tag( $new_element ) { $tag_name = $this->get_tag(); @@ -146,7 +145,12 @@ public function gutenberg_append_element_after_tag( $new_element ) { // Get position of the closer tag. $this->set_bookmark( 'closer_tag' ); $closer_tag_bookmark = $this->bookmarks['closer_tag']; - $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + // Compat for 6.4, where bookmarks and WP_HTML_Text_Replacement are different. + if ( ! empty( $closer_tag_bookmark->end ) ) { + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $closer_tag_bookmark->end + 1, $closer_tag_bookmark->end + 1, $new_element ); + return true; + } + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; /* * There was a bug in the HTML Processor token length fixed after 6.5. * This check is needed to add compatibility for that. @@ -158,6 +162,7 @@ public function gutenberg_append_element_after_tag( $new_element ) { // Append the new element. $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closer_tag, 0, $new_element ); + return true; } /** @@ -187,6 +192,12 @@ public function gutenberg_remove_current_tag_element() { $opener_tag_bookmark = $this->bookmarks['opener_tag']; $closer_tag_bookmark = $this->bookmarks['closer_tag']; + // Compat for 6.4, where bookmarks and WP_HTML_Text_Replacement are different. + if ( ! empty( $closer_tag_bookmark->end ) ) { + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $closer_tag_bookmark->end + 1, '' ); + return true; + } + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; /* * There was a bug in the HTML Processor token length fixed after 6.5. From 7311680db43119f2808082afc8cb171a6bb358bb Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 35/39] Clarify methods only apply to figcaption --- lib/compat/wordpress-6.6/blocks.php | 36 +++++++++++++--------- packages/block-library/src/image/index.php | 28 +++++++++++------ 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index 3763b92c1b2555..f365456dea936a 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -66,14 +66,14 @@ function gutenberg_block_bindings_replace_caption( $block_content, $block_name, */ /** - * Replace the inner text of an HTML with the passed content. + * Replace the inner content of a figcaption element with the passed content. * - * @param string $new_content New text to insert in the HTML element. - * @return bool Whether the inner text was properly replaced. + * @param string $new_content New content to insert in the figcaption element. + * @return bool Whether the inner content was properly replaced. */ - public function gutenberg_set_inner_text( $new_content ) { + public function gutenberg_set_content_between_figcaption_balanced_tags( $new_content ) { // Check that the processor is paused on an opener tag. - if ( $this->is_tag_closer() ) { + if ( $this->is_tag_closer() || 'FIGCAPTION' !== $this->get_tag() ) { return false; } @@ -123,13 +123,17 @@ public function gutenberg_set_inner_text( $new_content ) { } /** - * Add a new HTML element after the current tag. + * Add a new figcaption element after the `img` or `a` tag. * - * @param string $new_element New HTML element to append after the current tag. + * @param string $new_element New figcaption element to append after the tag. * @return bool Whether the element was properly appended. */ - public function gutenberg_append_element_after_tag( $new_element ) { + public function gutenberg_append_figcaption_element_after_tag( $new_element ) { $tag_name = $this->get_tag(); + // Ensure figcaption is only added in the correct place. + if ( 'IMG' !== $tag_name && 'A' !== $tag_name ) { + return false; + } $this->set_bookmark( 'current_tag' ); // Visit the closing tag if exists. if ( ! $this->next_tag( @@ -166,16 +170,20 @@ public function gutenberg_append_element_after_tag( $new_element ) { } /** - * Remove the current tag element. + * Remove the current figcaption tag element. * * @return bool Whether the element was properly removed. */ - public function gutenberg_remove_current_tag_element() { + public function gutenberg_remove_figcaption_tag_element() { + $tag_name = $this->get_tag(); + // Ensure only figcaption is removed. + if ( 'FIGCAPTION' !== $tag_name ) { + return false; + } // Set position of the opener tag. $this->set_bookmark( 'opener_tag' ); // Visit the closing tag. - $tag_name = $this->get_tag(); if ( ! $this->next_tag( array( 'tag_name' => $tag_name, @@ -228,9 +236,9 @@ public function gutenberg_remove_current_tag_element() { if ( $block_reader->next_tag( 'figcaption' ) ) { if ( empty( $new_value ) ) { - $block_reader->gutenberg_remove_current_tag_element(); + $block_reader->gutenberg_remove_figcaption_tag_element(); } else { - $block_reader->gutenberg_set_inner_text( $new_value ); + $block_reader->gutenberg_set_content_between_figcaption_balanced_tags( $new_value ); } } else { $block_reader->seek( 'figure' ); @@ -238,7 +246,7 @@ public function gutenberg_remove_current_tag_element() { $block_reader->seek( 'figure' ); $block_reader->next_tag( 'img' ); } - $block_reader->gutenberg_append_element_after_tag( '
' . $new_value . '
' ); + $block_reader->gutenberg_append_figcaption_element_after_tag( '
' . $new_value . '
' ); } return $block_reader->get_updated_html(); diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 2362de788e16d4..5e1e2e80f811d3 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -36,12 +36,17 @@ function render_block_core_image( $attributes, $content, $block ) { */ /** - * Add a new HTML element after the current tag. + * Add a new figcaption element after the `img` or `a` tag. * - * @param string $new_element New HTML element to append after the current tag. + * @param string $new_element New figcaption element to append after the tag. + * @return bool Whether the element was properly appended. */ - public function append_element_after_tag( $new_element ) { + public function append_figcaption_element_after_tag( $new_element ) { $tag_name = $this->get_tag(); + // Ensure figcaption is only added in the correct place. + if ( 'IMG' !== $tag_name && 'A' !== $tag_name ) { + return false; + } $this->set_bookmark( 'current_tag' ); // Visit the closing tag if exists. if ( ! $this->next_tag( @@ -70,19 +75,24 @@ public function append_element_after_tag( $new_element ) { // Append the new element. $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closer_tag, 0, $new_element ); + return true; } /** - * Remove the current tag element. + * Remove the current figcaption tag element. * * @return bool Whether the element was properly removed. */ - public function remove_current_tag_element() { + public function remove_figcaption_tag_element() { + $tag_name = $this->get_tag(); + // Ensure only figcaption is removed. + if ( 'FIGCAPTION' !== $tag_name ) { + return false; + } // Set position of the opener tag. $this->set_bookmark( 'opener_tag' ); // Visit the closing tag. - $tag_name = $this->get_tag(); if ( ! $this->next_tag( array( 'tag_name' => $tag_name, @@ -95,7 +105,7 @@ public function remove_current_tag_element() { // Set position of the closer tag. $this->set_bookmark( 'closer_tag' ); - // Get tag positions. + // Get position of the tags. $opener_tag_bookmark = $this->bookmarks['opener_tag']; $closer_tag_bookmark = $this->bookmarks['closer_tag']; @@ -180,7 +190,7 @@ public function remove_current_tag_element() { if ( $p->next_tag( 'figcaption' ) ) { // Remove `
` if exists and caption attribute exists but it is empty. if ( isset( $attributes['caption'] ) && strlen( $attributes['caption'] ) === 0 ) { - $p->remove_current_tag_element(); + $p->remove_figcaption_tag_element(); } } else { // Add caption if it doesn't exist and the caption is not empty. @@ -191,7 +201,7 @@ public function remove_current_tag_element() { $p->seek( 'figure' ); $p->next_tag( 'img' ); } - $p->append_element_after_tag( '
' . $attributes['caption'] . '
' ); + $p->append_figcaption_element_after_tag( '
' . $attributes['caption'] . '
' ); } } $p->release_bookmark( 'figure' ); From d38d6352c312ec7353e510f00d4e7f9218f19905 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 36/39] Remove check to always run caption processing --- lib/compat/wordpress-6.6/blocks.php | 413 ++++++++++++++-------------- 1 file changed, 205 insertions(+), 208 deletions(-) diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php index f365456dea936a..92855c92e08a33 100644 --- a/lib/compat/wordpress-6.6/blocks.php +++ b/lib/compat/wordpress-6.6/blocks.php @@ -45,239 +45,236 @@ function gutenberg_replace_pattern_override_default_binding( $parsed_block ) { add_filter( 'render_block_data', 'gutenberg_replace_pattern_override_default_binding', 10, 1 ); -// Only process caption for compat versions of WordPress. -if ( ! is_wp_version_compatible( '6.6' ) ) { - /** - * Replace the caption value in the HTML based on the binding value. - * - * @param string $block_content Block Content. - * @param string $block_name The name of the block to process. - * @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. - */ - function gutenberg_block_bindings_replace_caption( $block_content, $block_name, string $attribute_name, $source_value ) { - // Create private anonymous class until the HTML API provides `set_inner_html` method. - $block_reader = new class($block_content) extends WP_HTML_Tag_Processor{ - /** - * THESE METHODS ARE A TEMPORARY SOLUTION NOT TO BE EMULATED. - * IT IS A TEMPORARY SOLUTION THAT JUST WORKS FOR THIS SPECIFIC - * USE CASE UNTIL THE HTML PROCESSOR PROVIDES ITS OWN METHOD. - */ +/** + * Replace the caption value in the HTML based on the binding value. + * + * @param string $block_content Block Content. + * @param string $block_name The name of the block to process. + * @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. + */ +function gutenberg_block_bindings_replace_caption( $block_content, $block_name, string $attribute_name, $source_value ) { + // Create private anonymous class until the HTML API provides `set_inner_html` method. + $block_reader = new class($block_content) extends WP_HTML_Tag_Processor{ + /** + * THESE METHODS ARE A TEMPORARY SOLUTION NOT TO BE EMULATED. + * IT IS A TEMPORARY SOLUTION THAT JUST WORKS FOR THIS SPECIFIC + * USE CASE UNTIL THE HTML PROCESSOR PROVIDES ITS OWN METHOD. + */ - /** - * Replace the inner content of a figcaption element with the passed content. - * - * @param string $new_content New content to insert in the figcaption element. - * @return bool Whether the inner content was properly replaced. - */ - public function gutenberg_set_content_between_figcaption_balanced_tags( $new_content ) { - // Check that the processor is paused on an opener tag. - if ( $this->is_tag_closer() || 'FIGCAPTION' !== $this->get_tag() ) { - return false; - } - - // Set position of the opener tag. - $this->set_bookmark( 'opener_tag' ); - - /* - * This is a best-effort guess to visit the closer tag and check it exists. - * In the future, this code should rely on the HTML Processor for this kind of operation. - */ - $tag_name = $this->get_tag(); - if ( ! $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) || ! $this->is_tag_closer() ) { - return false; - } - - // Set position of the closer tag. - $this->set_bookmark( 'closer_tag' ); - - // Get opener and closer tag bookmarks. - $opener_tag_bookmark = $this->bookmarks['opener_tag']; - $closer_tag_bookmark = $this->bookmarks['closer_tag']; - - // Appends the new content. - // Compat for 6.4, where bookmarks and WP_HTML_Text_Replacement are different. - if ( ! empty( $opener_tag_bookmark->end ) ) { - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->end + 1, $closer_tag_bookmark->start, $new_content ); - return true; - } - - $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; - /* - * There was a bug in the HTML Processor token length fixed after 6.5. - * This check is needed to add compatibility for that. - * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 - */ - if ( '>' === $this->html[ $after_opener_tag ] ) { - ++$after_opener_tag; - } - $inner_content_length = $closer_tag_bookmark->start - $after_opener_tag; - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_opener_tag, $inner_content_length, $new_content ); - return true; + /** + * Replace the inner content of a figcaption element with the passed content. + * + * @param string $new_content New content to insert in the figcaption element. + * @return bool Whether the inner content was properly replaced. + */ + public function gutenberg_set_content_between_figcaption_balanced_tags( $new_content ) { + // Check that the processor is paused on an opener tag. + if ( $this->is_tag_closer() || 'FIGCAPTION' !== $this->get_tag() ) { + return false; } - /** - * Add a new figcaption element after the `img` or `a` tag. - * - * @param string $new_element New figcaption element to append after the tag. - * @return bool Whether the element was properly appended. + // Set position of the opener tag. + $this->set_bookmark( 'opener_tag' ); + + /* + * This is a best-effort guess to visit the closer tag and check it exists. + * In the future, this code should rely on the HTML Processor for this kind of operation. */ - public function gutenberg_append_figcaption_element_after_tag( $new_element ) { - $tag_name = $this->get_tag(); - // Ensure figcaption is only added in the correct place. - if ( 'IMG' !== $tag_name && 'A' !== $tag_name ) { - return false; - } - $this->set_bookmark( 'current_tag' ); - // Visit the closing tag if exists. - if ( ! $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) || ! $this->is_tag_closer() ) { - $this->seek( 'current_tag' ); - $this->release_bookmark( 'current_tag' ); - } - - // Get position of the closer tag. - $this->set_bookmark( 'closer_tag' ); - $closer_tag_bookmark = $this->bookmarks['closer_tag']; - // Compat for 6.4, where bookmarks and WP_HTML_Text_Replacement are different. - if ( ! empty( $closer_tag_bookmark->end ) ) { - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $closer_tag_bookmark->end + 1, $closer_tag_bookmark->end + 1, $new_element ); - return true; - } - $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; - /* - * There was a bug in the HTML Processor token length fixed after 6.5. - * This check is needed to add compatibility for that. - * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 - */ - if ( '>' === $this->html[ $after_closer_tag ] ) { - ++$after_closer_tag; - } - - // Append the new element. - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closer_tag, 0, $new_element ); + $tag_name = $this->get_tag(); + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + return false; + } + + // Set position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + + // Get opener and closer tag bookmarks. + $opener_tag_bookmark = $this->bookmarks['opener_tag']; + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + + // Appends the new content. + // Compat for 6.4, where bookmarks and WP_HTML_Text_Replacement are different. + if ( ! empty( $opener_tag_bookmark->end ) ) { + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->end + 1, $closer_tag_bookmark->start, $new_content ); return true; } - /** - * Remove the current figcaption tag element. - * - * @return bool Whether the element was properly removed. + $after_opener_tag = $opener_tag_bookmark->start + $opener_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 */ - public function gutenberg_remove_figcaption_tag_element() { - $tag_name = $this->get_tag(); - // Ensure only figcaption is removed. - if ( 'FIGCAPTION' !== $tag_name ) { - return false; - } - // Set position of the opener tag. - $this->set_bookmark( 'opener_tag' ); - - // Visit the closing tag. - if ( ! $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) || ! $this->is_tag_closer() ) { - return false; - } - - // Set position of the closer tag. - $this->set_bookmark( 'closer_tag' ); - - // Get position of the tags. - $opener_tag_bookmark = $this->bookmarks['opener_tag']; - $closer_tag_bookmark = $this->bookmarks['closer_tag']; - - // Compat for 6.4, where bookmarks and WP_HTML_Text_Replacement are different. - if ( ! empty( $closer_tag_bookmark->end ) ) { - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $closer_tag_bookmark->end + 1, '' ); - return true; - } - - $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; - /* - * There was a bug in the HTML Processor token length fixed after 6.5. - * This check is needed to add compatibility for that. - * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 - */ - if ( '>' === $this->html[ $after_closer_tag ] ) { - ++$after_closer_tag; - } - $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; - - // Remove the current tag. - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); - return true; + if ( '>' === $this->html[ $after_opener_tag ] ) { + ++$after_opener_tag; } - }; + $inner_content_length = $closer_tag_bookmark->start - $after_opener_tag; + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_opener_tag, $inner_content_length, $new_content ); + return true; + } - /* - * For backward compatibility, the logic from the image render needs to be replicated. - * This is because the block attributes can't be modified with the binding value with `render_block_data` filter, - * as it doesn't have access to the block instance. + /** + * Add a new figcaption element after the `img` or `a` tag. + * + * @param string $new_element New figcaption element to append after the tag. + * @return bool Whether the element was properly appended. */ - if ( $block_reader->next_tag( 'figure' ) ) { - $block_reader->set_bookmark( 'figure' ); + public function gutenberg_append_figcaption_element_after_tag( $new_element ) { + $tag_name = $this->get_tag(); + // Ensure figcaption is only added in the correct place. + if ( 'IMG' !== $tag_name && 'A' !== $tag_name ) { + return false; + } + $this->set_bookmark( 'current_tag' ); + // Visit the closing tag if exists. + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + $this->seek( 'current_tag' ); + $this->release_bookmark( 'current_tag' ); + } + + // Get position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + $closer_tag_bookmark = $this->bookmarks['closer_tag']; + // Compat for 6.4, where bookmarks and WP_HTML_Text_Replacement are different. + if ( ! empty( $closer_tag_bookmark->end ) ) { + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $closer_tag_bookmark->end + 1, $closer_tag_bookmark->end + 1, $new_element ); + return true; + } + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_closer_tag ] ) { + ++$after_closer_tag; + } + + // Append the new element. + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $after_closer_tag, 0, $new_element ); + return true; } - $new_value = wp_kses_post( $source_value ); + /** + * Remove the current figcaption tag element. + * + * @return bool Whether the element was properly removed. + */ + public function gutenberg_remove_figcaption_tag_element() { + $tag_name = $this->get_tag(); + // Ensure only figcaption is removed. + if ( 'FIGCAPTION' !== $tag_name ) { + return false; + } + // Set position of the opener tag. + $this->set_bookmark( 'opener_tag' ); + + // Visit the closing tag. + if ( ! $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) || ! $this->is_tag_closer() ) { + return false; + } + + // Set position of the closer tag. + $this->set_bookmark( 'closer_tag' ); + + // Get position of the tags. + $opener_tag_bookmark = $this->bookmarks['opener_tag']; + $closer_tag_bookmark = $this->bookmarks['closer_tag']; - if ( $block_reader->next_tag( 'figcaption' ) ) { - if ( empty( $new_value ) ) { - $block_reader->gutenberg_remove_figcaption_tag_element(); - } else { - $block_reader->gutenberg_set_content_between_figcaption_balanced_tags( $new_value ); + // Compat for 6.4, where bookmarks and WP_HTML_Text_Replacement are different. + if ( ! empty( $closer_tag_bookmark->end ) ) { + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $closer_tag_bookmark->end + 1, '' ); + return true; } - } else { - $block_reader->seek( 'figure' ); - if ( ! $block_reader->next_tag( 'a' ) ) { - $block_reader->seek( 'figure' ); - $block_reader->next_tag( 'img' ); + + $after_closer_tag = $closer_tag_bookmark->start + $closer_tag_bookmark->length; + /* + * There was a bug in the HTML Processor token length fixed after 6.5. + * This check is needed to add compatibility for that. + * Related issue: https://github.com/WordPress/wordpress-develop/pull/6625 + */ + if ( '>' === $this->html[ $after_closer_tag ] ) { + ++$after_closer_tag; } - $block_reader->gutenberg_append_figcaption_element_after_tag( '
' . $new_value . '
' ); + $current_tag_length = $after_closer_tag - $opener_tag_bookmark->start; + + // Remove the current tag. + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $opener_tag_bookmark->start, $current_tag_length, '' ); + return true; } + }; - return $block_reader->get_updated_html(); + /* + * For backward compatibility, the logic from the image render needs to be replicated. + * This is because the block attributes can't be modified with the binding value with `render_block_data` filter, + * as it doesn't have access to the block instance. + */ + if ( $block_reader->next_tag( 'figure' ) ) { + $block_reader->set_bookmark( 'figure' ); } - /** - * Process the block bindings attribute. - * - * @param string $block_content Block Content. - * @param array $parsed_block The full block, including name and attributes. - * @param WP_Block $block_instance The block instance. - * @return string Block content with the bind applied. - */ - function gutenberg_process_image_caption_binding( $block_content, $parsed_block, $block_instance ) { - if ( ! isset( $parsed_block['attrs']['metadata']['bindings']['caption'] ) ) { - return $block_content; + $new_value = wp_kses_post( $source_value ); + + if ( $block_reader->next_tag( 'figcaption' ) ) { + if ( empty( $new_value ) ) { + $block_reader->gutenberg_remove_figcaption_tag_element(); + } else { + $block_reader->gutenberg_set_content_between_figcaption_balanced_tags( $new_value ); + } + } else { + $block_reader->seek( 'figure' ); + if ( ! $block_reader->next_tag( 'a' ) ) { + $block_reader->seek( 'figure' ); + $block_reader->next_tag( 'img' ); } + $block_reader->gutenberg_append_figcaption_element_after_tag( '
' . $new_value . '
' ); + } - $caption_binding = $parsed_block['attrs']['metadata']['bindings']['caption']; + return $block_reader->get_updated_html(); +} - $caption_binding_source = get_block_bindings_source( $caption_binding['source'] ); - $source_args = ! empty( $caption_binding['args'] ) && is_array( $caption_binding['args'] ) ? $caption_binding['args'] : array(); - $source_value = $caption_binding_source->get_value( $source_args, $block_instance, 'caption' ); +/** + * Process the block bindings attribute. + * + * @param string $block_content Block Content. + * @param array $parsed_block The full block, including name and attributes. + * @param WP_Block $block_instance The block instance. + * @return string Block content with the bind applied. + */ +function gutenberg_process_image_caption_binding( $block_content, $parsed_block, $block_instance ) { + if ( ! isset( $parsed_block['attrs']['metadata']['bindings']['caption'] ) ) { + return $block_content; + } - // If the value is not null, process the HTML based on the block and the attribute. - if ( is_null( $source_value ) ) { - return $block_content; - } + $caption_binding = $parsed_block['attrs']['metadata']['bindings']['caption']; + + $caption_binding_source = get_block_bindings_source( $caption_binding['source'] ); + $source_args = ! empty( $caption_binding['args'] ) && is_array( $caption_binding['args'] ) ? $caption_binding['args'] : array(); + $source_value = $caption_binding_source->get_value( $source_args, $block_instance, 'caption' ); - return gutenberg_block_bindings_replace_caption( $block_content, $block_instance->name, 'caption', $source_value ); + // If the value is not null, process the HTML based on the block and the attribute. + if ( is_null( $source_value ) ) { + return $block_content; } - add_filter( 'render_block_core/image', 'gutenberg_process_image_caption_binding', 20, 3 ); + return gutenberg_block_bindings_replace_caption( $block_content, $block_instance->name, 'caption', $source_value ); } + +add_filter( 'render_block_core/image', 'gutenberg_process_image_caption_binding', 20, 3 ); From 586d7dfdb61f807454b6c06d1b278f8b7cafba7d Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 37/39] Add backport changelog --- backport-changelog/6.6/6838.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 backport-changelog/6.6/6838.md diff --git a/backport-changelog/6.6/6838.md b/backport-changelog/6.6/6838.md new file mode 100644 index 00000000000000..e33a4bcd8aac94 --- /dev/null +++ b/backport-changelog/6.6/6838.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/6838 + +* https://github.com/WordPress/gutenberg/pull/61255 From ac597ee6e280286dedd03607d7ca35e801d6531f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:22:03 +0200 Subject: [PATCH 38/39] Move changelog to 6.7 folder --- backport-changelog/{6.6 => 6.7}/6838.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backport-changelog/{6.6 => 6.7}/6838.md (100%) diff --git a/backport-changelog/6.6/6838.md b/backport-changelog/6.7/6838.md similarity index 100% rename from backport-changelog/6.6/6838.md rename to backport-changelog/6.7/6838.md From be4c4c207cf58ad4c7a77bfe0a4557c9ca1d4b09 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 1 Jul 2024 16:38:31 +0200 Subject: [PATCH 39/39] Remove caption restriction in pattern overrides --- .../block-editor/src/hooks/use-bindings-attributes.js | 3 +-- packages/block-library/src/image/image.js | 10 ++-------- .../src/components/pattern-overrides-controls.js | 5 ++--- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index a7ff6818e74a9c..89f28116c5e37e 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -231,9 +231,8 @@ export const withBlockBindingSupport = createHigherOrderComponent( ) && Object.keys( keptAttributes ).length ) { - // Don't update caption and href until they are supported. + // Don't update href until they are supported. if ( hasPatternOverridesDefaultBinding ) { - delete keptAttributes?.caption; delete keptAttributes?.href; } setAttributes( keptAttributes ); diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 3e5dace6d23dbc..741ec5bb0ada3c 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -454,7 +454,6 @@ export default function Image( { lockAltControlsMessage, lockTitleControls = false, lockTitleControlsMessage, - lockCaption = false, hideCaptionControls = false, } = useSelect( ( select ) => { @@ -491,10 +490,6 @@ export default function Image( { // Disable editing the link of the URL if the image is inside a pattern instance. // This is a temporary solution until we support overriding the link on the frontend. hasParentPattern || arePatternOverridesEnabled, - lockCaption: - // Disable editing the caption if the image is inside a pattern instance. - // This is a temporary solution until we support overriding the caption on the frontend. - hasParentPattern, lockAltControls: !! altBinding && ! altBindingSource?.canUserEditValue( { @@ -1002,10 +997,9 @@ export default function Image( { label={ __( 'Image caption text' ) } showToolbarButton={ isSingleSelected && - hasNonContentControls && - ! arePatternOverridesEnabled + ( hasNonContentControls || isContentOnlyMode ) && + ! hideCaptionControls } - readOnly={ lockCaption } /> ); diff --git a/packages/patterns/src/components/pattern-overrides-controls.js b/packages/patterns/src/components/pattern-overrides-controls.js index b8af8538d8b358..b2cd2b5cdcb935 100644 --- a/packages/patterns/src/components/pattern-overrides-controls.js +++ b/packages/patterns/src/components/pattern-overrides-controls.js @@ -76,13 +76,12 @@ function PatternOverridesControls( { } const hasUnsupportedImageAttributes = - blockName === 'core/image' && - ( !! attributes.caption?.length || !! attributes.href?.length ); + blockName === 'core/image' && !! attributes.href?.length; const helpText = ! hasOverrides && hasUnsupportedImageAttributes ? __( - `Overrides currently don't support image captions or links. Remove the caption or link first before enabling overrides.` + `Overrides currently don't support image links. Remove the link first before enabling overrides.` ) : __( 'Allow changes to this block throughout instances of this pattern.'