From b49b3174a27e677d35fc87b035add949745a9edc Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 7 Nov 2024 12:23:08 +0100 Subject: [PATCH 1/7] Add html api processor for setting inner html --- src/wp-includes/class-wp-block.php | 111 ++++++++++++++++++ .../html-api/class-wp-html-processor.php | 2 +- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index a1c52019c29a5..a1b90f124b14d 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -607,3 +607,114 @@ public function render( $options = array() ) { return $block_content; } } + +// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound +class PrivateProcessor extends WP_HTML_Processor { + + public function set_inner_html( ?string $html ) { + if ( $this->is_virtual() ) { + return false; + } + + if ( $this->get_token_type() !== '#tag' ) { + return false; + } + + if ( $this->is_tag_closer() ) { + return false; + } + + if ( ! $this->expects_closer() ) { + return false; + } + + // @todo check if this is necessary + /*if (*/ + /* 'html' !== $this->state->current_token->namespace &&*/ + /* $this->state->current_token->has_self_closing_flag*/ + /*) {*/ + /* return false;*/ + /*}*/ + + if ( null === $html ) { + $html = ''; + } + if ( '' !== $html ) { + $fragment_parser = $this->spawn_fragment_parser( $html ); + if ( + null === $fragment_parser + ) { + return false; + } + + try { + $html = $fragment_parser->serialize(); + } catch ( Exception $e ) { + return false; + } + } + + // @todo apply modifications if there are any??? + if ( ! parent::set_bookmark( 'SET_INNER_HTML: opener' ) ) { + return false; + } + + if ( ! $this->proceed_to_matching_closer() ) { + parent::seek( 'SET_INNER_HTML: opener' ); + return false; + } + + if ( ! parent::set_bookmark( 'SET_INNER_HTML: closer' ) ) { + return false; + } + + $inner_html_start = $this->bookmarks['SET_INNER_HTML: opener']->start + $this->bookmarks['SET_INNER_HTML: opener']->length; + $inner_html_length = $this->bookmarks['SET_INNER_HTML: closer']->start - $inner_html_start; + + $this->lexical_updates['innerHTML'] = new WP_HTML_Text_Replacement( + $inner_html_start, + $inner_html_length, + $html + ); + + parent::seek( 'SET_INNER_HTML: opener' ); + parent::release_bookmark( 'SET_INNER_HTML: opener' ); + parent::release_bookmark( 'SET_INNER_HTML: closer' ); + + // @todo check for whether that html will make a mess! + // Will it break out of tags? + return true; + } + + public function proceed_to_matching_closer(): bool { + $tag_name = $this->get_tag(); + + if ( null === $tag_name ) { + return false; + } + + if ( $this->is_tag_closer() ) { + return false; + } + + if ( ! $this->expects_closer() ) { + return false; + } + + $breadcrumbs = $this->get_breadcrumbs(); + array_pop( $breadcrumbs ); + + // @todo Can't use these queries together + while ( $this->next_tag( + array( + 'tag_name' => $this->get_tag(), + 'tag_closers' => 'visit', + ) + ) ) { + if ( $this->get_breadcrumbs() === $breadcrumbs ) { + return true; + } + } + return false; + } +} diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 4bdb75fcac3eb..75d2b959b3a76 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -844,7 +844,7 @@ public function is_tag_closer(): bool { * * @return bool Whether the current token is virtual. */ - private function is_virtual(): bool { + protected function is_virtual(): bool { return ( isset( $this->current_element->provenance ) && 'virtual' === $this->current_element->provenance From 28e93f2ae6a1067f7d24b29283925f45600e6782 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 7 Nov 2024 12:57:46 +0100 Subject: [PATCH 2/7] Working block bindings set_inner_html implementation --- src/wp-includes/class-wp-block.php | 76 +++++++++--------------------- 1 file changed, 23 insertions(+), 53 deletions(-) diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index a1b90f124b14d..e3e381e59cbdd 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -351,62 +351,25 @@ private function replace_html( string $block_content, string $attribute_name, $s switch ( $block_type->attributes[ $attribute_name ]['source'] ) { case 'html': case 'rich-text': - $block_reader = new WP_HTML_Tag_Processor( $block_content ); + $block_reader = PrivateProcessor::create_fragment( $block_content ); // TODO: Support for CSS selectors whenever they are ready in the HTML API. // In the meantime, support comma-separated selectors by exploding them into an array. + // NOTE! This assumes the selectors are element selectors, e.g. "a, button" or "p". $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' === $this->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 the current or any other tags match the selector, replace the HTML. if ( strcasecmp( $block_reader->get_tag(), $selector ) === 0 || $block_reader->next_tag( - array( - 'tag_name' => $selector, - ) + 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' === $this->name || 'core/heading' === $this->name ) { - return $amended_content->get_updated_html(); - } - if ( 'core/button' === $this->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(); - } + $block_reader->set_inner_html( wp_kses_post( $source_value ) ); + return $block_reader->get_updated_html(); } else { $block_reader->seek( 'iterate-selectors' ); } @@ -609,6 +572,13 @@ public function render( $options = array() ) { } // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound +/** + * HTML API Processor subclass implementing experimental set_inner_html. + * + * DO NOT USE THIS CLASS. Internal usage only. + * + * @access private + */ class PrivateProcessor extends WP_HTML_Processor { public function set_inner_html( ?string $html ) { @@ -655,31 +625,31 @@ public function set_inner_html( ?string $html ) { } // @todo apply modifications if there are any??? - if ( ! parent::set_bookmark( 'SET_INNER_HTML: opener' ) ) { + if ( ! $this->set_bookmark( 'SET_INNER_HTML: opener' ) ) { return false; } if ( ! $this->proceed_to_matching_closer() ) { - parent::seek( 'SET_INNER_HTML: opener' ); + $this->seek( 'SET_INNER_HTML: opener' ); return false; } - if ( ! parent::set_bookmark( 'SET_INNER_HTML: closer' ) ) { + if ( ! $this->set_bookmark( 'SET_INNER_HTML: closer' ) ) { return false; } - $inner_html_start = $this->bookmarks['SET_INNER_HTML: opener']->start + $this->bookmarks['SET_INNER_HTML: opener']->length; - $inner_html_length = $this->bookmarks['SET_INNER_HTML: closer']->start - $inner_html_start; + $inner_html_start = $this->bookmarks['_SET_INNER_HTML: opener']->start + $this->bookmarks['_SET_INNER_HTML: opener']->length; + $inner_html_length = $this->bookmarks['_SET_INNER_HTML: closer']->start - $inner_html_start; - $this->lexical_updates['innerHTML'] = new WP_HTML_Text_Replacement( + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $inner_html_start, $inner_html_length, $html ); - parent::seek( 'SET_INNER_HTML: opener' ); - parent::release_bookmark( 'SET_INNER_HTML: opener' ); - parent::release_bookmark( 'SET_INNER_HTML: closer' ); + $this->seek( 'SET_INNER_HTML: opener' ); + $this->release_bookmark( 'SET_INNER_HTML: opener' ); + $this->release_bookmark( 'SET_INNER_HTML: closer' ); // @todo check for whether that html will make a mess! // Will it break out of tags? From 62cc6d3889c171c88dada036443c2887a1b117b1 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 7 Nov 2024 13:12:10 +0100 Subject: [PATCH 3/7] Fix next_tag selector --- src/wp-includes/class-wp-block.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index e3e381e59cbdd..b3ce6b5f19ba8 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -364,9 +364,10 @@ private function replace_html( string $block_content, string $attribute_name, $s foreach ( $selectors as $selector ) { // If the current or any other tags match the selector, replace the HTML. - if ( strcasecmp( $block_reader->get_tag(), $selector ) === 0 || $block_reader->next_tag( - array( 'tag_name' => $selector ) - ) ) { + if ( + strcasecmp( $block_reader->get_tag(), $selector ) === 0 || + $block_reader->next_tag( $selector ) + ) { $block_reader->release_bookmark( 'iterate-selectors' ); $block_reader->set_inner_html( wp_kses_post( $source_value ) ); return $block_reader->get_updated_html(); From 0fd7a103c7ed528303d638c07e05f4a9874e2eb6 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 27 Nov 2024 10:37:22 +0100 Subject: [PATCH 4/7] Simplify set_inner_html function --- src/wp-includes/class-wp-block.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index b3ce6b5f19ba8..551b71769f63e 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -582,7 +582,12 @@ public function render( $options = array() ) { */ class PrivateProcessor extends WP_HTML_Processor { - public function set_inner_html( ?string $html ) { + /** + * @param string $html + * + * @return bool + */ + public function set_inner_html( string $html ): bool { if ( $this->is_virtual() ) { return false; } @@ -607,14 +612,9 @@ public function set_inner_html( ?string $html ) { /* return false;*/ /*}*/ - if ( null === $html ) { - $html = ''; - } if ( '' !== $html ) { - $fragment_parser = $this->spawn_fragment_parser( $html ); - if ( - null === $fragment_parser - ) { + $fragment_parser = $this->create_fragment_at_current_node( $html ); + if ( null === $fragment_parser ) { return false; } From c49dc0b8601c74d0fa6becf9de7d6b768bd06e7a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 27 Nov 2024 10:37:43 +0100 Subject: [PATCH 5/7] Add todo --- src/wp-includes/class-wp-block.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index 551b71769f63e..07ed825d3780c 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -657,6 +657,9 @@ public function set_inner_html( string $html ): bool { return true; } + /** + * @todo check for self-closing foreign content tags + */ public function proceed_to_matching_closer(): bool { $tag_name = $this->get_tag(); From 058d473ffd5da90a2dd9cf1c5add77b9927a26b2 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 27 Nov 2024 10:39:15 +0100 Subject: [PATCH 6/7] Documentation --- src/wp-includes/class-wp-block.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index 07ed825d3780c..aa4aee0d8fd40 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -583,9 +583,13 @@ public function render( $options = array() ) { class PrivateProcessor extends WP_HTML_Processor { /** - * @param string $html + * Set the inner HTML of the currrent node. * - * @return bool + * @todo This method needs to check if the inner HTML can leak out of the current node. + * + * @param string $html The inner HTML to set. + * + * @return bool True if the inner HTML was set, false otherwise. */ public function set_inner_html( string $html ): bool { if ( $this->is_virtual() ) { @@ -659,6 +663,7 @@ public function set_inner_html( string $html ): bool { /** * @todo check for self-closing foreign content tags + * @todo document */ public function proceed_to_matching_closer(): bool { $tag_name = $this->get_tag(); From c0a96fb6456c61ef1e3ccf33d5f05f388b0924fb Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 29 Nov 2024 13:24:58 +0100 Subject: [PATCH 7/7] DO_NOT_MERGE: Make create fragment method public --- src/wp-includes/html-api/class-wp-html-processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 75d2b959b3a76..70d38f830440d 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -463,7 +463,7 @@ function ( WP_HTML_Token $token ): void { * @param string $html Input HTML fragment to process. * @return static|null The created processor if successful, otherwise null. */ - public function create_fragment_at_current_node( string $html ) { + protected function create_fragment_at_current_node( string $html ) { if ( $this->get_token_type() !== '#tag' || $this->is_tag_closer() ) { return null; }