From 81ef71d605a9027f95f976d243a1cab3dd3ea091 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 19 Oct 2023 15:08:44 -0500 Subject: [PATCH] WIP: HTML API: Add support for H1-H6 elements in HTML Processor --- src/wp-config.local.php | 107 ++++++++++++++++++ .../html-api/class-wp-html-open-elements.php | 30 ++++- .../html-api/class-wp-html-processor.php | 59 ++++++++++ .../tests/html-api/wpHtmlProcessor.php | 2 - .../html-api/wpHtmlProcessorBreadcrumbs.php | 12 +- .../html-api/wpHtmlProcessorSemanticRules.php | 48 +++++--- 6 files changed, 234 insertions(+), 24 deletions(-) create mode 100644 src/wp-config.local.php diff --git a/src/wp-config.local.php b/src/wp-config.local.php new file mode 100644 index 0000000000000..be204dccf65de --- /dev/null +++ b/src/wp-config.local.php @@ -0,0 +1,107 @@ +nn@&%|l0flI6:VON4WelK EeRqf)a&EbFfG8@ ]R} )D`N12^th4_{TNi' ); +define( 'SECURE_AUTH_KEY', 'qD9}:B2$6*w7.{;!pZk]]D?y=D[zz#lpqm4c&+j049^vBVf8 Y]IxSA+2x%nH_[KqJk|nbOyBKF}Ir[VF8`qC Zk+g_$C1XL@C|WMRv*Q<4R,]f6Lbu_)#ml' ); +define( 'AUTH_SALT', '^o|a:{J &rTHA?qheBwZE?;2xh&P_S{D:nxI!/|+@n[z-C^z*rr,fxhg *,&7vrB' ); +define( 'SECURE_AUTH_SALT', 'fhk&Z4_rl|=U,kC!pMlJz@afe#]uRH>:I6k>Q bcz/3E9TV`JEyN.Rk)baXDJM&8' ); +define( 'LOGGED_IN_SALT', 'w2Dl23NuZNJU<9+ -8F{l%eZAjXN,D,8=k#>.k$IuAf/ZtoryWohrIgGRpiv{tnode_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) + ) { + return true; + } + switch ( $node->node_name ) { case 'HTML': return false; @@ -263,13 +270,21 @@ public function pop() { * * @see WP_HTML_Open_Elements::pop * - * @param string $tag_name Name of tag that needs to be popped off of the stack of open elements. + * @param string $tag_name Name of tag that needs to be popped off of the stack of open elements, + * or the class constant HEADING_ELEMENTS to specify any of H1-H6. * @return bool Whether a tag of the given name was found and popped off of the stack of open elements. */ public function pop_until( $tag_name ) { foreach ( $this->walk_up() as $item ) { $this->pop(); + if ( + self::HEADING_ELEMENTS === $tag_name && + in_array( $item->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) + ) { + return true; + } + if ( $tag_name === $item->node_name ) { return true; } @@ -429,4 +444,15 @@ public function after_element_pop( $item ) { break; } } + + /** + * Represents the collection of H1-H6 elements. + * + * @since 6.5.0 + * + * @see has_element_in_scope() + * + * @var string + */ + const HEADING_ELEMENTS = 'heading-elements'; } 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 f27f83b028cd2..9dd85228e1092 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -103,6 +103,7 @@ * - The formatting elements: B, BIG, CODE, EM, FONT, I, SMALL, STRIKE, STRONG, TT, U. * - Containers: DIV, FIGCAPTION, FIGURE, SPAN. * - Form elements: BUTTON. + * - Heading elements: H1, H2, H3, H4, H5, H6. * - Paragraph: P. * - Void elements: IMG. * @@ -657,6 +658,64 @@ private function step_in_body() { $this->state->stack_of_open_elements->pop_until( $tag_name ); return true; + /* + * > A start tag whose tag name is one of: "h1", "h2", "h3", "h4", "h5", "h6" + */ + case '+H1': + case '+H2': + case '+H3': + case '+H4': + case '+H5': + case '+H6': + if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) { + $this->close_a_p_element(); + } + + if ( + in_array( + $this->state->stack_of_open_elements->current_node()->node_name, + array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), + true + ) + ) { + // @TODO: Indicate a parse error once it's possible. + $this->state->stack_of_open_elements->pop(); + } + + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > An end tag whose tag name is one of: "h1", "h2", "h3", "h4", "h5", "h6" + */ + case '-H1': + case '-H2': + case '-H3': + case '-H4': + case '-H5': + case '-H6': + if ( + ! $this->state->stack_of_open_elements->has_element_in_scope( + WP_HTML_Open_Elements::HEADING_ELEMENTS + ) + ) { + /* + * This is a parse error; ignore the token. + * + * @TODO: Indicate a parse error once it's possible. + */ + return $this->step(); + } + + $this->generate_implied_end_tags(); + + if ( $this->state->stack_of_open_elements->current_node()->node_name !== $tag_name ) { + // @TODO: Record parse error: this error doesn't impact parsing. + } + + $this->state->stack_of_open_elements->pop_until( WP_HTML_Open_Elements::HEADING_ELEMENTS ); + return true; + /* * > An end tag whose tag name is "p" */ diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php index 37e3aa5de87fb..a9af5d790fc53 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -91,8 +91,6 @@ public function test_stops_processing_after_unsupported_elements() { * * @covers WP_HTML_Processor::next_tag * @covers WP_HTML_Processor::seek - * - * @throws WP_HTML_Unsupported_Exception */ public function test_clear_to_navigate_after_seeking() { $p = WP_HTML_Processor::create_fragment( '

' ); diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php index d454ab4842fbf..c0a7bc46459d3 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php @@ -46,6 +46,12 @@ public function data_single_tag_of_supported_elements() { 'FIGCAPTION', 'FIGURE', 'FONT', + 'H1', + 'H2', + 'H3', + 'H4', + 'H5', + 'H6', 'I', 'IMG', 'P', @@ -133,12 +139,6 @@ public function data_unsupported_elements() { 'FORM', 'FRAME', 'FRAMESET', - 'H1', - 'H2', - 'H3', - 'H4', - 'H5', - 'H6', 'HEAD', 'HEADER', 'HGROUP', diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php index 01bb41ba844f1..d9f8cb2eeb983 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php @@ -21,10 +21,6 @@ class Tests_HtmlApi_WpHtmlProcessorSemanticRules extends WP_UnitTestCase { * element in scope, that it skips the tag entirely. * * @ticket 58961 - * - * @since 6.4.0 - * - * @throws Exception */ public function test_in_body_skips_unexpected_button_closer() { $p = WP_HTML_Processor::create_fragment( '
Test
' ); @@ -46,10 +42,6 @@ public function test_in_body_skips_unexpected_button_closer() { * Verifies insertion of a BUTTON element when no existing BUTTON is already in scope. * * @ticket 58961 - * - * @since 6.4.0 - * - * @throws WP_HTML_Unsupported_Exception */ public function test_in_body_button_with_no_button_in_scope() { $p = WP_HTML_Processor::create_fragment( '

Click the button !

' ); @@ -75,8 +67,6 @@ public function test_in_body_button_with_no_button_in_scope() { * @ticket 58961 * * @since 6.4.0 - * - * @throws WP_HTML_Unsupported_Exception */ public function test_in_body_button_with_button_in_scope_as_parent() { $p = WP_HTML_Processor::create_fragment( '

Click the button !

' ); @@ -110,8 +100,6 @@ public function test_in_body_button_with_button_in_scope_as_parent() { * @ticket 58961 * * @since 6.4.0 - * - * @throws WP_HTML_Unsupported_Exception */ public function test_in_body_button_with_button_in_scope_as_ancestor() { $p = WP_HTML_Processor::create_fragment( '
!

' ); @@ -137,7 +125,39 @@ 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_heading_element_closes_open_p_tag( $tag_name ) { + $p = WP_HTML_Processor::create_fragment( "

Open<{$tag_name}>Closed P

" ); + + $p->next_tag( $tag_name ); + $this->assertSame( array( 'HTML', 'BODY', $tag_name ), $p->get_breadcrumbs() ); + } + + /** + * Data provider. + * + * @return array[]. + */ + public function data_heading_elements() { + return array( + array( 'H1' ), + array( 'H2' ), + array( 'H3' ), + array( 'H4' ), + array( 'H5' ), + array( 'H5' ), + ); + } + + /** * Verifies that when "in body" and encountering "any other end tag" * that the HTML processor ignores the end tag if there's a special * element on the stack of open elements before the matching opening. @@ -160,7 +180,7 @@ public function test_in_body_any_other_end_tag_with_unclosed_special_element() { $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.