From 41bffadb833d65794c81166acdd8b31e761a8978 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Tue, 1 Aug 2023 07:54:54 +0000 Subject: [PATCH] HTML API: Add support for SPAN element. In this patch we're introducing support for the SPAN element, which is the first in the class of "any other tag" in the "in body" insertion mode. This patch introduces the mechanisms required to handle that class of tags but only introduces SPAN to keep the change focused. With the tests and mechanisms in place it will be possible to follow-up and add another limited set of tags. It's important that this not use the default catch-all in the switch handling `step_in_body` because that would catch tags that have specific rules in previous case statements that aren't yet added. For example, we don't want to treat the `TABLE` element as "any other tag". Props dmsnell. Fixes #58907. git-svn-id: https://develop.svn.wordpress.org/trunk@56331 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-processor.php | 53 ++++++++- .../html-api/wpHtmlProcessorBreadcrumbs.php | 2 +- .../html-api/wpHtmlProcessorSemanticRules.php | 68 ++++++++++++ .../wpHtmlSupportRequiredHtmlProcessor.php | 101 ++++++++++++++++++ 4 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php create mode 100644 tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 68bd3418f2..c8e898389c 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -626,6 +626,37 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor { $this->insert_html_element( $this->current_token ); return true; + /* + * > Any other start tag + */ + case '+SPAN': + $this->reconstruct_active_formatting_elements(); + $this->insert_html_element( $this->current_token ); + return true; + + /* + * Any other end tag + */ + case '-SPAN': + foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) { + // > If node is an HTML element with the same tag name as the token, then: + if ( $item->node_name === $tag_name ) { + $this->generate_implied_end_tags( $tag_name ); + + // > If node is not the current node, then this is a parse error. + + $this->state->stack_of_open_elements->pop_until( $tag_name ); + return true; + } + + // > Otherwise, if node is in the special category, then this is a parse error; ignore the token, and return. + if ( self::is_special( $item->node_name ) ) { + return $this->step(); + } + } + // Execution should not reach here; if it does then something went wrong. + return false; + default: $this->last_error = self::ERROR_UNSUPPORTED; throw new WP_HTML_Unsupported_Exception( "Cannot process {$tag_name} element." ); @@ -873,7 +904,7 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor { * * @since 6.4.0 * - * @throws Exception + * @throws WP_HTML_Unsupported_Exception * * @see https://html.spec.whatwg.org/#generate-implied-end-tags * @@ -893,6 +924,26 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor { } } + /* + * Closes elements that have implied end tags, thoroughly. + * + * See the HTML specification for an explanation why this is + * different from {@see WP_HTML_Processor::generate_implied_end_tags}. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#generate-implied-end-tags + */ + private function generate_implied_end_tags_thoroughly() { + $elements_with_implied_end_tags = array( + 'P', + ); + + while ( in_array( $this->state->stack_of_open_elements->current_node(), $elements_with_implied_end_tags, true ) ) { + $this->state->stack_of_open_elements->pop(); + } + } + /** * Reconstructs the active formatting elements. * diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php index 4bf46a173f..229ae38c08 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php @@ -49,6 +49,7 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase { 'IMG', 'P', 'SMALL', + 'SPAN', 'STRIKE', 'STRONG', 'TT', @@ -191,7 +192,6 @@ class Tests_HtmlApi_WpHtmlProcessorBreadcrumbs extends WP_UnitTestCase { 'SLOT', 'SOURCE', 'SPACER', // Deprecated - 'SPAN', 'STYLE', 'SUB', 'SUMMARY', diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php new file mode 100644 index 0000000000..e16d496cb4 --- /dev/null +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php @@ -0,0 +1,68 @@ +

' ); + + $p->next_tag( 'P' ); + $this->assertSame( 'P', $p->get_tag(), "Expected to start test on P element but found {$p->get_tag()} instead." ); + $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'P' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting.' ); + + $this->assertTrue( $p->next_tag(), 'Failed to advance past P tag to expected DIV opener.' ); + $this->assertSame( 'DIV', $p->get_tag(), "Expected to find DIV element, but found {$p->get_tag()} instead." ); + $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'DIV' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting: SPAN should still be open and DIV should be its child.' ); + } + + /* + * Verifies that when "in body" and encountering "any other end tag" + * that the HTML processor closes appropriate elements on the stack of + * open elements up to the matching opening. + * + * @ticket 58907 + * + * @since 6.4.0 + * + * @covers WP_HTML_Processor::step_in_body + */ + public function test_in_body_any_other_end_tag_with_unclosed_non_special_element() { + $p = WP_HTML_Processor::createFragment( '
' ); + + $p->next_tag( 'CODE' ); + $this->assertSame( 'CODE', $p->get_tag(), "Expected to start test on CODE element but found {$p->get_tag()} instead." ); + $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'CODE' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting.' ); + + $this->assertTrue( $p->next_tag(), 'Failed to advance past CODE tag to expected SPAN closer.' ); + $this->assertTrue( $p->is_tag_closer(), 'Expected to find closing SPAN, but found opener instead.' ); + $this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $p->get_breadcrumbs(), 'Failed to advance past CODE tag to expected DIV opener.' ); + + $this->assertTrue( $p->next_tag(), 'Failed to advance past SPAN closer to expected DIV opener.' ); + $this->assertSame( 'DIV', $p->get_tag(), "Expected to find DIV element, but found {$p->get_tag()} instead." ); + $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'DIV' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting: SPAN should be closed and DIV should be its sibling.' ); + } +} diff --git a/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php new file mode 100644 index 0000000000..b9dbbc8cb8 --- /dev/null +++ b/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php @@ -0,0 +1,101 @@ +" ); + + $this->assertFalse( $p->step(), "Must support terminating elements in specific scope check before adding support for the {$tag_name} element." ); + } + + /** + * Generating implied end tags walks up the stack of open elements + * as long as any of the following missing elements is the current node. + * + * @since 6.4.0 + * + * @ticket 58907 + * + * @covers WP_HTML_Processor::generate_implied_end_tags + */ + public function test_generate_implied_end_tags_needs_support() { + $this->ensure_support_is_added_everywhere( 'DD' ); + $this->ensure_support_is_added_everywhere( 'DT' ); + $this->ensure_support_is_added_everywhere( 'LI' ); + $this->ensure_support_is_added_everywhere( 'OPTGROUP' ); + $this->ensure_support_is_added_everywhere( 'OPTION' ); + $this->ensure_support_is_added_everywhere( 'RB' ); + $this->ensure_support_is_added_everywhere( 'RP' ); + $this->ensure_support_is_added_everywhere( 'RT' ); + $this->ensure_support_is_added_everywhere( 'RTC' ); + } + + /** + * Generating implied end tags thoroughly walks up the stack of open elements + * as long as any of the following missing elements is the current node. + * + * @since 6.4.0 + * + * @ticket 58907 + * + * @covers WP_HTML_Processor::generate_implied_end_tags_thoroughly + */ + public function test_generate_implied_end_tags_thoroughly_needs_support() { + $this->ensure_support_is_added_everywhere( 'CAPTION' ); + $this->ensure_support_is_added_everywhere( 'COLGROUP' ); + $this->ensure_support_is_added_everywhere( 'DD' ); + $this->ensure_support_is_added_everywhere( 'DT' ); + $this->ensure_support_is_added_everywhere( 'LI' ); + $this->ensure_support_is_added_everywhere( 'OPTGROUP' ); + $this->ensure_support_is_added_everywhere( 'OPTION' ); + $this->ensure_support_is_added_everywhere( 'RB' ); + $this->ensure_support_is_added_everywhere( 'RP' ); + $this->ensure_support_is_added_everywhere( 'RT' ); + $this->ensure_support_is_added_everywhere( 'RTC' ); + $this->ensure_support_is_added_everywhere( 'TBODY' ); + $this->ensure_support_is_added_everywhere( 'TD' ); + $this->ensure_support_is_added_everywhere( 'TFOOT' ); + $this->ensure_support_is_added_everywhere( 'TH' ); + $this->ensure_support_is_added_everywhere( 'HEAD' ); + $this->ensure_support_is_added_everywhere( 'TR' ); + } +}