Skip to content

Commit

Permalink
Add set_inner_markup()
Browse files Browse the repository at this point in the history
  • Loading branch information
dmsnell committed Aug 11, 2023
1 parent 947fad4 commit 76e019e
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 23 deletions.
90 changes: 67 additions & 23 deletions src/wp-includes/html-api/class-wp-html-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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;
}

Expand Down
123 changes: 123 additions & 0 deletions tests/phpunit/tests/html-api/wpHtmlProcessorSetInnerMarkup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php
/**
* Unit tests covering WP_HTML_Processor::set_inner_markup()
*
* @package WordPress
* @subpackage HTML-API
*
* @since 6.4.0
*
* @group html-api
*
* @coversDefaultClass WP_HTML_Processor
*/
class Tests_HtmlApi_WpHtmlProcessorSetInnerMarkup extends WP_UnitTestCase {
/**
* @ticket {TICKET_NUMBER}
*
* @covers WP_HTML_Processor::set_inner_markup
*
* @dataProvider data_html_with_inner_markup_changes
*
* @since 6.4.0
*
* @param string $html_with_target_node HTML containing a node with the `target` attribute set.
* @param string $new_markup HTML for replacing the inner markup of the target node.
* @param string $expected_output New HTMl after replacing inner markup.
*/
public function test_replaces_inner_html_appropriately( $html_with_target_node, $new_markup, $expected_output ) {
$p = WP_HTML_Processor::createFragment( $html_with_target_node );

while ( $p->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( '<img target>', '', '<img target>' ),
'Void element inside text' => array( 'before<img src="atat.png" loading=lazy target>after', '', 'before<img src="atat.png" loading=lazy target>after' ),
'Void element inside another element' => array( '<p>Look at this <img target> graph.</p>', '', '<p>Look at this <img target> graph.</p>' ),
'Empty elements' => array( '<div target></div>', '', '<div target></div>' ),
'Element with nested tags' => array( '<div target>inside <span>the</span> div</div>', '', '<div target></div>' ),
'Element inside another element' => array( '<div>inside <span target>the</span> div</div>', '', '<div>inside <span target></span> div</div>' ),
'Unclosed element' => array( '<div target>This is <em>all</em> inside the DIV', '', '<div target>' ),
'Unclosed nested element' => array( '<div><p target>One thought<p>And another', '', '<div><p target><p>And another' ),
'Partially-closed element' => array( '<div target>This is <em>all</em> inside the DIV</div', '', '<div target>' ),
'Implicitly-closed element' => array( '<div><p target>Inside the P</div>Outside the P</p>', '', '<div><p target></div>Outside the P</p>' ),
);

$inner_html = <<<HTML
<p>This is inside the <strong>Match</strong></p>
<p><img></p>
<div>
<figure>
<img>
<figcaption>Look at the <strike>picture</strike> photograph.</figcaption>
</figure>
</div>
HTML;

$prefix = <<<HTML
<div>
<p>This is not in the match.
<p>This is another paragraph not <a href="#">in</a> the match.
</div>
<div target>
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 = <<<HTML
</div>
<div>
<p>This is also note in the match.</p>
</div>
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( '<div><p><span>The <code target>cursor</code> should not move <em next-target>unexpectedly</em>.</span></p></div>' );

while ( $p->next_tag() && null === $p->get_attribute( 'target' ) ) {
continue;
}

$this->assertTrue( $p->set_inner_markup( '<img next-target>' ) );
$this->assertSame(
'<div><p><span>The <code target><img next-target></code> should not move <em next-target>unexpectedly</em>.</span></p></div>',
$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." );
}
}

0 comments on commit 76e019e

Please sign in to comment.