From 26028e9e1fadba0c7b8bb45701061aa82ea147e7 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Tue, 22 Nov 2022 20:12:50 -0700 Subject: [PATCH 01/11] WIP: Tag Processor: Add bookmark system for tracking semantic locations in document It can be helpful to track a location in an HTML document while updates are being made to it such that we can instruct the Tag Processor to seek to the location of one of the bookmarks. In this patch we're introducing a bookmarks system to do just that. The bookmark is a resource handle that represents an internal tracking object which will follow all updates made to the document. It will be possible to rewind or jump around a document by setting a bookmark. --- .../html/class-wp-html-tag-processor.php | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/lib/experimental/html/class-wp-html-tag-processor.php b/lib/experimental/html/class-wp-html-tag-processor.php index 5dc5981ef15f78..1e6aa605641f73 100644 --- a/lib/experimental/html/class-wp-html-tag-processor.php +++ b/lib/experimental/html/class-wp-html-tag-processor.php @@ -180,6 +180,7 @@ * @since 6.2.0 */ class WP_HTML_Tag_Processor { + const MAX_BOOKMARKS = 10; /** * The HTML document to parse. @@ -362,6 +363,15 @@ class WP_HTML_Tag_Processor { */ private $classname_updates = array(); + /** + * Tracks a semantic location in the original HTML that shuffles + * with the updates applied to the document. + * + * @since 6.2.0 + * @var array + */ + private $bookmarks = array(); + const ADD_CLASS = true; const REMOVE_CLASS = false; const SKIP_CLASS = null; @@ -479,6 +489,34 @@ public function next_tag( $query = null ) { return true; } + + public function set_bookmark( $name ) { + if ( null === $this->tag_name_starts_at ) { + return false; + } + + if ( ! array_key_exists( $name, $this->bookmarks ) && count( $this->bookmarks ) > self::MAX_BOOKMARKS ) { + return false; + } + + $this->bookmarks[ $name ] = new WP_HTML_Text_Replacement( + $this->tag_name_starts_at - 1, + $this->tag_ends_at, + '' + ); + } + + + public function release_bookmark( $name ) { + if ( ! array_key_exists( $name, $this->bookmarks ) ) { + return false; + } + + unset( $this->bookmarks[ $name ] ); + return true; + } + + /** * Skips the contents of the title and textarea tags until an appropriate * tag closer is found. @@ -1102,6 +1140,25 @@ private function apply_attributes_updates() { $this->updated_html .= substr( $this->html, $this->updated_bytes, $diff->start - $this->updated_bytes ); $this->updated_html .= $diff->text; $this->updated_bytes = $diff->end; + + foreach ( $this->bookmarks as $name => &$position ) { + $update_head = $position->start >= $diff->start; + $update_tail = $position->end >= $diff->start; + + if ( ! $update_head && ! $update_tail ) { + continue; + } + + $delta = strlen( $diff->text ) - ( $diff->end - $diff->start ); + + if ( $update_head ) { + $position->start += $delta; + } + + if ( $update_tail ) { + $position->start += $delta; + } + } } $this->attribute_updates = array(); From 17cb5fab2df8f2bb797a3f784548b9b2a68275a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 24 Nov 2022 20:03:35 -0700 Subject: [PATCH 02/11] Draft the rewind() method --- .../html/class-wp-html-tag-processor.php | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/lib/experimental/html/class-wp-html-tag-processor.php b/lib/experimental/html/class-wp-html-tag-processor.php index 1e6aa605641f73..865f88d051e642 100644 --- a/lib/experimental/html/class-wp-html-tag-processor.php +++ b/lib/experimental/html/class-wp-html-tag-processor.php @@ -490,6 +490,12 @@ public function next_tag( $query = null ) { } + /** + * Sets a bookmark in the HTML document. + * + * @param string $name Identifies this particular bookmark. + * @return false|void + */ public function set_bookmark( $name ) { if ( null === $this->tag_name_starts_at ) { return false; @@ -507,7 +513,14 @@ public function set_bookmark( $name ) { } + /** + * Removes a bookmark once it's not necessary anymore. + * + * @param string $name Name of the bookmark to remove. + * @return bool + */ public function release_bookmark( $name ) { + if ( ! array_key_exists( $name, $this->bookmarks ) ) { return false; } @@ -1141,9 +1154,9 @@ private function apply_attributes_updates() { $this->updated_html .= $diff->text; $this->updated_bytes = $diff->end; - foreach ( $this->bookmarks as $name => &$position ) { + foreach ( $this->bookmarks as &$position ) { $update_head = $position->start >= $diff->start; - $update_tail = $position->end >= $diff->start; + $update_tail = $position->end >= $diff->start; if ( ! $update_head && ! $update_tail ) { continue; @@ -1156,7 +1169,7 @@ private function apply_attributes_updates() { } if ( $update_tail ) { - $position->start += $delta; + $position->end += $delta; } } } @@ -1164,6 +1177,31 @@ private function apply_attributes_updates() { $this->attribute_updates = array(); } + /** + * Move the current pointer in the Tag Processor to a given bookmark's location. + * + * @param string $bookmark_name Name of bookmark to which to rewind. + * @return bool + * @throws Exception Throws on invalid bookmark name if WP_DEBUG set. + */ + public function rewind( $bookmark_name ) { + if ( ! array_key_exists( $bookmark_name, $this->bookmarks ) ) { + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + throw new Exception( 'Invalid bookmark name' ); + } + return false; + } + + // Apply all the updates. + $this->apply_string_diffs(); + + $start = $this->bookmarks[ $bookmark_name ]->start; + $this->parsed_bytes = $start; + $this->updated_bytes = $start; + $this->updated_html = substr( $this->html, 0, $this->parsed_bytes ); + return $this->next_tag(); + } + /** * Sort function to arrange objects with a start property in ascending order. * @@ -1473,6 +1511,15 @@ public function get_updated_html() { return $this->updated_html . substr( $this->html, $this->updated_bytes ); } + return $this->apply_string_diffs(); + } + + /** + * I just ripped out the part I need to call in the rewind(). + * + * @TODO separate it more cleanly. + */ + private function apply_string_diffs() { /* * Parsing is in progress – let's apply the attribute updates without moving on to the next tag. * From b05e97573626ce1796ff785a2afb8d8278b03bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 24 Nov 2022 20:29:02 -0700 Subject: [PATCH 03/11] Add tests for rewind() functionality --- .../wp-html-tag-processor-rewind-test.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 phpunit/html/wp-html-tag-processor-rewind-test.php diff --git a/phpunit/html/wp-html-tag-processor-rewind-test.php b/phpunit/html/wp-html-tag-processor-rewind-test.php new file mode 100644 index 00000000000000..f78a6d70db13ef --- /dev/null +++ b/phpunit/html/wp-html-tag-processor-rewind-test.php @@ -0,0 +1,34 @@ +
  • One
  • Two
  • Three
  • ' ); + $p->next_tag( 'li' ); + $p->set_bookmark( 'first li' ); + $p->next_tag( 'li' ); + $p->set_bookmark( 'second li' ); + $p->set_attribute( 'foo-2', 'bar-2' ); + $p->rewind( 'first li' ); + $p->set_attribute( 'foo-1', 'bar-1' ); + $p->rewind( 'second li' ); + $p->next_tag( 'li' ); + $p->set_attribute( 'foo-3', 'bar-3' ); + $this->assertEquals( + '
    • One
    • Two
    • Three
    ', + $p->get_updated_html() + ); + } +} From 8119754009d5859438174e594a737a0d01ef9804 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 25 Nov 2022 13:26:34 -0700 Subject: [PATCH 04/11] Add a couple of tests for before/after behaviors --- .../wp-html-tag-processor-rewind-test.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/phpunit/html/wp-html-tag-processor-rewind-test.php b/phpunit/html/wp-html-tag-processor-rewind-test.php index f78a6d70db13ef..d57228ddf90039 100644 --- a/phpunit/html/wp-html-tag-processor-rewind-test.php +++ b/phpunit/html/wp-html-tag-processor-rewind-test.php @@ -31,4 +31,39 @@ public function test_bookmark() { $p->get_updated_html() ); } + + public function test_updates_bookmark_for_changes_after_both_sides() { + $p = new WP_HTML_Tag_Processor( '
    First
    Second
    ' ); + $p->next_tag(); + $p->set_bookmark( 'first' ); + $p->next_tag(); + $p->add_class( 'second' ); + + $p->rewind( 'first' ); + $p->add_class( 'first' ); + + $this->assertEquals( + '
    First
    Second
    ', + $p->get_updated_html() + ); + } + + public function test_updates_bookmark_for_changes_before_both_sides() { + $p = new WP_HTML_Tag_Processor( '
    First
    Second
    ' ); + $p->next_tag(); + $p->set_bookmark( 'first' ); + $p->next_tag(); + $p->set_bookmark( 'second' ); + + $p->rewind( 'first' ); + $p->add_class( 'first' ); + + $p->rewind( 'second' ); + $p->add_class( 'second' ); + + $this->assertEquals( + '
    First
    Second
    ', + $p->get_updated_html() + ); + } } From 441984fe68258c9b2952c809c29109a7b452a9b5 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 25 Nov 2022 13:27:18 -0700 Subject: [PATCH 05/11] Rename `rewind()` to `seek()` because we can move forward as well. --- lib/experimental/html/class-wp-html-tag-processor.php | 2 +- phpunit/html/wp-html-tag-processor-rewind-test.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/experimental/html/class-wp-html-tag-processor.php b/lib/experimental/html/class-wp-html-tag-processor.php index 865f88d051e642..0c744fe71f627b 100644 --- a/lib/experimental/html/class-wp-html-tag-processor.php +++ b/lib/experimental/html/class-wp-html-tag-processor.php @@ -1184,7 +1184,7 @@ private function apply_attributes_updates() { * @return bool * @throws Exception Throws on invalid bookmark name if WP_DEBUG set. */ - public function rewind( $bookmark_name ) { + public function seek( $bookmark_name ) { if ( ! array_key_exists( $bookmark_name, $this->bookmarks ) ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { throw new Exception( 'Invalid bookmark name' ); diff --git a/phpunit/html/wp-html-tag-processor-rewind-test.php b/phpunit/html/wp-html-tag-processor-rewind-test.php index d57228ddf90039..33002f2a5870ba 100644 --- a/phpunit/html/wp-html-tag-processor-rewind-test.php +++ b/phpunit/html/wp-html-tag-processor-rewind-test.php @@ -21,9 +21,9 @@ public function test_bookmark() { $p->next_tag( 'li' ); $p->set_bookmark( 'second li' ); $p->set_attribute( 'foo-2', 'bar-2' ); - $p->rewind( 'first li' ); + $p->seek( 'first li' ); $p->set_attribute( 'foo-1', 'bar-1' ); - $p->rewind( 'second li' ); + $p->seek( 'second li' ); $p->next_tag( 'li' ); $p->set_attribute( 'foo-3', 'bar-3' ); $this->assertEquals( @@ -39,7 +39,7 @@ public function test_updates_bookmark_for_changes_after_both_sides() { $p->next_tag(); $p->add_class( 'second' ); - $p->rewind( 'first' ); + $p->seek( 'first' ); $p->add_class( 'first' ); $this->assertEquals( @@ -55,10 +55,10 @@ public function test_updates_bookmark_for_changes_before_both_sides() { $p->next_tag(); $p->set_bookmark( 'second' ); - $p->rewind( 'first' ); + $p->seek( 'first' ); $p->add_class( 'first' ); - $p->rewind( 'second' ); + $p->seek( 'second' ); $p->add_class( 'second' ); $this->assertEquals( From ca3565a4eff7f0ad8dcb9c0d5a829fa24d8f871d Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 25 Nov 2022 13:38:40 -0700 Subject: [PATCH 06/11] Test for removal of text before bookmarks. --- .../wp-html-tag-processor-rewind-test.php | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/phpunit/html/wp-html-tag-processor-rewind-test.php b/phpunit/html/wp-html-tag-processor-rewind-test.php index 33002f2a5870ba..f815c386b3d537 100644 --- a/phpunit/html/wp-html-tag-processor-rewind-test.php +++ b/phpunit/html/wp-html-tag-processor-rewind-test.php @@ -1,6 +1,6 @@ First
    Second
    ' ); $p->next_tag(); $p->set_bookmark( 'first' ); @@ -48,7 +53,7 @@ public function test_updates_bookmark_for_changes_after_both_sides() { ); } - public function test_updates_bookmark_for_changes_before_both_sides() { + public function test_updates_bookmark_for_additions_before_both_sides() { $p = new WP_HTML_Tag_Processor( '
    First
    Second
    ' ); $p->next_tag(); $p->set_bookmark( 'first' ); @@ -66,4 +71,41 @@ public function test_updates_bookmark_for_changes_before_both_sides() { $p->get_updated_html() ); } + + public function test_updates_bookmark_for_deletions_after_both_sides() { + $p = new WP_HTML_Tag_Processor( '
    First
    Second
    ' ); + $p->next_tag(); + $p->set_bookmark( 'first' ); + $p->next_tag(); + $p->remove_attribute( 'disabled' ); + + $p->seek( 'first' ); + $p->set_attribute( 'untouched', true ); + + $this->assertEquals( + /** @TODO: we shouldn't have to assert the extra space after removing the attribute. */ + '
    First
    Second
    ', + $p->get_updated_html() + ); + } + + public function test_updates_bookmark_for_deletions_before_both_sides() { + $p = new WP_HTML_Tag_Processor( '
    First
    Second
    ' ); + $p->next_tag(); + $p->set_bookmark( 'first' ); + $p->next_tag(); + $p->set_bookmark( 'second' ); + + $p->seek( 'first' ); + $p->remove_attribute( 'disabled' ); + + $p->seek( 'second' ); + $p->set_attribute( 'safe', true ); + + $this->assertEquals( + /** @TODO: we shouldn't have to assert the extra space after removing the attribute. */ + '
    First
    Second
    ', + $p->get_updated_html() + ); + } } From e6db15aa60b5e984507b2fde625c223f57ca0cb7 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 25 Nov 2022 13:41:10 -0700 Subject: [PATCH 07/11] Rename test file --- ...-rewind-test.php => wp-html-tag-processor-bookmark-test.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename phpunit/html/{wp-html-tag-processor-rewind-test.php => wp-html-tag-processor-bookmark-test.php} (97%) diff --git a/phpunit/html/wp-html-tag-processor-rewind-test.php b/phpunit/html/wp-html-tag-processor-bookmark-test.php similarity index 97% rename from phpunit/html/wp-html-tag-processor-rewind-test.php rename to phpunit/html/wp-html-tag-processor-bookmark-test.php index f815c386b3d537..a5e0b18e190927 100644 --- a/phpunit/html/wp-html-tag-processor-rewind-test.php +++ b/phpunit/html/wp-html-tag-processor-bookmark-test.php @@ -13,7 +13,7 @@ * * @coversDefaultClass WP_HTML_Tag_Processor */ -class WP_HTML_Tag_Processor_Rewind_Test extends WP_UnitTestCase { +class WP_HTML_Tag_Processor_Bookmark_Test extends WP_UnitTestCase { /** * @ticket 56299 * From e2a4fba2b29c980d7ae57f7be65d5fa3bbdf7f56 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 25 Nov 2022 15:02:59 -0700 Subject: [PATCH 08/11] WIP: Tag Processor: Explore cut and replace operations --- .../html/class-wp-html-tag-processor.php | 11 ++++++++ .../wp-html-tag-processor-bookmark-test.php | 28 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/lib/experimental/html/class-wp-html-tag-processor.php b/lib/experimental/html/class-wp-html-tag-processor.php index 0c744fe71f627b..a296458be51010 100644 --- a/lib/experimental/html/class-wp-html-tag-processor.php +++ b/lib/experimental/html/class-wp-html-tag-processor.php @@ -1202,6 +1202,17 @@ public function seek( $bookmark_name ) { return $this->next_tag(); } + public function dangerously_replace( $start_bookmark, $end_bookmark, $text, $region = 'outside' ) { + $start = $this->bookmarks[ $start_bookmark ]; + $start = 'outside' === $region ? $start->start : $start->end + 1; + + $end = $this->bookmarks[ $end_bookmark ]; + $end = 'outside' === $region ? $end->end + 1 : $end->start - 1; + + $this->attribute_updates[] = new WP_HTML_Text_Replacement( $start, $end, $text ); + $this->apply_attributes_updates(); + } + /** * Sort function to arrange objects with a start property in ascending order. * diff --git a/phpunit/html/wp-html-tag-processor-bookmark-test.php b/phpunit/html/wp-html-tag-processor-bookmark-test.php index a5e0b18e190927..a46919bead84e9 100644 --- a/phpunit/html/wp-html-tag-processor-bookmark-test.php +++ b/phpunit/html/wp-html-tag-processor-bookmark-test.php @@ -108,4 +108,32 @@ public function test_updates_bookmark_for_deletions_before_both_sides() { $p->get_updated_html() ); } + + public function test_replaces_inside_contents() { + $p = new WP_HTML_Tag_Processor( '
    Before
    Inside
    After
    ' ); + $p->next_tag( [ 'class_name' => 'inner' ] ); + $p->set_bookmark( 'start' ); + $p->next_tag( [ 'tag_name' => 'div', 'tag_closers' => 'visit' ] ); + $p->set_bookmark( 'end' ); + $p->dangerously_replace( 'start', 'end', '--', 'inside' ); + + $this->assertEquals( + '
    Before
    --
    After
    ', + $p->get_updated_html() + ); + } + + public function test_replaces_outside_contents() { + $p = new WP_HTML_Tag_Processor( '
    Before
    Inside
    After
    ' ); + $p->next_tag( [ 'class_name' => 'inner' ] ); + $p->set_bookmark( 'start' ); + $p->next_tag( [ 'tag_name' => 'div', 'tag_closers' => 'visit' ] ); + $p->set_bookmark( 'end' ); + $p->dangerously_replace( 'start', 'end', '--' ); + + $this->assertEquals( + '
    Before--After
    ', + $p->get_updated_html() + ); + } } From 349253d8302bf1155fced144cd5a8be6084fa05e Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 25 Nov 2022 16:13:31 -0700 Subject: [PATCH 09/11] more stuff to get and replace contents --- .../html/class-wp-html-tag-processor.php | 59 +++++++++- .../wp-html-tag-processor-bookmark-test.php | 105 +++++++++++++++++- 2 files changed, 157 insertions(+), 7 deletions(-) diff --git a/lib/experimental/html/class-wp-html-tag-processor.php b/lib/experimental/html/class-wp-html-tag-processor.php index a296458be51010..49a81371fc006c 100644 --- a/lib/experimental/html/class-wp-html-tag-processor.php +++ b/lib/experimental/html/class-wp-html-tag-processor.php @@ -1154,7 +1154,7 @@ private function apply_attributes_updates() { $this->updated_html .= $diff->text; $this->updated_bytes = $diff->end; - foreach ( $this->bookmarks as &$position ) { + foreach ( $this->bookmarks as $name => &$position ) { $update_head = $position->start >= $diff->start; $update_tail = $position->end >= $diff->start; @@ -1162,6 +1162,19 @@ private function apply_attributes_updates() { continue; } + /* + * If a change is made that encompasses an entire bookmark then we + * have to remove the bookmark as the semantic place it pointed to + * no longer exists. It could seem like we can let it remain as + * long as we haven't removed the text, but if the text is different + * then the place likely doesn't exist at all either and we need to + * start over. + */ + if ( $diff->start <= $position->start && $diff->end >= $position->end ) { + unset( $this->bookmarks[ $name ] ); + continue; + } + $delta = strlen( $diff->text ) - ( $diff->end - $diff->start ); if ( $update_head ) { @@ -1202,12 +1215,48 @@ public function seek( $bookmark_name ) { return $this->next_tag(); } - public function dangerously_replace( $start_bookmark, $end_bookmark, $text, $region = 'outside' ) { + public function dangerously_get_contents( $start_bookmark, $end_bookmark, $region = 'outer' ) { + if ( + empty( $start_bookmark ) || + empty( $end_bookmark ) || + ! isset( $this->bookmarks[ $start_bookmark ], $this->bookmarks[ $end_bookmark ] ) || + ( $start_bookmark === $end_bookmark && $region !== 'outer' ) + ) { + return false; + } + + $start = $this->bookmarks[ $start_bookmark ]; + $end = $this->bookmarks[ $end_bookmark ]; + + if ( $start->start > $end->start || $start->end > $end->end ) { + return false; + } + + $start = 'outer' === $region ? $start->start : $start->end + 1; + $end = 'outer' === $region ? $end->end + 1 : $end->start - 1; + + return substr( $this->html, $start, $end - $start ); + } + + public function dangerously_replace( $start_bookmark, $end_bookmark, $text, $region = 'outer' ) { + if ( + empty( $start_bookmark ) || + empty( $end_bookmark ) || + ! isset( $this->bookmarks[ $start_bookmark ], $this->bookmarks[ $end_bookmark ] ) || + ( $start_bookmark === $end_bookmark && $region !== 'outer' ) + ) { + return false; + } + $start = $this->bookmarks[ $start_bookmark ]; - $start = 'outside' === $region ? $start->start : $start->end + 1; + $end = $this->bookmarks[ $end_bookmark ]; + + if ( $start->start > $end->start || $start->end > $end->end ) { + return false; + } - $end = $this->bookmarks[ $end_bookmark ]; - $end = 'outside' === $region ? $end->end + 1 : $end->start - 1; + $start = 'outer' === $region ? $start->start : $start->end + 1; + $end = 'outer' === $region ? $end->end + 1 : $end->start - 1; $this->attribute_updates[] = new WP_HTML_Text_Replacement( $start, $end, $text ); $this->apply_attributes_updates(); diff --git a/phpunit/html/wp-html-tag-processor-bookmark-test.php b/phpunit/html/wp-html-tag-processor-bookmark-test.php index a46919bead84e9..6fd4e286e1cefa 100644 --- a/phpunit/html/wp-html-tag-processor-bookmark-test.php +++ b/phpunit/html/wp-html-tag-processor-bookmark-test.php @@ -109,13 +109,13 @@ public function test_updates_bookmark_for_deletions_before_both_sides() { ); } - public function test_replaces_inside_contents() { + public function test_replaces_inner_contents() { $p = new WP_HTML_Tag_Processor( '
    Before
    Inside
    After
    ' ); $p->next_tag( [ 'class_name' => 'inner' ] ); $p->set_bookmark( 'start' ); $p->next_tag( [ 'tag_name' => 'div', 'tag_closers' => 'visit' ] ); $p->set_bookmark( 'end' ); - $p->dangerously_replace( 'start', 'end', '--', 'inside' ); + $p->dangerously_replace( 'start', 'end', '--', 'inner' ); $this->assertEquals( '
    Before
    --
    After
    ', @@ -136,4 +136,105 @@ public function test_replaces_outside_contents() { $p->get_updated_html() ); } + + public function test_replaces_single_token() { + $p = new WP_HTML_Tag_Processor( 'This is an tag.' ); + $p->next_tag(); + $p->set_bookmark( 'image' ); + $p->dangerously_replace( 'image', 'image', '(image)' ); + + $this->assertEquals( + 'This is an (image) tag.', + $p->get_updated_html() + ); + } + + public function test_does_nothing_when_replacing_inner_of_single_token() { + $p = new WP_HTML_Tag_Processor( 'This is an tag.' ); + $p->next_tag(); + $p->set_bookmark( 'image' ); + $p->dangerously_replace( 'image', 'image', '(image)', 'inner' ); + + $this->assertEquals( + 'This is an tag.', + $p->get_updated_html() + ); + } + + public function test_does_nothing_when_given_twisted_bookmarks() { + $p = new WP_HTML_Tag_Processor( '
    ' ); + $p->next_tag(); + $p->set_bookmark( 'first' ); + $p->next_tag(); + $p->set_bookmark( 'second' ); + $p->dangerously_replace( 'second', 'first', '--' ); + + $this->assertEquals( + '
    ', + $p->get_updated_html() + ); + } + + public function test_bookmarks_deactive_when_bookmarked_token_disappears() { + $p = new WP_HTML_Tag_Processor( '
    Before
    Inside
    After
    ' ); + $p->next_tag(); + $p->set_bookmark( 'first' ); + $p->next_tag(); + $p->set_bookmark( 'inner_start' ); + $p->next_tag( [ 'tag_closers' => 'visit' ] ); + $p->set_bookmark( 'inner_end' ); + $p->dangerously_replace( 'first', 'inner_end', '' ); + + $this->expectException( Exception::class ); + $p->seek( 'inner_start' ); + + $p->set_attribute( 'wonky', true ); + + $this->assertEquals( + 'After', + $p->get_updated_html() + ); + } + + public function test_gets_inner_content() { + $p = new WP_HTML_Tag_Processor( '
    Before
    Inside
    After
    ' ); + $p->next_tag(); + $p->set_bookmark( 'start' ); + $p->next_tag( [ 'tag_closers' => 'visit', 'match_offset' => 3 ] ); + $p->set_bookmark( 'end' ); + + $this->assertEquals( + 'Before
    Inside
    After', + $p->dangerously_get_contents( 'start', 'end', 'inner' ) + ); + } + + public function test_gets_outer_content() { + $p = new WP_HTML_Tag_Processor( '
    Before
    Inside
    After
    ' ); + $p->next_tag( [ 'class_name' => 'start' ] ); + $p->set_bookmark( 'start' ); + $p->next_tag( [ 'tag_closers' => 'visit' ] ); + $p->set_bookmark( 'end' ); + + $this->assertEquals( + '
    Inside
    ', + $p->dangerously_get_contents( 'start', 'end' ) + ); + } + + public function test_can_replace_parent_with_children() { + $p = new WP_HTML_Tag_Processor( '

    Unwrapping HTML

    Blah blah

    ' ); + $p->next_tag( [ 'class_name' => 'wrapper' ] ); + $p->set_bookmark( 'start' ); + $p->next_tag( [ 'tag_name' => 'div', 'tag_closers' => 'visit' ] ); + $p->set_bookmark( 'end' ); + + $inner_html = $p->dangerously_get_contents( 'start', 'end', 'inner' ); + $p->dangerously_replace( 'start', 'end', $inner_html, 'outer' ); + + $this->assertEquals( + '

    Unwrapping HTML

    Blah blah

    ', + $p->get_updated_html() + ); + } } From a421ae3ad241daee56ab4ee3e248cd5846ea2b32 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 25 Nov 2022 16:26:13 -0700 Subject: [PATCH 10/11] Add test demonstrating an unsafe replace-inner-html function. --- .../wp-html-tag-processor-bookmark-test.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/phpunit/html/wp-html-tag-processor-bookmark-test.php b/phpunit/html/wp-html-tag-processor-bookmark-test.php index 6fd4e286e1cefa..9de046c71bd24d 100644 --- a/phpunit/html/wp-html-tag-processor-bookmark-test.php +++ b/phpunit/html/wp-html-tag-processor-bookmark-test.php @@ -237,4 +237,32 @@ public function test_can_replace_parent_with_children() { $p->get_updated_html() ); } + + public function test_can_write_dangerous_functions_to_replace_inner_html() { + $replace_inner_html = function ( WP_HTML_Tag_Processor $p, $html ) { + $tag = $p->get_tag(); + $p->set_bookmark( '__start_of_node' ); + + $depth = 1; + while ( $depth > 0 && $p->next_tag( [ 'tag_name' => $tag, 'tag_closers' => 'visit' ] ) ) { + $depth += $p->is_tag_closer() ? -1 : 1; + + if ( $depth === 0 ) { + $p->set_bookmark( '__end_of_node' ); + break; + } + } + + $p->dangerously_replace( '__start_of_node', '__end_of_node', $html, 'inner' ); + }; + + $p = new WP_HTML_Tag_Processor( '

    Unwrapping HTML

    Blah blah

    untouched
    ' ); + $p->next_tag( [ 'class_name' => 'wrapper' ] ); + + $replace_inner_html( $p, 'Weee!' ); + $this->assertEquals( + '

    Unwrapping HTML

    Weee!
    untouched
    ', + $p->get_updated_html() + ); + } } From ff47c9ded804d4acea9c1ad9e451a032f8b67a37 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 25 Nov 2022 16:28:00 -0700 Subject: [PATCH 11/11] fixup! Add test demonstrating an unsafe replace-inner-html function. --- phpunit/html/wp-html-tag-processor-bookmark-test.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpunit/html/wp-html-tag-processor-bookmark-test.php b/phpunit/html/wp-html-tag-processor-bookmark-test.php index 9de046c71bd24d..b7e05a0a212f91 100644 --- a/phpunit/html/wp-html-tag-processor-bookmark-test.php +++ b/phpunit/html/wp-html-tag-processor-bookmark-test.php @@ -254,6 +254,8 @@ public function test_can_write_dangerous_functions_to_replace_inner_html() { } $p->dangerously_replace( '__start_of_node', '__end_of_node', $html, 'inner' ); + $p->release_bookmark( '__start_of_node' ); + $p->release_bookmark( '__end_of_node' ); }; $p = new WP_HTML_Tag_Processor( '

    Unwrapping HTML

    Blah blah

    untouched
    ' );