diff --git a/phpunit/directives/wp-directive-processor.php b/phpunit/directives/wp-directive-processor.php new file mode 100644 index 00000000..8ca135de --- /dev/null +++ b/phpunit/directives/wp-directive-processor.php @@ -0,0 +1,117 @@ +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_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 ); + + $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 ); + } +} diff --git a/src/directives/class-wp-directive-processor.php b/src/directives/class-wp-directive-processor.php new file mode 100644 index 00000000..324df9f5 --- /dev/null +++ b/src/directives/class-wp-directive-processor.php @@ -0,0 +1,137 @@ +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; + } + + 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; + } + +}