Skip to content

Commit 51db0ac

Browse files
committed
Merge branch 'html-api/improve-active-element-reconstruction' into html-api/try-active-formatting-attributes
2 parents b20e852 + 374cba3 commit 51db0ac

4 files changed

Lines changed: 136 additions & 49 deletions

File tree

src/wp-includes/html-api/class-wp-html-active-formatting-elements.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@ class WP_HTML_Active_Formatting_Elements {
4343
*/
4444
private $stack = array();
4545

46+
/**
47+
* Returns the node at the given 1-offset index in the list of active formatting elements.
48+
*
49+
* Do not use this method; it is meant to be used only by the HTML Processor.
50+
*
51+
* @since 6.7.0
52+
*
53+
* @access private
54+
*
55+
* @param int $index Number of nodes from the top node to return.
56+
* @return WP_HTML_Token|null Node at the given index in the stack, if one exists, otherwise null.
57+
*/
58+
public function at( $nth ) {
59+
return $this->stack[ $nth - 1 ];
60+
}
61+
4662
/**
4763
* Reports if a specific node is in the stack of active formatting elements.
4864
*

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

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5375,6 +5375,7 @@ private function get_adjusted_current_node(): ?WP_HTML_Token {
53755375
* > been explicitly closed.
53765376
*
53775377
* @since 6.4.0
5378+
* @since 6.7.0 Added additional support.
53785379
*
53795380
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
53805381
*
@@ -5383,34 +5384,89 @@ private function get_adjusted_current_node(): ?WP_HTML_Token {
53835384
* @return bool Whether any formatting elements needed to be reconstructed.
53845385
*/
53855386
private function reconstruct_active_formatting_elements(): bool {
5387+
$count = $this->state->active_formatting_elements->count();
53865388
/*
5387-
* > If there are no entries in the list of active formatting elements, then there is nothing
5388-
* > to reconstruct; stop this algorithm.
5389+
* > 1. If there are no entries in the list of active formatting elements,
5390+
* > then there is nothing to reconstruct; stop this algorithm.
53895391
*/
5390-
if ( 0 === $this->state->active_formatting_elements->count() ) {
5392+
if ( 0 === $count ) {
53915393
return false;
53925394
}
53935395

5394-
$last_entry = $this->state->active_formatting_elements->current_node();
5396+
$currently_at = $count;
5397+
$last_entry = $this->state->active_formatting_elements->at( $currently_at );
5398+
/*
5399+
* > 2. If the last (most recently added) entry in the list of active formatting
5400+
* > elements is a marker, or if it is an element that is in the stack of open
5401+
* > elements, then there is nothing to reconstruct; stop this algorithm.
5402+
*/
53955403
if (
5396-
5397-
/*
5398-
* > If the last (most recently added) entry in the list of active formatting elements is a marker;
5399-
* > stop this algorithm.
5400-
*/
54015404
'marker' === $last_entry->node_name ||
5402-
5403-
/*
5404-
* > If the last (most recently added) entry in the list of active formatting elements is an
5405-
* > element that is in the stack of open elements, then there is nothing to reconstruct;
5406-
* > stop this algorithm.
5407-
*/
54085405
$this->state->stack_of_open_elements->contains_node( $last_entry )
54095406
) {
54105407
return false;
54115408
}
54125409

5413-
$this->bail( 'Cannot reconstruct active formatting elements when advancing and rewinding is required.' );
5410+
/*
5411+
* > 3. Let entry be the last (most recently added) element
5412+
* > in the list of active formatting elements.
5413+
*/
5414+
$entry = $last_entry;
5415+
5416+
/*
5417+
* > 4. Rewind: If there are no entries before entry in the list of active
5418+
* > formatting elements, then jump to the step labeled create.
5419+
*/
5420+
rewind:
5421+
if ( 1 === $currently_at ) {
5422+
goto create;
5423+
}
5424+
5425+
/*
5426+
* > 5. Let entry be the entry one earlier than entry
5427+
* > in the list of active formatting elements.
5428+
*/
5429+
$entry = $this->state->active_formatting_elements->at( --$currently_at );
5430+
5431+
/*
5432+
* > 6. If entry is neither a marker nor an element that is also in
5433+
* > the stack of open elements, go to the step labeled rewind.
5434+
*/
5435+
if (
5436+
'marker' !== $entry->node_name &&
5437+
! $this->state->stack_of_open_elements->contains_node( $entry )
5438+
) {
5439+
goto rewind;
5440+
}
5441+
5442+
/*
5443+
* > 7. Advance: Let entry be the element one later than entry
5444+
* > in the list of active formatting elements.
5445+
*/
5446+
advance:
5447+
$entry = $this->state->active_formatting_elements->at( ++$currently_at );
5448+
5449+
/*
5450+
* > 8. Create: Insert an HTML element for the token for which the
5451+
* > element entry was created, to obtain new element.
5452+
*/
5453+
create:
5454+
$this->insert_html_element( $entry );
5455+
5456+
/*
5457+
* > 9. Replace the entry for entry in the list with an entry for new element.
5458+
* > This doesn't need to happen here since no DOM is being created.
5459+
*/
5460+
5461+
/*
5462+
* > 10. If the entry for new element in the list of active formatting elements
5463+
* > is not the last entry in the list, return to the step labeled advance.
5464+
*/
5465+
if ( $count !== $currently_at ) {
5466+
goto advance;
5467+
}
5468+
5469+
return true;
54145470
}
54155471

54165472
/**

tests/phpunit/tests/html-api/wpHtmlProcessor.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,23 @@ public function test_clear_to_navigate_after_seeking() {
112112
}
113113

114114
/**
115-
* Ensures that support is added for reconstructing active formatting elements
116-
* before the HTML Processor handles situations with unclosed formats requiring it.
115+
* Ensures that support is added for reconstructing active formatting elements.
117116
*
118117
* @ticket 58517
119118
*
120119
* @covers WP_HTML_Processor::reconstruct_active_formatting_elements
121120
*/
122-
public function test_fails_to_reconstruct_formatting_elements() {
123-
$processor = WP_HTML_Processor::create_fragment( '<p><em>One<p><em>Two<p><em>Three<p><em>Four' );
121+
public function test_reconstructs_formatting_elements() {
122+
$processor = WP_HTML_Processor::create_fragment( '<p><em>One<p><em><span>Two<p><em>Three<p><em>Four' );
124123

125124
$this->assertTrue( $processor->next_tag( 'EM' ), 'Could not find first EM.' );
126-
$this->assertFalse( $processor->next_tag( 'EM' ), 'Should have aborted before finding second EM as it required reconstructing the first EM.' );
125+
$this->assertSame( array( 'HTML', 'BODY', 'P', 'EM' ), $processor->get_breadcrumbs(), 'Found incorrect breadcrumbs for first EM.' );
126+
$this->assertTrue( $processor->next_tag( 'SPAN' ), 'Could not find test span.' );
127+
$this->assertSame(
128+
array( 'HTML', 'BODY', 'P', 'EM', 'EM', 'SPAN' ),
129+
$processor->get_breadcrumbs(),
130+
'Found incorrect breadcrumbs for test SPAN; should have created two EMs.'
131+
);
127132
}
128133

129134
/**

tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -165,45 +165,55 @@ public static function data_single_tag_of_supported_elements() {
165165
}
166166

167167
/**
168-
* @ticket 58517
169-
*
170-
* @dataProvider data_unsupported_markup
168+
* Ensures that formats inside unclosed A elements are reconstructed.
171169
*
172-
* @param string $html HTML containing unsupported markup.
170+
* @ticket 61576
173171
*/
174-
public function test_fails_when_encountering_unsupported_markup( $html, $description ) {
175-
$processor = WP_HTML_Processor::create_fragment( $html );
176-
177-
while ( $processor->next_token() && null === $processor->get_attribute( 'supported' ) ) {
178-
continue;
179-
}
172+
public function test_reconstructs_formatting_from_unclosed_a_elements() {
173+
$processor = WP_HTML_Processor::create_fragment( '<a><strong>Click <a><big>Here</big></a></strong></a>' );
180174

181-
$this->assertNull(
182-
$processor->get_last_error(),
183-
'Bailed on unsupported input before finding supported checkpoint: check test code.'
175+
$processor->next_tag( 'STRONG' );
176+
$this->assertSame(
177+
array( 'HTML', 'BODY', 'A', 'STRONG' ),
178+
$processor->get_breadcrumbs(),
179+
'Failed to construct starting breadcrumbs properly.'
184180
);
185181

186-
$this->assertTrue( $processor->get_attribute( 'supported' ), 'Did not find required supported element.' );
187-
$processor->next_token();
188-
$this->assertNotNull( $processor->get_last_error(), "Didn't properly reject unsupported markup: {$description}" );
182+
$processor->next_tag( 'BIG' );
183+
$this->assertSame(
184+
array( 'HTML', 'BODY', 'STRONG', 'A', 'BIG' ),
185+
$processor->get_breadcrumbs(),
186+
'Failed to reconstruct the active formatting elements after an unclosed A element.'
187+
);
189188
}
190189

191190
/**
192-
* Data provider.
191+
* Ensures that unclosed A elements are reconstructed.
193192
*
194-
* @return array[]
193+
* @ticket 61576
195194
*/
196-
public static function data_unsupported_markup() {
197-
return array(
198-
'A with formatting following unclosed A' => array(
199-
'<a><strong>Click <span supported><a unsupported><big>Here</big></a></strong></a>',
200-
'Unclosed formatting requires complicated reconstruction.',
201-
),
195+
public function test_reconstructs_unclosed_a_elements() {
196+
$processor = WP_HTML_Processor::create_fragment( '<a><div><a></div></a>' );
202197

203-
'A after unclosed A inside DIV' => array(
204-
'<a><div supported><a unsupported></div></a>',
205-
'A is a formatting element, which requires more complicated reconstruction.',
206-
),
198+
$processor->next_tag( 'DIV' );
199+
$this->assertSame(
200+
array( 'HTML', 'BODY', 'DIV' ),
201+
$processor->get_breadcrumbs(),
202+
'Failed to construct breadcrumbs properly - the DIV should have closed the A element.'
203+
);
204+
205+
// When the DIV re-opens, it reconstructs an unclosed A, then the A in the text is a second A.
206+
$processor->next_tag( 'A' );
207+
$this->assertSame(
208+
array( 'HTML', 'BODY', 'DIV', 'A' ),
209+
'Failed to create proper breadcrumbs for recreated A element.'
210+
);
211+
212+
// This is the one that's second in the raw text.
213+
$processor->next_tag( 'A' );
214+
$this->assertSame(
215+
array( 'HTML', 'BODY', 'DIV', 'A' ),
216+
'Failed to create proper breadcrumbs for explicit A element - this A should have closed the reconstructed A.'
207217
);
208218
}
209219

0 commit comments

Comments
 (0)