OneTwoThreeFour' );
+ public function test_reconstructs_formatting_elements() {
+ $processor = WP_HTML_Processor::create_fragment( 'OneTwoThreeFour' );
$this->assertTrue( $processor->next_tag( 'EM' ), 'Could not find first EM.' );
- $this->assertFalse( $processor->next_tag( 'EM' ), 'Should have aborted before finding second EM as it required reconstructing the first EM.' );
+ $this->assertSame( array( 'HTML', 'BODY', 'P', 'EM' ), $processor->get_breadcrumbs(), 'Found incorrect breadcrumbs for first EM.' );
+ $this->assertTrue( $processor->next_tag( 'SPAN' ), 'Could not find test span.' );
+ $this->assertSame(
+ array( 'HTML', 'BODY', 'P', 'EM', 'EM', 'SPAN' ),
+ $processor->get_breadcrumbs(),
+ 'Found incorrect breadcrumbs for test SPAN; should have created two EMs.'
+ );
+ }
+
+ /**
+ * Ensures reconstructed active formatting elements expose their original attributes.
+ *
+ * @ticket 58517
+ *
+ * @covers WP_HTML_Processor::reconstruct_active_formatting_elements
+ * @covers WP_HTML_Processor::get_attribute
+ * @covers WP_HTML_Processor::get_attribute_names_with_prefix
+ * @covers WP_HTML_Processor::get_qualified_attribute_name
+ * @covers WP_HTML_Processor::has_class
+ * @covers WP_HTML_Processor::class_list
+ */
+ public function test_reconstructed_formatting_elements_expose_source_attributes() {
+ $processor = WP_HTML_Processor::create_fragment(
+ 'OneTwo'
+ );
+
+ $this->assertTrue( $processor->next_tag( 'EM' ), 'Could not find original EM.' );
+ $this->assertTrue( $processor->next_tag( 'EM' ), 'Could not find reconstructed EM.' );
+
+ $this->assertSame( 'one two', $processor->get_attribute( 'class' ) );
+ $this->assertSame( '14', $processor->get_attribute( 'data-test-id' ) );
+ $this->assertTrue( $processor->get_attribute( 'inert' ) );
+ $this->assertSame( 'data-test-id', $processor->get_qualified_attribute_name( 'data-test-id' ) );
+ $this->assertTrue( $processor->has_class( 'one' ) );
+ $this->assertFalse( $processor->has_class( 'missing' ) );
+ $this->assertSame( array( 'one', 'two' ), iterator_to_array( $processor->class_list() ) );
+
+ $attribute_names = $processor->get_attribute_names_with_prefix( '' );
+ sort( $attribute_names );
+ $this->assertSame( array( 'class', 'data-test-id', 'inert' ), $attribute_names );
+ }
+
+ /**
+ * Ensures reconstructed active formatting elements reparse updated source attributes.
+ *
+ * @ticket 58517
+ *
+ * @covers WP_HTML_Processor::reconstruct_active_formatting_elements
+ * @covers WP_HTML_Processor::get_attribute
+ */
+ public function test_reconstructed_formatting_elements_expose_updated_source_attributes() {
+ $processor = WP_HTML_Processor::create_fragment( 'OneTwo' );
+
+ $this->assertTrue( $processor->next_tag( 'EM' ), 'Could not find original EM.' );
+ $processor->set_attribute( 'class', 'after' );
+ $processor->get_updated_html();
+
+ $this->assertTrue( $processor->next_tag( 'EM' ), 'Could not find reconstructed EM.' );
+ $this->assertSame( 'after', $processor->get_attribute( 'class' ) );
+ }
+
+ /**
+ * Ensures synthetic virtual elements do not report source attributes.
+ *
+ * @ticket 58517
+ *
+ * @covers WP_HTML_Processor::get_attribute
+ * @covers WP_HTML_Processor::get_attribute_names_with_prefix
+ * @covers WP_HTML_Processor::get_qualified_attribute_name
+ * @covers WP_HTML_Processor::has_class
+ * @covers WP_HTML_Processor::class_list
+ */
+ public function test_synthetic_virtual_elements_do_not_expose_attributes() {
+ $processor = WP_HTML_Processor::create_fragment( '
' );
+
+ $this->assertTrue( $processor->next_tag(), 'Could not find implied P opener.' );
+ $this->assertSame( 'P', $processor->get_tag() );
+ $this->assertFalse( $processor->is_tag_closer() );
+ $this->assertNull( $processor->get_attribute( 'class' ) );
+ $this->assertNull( $processor->get_attribute_names_with_prefix( '' ) );
+ $this->assertNull( $processor->get_qualified_attribute_name( 'class' ) );
+ $this->assertNull( $processor->has_class( 'anything' ) );
+ $this->assertNull( $processor->class_list() );
}
/**
@@ -1104,8 +1185,6 @@ public function test_next_tag_lowercase_tag_name() {
* Ensure that the processor does not throw errors in cases of extreme HTML nesting.
*
* @ticket 64394
- *
- * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_bookmark
*/
public function test_deep_nesting_fails_process_without_error() {
$html = str_repeat( '', WP_HTML_Processor::MAX_BOOKMARKS * 2 );
@@ -1116,9 +1195,9 @@ public function test_deep_nesting_fails_process_without_error() {
}
$this->assertSame(
- WP_HTML_Processor::ERROR_EXCEEDED_MAX_BOOKMARKS,
+ WP_HTML_Processor::ERROR_UNSUPPORTED,
$processor->get_last_error(),
- 'Failed to report exceeded-max-bookmarks error.'
+ 'Failed to report unsupported markup for deeply nested formatting elements.'
);
}
diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php
index b54fc047ab040..cf609e36c60f5 100644
--- a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php
@@ -165,46 +165,58 @@ public static function data_single_tag_of_supported_elements() {
}
/**
- * @ticket 58517
- *
- * @dataProvider data_unsupported_markup
+ * Ensures that formats inside unclosed A elements are reconstructed.
*
- * @param string $html HTML containing unsupported markup.
+ * @ticket 61576
*/
- public function test_fails_when_encountering_unsupported_markup( $html, $description ) {
- $processor = WP_HTML_Processor::create_fragment( $html );
-
- while ( $processor->next_token() && null === $processor->get_attribute( 'supported' ) ) {
- continue;
- }
+ public function test_reconstructs_formatting_from_unclosed_a_elements() {
+ $processor = WP_HTML_Processor::create_fragment( 'Click Here' );
- $this->assertNull(
- $processor->get_last_error(),
- 'Bailed on unsupported input before finding supported checkpoint: check test code.'
+ $processor->next_tag( 'STRONG' );
+ $this->assertSame(
+ array( 'HTML', 'BODY', 'A', 'STRONG' ),
+ $processor->get_breadcrumbs(),
+ 'Failed to construct starting breadcrumbs properly.'
);
- $this->assertTrue( $processor->get_attribute( 'supported' ), 'Did not find required supported element.' );
- $processor->next_token();
- $this->assertNotNull( $processor->get_last_error(), "Didn't properly reject unsupported markup: {$description}" );
+ $processor->next_tag( 'BIG' );
+ $this->assertSame(
+ array( 'HTML', 'BODY', 'STRONG', 'A', 'BIG' ),
+ $processor->get_breadcrumbs(),
+ 'Failed to reconstruct the active formatting elements after an unclosed A element.'
+ );
}
/**
- * Data provider.
+ * Ensures that unclosed A elements are reconstructed.
*
- * @return array[]
+ * @ticket 61576
*/
- public static function data_unsupported_markup() {
- return array(
- 'A with formatting following unclosed A' => array(
- 'Click Here',
- 'Unclosed formatting requires complicated reconstruction.',
- ),
+ public function test_reconstructs_unclosed_a_elements() {
+ $processor = WP_HTML_Processor::create_fragment( 'Found me!' );
- 'A after unclosed A inside DIV' => array(
- '',
- 'A is a formatting element, which requires more complicated reconstruction.',
- ),
+ // First, there's an A tag inside the DIV.
+ $this->assertTrue( $processor->next_tag( 'A' ) );
+ $this->assertSame(
+ array( 'HTML', 'BODY', 'DIV', 'A' ),
+ $processor->get_breadcrumbs()
);
+
+ /*
+ * There's a second A tag containing the text outside the DIV.
+ * When the DIV closes, the unclosed A is reconstructed from inside the DIV
+ * to contain the following text.
+ */
+ $this->assertTrue( $processor->next_tag( 'A' ) );
+ $this->assertSame(
+ array( 'HTML', 'BODY', 'A' ),
+ $processor->get_breadcrumbs()
+ );
+
+ // Finally, the trailing text is inside the A.
+ $processor->next_token();
+ $this->assertSame( '#text', $processor->get_token_type() );
+ $this->assertSame( 'Found me!', $processor->get_modifiable_text() );
}
/**
diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php b/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php
index d87d784dbf2d4..bf6956eebbb57 100644
--- a/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php
@@ -152,6 +152,9 @@ private static function should_skip_test( ?string $test_context_element, string
/**
* Generates the tree-like structure represented in the Html5lib tests.
*
+ * @throws WP_HTML_Unsupported_Exception Raises unsupported exceptions for test reporting.
+ * @throws Error For unexpected "impossible" cases.
+ *
* @param string|null $fragment_context Context element in which to parse HTML, such as BODY or SVG.
* @param string $html Given test HTML.
* @return string|null Tree structure of parsed HTML, if supported, else null.
@@ -160,6 +163,7 @@ private static function build_tree_representation( ?string $fragment_context, st
$processor = $fragment_context
? WP_HTML_Processor::create_fragment( $html, "<{$fragment_context}>" )
: WP_HTML_Processor::create_full_parser( $html );
+
if ( null === $processor ) {
throw new WP_HTML_Unsupported_Exception( "Could not create a parser with the given fragment context: {$fragment_context}.", '', 0, '', array(), array() );
}
@@ -264,6 +268,7 @@ static function ( $a, $b ) {
foreach ( $sorted_attributes as $attribute_name => $display_name ) {
$val = $processor->get_attribute( $attribute_name );
+
/*
* Attributes with no value are `true` with the HTML API,
* We map use the empty string value in the tree structure.