Skip to content

Commit 7358023

Browse files
committed
Add set_outer_markup()
1 parent 5732aee commit 7358023

4 files changed

Lines changed: 212 additions & 12 deletions

File tree

src/wp-includes/html-api/class-wp-html-processor.php

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,45 @@ public function get_raw_outer_markup() {
560560
return $outer_markup;
561561
}
562562

563+
/**
564+
* Replaces the raw HTML of the currently-matched tag with new HTML.
565+
* This replaces the entire contents of the tag including the tag itself.
566+
*
567+
* @param string $new_html
568+
* @return bool|null Whether the contents were updated.
569+
*/
570+
public function set_raw_outer_markup( $new_html ) {
571+
if ( null === $this->get_tag() ) {
572+
return null;
573+
}
574+
575+
$this->set_bookmark( 'start' );
576+
$start_tag = $this->current_token->node_name;
577+
578+
if ( self::is_void( $start_tag ) ) {
579+
$this->replace_using_bookmarks( $new_html, 'before', 'start', 'after', 'start' );
580+
$this->release_bookmark( 'start' );
581+
return true;
582+
}
583+
584+
$found_tag = $this->step_until_tag_is_closed();
585+
$this->set_bookmark( 'end' );
586+
587+
if ( $found_tag ) {
588+
$did_close = $this->get_tag() === $start_tag && $this->is_tag_closer();
589+
$end_position = $did_close ? 'after' : 'before';
590+
$this->replace_using_bookmarks( $new_html, 'before', 'start', $end_position, 'end' );
591+
} else {
592+
// If there's no closing tag then the outer markup continues to the end of the document.
593+
$this->replace_using_bookmark( $new_html, 'before', 'start' );
594+
}
595+
596+
$this->seek( 'start' );
597+
$this->release_bookmark( 'start' );
598+
$this->release_bookmark( 'end' );
599+
return true;
600+
}
601+
563602
/**
564603
* Steps through the HTML document and stop at the next tag, if any.
565604
*
@@ -940,17 +979,55 @@ private function step_until_tag_is_closed() {
940979
return $found_tag;
941980
}
942981

982+
/**
983+
* Replaces content in the HTML document from a bookmark to the end of the document.
984+
*
985+
* @since 6.4.0
986+
*
987+
* @param string $html New HTML to insert into document.
988+
* @param string $start_position "before" to clip before bookmark, "after" to clip after.
989+
* @param string $start_bookmark_name Bookmark name at which to start clipping.
990+
*/
991+
private function replace_using_bookmark( $html, $start_position, $start_bookmark_name ) {
992+
$start_bookmark = $this->bookmarks[ "_{$start_bookmark_name}" ];
993+
$start_offset = 'before' === $start_position ? $start_bookmark->start : $start_bookmark->end + 1;
994+
995+
$this->lexical_updates[] = new WP_HTML_Text_Replacement( $start_offset, strlen( $this->html ), $html );
996+
$this->apply_attributes_updates();
997+
}
998+
999+
/**
1000+
* Replaces content in the HTML document from one bookmark to another.
1001+
*
1002+
* @since 6.4.0
1003+
*
1004+
* @param string $html New HTML to insert into document.
1005+
* @param string $start_position "before" to clip before bookmark, "after" to clip after.
1006+
* @param string $start_bookmark_name Bookmark name at which to start clipping.
1007+
* @param string $end_position "before" to clip before bookmark, "after" to clip after.
1008+
* @param string $end_bookmark_name Bookmark name at which to end clipping.
1009+
*/
1010+
private function replace_using_bookmarks( $html, $start_position, $start_bookmark_name, $end_position, $end_bookmark_name ) {
1011+
$start_bookmark = $this->bookmarks[ "_{$start_bookmark_name}" ];
1012+
$end_bookmark = $this->bookmarks[ "_{$end_bookmark_name}" ];
1013+
$start_offset = 'before' === $start_position ? $start_bookmark->start : $start_bookmark->end + 1;
1014+
$end_offset = 'before' === $end_position ? $end_bookmark->start : $end_bookmark->end + 1;
1015+
1016+
$this->lexical_updates[] = new WP_HTML_Text_Replacement( $start_offset, $end_offset, $html );
1017+
$this->apply_attributes_updates();
1018+
}
1019+
9431020
/**
9441021
* Returns a substring of the input HTML document from a bookmark until the end.
9451022
*
9461023
* @since 6.4.0
9471024
*
948-
* @param string $start_position "before" to clip before bookmark, "after" to clip after.
949-
* @param string $start Bookmark name at which to start clipping.
1025+
* @param string $start_position "before" to clip before bookmark, "after" to clip after.
1026+
* @param string $start_bookmark_name Bookmark name at which to start clipping.
9501027
* @return string Clipped substring of input HTMl document.
9511028
*/
952-
private function substr_bookmark( $start_position, $start ) {
953-
$start_bookmark = $this->bookmarks[ "_{$start}" ];
1029+
private function substr_bookmark( $start_position, $start_bookmark_name ) {
1030+
$start_bookmark = $this->bookmarks[ "_{$start_bookmark_name}" ];
9541031
$start_offset = 'before' === $start_position ? $start_bookmark->start : $start_bookmark->end + 1;
9551032

9561033
return substr( $this->html, $start_offset );
@@ -961,15 +1038,15 @@ private function substr_bookmark( $start_position, $start ) {
9611038
*
9621039
* @since 6.4.0
9631040
*
964-
* @param string $start_position "before" to clip before bookmark, "after" to clip after.
965-
* @param string $start Bookmark name at which to start clipping.
966-
* @param string $end_position "before" to clip before bookmark, "after" to clip after.
967-
* @param string $end Bookmark name at which to end clipping.
1041+
* @param string $start_position "before" to clip before bookmark, "after" to clip after.
1042+
* @param string $start_bookmark_name Bookmark name at which to start clipping.
1043+
* @param string $end_position "before" to clip before bookmark, "after" to clip after.
1044+
* @param string $end_bookmark_name Bookmark name at which to end clipping.
9681045
* @return string Clipped substring of input HTMl document.
9691046
*/
970-
private function substr_bookmarks( $start_position, $start, $end_position, $end ) {
971-
$start_bookmark = $this->bookmarks[ "_{$start}" ];
972-
$end_bookmark = $this->bookmarks[ "_{$end}" ];
1047+
private function substr_bookmarks( $start_position, $start_bookmark_name, $end_position, $end_bookmark_name ) {
1048+
$start_bookmark = $this->bookmarks[ "_{$start_bookmark_name}" ];
1049+
$end_bookmark = $this->bookmarks[ "_{$end_bookmark_name}" ];
9731050
$start_offset = 'before' === $start_position ? $start_bookmark->start : $start_bookmark->end + 1;
9741051
$end_offset = 'before' === $end_position ? $end_bookmark->start : $end_bookmark->end + 1;
9751052

src/wp-includes/html-api/class-wp-html-tag-processor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1584,7 +1584,7 @@ private function class_name_updates_to_attributes_updates() {
15841584
* @param int $shift_this_point Accumulate and return shift for this position.
15851585
* @return int How many bytes the given pointer moved in response to the updates.
15861586
*/
1587-
private function apply_attributes_updates( $shift_this_point = 0 ) {
1587+
protected function apply_attributes_updates( $shift_this_point = 0 ) {
15881588
if ( ! count( $this->lexical_updates ) ) {
15891589
return 0;
15901590
}

tests/phpunit/tests/html-api/wpHtmlProcessorSetOuterHtml.php renamed to tests/phpunit/tests/html-api/wpHtmlProcessorSetInnerMarkup.php

File renamed without changes.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
/**
3+
* Unit tests covering WP_HTML_Processor::set_raw_outer_markup()
4+
*
5+
* @package WordPress
6+
* @subpackage HTML-API
7+
*
8+
* @since 6.4.0
9+
*
10+
* @group html-api
11+
*
12+
* @coversDefaultClass WP_HTML_Processor
13+
*/
14+
class Tests_HtmlApi_WpHtmlProcessorSetOuterMarkup extends WP_UnitTestCase {
15+
/**
16+
* @ticket {TICKET_NUMBER}
17+
*
18+
* @covers WP_HTML_Processor::set_raw_outer_markup
19+
*
20+
* @dataProvider data_html_with_outer_markup_changes
21+
*
22+
* @since 6.4.0
23+
*
24+
* @param string $html_with_target_node HTML containing a node with the `target` attribute set.
25+
* @param string $new_markup HTML for replacing the outer markup of the target node.
26+
* @param string $expected_output New HTMl after replacing outer markup.
27+
*/
28+
public function test_replaces_outer_html_appropriately( $html_with_target_node, $new_markup, $expected_output ) {
29+
$p = WP_HTML_Processor::createFragment( $html_with_target_node );
30+
31+
while ( $p->next_tag() && null === $p->get_attribute( 'target' ) ) {
32+
continue;
33+
}
34+
35+
$this->assertTrue( $p->set_raw_outer_markup( $new_markup ), 'Failed to set outer markup.' );
36+
$this->assertSame( $expected_output, $p->get_updated_html(), 'Failed to appropriately set outer markup.' );
37+
}
38+
39+
/**
40+
* Data provider.
41+
*
42+
* @return array[]
43+
*/
44+
public function data_html_with_outer_markup_changes() {
45+
$data = array(
46+
'Void element' => array( '<img target>', '', '' ),
47+
'Void element inside text' => array( 'before<img src="atat.png" loading=lazy target>after', '', 'beforeafter' ),
48+
'Void element inside another element' => array( '<p>Look at this <img target> graph.</p>', '', '<p>Look at this graph.</p>' ),
49+
'Empty elements' => array( '<div target></div>', '', '' ),
50+
'Element with nested tags' => array( '<div target>inside <span>the</span> div</div>', '', '' ),
51+
'Element inside another element' => array( '<div>inside <span target>the</span> div</div>', '', '<div>inside div</div>' ),
52+
'Unclosed element' => array( '<div target>This is <em>all</em> inside the DIV', '', '' ),
53+
'Unclosed nested element' => array( '<div><p target>One thought<p>And another', '', '<div><p>And another' ),
54+
'Partially-closed element' => array( '<div target>This is <em>all</em> inside the DIV</div', '', '' ),
55+
'Implicitly-closed element' => array( '<div><p target>Inside the P</div>Outside the P</p>', '', '<div></div>Outside the P</p>' ),
56+
);
57+
58+
/*
59+
* Removing the indent on this variable keeps the test easy to reason about,
60+
* otherwise extra indents appear after removing the inner content, because
61+
* that indentation before and after is whitespace and not part of the tag.
62+
*/
63+
$inner_html = <<<HTML
64+
<div target>
65+
<p>This is inside the <strong>Match</strong></p>
66+
<p><img></p>
67+
<div>
68+
<figure>
69+
<img>
70+
<figcaption>Look at the <strike>picture</strike> photograph.</figcaption>
71+
</figure>
72+
</div>
73+
</div>
74+
HTML;
75+
76+
$prefix = <<<HTML
77+
<div>
78+
<p>This is not in the match.
79+
<p>This is another paragraph not <a href="#">in</a> the match.
80+
</div>
81+
HTML;
82+
83+
$suffix = <<<HTML
84+
<div>
85+
<p>This is also note in the match.</p>
86+
</div>
87+
HTML;
88+
89+
$data['Complicated inner nesting'] = array( $prefix . $inner_html . $suffix, '', $prefix . $suffix );
90+
91+
return $data;
92+
}
93+
94+
/**
95+
* Ensures that the cursor isn't moved when setting the outer markup. It should
96+
* remain at the same location as the tag opener where it was called.
97+
*
98+
* @ticket {TICKET_NUMBER}
99+
*
100+
* @covers WP_HTML_Processor::set_raw_outer_markup
101+
*
102+
* @since 6.4.0
103+
*/
104+
public function test_preserves_cursor() {
105+
$p = WP_HTML_Processor::createFragment( '<div><p><span>The <code target>cursor</code> should not move <em next-target>unexpectedly</em>.</span></p></div>' );
106+
107+
while ( $p->next_tag() && null === $p->get_attribute( 'target' ) ) {
108+
continue;
109+
}
110+
111+
$this->assertTrue( $p->set_raw_outer_markup( '<img>' ) );
112+
$this->assertSame(
113+
'<div><p><span>The <img> should not move <em next-target>unexpectedly</em>.</span></p></div>',
114+
$p->get_updated_html(),
115+
'Failed to replace appropriate outer markup.'
116+
);
117+
118+
$this->assertSame( 'IMG', $p->get_tag(), "Should have remained on IMG, but found {$p->get_tag()} instead." );
119+
120+
$p->next_tag();
121+
$this->assertNotNull( $p->get_attribute( 'next-target' ), "Expected to move to following EM element, but found {$p->get_tag()} instead." );
122+
}
123+
}

0 commit comments

Comments
 (0)