', array( 'HTML', 'BODY', 'DD' ), 3 ),
+ 'DD and DT mutually close, LI self-closes (li 1)' => array( '
', array( 'HTML', 'BODY', 'DD', 'LI' ), 1 ),
+ 'DD and DT mutually close, LI self-closes (li 2)' => array( '
', array( 'HTML', 'BODY', 'DD', 'LI' ), 2 ),
// H1 - H6 close out _any_ H1 - H6 when encountering _any_ of H1 - H6, making this section surprising.
'EM inside H3 after unclosed P' => array( '
Important Message
', array( 'HTML', 'BODY', 'H3', 'EM' ), 1 ),
diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php
index bd3996d51d7b7..d2f9bec4a8400 100644
--- a/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php
@@ -224,6 +224,107 @@ public function test_in_body_button_with_button_in_scope_as_ancestor() {
$this->assertSame( array( 'HTML', 'BODY', 'BUTTON' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting for third button.' );
}
+ /**
+ * Verifies that H1 through H6 elements close an open P element.
+ *
+ * @ticket {TICKET_NUMBER}
+ *
+ * @dataProvider data_heading_elements
+ *
+ * @param string $tag_name Name of H1 - H6 element under test.
+ */
+ public function test_in_body_heading_element_closes_open_p_tag( $tag_name ) {
+ $processor = WP_HTML_Processor::create_fragment(
+ "
Open<{$tag_name}>Closed P{$tag_name}>
"
+ );
+
+ $processor->next_tag( $tag_name );
+ $this->assertSame(
+ array( 'HTML', 'BODY', $tag_name ),
+ $processor->get_breadcrumbs(),
+ "Expected {$tag_name} to be a direct child of the BODY, having closed the open P element."
+ );
+
+ $processor->next_tag( 'IMG' );
+ $this->assertSame(
+ array( 'HTML', 'BODY', 'IMG' ),
+ $processor->get_breadcrumbs(),
+ 'Expected IMG to be a direct child of BODY, having closed the open P element.'
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[].
+ */
+ public function data_heading_elements() {
+ return array(
+ 'H1' => array( 'H1' ),
+ 'H2' => array( 'H2' ),
+ 'H3' => array( 'H3' ),
+ 'H4' => array( 'H4' ),
+ 'H5' => array( 'H5' ),
+ 'H6' => array( 'H5' ),
+ );
+ }
+
+ /**
+ * Verifies that H1 through H6 elements close an open H1 through H6 element.
+ *
+ * @dataProvider data_heading_combinations
+ *
+ * @param string $first_heading H1 - H6 element appearing (unclosed) before the second.
+ * @param string $second_heading H1 - H6 element appearing after the first.
+ */
+ public function test_in_body_heading_element_closes_other_heading_elements( $first_heading, $second_heading ) {
+ $processor = WP_HTML_Processor::create_fragment(
+ "
<{$first_heading} first> then <{$second_heading} second> and end {$second_heading}>{$first_heading}>
"
+ );
+
+ while ( $processor->next_tag() && null === $processor->get_attribute( 'second' ) ) {
+ continue;
+ }
+
+ $this->assertTrue(
+ $processor->get_attribute( 'second' ),
+ "Failed to find expected {$second_heading} tag."
+ );
+
+ $this->assertSame(
+ array( 'HTML', 'BODY', 'DIV', $second_heading ),
+ $processor->get_breadcrumbs(),
+ "Expected {$second_heading} to be a direct child of the DIV, having closed the open {$first_heading} element."
+ );
+
+ $processor->next_tag( 'IMG' );
+ $this->assertSame(
+ array( 'HTML', 'BODY', 'DIV', 'IMG' ),
+ $processor->get_breadcrumbs(),
+ "Expected IMG to be a direct child of DIV, having closed the open {$first_heading} element."
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_heading_combinations() {
+ $headings = array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' );
+
+ $combinations = array();
+
+ // Create all unique pairs of H1 - H6 elements.
+ foreach ( $headings as $first_tag ) {
+ foreach ( $headings as $second_tag ) {
+ $combinations[ "{$first_tag} then {$second_tag}" ] = array( $first_tag, $second_tag );
+ }
+ }
+
+ return $combinations;
+ }
+
/**
* Verifies that when "in body" and encountering "any other end tag"
* that the HTML processor ignores the end tag if there's a special
diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRulesListElements.php b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRulesListElements.php
new file mode 100644
index 0000000000000..e7382a137eb1b
--- /dev/null
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRulesListElements.php
@@ -0,0 +1,348 @@
+
' );
+
+ while (
+ null === $processor->get_attribute( 'target' ) &&
+ $processor->next_tag()
+ ) {
+ continue;
+ }
+
+ $this->assertTrue(
+ $processor->get_attribute( 'target' ),
+ 'Failed to find target node.'
+ );
+
+ $this->assertSame(
+ array( 'HTML', 'BODY', 'LI' ),
+ $processor->get_breadcrumbs(),
+ "LI should have closed open LI, but didn't."
+ );
+ }
+
+ public function test_in_body_li_generates_implied_end_tags_inside_open_li() {
+ $processor = WP_HTML_Processor::create_fragment( '
' );
+
+ while (
+ null === $processor->get_attribute( 'target' ) &&
+ $processor->next_tag()
+ ) {
+ continue;
+ }
+
+ $this->assertTrue(
+ $processor->get_attribute( 'target' ),
+ 'Failed to find target node.'
+ );
+
+ $this->assertSame(
+ array( 'HTML', 'BODY', 'LI' ),
+ $processor->get_breadcrumbs(),
+ "LI should have closed open LI, but didn't."
+ );
+ }
+
+ public function test_in_body_li_generates_implied_end_tags_inside_open_li_but_stopping_at_special_tags() {
+ $processor = WP_HTML_Processor::create_fragment( '
' );
+
+ while (
+ null === $processor->get_attribute( 'target' ) &&
+ $processor->next_tag()
+ ) {
+ continue;
+ }
+
+ $this->assertTrue(
+ $processor->get_attribute( 'target' ),
+ 'Failed to find target node.'
+ );
+
+ $this->assertSame(
+ array( 'HTML', 'BODY', 'LI', 'BLOCKQUOTE', 'LI' ),
+ $processor->get_breadcrumbs(),
+ 'LI should have left the BLOCKQOUTE open, but closed it.'
+ );
+ }
+
+ public function test_in_body_li_in_li_closes_p_in_button_scope() {
+ $processor = WP_HTML_Processor::create_fragment( '