From 76e019e298c69f286f1d3258542aa30ecc12efc0 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 3 Aug 2023 22:20:16 -0700 Subject: [PATCH] Add set_inner_markup() --- .../html-api/class-wp-html-processor.php | 90 +++++++++---- .../wpHtmlProcessorSetInnerMarkup.php | 123 ++++++++++++++++++ 2 files changed, 190 insertions(+), 23 deletions(-) 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 973effd7bff1b..60d7849be5f94 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -442,20 +442,20 @@ public function get_inner_markup() { return null; } - $this->set_bookmark( 'start' ); + $this->set_bookmark( 'opener' ); $found_tag = $this->step_until_tag_is_closed(); - $this->set_bookmark( 'end' ); + $this->set_bookmark( 'closer' ); if ( $found_tag ) { - $inner_markup = $this->substr_bookmarks( 'after', 'start', 'before', 'end' ); + $inner_markup = $this->substr_bookmarks( 'after', 'opener', 'before', 'closer' ); } else { // If there's no closing tag then the inner markup continues to the end of the document. - $inner_markup = $this->substr_bookmark( 'after', 'start' ); + $inner_markup = $this->substr_bookmark( 'after', 'opener' ); } - $this->seek( 'start' ); - $this->release_bookmark( 'start' ); - $this->release_bookmark( 'end' ); + $this->seek( 'opener' ); + $this->release_bookmark( 'opener' ); + $this->release_bookmark( 'closer' ); return $inner_markup; } @@ -484,31 +484,75 @@ public function get_outer_markup() { return null; } - $this->set_bookmark( 'start' ); + $this->set_bookmark( 'opener' ); $start_tag = $this->current_token->node_name; $found_tag = $this->step_until_tag_is_closed(); - $this->set_bookmark( 'end' ); + $this->set_bookmark( 'closer' ); if ( $found_tag ) { $did_close = $this->get_tag() === $start_tag && $this->is_tag_closer(); $end_position = $did_close ? 'after' : 'before'; - $outer_markup = $this->substr_bookmarks( 'before', 'start', $end_position, 'end' ); + $outer_markup = $this->substr_bookmarks( 'before', 'opener', $end_position, 'closer' ); } else { // If there's no closing tag then the outer markup continues to the end of the document. - $outer_markup = $this->substr_bookmark( 'before', 'start' ); + $outer_markup = $this->substr_bookmark( 'before', 'opener' ); } - $this->seek( 'start' ); - $this->release_bookmark( 'start' ); - $this->release_bookmark( 'end' ); + $this->seek( 'opener' ); + $this->release_bookmark( 'opener' ); + $this->release_bookmark( 'closer' ); return $outer_markup; } + /** + * Replaces the raw HTML of the currently-matched tag's inner markup with new HTML. + * This replaces the content between the tag opener and tag closer. + * + * @throws Exception When unable to set bookmark for internal tracking. + * + * @since 6.4.0 + * + * @param string $new_html + * @return bool|null Whether the contents were updated. + */ + public function set_inner_markup( $new_html ) { + if ( null === $this->get_tag() ) { + return null; + } + + $this->set_bookmark( 'opener' ); + $start_tag = $this->current_token->node_name; + + if ( self::is_void( $start_tag ) ) { + $this->release_bookmark( 'opener' ); + return true; + } + + $found_tag = $this->step_until_tag_is_closed(); + $this->set_bookmark( 'closer' ); + + if ( $found_tag ) { + $this->replace_using_bookmarks( $new_html, 'after', 'opener', 'before', 'closer' ); + } else { + // If there's no closing tag then the inner markup continues to the end of the document. + $this->replace_using_bookmark( $new_html, 'after', 'opener' ); + } + + $this->seek( 'opener' ); + $this->release_bookmark( 'opener' ); + $this->release_bookmark( 'closer' ); + return true; + } + /** * Replaces the raw HTML of the currently-matched tag with new HTML. * This replaces the entire contents of the tag including the tag itself. * + * @throws Exception When unable to set bookmark for internal tracking. + * + * @since 6.4.0 + * * @param string $new_html * @return bool|null Whether the contents were updated. */ @@ -517,30 +561,30 @@ public function set_outer_markup( $new_html ) { return null; } - $this->set_bookmark( 'start' ); + $this->set_bookmark( 'opener' ); $start_tag = $this->current_token->node_name; if ( self::is_void( $start_tag ) ) { - $this->replace_using_bookmarks( $new_html, 'before', 'start', 'after', 'start' ); - $this->release_bookmark( 'start' ); + $this->replace_using_bookmarks( $new_html, 'before', 'opener', 'after', 'opener' ); + $this->release_bookmark( 'opener' ); return true; } $found_tag = $this->step_until_tag_is_closed(); - $this->set_bookmark( 'end' ); + $this->set_bookmark( 'closer' ); if ( $found_tag ) { $did_close = $this->get_tag() === $start_tag && $this->is_tag_closer(); $end_position = $did_close ? 'after' : 'before'; - $this->replace_using_bookmarks( $new_html, 'before', 'start', $end_position, 'end' ); + $this->replace_using_bookmarks( $new_html, 'before', 'opener', $end_position, 'closer' ); } else { // If there's no closing tag then the outer markup continues to the end of the document. - $this->replace_using_bookmark( $new_html, 'before', 'start' ); + $this->replace_using_bookmark( $new_html, 'before', 'opener' ); } - $this->seek( 'start' ); - $this->release_bookmark( 'start' ); - $this->release_bookmark( 'end' ); + $this->seek( 'opener' ); + $this->release_bookmark( 'opener' ); + $this->release_bookmark( 'closer' ); return true; } diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorSetInnerMarkup.php b/tests/phpunit/tests/html-api/wpHtmlProcessorSetInnerMarkup.php index e69de29bb2d1d..20ec56a63e47c 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorSetInnerMarkup.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorSetInnerMarkup.php @@ -0,0 +1,123 @@ +next_tag() && null === $p->get_attribute( 'target' ) ) { + continue; + } + + $this->assertTrue( $p->set_inner_markup( $new_markup ), 'Failed to set inner markup.' ); + $this->assertSame( $expected_output, $p->get_updated_html(), 'Failed to appropriately set inner markup.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_html_with_inner_markup_changes() { + $data = array( + 'Void element' => array( '', '', '' ), + 'Void element inside text' => array( 'beforeafter', '', 'beforeafter' ), + 'Void element inside another element' => array( '

Look at this graph.

', '', '

Look at this graph.

' ), + 'Empty elements' => array( '
', '', '
' ), + 'Element with nested tags' => array( '
inside the div
', '', '
' ), + 'Element inside another element' => array( '
inside the div
', '', '
inside div
' ), + 'Unclosed element' => array( '
This is all inside the DIV', '', '
' ), + 'Unclosed nested element' => array( '

One thought

And another', '', '

And another' ), + 'Partially-closed element' => array( '

This is all inside the DIV' ), + 'Implicitly-closed element' => array( '

Inside the P

Outside the P

', '', '

Outside the P

' ), + ); + + $inner_html = <<This is inside the Match

+

+
+
+ +
Look at the picture photograph.
+
+
+HTML; + + $prefix = << +

This is not in the match. +

This is another paragraph not in the match. +

+
+HTML; + + /* + * Removing the indent on this first line keeps the test easy to reason about, + * otherwise extra indents appear after removing the inner content, because + * that indentation before and after is whitespace and not part of the tag. + */ + $suffix = << +
+

This is also note in the match.

+
+HTML; + + $data['Complicated inner nesting'] = array( $prefix . $inner_html . $suffix, '', $prefix . $suffix ); + + return $data; + } + + /** + * Ensures that the cursor isn't moved when setting the inner markup. It should + * remain at the same location as the tag opener where it was called. + * + * @ticket {TICKET_NUMBER} + * + * @covers WP_HTML_Processor::set_inner_markup + * + * @since 6.4.0 + */ + public function test_preserves_cursor() { + $p = WP_HTML_Processor::createFragment( '

The cursor should not move unexpectedly.

' ); + + while ( $p->next_tag() && null === $p->get_attribute( 'target' ) ) { + continue; + } + + $this->assertTrue( $p->set_inner_markup( '' ) ); + $this->assertSame( + '

The should not move unexpectedly.

', + $p->get_updated_html(), + 'Failed to replace appropriate inner markup.' + ); + + $this->assertSame( 'CODE', $p->get_tag(), "Should have remained on CODE, but found {$p->get_tag()} instead." ); + + $p->next_tag(); + $this->assertNotNull( $p->get_attribute( 'next-target' ), "Expected to move to inserted IMG element, but found {$p->get_tag()} instead." ); + } +}