From bd368510e637b96037ac9dbf3cda0851400da802 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Tue, 21 Feb 2023 15:02:49 -0700 Subject: [PATCH 1/6] WIP: Introduce directive processor The directive processor builds on the HTML Tag Processor to provide additional functionality, assuming normative markup generated from React, assuming that all tag directives contain a balanced closer. This will break in practice, and is probably entirely wrong as-is. The purpose of this commit is to introduce the idea and explore using the code to get a feel for whether it will work. --- src/class-wp-directive-processor.php | 92 ++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/class-wp-directive-processor.php diff --git a/src/class-wp-directive-processor.php b/src/class-wp-directive-processor.php new file mode 100644 index 00000000..1a346efb --- /dev/null +++ b/src/class-wp-directive-processor.php @@ -0,0 +1,92 @@ +might_have_directives = false; + } + } + + public function next_directive() { + if ( false === $this->might_have_directives ) { + return false; + } + + while ( $this->next_tag( array( 'tag_closers' => 'visit' ) ) ) { + $tag_name = $this->get_tag(); + if ( 0 === stripos( self::DIRECTIVE_PREFIX, $tag_name ) ) { + return true; + } + + $attribute_directives = $this->get_attribute_names_with_prefix( self::DIRECTIVE_PREFIX ); + if ( 0 < count( $attribute_directives ) ) { + return true; + } + } + + return false; + } + + public function next_balanced_closer() { + $depth = 0; + + $tag_name = $this->get_tag(); + while ( $this->next_tag( array( 'tag_name' => $tag_name, 'tag_closers' => 'visit' ) ) ) { + if ( ! $this->is_tag_closer() ) { + $depth++; + continue; + } + + if ( 0 === $depth ) { + return true; + } + + $depth--; + } + + return false; + } + + public function get_inner_html() { + $this->set_bookmark( 'start' ); + if ( ! $this->next_balanced_closer() ) { + $this->release_bookmark( 'start' ); + return false; + } + $this->set_bookmark( 'end' ); + + $start = $this->bookmarks['start']->end + 1; + $end = $this->bookmarks['end']->start; + + $this->release_bookmark( 'start' ); + $this->release_bookmark( 'end' ); + + return substr( $this->html, $start, $end - $start ); + } + + public function set_inner_html( $new_html ) { + $this->set_bookmark( 'start' ); + if ( ! $this->next_balanced_closer() ) { + $this->release_bookmark( 'start' ); + return false; + } + $this->set_bookmark( 'end' ); + + $start = $this->bookmarks['start']->end + 1; + $end = $this->bookmarks['end']->start; + + $this->release_bookmark( 'start' ); + $this->release_bookmark( 'end' ); + + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $start, $end, $new_html ); + return true; + } +} \ No newline at end of file From 5747eb38f1134a80718e8bbc822649a26a0ac5b7 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 2 Mar 2023 12:13:50 +0100 Subject: [PATCH 2/6] Move to directives/ dir --- src/{ => directives}/class-wp-directive-processor.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{ => directives}/class-wp-directive-processor.php (100%) diff --git a/src/class-wp-directive-processor.php b/src/directives/class-wp-directive-processor.php similarity index 100% rename from src/class-wp-directive-processor.php rename to src/directives/class-wp-directive-processor.php From d0aae6fb277d3a6405389e046530caa83c69bdc8 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 2 Mar 2023 12:14:03 +0100 Subject: [PATCH 3/6] Add basic test coverage --- phpunit/directives/wp-directive-processor.php | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 phpunit/directives/wp-directive-processor.php diff --git a/phpunit/directives/wp-directive-processor.php b/phpunit/directives/wp-directive-processor.php new file mode 100644 index 00000000..83ba643c --- /dev/null +++ b/phpunit/directives/wp-directive-processor.php @@ -0,0 +1,98 @@ +outside
inside
'; + + public function test_get_inner_html_returns_correct_result() { + $tags = new WP_Directive_Processor( self::HTML ); + + $tags->next_tag( 'section' ); + $this->assertSame( '
inside
', $tags->get_inner_html() ); + } + + public function test_set_inner_html_on_void_element_has_no_effect() { + $tags = new WP_Directive_Processor( self::HTML ); + + $tags->next_tag( 'img' ); + $content = $tags->set_inner_html( 'This is the new img content' ); + $this->assertFalse( $content ); + $this->assertSame( self::HTML, $tags->get_updated_html() ); + } + + public function test_set_inner_html_sets_content_correctly() { + $tags = new WP_Directive_Processor( self::HTML ); + + $tags->next_tag( 'section' ); + $tags->set_inner_html( 'This is the new section content.' ); + $this->assertSame( '
outside
This is the new section content.
', $tags->get_updated_html() ); + } + + public function test_set_inner_html_updates_bookmarks_correctly() { + $tags = new WP_Directive_Processor( self::HTML ); + + $tags->next_tag( 'div' ); + $tags->set_bookmark( 'start' ); + $tags->next_tag( 'img' ); + $this->assertSame( 'IMG', $tags->get_tag() ); + $tags->set_bookmark( 'after' ); + $tags->seek( 'start' ); + + $tags->set_inner_html( 'This is the new div content.' ); + $this->assertSame( '
This is the new div content.
inside
', $tags->get_updated_html() ); + $tags->seek( 'after' ); + $this->assertSame( 'IMG', $tags->get_tag() ); + } + + public function test_set_inner_html_subsequent_updates_on_the_same_tag_work() { + $tags = new WP_Directive_Processor( self::HTML ); + + $tags->next_tag( 'section' ); + $tags->set_inner_html( 'This is the new section content.' ); + $tags->set_inner_html( 'This is the even newer section content.' ); + $this->assertSame( '
outside
This is the even newer section content.
', $tags->get_updated_html() ); + } + + public function test_set_inner_html_followed_by_set_attribute_works() { + $tags = new WP_Directive_Processor( self::HTML ); + + $tags->next_tag( 'section' ); + $tags->set_inner_html( 'This is the new section content.' ); + $tags->set_attribute( 'id', 'thesection' ); + $this->assertSame( '
outside
This is the new section content.
', $tags->get_updated_html() ); + } + + public function test_set_inner_html_preceded_by_set_attribute_works() { + $tags = new WP_Directive_Processor( self::HTML ); + + $tags->next_tag( 'section' ); + $tags->set_attribute( 'id', 'thesection' ); + $tags->set_inner_html( 'This is the new section content.' ); + $this->assertSame( '
outside
This is the new section content.
', $tags->get_updated_html() ); + } + + public function test_set_inner_html_invalidates_bookmarks_that_point_to_replaced_content() { + $tags = new WP_Directive_Processor( self::HTML ); + + $tags->next_tag( 'section' ); + $tags->set_bookmark( 'start' ); + $tags->next_tag( 'img' ); + $tags->set_bookmark( 'replaced' ); + $tags->seek( 'start' ); + + $tags->set_inner_html( 'This is the new section content.' ); + $this->assertSame( '
outside
This is the new section content.
', $tags->get_updated_html() ); + + $this->expectExceptionMessage( 'Invalid bookmark name' ); + $successful_seek = $tags->seek( 'replaced' ); + $this->assertFalse( $successful_seek ); + } +} From f124dbff1847757469adc32319ba3af412719111 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 2 Mar 2023 12:21:45 +0100 Subject: [PATCH 4/6] Add basic test for next_balanced_closer --- phpunit/directives/wp-directive-processor.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/phpunit/directives/wp-directive-processor.php b/phpunit/directives/wp-directive-processor.php index 83ba643c..d391de2e 100644 --- a/phpunit/directives/wp-directive-processor.php +++ b/phpunit/directives/wp-directive-processor.php @@ -12,6 +12,15 @@ class WP_Directive_Processor_Test extends WP_UnitTestCase { const HTML = '
outside
inside
'; + public function test_next_balanced_closer_proceeds_to_correct_tag() { + $tags = new WP_Directive_Processor( self::HTML ); + + $tags->next_tag( 'section' ); + $tags->next_balanced_closer(); + $this->assertSame( 'SECTION', $tags->get_tag() ); + $this->assertTrue( $tags->is_tag_closer() ); + } + public function test_get_inner_html_returns_correct_result() { $tags = new WP_Directive_Processor( self::HTML ); From 5893878244ebfb9d7eb391663b1e95a55d04e73a Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 2 Mar 2023 12:24:51 +0100 Subject: [PATCH 5/6] More test coverage for next_balanced_tag --- phpunit/directives/wp-directive-processor.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/phpunit/directives/wp-directive-processor.php b/phpunit/directives/wp-directive-processor.php index d391de2e..8ca135de 100644 --- a/phpunit/directives/wp-directive-processor.php +++ b/phpunit/directives/wp-directive-processor.php @@ -21,6 +21,16 @@ public function test_next_balanced_closer_proceeds_to_correct_tag() { $this->assertTrue( $tags->is_tag_closer() ); } + public function test_next_balanced_closer_proceeds_to_correct_tag_for_nested_tag() { + $tags = new WP_Directive_Processor( self::HTML ); + + $tags->next_tag( 'div' ); + $tags->next_tag( 'div' ); + $tags->next_balanced_closer(); + $this->assertSame( 'DIV', $tags->get_tag() ); + $this->assertTrue( $tags->is_tag_closer() ); + } + public function test_get_inner_html_returns_correct_result() { $tags = new WP_Directive_Processor( self::HTML ); From 51bf3403a5f2d1ea628854f838dd3ce6f300e4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 2 Mar 2023 17:08:26 +0100 Subject: [PATCH 6/6] Implement get_outer_html and set_outer_html --- .../class-wp-directive-processor.php | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/directives/class-wp-directive-processor.php b/src/directives/class-wp-directive-processor.php index 1a346efb..324df9f5 100644 --- a/src/directives/class-wp-directive-processor.php +++ b/src/directives/class-wp-directive-processor.php @@ -89,4 +89,49 @@ public function set_inner_html( $new_html ) { $this->lexical_updates[] = new WP_HTML_Text_Replacement( $start, $end, $new_html ); return true; } -} \ No newline at end of file + + public function get_outer_html( $new_html ) { + $this->set_bookmark( 'start' ); + if ( ! $this->next_balanced_closer() ) { + $this->release_bookmark( 'start' ); + return false; + } + $this->set_bookmark( 'end' ); + + $start = $this->bookmarks['start']->start; + $end = $this->bookmarks['end']->end + 1; + + $this->release_bookmark( 'start' ); + $this->release_bookmark( 'end' ); + + // For consistency with set_outer_html: + $this->next_tag(); + return substr( $this->html, $start, $end - $start ); + } + + public function set_outer_html( $new_html ) { + $this->set_bookmark( 'start' ); + if ( ! $this->next_balanced_closer() ) { + $this->release_bookmark( 'start' ); + return false; + } + $this->set_bookmark( 'end' ); + + $start = $this->bookmarks['start']->start; + $end = $this->bookmarks['end']->end + 1; + + $this->release_bookmark( 'start' ); + $this->release_bookmark( 'end' ); + + $this->next_tag(); + $this->set_bookmark( 'next' ); + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $start, $end, $new_html ); + // updates before the current position are not supported well and we end + // up at an invalid combination of copied bytes and parsed bytes index. + // bookmarks are updated correctly, though, so seek() makes it right again. + $this->seek('next'); + $this->release_bookmark( 'next' ); + return true; + } + +}