Skip to content

Commit 76e019e

Browse files
committed
Add set_inner_markup()
1 parent 947fad4 commit 76e019e

2 files changed

Lines changed: 190 additions & 23 deletions

File tree

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

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -442,20 +442,20 @@ public function get_inner_markup() {
442442
return null;
443443
}
444444

445-
$this->set_bookmark( 'start' );
445+
$this->set_bookmark( 'opener' );
446446
$found_tag = $this->step_until_tag_is_closed();
447-
$this->set_bookmark( 'end' );
447+
$this->set_bookmark( 'closer' );
448448

449449
if ( $found_tag ) {
450-
$inner_markup = $this->substr_bookmarks( 'after', 'start', 'before', 'end' );
450+
$inner_markup = $this->substr_bookmarks( 'after', 'opener', 'before', 'closer' );
451451
} else {
452452
// If there's no closing tag then the inner markup continues to the end of the document.
453-
$inner_markup = $this->substr_bookmark( 'after', 'start' );
453+
$inner_markup = $this->substr_bookmark( 'after', 'opener' );
454454
}
455455

456-
$this->seek( 'start' );
457-
$this->release_bookmark( 'start' );
458-
$this->release_bookmark( 'end' );
456+
$this->seek( 'opener' );
457+
$this->release_bookmark( 'opener' );
458+
$this->release_bookmark( 'closer' );
459459

460460
return $inner_markup;
461461
}
@@ -484,31 +484,75 @@ public function get_outer_markup() {
484484
return null;
485485
}
486486

487-
$this->set_bookmark( 'start' );
487+
$this->set_bookmark( 'opener' );
488488
$start_tag = $this->current_token->node_name;
489489
$found_tag = $this->step_until_tag_is_closed();
490-
$this->set_bookmark( 'end' );
490+
$this->set_bookmark( 'closer' );
491491

492492
if ( $found_tag ) {
493493
$did_close = $this->get_tag() === $start_tag && $this->is_tag_closer();
494494
$end_position = $did_close ? 'after' : 'before';
495-
$outer_markup = $this->substr_bookmarks( 'before', 'start', $end_position, 'end' );
495+
$outer_markup = $this->substr_bookmarks( 'before', 'opener', $end_position, 'closer' );
496496
} else {
497497
// If there's no closing tag then the outer markup continues to the end of the document.
498-
$outer_markup = $this->substr_bookmark( 'before', 'start' );
498+
$outer_markup = $this->substr_bookmark( 'before', 'opener' );
499499
}
500500

501-
$this->seek( 'start' );
502-
$this->release_bookmark( 'start' );
503-
$this->release_bookmark( 'end' );
501+
$this->seek( 'opener' );
502+
$this->release_bookmark( 'opener' );
503+
$this->release_bookmark( 'closer' );
504504

505505
return $outer_markup;
506506
}
507507

508+
/**
509+
* Replaces the raw HTML of the currently-matched tag's inner markup with new HTML.
510+
* This replaces the content between the tag opener and tag closer.
511+
*
512+
* @throws Exception When unable to set bookmark for internal tracking.
513+
*
514+
* @since 6.4.0
515+
*
516+
* @param string $new_html
517+
* @return bool|null Whether the contents were updated.
518+
*/
519+
public function set_inner_markup( $new_html ) {
520+
if ( null === $this->get_tag() ) {
521+
return null;
522+
}
523+
524+
$this->set_bookmark( 'opener' );
525+
$start_tag = $this->current_token->node_name;
526+
527+
if ( self::is_void( $start_tag ) ) {
528+
$this->release_bookmark( 'opener' );
529+
return true;
530+
}
531+
532+
$found_tag = $this->step_until_tag_is_closed();
533+
$this->set_bookmark( 'closer' );
534+
535+
if ( $found_tag ) {
536+
$this->replace_using_bookmarks( $new_html, 'after', 'opener', 'before', 'closer' );
537+
} else {
538+
// If there's no closing tag then the inner markup continues to the end of the document.
539+
$this->replace_using_bookmark( $new_html, 'after', 'opener' );
540+
}
541+
542+
$this->seek( 'opener' );
543+
$this->release_bookmark( 'opener' );
544+
$this->release_bookmark( 'closer' );
545+
return true;
546+
}
547+
508548
/**
509549
* Replaces the raw HTML of the currently-matched tag with new HTML.
510550
* This replaces the entire contents of the tag including the tag itself.
511551
*
552+
* @throws Exception When unable to set bookmark for internal tracking.
553+
*
554+
* @since 6.4.0
555+
*
512556
* @param string $new_html
513557
* @return bool|null Whether the contents were updated.
514558
*/
@@ -517,30 +561,30 @@ public function set_outer_markup( $new_html ) {
517561
return null;
518562
}
519563

520-
$this->set_bookmark( 'start' );
564+
$this->set_bookmark( 'opener' );
521565
$start_tag = $this->current_token->node_name;
522566

523567
if ( self::is_void( $start_tag ) ) {
524-
$this->replace_using_bookmarks( $new_html, 'before', 'start', 'after', 'start' );
525-
$this->release_bookmark( 'start' );
568+
$this->replace_using_bookmarks( $new_html, 'before', 'opener', 'after', 'opener' );
569+
$this->release_bookmark( 'opener' );
526570
return true;
527571
}
528572

529573
$found_tag = $this->step_until_tag_is_closed();
530-
$this->set_bookmark( 'end' );
574+
$this->set_bookmark( 'closer' );
531575

532576
if ( $found_tag ) {
533577
$did_close = $this->get_tag() === $start_tag && $this->is_tag_closer();
534578
$end_position = $did_close ? 'after' : 'before';
535-
$this->replace_using_bookmarks( $new_html, 'before', 'start', $end_position, 'end' );
579+
$this->replace_using_bookmarks( $new_html, 'before', 'opener', $end_position, 'closer' );
536580
} else {
537581
// If there's no closing tag then the outer markup continues to the end of the document.
538-
$this->replace_using_bookmark( $new_html, 'before', 'start' );
582+
$this->replace_using_bookmark( $new_html, 'before', 'opener' );
539583
}
540584

541-
$this->seek( 'start' );
542-
$this->release_bookmark( 'start' );
543-
$this->release_bookmark( 'end' );
585+
$this->seek( 'opener' );
586+
$this->release_bookmark( 'opener' );
587+
$this->release_bookmark( 'closer' );
544588
return true;
545589
}
546590

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_inner_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_WpHtmlProcessorSetInnerMarkup extends WP_UnitTestCase {
15+
/**
16+
* @ticket {TICKET_NUMBER}
17+
*
18+
* @covers WP_HTML_Processor::set_inner_markup
19+
*
20+
* @dataProvider data_html_with_inner_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 inner markup of the target node.
26+
* @param string $expected_output New HTMl after replacing inner markup.
27+
*/
28+
public function test_replaces_inner_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_inner_markup( $new_markup ), 'Failed to set inner markup.' );
36+
$this->assertSame( $expected_output, $p->get_updated_html(), 'Failed to appropriately set inner markup.' );
37+
}
38+
39+
/**
40+
* Data provider.
41+
*
42+
* @return array[]
43+
*/
44+
public function data_html_with_inner_markup_changes() {
45+
$data = array(
46+
'Void element' => array( '<img target>', '', '<img target>' ),
47+
'Void element inside text' => array( 'before<img src="atat.png" loading=lazy target>after', '', 'before<img src="atat.png" loading=lazy target>after' ),
48+
'Void element inside another element' => array( '<p>Look at this <img target> graph.</p>', '', '<p>Look at this <img target> graph.</p>' ),
49+
'Empty elements' => array( '<div target></div>', '', '<div target></div>' ),
50+
'Element with nested tags' => array( '<div target>inside <span>the</span> div</div>', '', '<div target></div>' ),
51+
'Element inside another element' => array( '<div>inside <span target>the</span> div</div>', '', '<div>inside <span target></span> div</div>' ),
52+
'Unclosed element' => array( '<div target>This is <em>all</em> inside the DIV', '', '<div target>' ),
53+
'Unclosed nested element' => array( '<div><p target>One thought<p>And another', '', '<div><p target><p>And another' ),
54+
'Partially-closed element' => array( '<div target>This is <em>all</em> inside the DIV</div', '', '<div target>' ),
55+
'Implicitly-closed element' => array( '<div><p target>Inside the P</div>Outside the P</p>', '', '<div><p target></div>Outside the P</p>' ),
56+
);
57+
58+
$inner_html = <<<HTML
59+
<p>This is inside the <strong>Match</strong></p>
60+
<p><img></p>
61+
<div>
62+
<figure>
63+
<img>
64+
<figcaption>Look at the <strike>picture</strike> photograph.</figcaption>
65+
</figure>
66+
</div>
67+
HTML;
68+
69+
$prefix = <<<HTML
70+
<div>
71+
<p>This is not in the match.
72+
<p>This is another paragraph not <a href="#">in</a> the match.
73+
</div>
74+
<div target>
75+
HTML;
76+
77+
/*
78+
* Removing the indent on this first line keeps the test easy to reason about,
79+
* otherwise extra indents appear after removing the inner content, because
80+
* that indentation before and after is whitespace and not part of the tag.
81+
*/
82+
$suffix = <<<HTML
83+
</div>
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 inner 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_inner_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_inner_markup( '<img next-target>' ) );
112+
$this->assertSame(
113+
'<div><p><span>The <code target><img next-target></code> should not move <em next-target>unexpectedly</em>.</span></p></div>',
114+
$p->get_updated_html(),
115+
'Failed to replace appropriate inner markup.'
116+
);
117+
118+
$this->assertSame( 'CODE', $p->get_tag(), "Should have remained on CODE, but found {$p->get_tag()} instead." );
119+
120+
$p->next_tag();
121+
$this->assertNotNull( $p->get_attribute( 'next-target' ), "Expected to move to inserted IMG element, but found {$p->get_tag()} instead." );
122+
}
123+
}

0 commit comments

Comments
 (0)