From b867b58f3761b9f363d382362dd1634b7301ef4e Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 19 Jan 2024 19:18:18 +0100 Subject: [PATCH 01/22] Implement SELECT and related tags handling --- .../html-api/class-wp-html-open-elements.php | 23 ++- .../html-api/class-wp-html-processor.php | 164 +++++++++++++++++- 2 files changed, 179 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php index 15479801dda10..c304ba57a0cca 100644 --- a/src/wp-includes/html-api/class-wp-html-open-elements.php +++ b/src/wp-includes/html-api/class-wp-html-open-elements.php @@ -269,19 +269,34 @@ public function has_element_in_table_scope( $tag_name ) { /** * Returns whether a particular element is in select scope. * - * @since 6.4.0 + * @since 6.4.0 - Stub, always throws. + * @since 6.5.0 - Implemented. * * @see https://html.spec.whatwg.org/#has-an-element-in-select-scope * - * @throws WP_HTML_Unsupported_Exception Always until this function is implemented. + * > The stack of open elements is said to have a particular element in select scope when it has + * > that element in the specific scope consisting of all element types except the following: + * > - optgroup in the HTML namespace + * > - option in the HTML namespace * * @param string $tag_name Name of tag to check. * @return bool Whether given element is in scope. */ public function has_element_in_select_scope( $tag_name ) { - throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on select scope.' ); + foreach ( $this->walk_up() as $node ) { + if ( $node->node_name === $tag_name ) { + return true; + } - return false; // The linter requires this unreachable code until the function is implemented and can return. + if ( + 'OPTION' !== $node->node_name && + 'OPTGROUP' !== $node->node_name + ) { + return false; + } + } + + return false; } /** 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 32800218f6404..cc371519f775b 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -101,7 +101,7 @@ * * - Containers: ADDRESS, BLOCKQUOTE, DETAILS, DIALOG, DIV, FOOTER, HEADER, MAIN, MENU, SPAN, SUMMARY. * - Custom elements: All custom elements are supported. :) - * - Form elements: BUTTON, DATALIST, FIELDSET, INPUT, LABEL, LEGEND, METER, PROGRESS, SEARCH. + * - Form elements: BUTTON, DATALIST, FIELDSET, INPUT, LABEL, LEGEND, METER, OPTGROUP, OPTION, PROGRESS, SEARCH, SELECT. * - Formatting elements: B, BIG, CODE, EM, FONT, I, PRE, SMALL, STRIKE, STRONG, TT, U, WBR. * - Heading elements: H1, H2, H3, H4, H5, H6, HGROUP. * - Links: A. @@ -757,6 +757,9 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ) { case WP_HTML_Processor_State::INSERTION_MODE_IN_BODY: return $this->step_in_body(); + case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT: + return $this->step_in_select(); + default: $this->last_error = self::ERROR_UNSUPPORTED; throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." ); @@ -1336,6 +1339,50 @@ private function step_in_body() { case '+TRACK': $this->insert_html_element( $this->state->current_token ); return true; + + /* + * > A start tag whose tag name is "select" + */ + case '+SELECT': + $this->reconstruct_active_formatting_elements(); + $this->insert_html_element( $this->state->current_token ); + $this->state->frameset_ok = false; + + // If the insertion mode is one of + // - "in table" + // - "in caption" + // - "in table body" + // - "in row" + // - "in cell" + // then switch the insertion mode to "in select in table" + // + // Otherwise, switch the insertion mode to "in select". + switch ( $this->state->insertion_mode ) { + case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE: + case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION: + case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY: + case WP_HTML_Processor_State::INSERTION_MODE_IN_ROW: + case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL: + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE; + break; + default: + $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT; + break; + } + return true; + + /* + * > A start tag whose tag name is one of: "optgroup", "option" + */ + case '+OPTGROUP': + case '+OPTION': + $current_node = $this->state->stack_of_open_elements->current_node(); + if ( $current_node && 'OPTION' === $current_node->node_name ) { + $this->state->stack_of_open_elements->pop(); + } + $this->reconstruct_active_formatting_elements(); + $this->insert_html_element( $this->state->current_token ); + return true; } /* @@ -1378,8 +1425,6 @@ private function step_in_body() { case 'NOFRAMES': case 'NOSCRIPT': case 'OBJECT': - case 'OPTGROUP': - case 'OPTION': case 'PLAINTEXT': case 'RB': case 'RP': @@ -1387,7 +1432,6 @@ private function step_in_body() { case 'RTC': case 'SARCASM': case 'SCRIPT': - case 'SELECT': case 'STYLE': case 'SVG': case 'TABLE': @@ -1448,6 +1492,118 @@ private function step_in_body() { } } + /** + * Parses next element in the 'in select' insertion mode. + * + * This internal function performs the 'in select' insertion mode + * logic for the generalized WP_HTML_Processor::step() function. + * + * @since 6.5.0 + * + * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselect + * @see WP_HTML_Processor::step + * + * @return bool Whether an element was found. + */ + private function step_in_select() { + $tag_name = $this->get_tag(); + $op_sigil = $this->is_tag_closer() ? '-' : '+'; + $op = "{$op_sigil}{$tag_name}"; + + switch ( $op ) { + /* + * > A start tag whose tag name is "html" + */ + case '+HTML': + $this->last_error = self::ERROR_UNSUPPORTED; + throw new WP_HTML_Unsupported_Exception( "Cannot process {$tag_name} element." ); + + /* + * > A start tag whose tag name is "option" + */ + case '+OPTION': + $current_node = $this->state->stack_of_open_elements->current_node(); + if ( $current_node && 'OPTION' === $current_node->node_name ) { + $this->state->stack_of_open_elements->pop(); + } + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A start tag whose tag name is "optgroup" + * > A start tag whose tag name is "hr" + */ + case '+OPTGROUP': + case '+HR': + $current_node = $this->state->stack_of_open_elements->current_node(); + if ( $current_node && 'OPTION' === $current_node->node_name ) { + $this->state->stack_of_open_elements->pop(); + // If we've popped, update the current_node + $current_node = $this->state->stack_of_open_elements->current_node(); + } + + if ( $current_node && 'OPTGROUP' === $current_node->node_name ) { + $this->state->stack_of_open_elements->pop(); + } + + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > An end tag whose tag name is "optgroup" + */ + case '-OPTGROUP': + $walker = $this->state->stack_of_open_elements->walk_up(); + $current_node = $walker->current(); + if ( ! $current_node ) { + return $this->step(); + } + if ( 'OPTGROUP' === $current_node->node_name ) { + $this->state->stack_of_open_elements->pop(); + return true; + } + + $walker->next(); + $current_node_parent = $walker->current(); + if ( 'OPTION' === $current_node->node_name && 'OPTGROUP' === $current_node_parent->node_name ) { + $this->state->stack_of_open_elements->pop(); + $this->state->stack_of_open_elements->pop(); + return true; + } + return $this->step(); + + /* + * > An end tag whose tag name is "option" + */ + case '-OPTION': + $current_node = $this->state->stack_of_open_elements->current_node(); + if ( $current_node && 'OPTION' === $current_node->node_name ) { + $this->state->stack_of_open_elements->pop(); + return true; + } + return $this->step(); + + /* + * > An end tag whose tag name is "select" + * > A start tag whose tag name is "select" + */ + case '-SELECT': + case '+SELECT': + if ( ! $this->state->stack_of_open_elements->has_element_in_select_scope( 'SELECT' ) ) { + return $this->step(); + } + $this->state->stack_of_open_elements->pop_until( 'SELECT' ); + $this->state->stack_of_open_elements->pop(); + $this->reset_insertion_mode(); + return true; + } + + $this->last_error = self::ERROR_UNSUPPORTED; + throw new WP_HTML_Unsupported_Exception( "Cannot process {$tag_name} element." ); + } + /* * Internal helpers */ From 9245930e6d8697a7de47ef1bbb1b75a402e66dbf Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 3 Jul 2024 20:54:44 +0200 Subject: [PATCH 02/22] Remove SELECT from unsupported elements test --- tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php index 01dcd9c32d7fb..403f40a1da032 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php @@ -189,15 +189,12 @@ public static function data_unsupported_elements() { 'NOFRAMES', // Neutralized. 'NOSCRIPT', 'OBJECT', - 'OPTGROUP', - 'OPTION', 'PLAINTEXT', // Neutralized. 'RB', // Neutralized. 'RP', 'RT', 'RTC', // Neutralized. 'SCRIPT', - 'SELECT', 'STYLE', 'SVG', 'TABLE', From 267a27be1007181eceb06b95aa68de5e7109df18 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 3 Jul 2024 20:57:11 +0200 Subject: [PATCH 03/22] Remove SELECT scope optgroup/option tests --- .../tests/html-api/wpHtmlSupportRequiredOpenElements.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php b/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php index c2e8c697e8156..48255190ad50c 100644 --- a/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php +++ b/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php @@ -308,9 +308,5 @@ public function test_has_element_in_select_scope_needs_support() { * FOREIGNOBJECT, DESC, TITLE. */ $this->ensure_support_is_added_everywhere( 'SVG' ); - - // These elements are specific to SELECT scope. - $this->ensure_support_is_added_everywhere( 'OPTGROUP' ); - $this->ensure_support_is_added_everywhere( 'OPTION' ); } } From b0475fb11c4c02b9484411764991de1a119c6ead Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 3 Jul 2024 20:57:57 +0200 Subject: [PATCH 04/22] Remove SELECT, OPTION, OPTGROUP from unsupported tags --- tests/phpunit/tests/html-api/wpHtmlProcessor.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php index 536f6fdf4dd8f..b842703a7a135 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -406,8 +406,6 @@ public static function data_unsupported_special_in_body_tags() { 'NOFRAMES' => array( 'NOFRAMES' ), 'NOSCRIPT' => array( 'NOSCRIPT' ), 'OBJECT' => array( 'OBJECT' ), - 'OPTGROUP' => array( 'OPTGROUP' ), - 'OPTION' => array( 'OPTION' ), 'PLAINTEXT' => array( 'PLAINTEXT' ), 'RB' => array( 'RB' ), 'RP' => array( 'RP' ), @@ -415,7 +413,6 @@ public static function data_unsupported_special_in_body_tags() { 'RTC' => array( 'RTC' ), 'SARCASM' => array( 'SARCASM' ), 'SCRIPT' => array( 'SCRIPT' ), - 'SELECT' => array( 'SELECT' ), 'STYLE' => array( 'STYLE' ), 'SVG' => array( 'SVG' ), 'TABLE' => array( 'TABLE' ), From 1944b6646c5945e75c787284637bbffcd1353cf6 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 3 Jul 2024 20:58:54 +0200 Subject: [PATCH 05/22] Remove OPTION,OPTGROUP from gen implied end tags unsupported test --- .../tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php index 2d3cd21ce461b..07943cd62a2f4 100644 --- a/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php @@ -58,8 +58,6 @@ private function ensure_support_is_added_everywhere( $tag_name ) { * @covers WP_HTML_Processor::generate_implied_end_tags */ public function test_generate_implied_end_tags_needs_support() { - $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' ); @@ -79,8 +77,6 @@ public function test_generate_implied_end_tags_needs_support() { 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( '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' ); From 8d4b4dfd5ef7a550b5d4cfbd2bd3a947a239d899 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 13:28:22 +0200 Subject: [PATCH 06/22] Add OPTION, OPTGROUP to implied end tags --- src/wp-includes/html-api/class-wp-html-processor.php | 5 +++++ 1 file changed, 5 insertions(+) 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 cc371519f775b..509cfb5b164d6 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -2192,6 +2192,7 @@ private function close_a_p_element() { * Closes elements that have implied end tags. * * @since 6.4.0 + * @since 6.7.0 Support "option" and "optgroup". * * @see https://html.spec.whatwg.org/#generate-implied-end-tags * @@ -2202,6 +2203,8 @@ private function generate_implied_end_tags( $except_for_this_element = null ) { 'DD', 'DT', 'LI', + 'OPTGROUP', + 'OPTION', 'P', ); @@ -2230,6 +2233,8 @@ private function generate_implied_end_tags_thoroughly() { 'DD', 'DT', 'LI', + 'OPTGROUP', + 'OPTION', 'P', ); From 140411904e9da10d44878a0f595111111eb828fd Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 13:44:42 +0200 Subject: [PATCH 07/22] Update step_in_select since tag --- src/wp-includes/html-api/class-wp-html-processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 509cfb5b164d6..eb1d092f698e5 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1498,7 +1498,7 @@ private function step_in_body() { * This internal function performs the 'in select' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * - * @since 6.5.0 + * @since 6.7.0 * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * From dc0bbb90d8205c24d84287d8292078cf55505457 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 16:04:09 +0200 Subject: [PATCH 08/22] Update since tags on in_select_scope --- src/wp-includes/html-api/class-wp-html-open-elements.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php index c304ba57a0cca..117bfb4a04eac 100644 --- a/src/wp-includes/html-api/class-wp-html-open-elements.php +++ b/src/wp-includes/html-api/class-wp-html-open-elements.php @@ -269,8 +269,8 @@ public function has_element_in_table_scope( $tag_name ) { /** * Returns whether a particular element is in select scope. * - * @since 6.4.0 - Stub, always throws. - * @since 6.5.0 - Implemented. + * @since 6.4.0 Stub implementation (throws). + * @since 6.7.0 Full implementation. * * @see https://html.spec.whatwg.org/#has-an-element-in-select-scope * From 9d9984463dac791c28fab4434c6e26a24d9d4b43 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 16:14:32 +0200 Subject: [PATCH 09/22] =?UTF-8?q?Implement=20in=5Fselect=5Fscope=20via=20?= =?UTF-8?q?=E2=80=A6in=5Fspecific=5Fscope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../html-api/class-wp-html-open-elements.php | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php index 117bfb4a04eac..0fd8ae43b0c42 100644 --- a/src/wp-includes/html-api/class-wp-html-open-elements.php +++ b/src/wp-includes/html-api/class-wp-html-open-elements.php @@ -156,11 +156,15 @@ public function current_node() { * * @see https://html.spec.whatwg.org/#has-an-element-in-the-specific-scope * - * @param string $tag_name Name of tag check. - * @param string[] $termination_list List of elements that terminate the search. + * @param string $tag_name Name of tag check. + * @param string[] $termination_list List of elements that terminate the search. + * @param string $termination_behavior Optional. "include" or "exclude". If "include" + * (default), the terminate when any tag in + * `$termination_list` is reached. Otherwise, + * terminate when any _other_ tag is reached. * @return bool Whether the element was found in a specific scope. */ - public function has_element_in_specific_scope( $tag_name, $termination_list ) { + public function has_element_in_specific_scope( $tag_name, $termination_list, $termination_behavior = 'include' ) { foreach ( $this->walk_up() as $node ) { if ( $node->node_name === $tag_name ) { return true; @@ -178,7 +182,10 @@ public function has_element_in_specific_scope( $tag_name, $termination_list ) { return false; } - if ( in_array( $node->node_name, $termination_list, true ) ) { + $terminate = 'include' === $termination_behavior + ? in_array( $node->node_name, $termination_list, true ) + : ! in_array( $node->node_name, $termination_list, true ); + if ( $terminate ) { return false; } } @@ -283,20 +290,11 @@ public function has_element_in_table_scope( $tag_name ) { * @return bool Whether given element is in scope. */ public function has_element_in_select_scope( $tag_name ) { - foreach ( $this->walk_up() as $node ) { - if ( $node->node_name === $tag_name ) { - return true; - } - - if ( - 'OPTION' !== $node->node_name && - 'OPTGROUP' !== $node->node_name - ) { - return false; - } - } - - return false; + return $this->has_element_in_specific_scope( + $tag_name, + array( 'OPTION', 'OPTGROUP' ), + 'exclude' + ); } /** From 30854052ac646e75965e491736f25d8958b44d38 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 19:02:39 +0200 Subject: [PATCH 10/22] =?UTF-8?q?Revert=20"Implement=20in=5Fselect=5Fscope?= =?UTF-8?q?=20via=20=E2=80=A6in=5Fspecific=5Fscope"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 9d9984463dac791c28fab4434c6e26a24d9d4b43. --- .../html-api/class-wp-html-open-elements.php | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php index 0fd8ae43b0c42..117bfb4a04eac 100644 --- a/src/wp-includes/html-api/class-wp-html-open-elements.php +++ b/src/wp-includes/html-api/class-wp-html-open-elements.php @@ -156,15 +156,11 @@ public function current_node() { * * @see https://html.spec.whatwg.org/#has-an-element-in-the-specific-scope * - * @param string $tag_name Name of tag check. - * @param string[] $termination_list List of elements that terminate the search. - * @param string $termination_behavior Optional. "include" or "exclude". If "include" - * (default), the terminate when any tag in - * `$termination_list` is reached. Otherwise, - * terminate when any _other_ tag is reached. + * @param string $tag_name Name of tag check. + * @param string[] $termination_list List of elements that terminate the search. * @return bool Whether the element was found in a specific scope. */ - public function has_element_in_specific_scope( $tag_name, $termination_list, $termination_behavior = 'include' ) { + public function has_element_in_specific_scope( $tag_name, $termination_list ) { foreach ( $this->walk_up() as $node ) { if ( $node->node_name === $tag_name ) { return true; @@ -182,10 +178,7 @@ public function has_element_in_specific_scope( $tag_name, $termination_list, $te return false; } - $terminate = 'include' === $termination_behavior - ? in_array( $node->node_name, $termination_list, true ) - : ! in_array( $node->node_name, $termination_list, true ); - if ( $terminate ) { + if ( in_array( $node->node_name, $termination_list, true ) ) { return false; } } @@ -290,11 +283,20 @@ public function has_element_in_table_scope( $tag_name ) { * @return bool Whether given element is in scope. */ public function has_element_in_select_scope( $tag_name ) { - return $this->has_element_in_specific_scope( - $tag_name, - array( 'OPTION', 'OPTGROUP' ), - 'exclude' - ); + foreach ( $this->walk_up() as $node ) { + if ( $node->node_name === $tag_name ) { + return true; + } + + if ( + 'OPTION' !== $node->node_name && + 'OPTGROUP' !== $node->node_name + ) { + return false; + } + } + + return false; } /** From e095c6206c3a32279016e35c8f909eb270ce3fd7 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 20:36:01 +0200 Subject: [PATCH 11/22] Add current_node_is method --- .../html-api/class-wp-html-open-elements.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php index 15479801dda10..9ba5a0cf4aa70 100644 --- a/src/wp-includes/html-api/class-wp-html-open-elements.php +++ b/src/wp-includes/html-api/class-wp-html-open-elements.php @@ -144,6 +144,28 @@ public function current_node() { return $current_node ? $current_node : null; } + /** + * Checks if the node at the top of the stack matches provided node name. + * + * @example + * // Is the current node a text node: + * $stack->current_node_is( '#text' ); + * + * // Is the current node a DIV element: + * $stack->current_node_is( 'DIV' ); + * + * @since 6.7.0 + * + * @param string $node_name The node name to match. Provide a tag name for tags or a + * token name for other types of tokens. + * @return bool True if there are nodes on the stack and the top node has + * a matching node_name. + */ + public function current_node_is( string $node_name ): bool { + $current_node = end( $this->stack ); + return $current_node && $current_node->node_name === $node_name; + } + /** * Returns whether an element is in a specific scope. * From 76c899a2d8bb9d43d8bae9d0eebac805a2a746dc Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 21:23:20 +0200 Subject: [PATCH 12/22] Add text, comment, doctype handling --- .../html-api/class-wp-html-processor.php | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) 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 eb1d092f698e5..bb0564233f66c 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1508,11 +1508,35 @@ private function step_in_body() { * @return bool Whether an element was found. */ private function step_in_select() { - $tag_name = $this->get_tag(); - $op_sigil = $this->is_tag_closer() ? '-' : '+'; - $op = "{$op_sigil}{$tag_name}"; + $token_name = $this->get_token_name(); + $token_type = $this->get_token_type(); + $op_sigil = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : ''; + $op = "{$op_sigil}{$token_name}"; switch ( $op ) { + /* + * > Any other character token + */ + case '#text': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A comment token + */ + case '#comment': + case '#funky-comment': + case '#presumptuous-tag': + $this->insert_html_element( $this->state->current_token ); + return true; + + /* + * > A DOCTYPE token + */ + case 'html': + // Parse error. Ignore the token. + return $this->step(); + /* * > A start tag whose tag name is "html" */ From 52fd8e58027edc16eb2bdb90ba7035e31fdb9e20 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 21:23:49 +0200 Subject: [PATCH 13/22] Handle +HTML case --- src/wp-includes/html-api/class-wp-html-processor.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 bb0564233f66c..2f5f2468a132d 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1541,8 +1541,7 @@ private function step_in_select() { * > A start tag whose tag name is "html" */ case '+HTML': - $this->last_error = self::ERROR_UNSUPPORTED; - throw new WP_HTML_Unsupported_Exception( "Cannot process {$tag_name} element." ); + return $this->step_in_body(); /* * > A start tag whose tag name is "option" From 6a10eb0222d462ac8d7e6f7ce19630d9e9ad7689 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 21:25:07 +0200 Subject: [PATCH 14/22] Update several cases to use current_node_is --- .../html-api/class-wp-html-processor.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) 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 2f5f2468a132d..d772eb38739dd 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1547,8 +1547,7 @@ private function step_in_select() { * > A start tag whose tag name is "option" */ case '+OPTION': - $current_node = $this->state->stack_of_open_elements->current_node(); - if ( $current_node && 'OPTION' === $current_node->node_name ) { + if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { $this->state->stack_of_open_elements->pop(); } $this->insert_html_element( $this->state->current_token ); @@ -1557,17 +1556,17 @@ private function step_in_select() { /* * > A start tag whose tag name is "optgroup" * > A start tag whose tag name is "hr" + * + * These rules are identical except for the treatment of the self-closing flag and + * the subsequent pop of the HR void element, all of which is handled elsewhere in the processor. */ case '+OPTGROUP': case '+HR': - $current_node = $this->state->stack_of_open_elements->current_node(); - if ( $current_node && 'OPTION' === $current_node->node_name ) { + if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { $this->state->stack_of_open_elements->pop(); - // If we've popped, update the current_node - $current_node = $this->state->stack_of_open_elements->current_node(); } - if ( $current_node && 'OPTGROUP' === $current_node->node_name ) { + if ( $this->state->stack_of_open_elements->current_node_is( 'OPTGROUP' ) ) { $this->state->stack_of_open_elements->pop(); } @@ -1601,11 +1600,11 @@ private function step_in_select() { * > An end tag whose tag name is "option" */ case '-OPTION': - $current_node = $this->state->stack_of_open_elements->current_node(); - if ( $current_node && 'OPTION' === $current_node->node_name ) { + if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { $this->state->stack_of_open_elements->pop(); return true; } + // Parse error: ignore the token. return $this->step(); /* From 8b3a9e43eee325f70ae95e49ed1568a13a1c58ca Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 21:25:29 +0200 Subject: [PATCH 15/22] Simplify -OPTGROUP handling implementation --- .../html-api/class-wp-html-processor.php | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) 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 d772eb38739dd..d8c0e9016d737 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1577,23 +1577,21 @@ private function step_in_select() { * > An end tag whose tag name is "optgroup" */ case '-OPTGROUP': - $walker = $this->state->stack_of_open_elements->walk_up(); - $current_node = $walker->current(); - if ( ! $current_node ) { - return $this->step(); - } - if ( 'OPTGROUP' === $current_node->node_name ) { - $this->state->stack_of_open_elements->pop(); - return true; + $current_node = $this->state->stack_of_open_elements->current_node(); + if ( $current_node && 'OPTION' === $current_node->node_name ) { + foreach ( $this->state->stack_of_open_elements->walk_up( $current_node ) as $parent ) { + break; + } + if ( $parent && 'OPTGROUP' === $parent->node_name ) { + $this->state->stack_of_open_elements->pop(); + } } - $walker->next(); - $current_node_parent = $walker->current(); - if ( 'OPTION' === $current_node->node_name && 'OPTGROUP' === $current_node_parent->node_name ) { - $this->state->stack_of_open_elements->pop(); + if ( $this->state->stack_of_open_elements->current_node_is( 'OPTGROUP' ) ) { $this->state->stack_of_open_elements->pop(); return true; } + // Parse error: ignore the token. return $this->step(); /* From 87c65969b965ad313e9e49097232c9f26537bd23 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 21:25:50 +0200 Subject: [PATCH 16/22] Add input,keygen,textarea,script,template handling --- .../html-api/class-wp-html-processor.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 d8c0e9016d737..166847c9f7231 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1618,6 +1618,29 @@ private function step_in_select() { $this->state->stack_of_open_elements->pop(); $this->reset_insertion_mode(); return true; + + /* + * > A start tag whose tag name is one of: "input", "keygen", "textarea" + */ + case '+INPUT': + case '+KEYGEN': + case '+TEXTAREA': + // Parse error. + if ( ! $this->state->stack_of_open_elements->has_element_in_select_scope( 'SELECT' ) ) { + return $this->step(); + } + $this->state->stack_of_open_elements->pop_until( 'SELECT' ); + $this->reset_insertion_mode(); + return $this->step( self::REPROCESS_CURRENT_NODE ); + + /* + * > A start tag whose tag name is one of: "script", "template" + * > An end tag whose tag name is "template" + */ + case '+SCRIPT': + case '+TEMPLATE': + case '-TEMPLATE': + return $this->step_in_head(); } $this->last_error = self::ERROR_UNSUPPORTED; From d46433071552a0e3b5c740dff3de1f59a4f63f55 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 21:26:02 +0200 Subject: [PATCH 17/22] Add "anything else" handling --- src/wp-includes/html-api/class-wp-html-processor.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 166847c9f7231..bb6bb1057d56a 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1643,8 +1643,11 @@ private function step_in_select() { return $this->step_in_head(); } - $this->last_error = self::ERROR_UNSUPPORTED; - throw new WP_HTML_Unsupported_Exception( "Cannot process {$tag_name} element." ); + /* + * > Anything else + * > Parse error: ignore the token. + */ + return $this->step(); } /* From ddbd676d5e33ed032cb513c0b4d3201f782f4009 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 21:28:01 +0200 Subject: [PATCH 18/22] Add step_in_head stub --- .../html-api/class-wp-html-processor.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 bb6bb1057d56a..6ba85262f3a1d 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -757,6 +757,9 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ) { case WP_HTML_Processor_State::INSERTION_MODE_IN_BODY: return $this->step_in_body(); + case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD: + return $this->step_in_head(); + case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT: return $this->step_in_select(); @@ -1492,6 +1495,26 @@ private function step_in_body() { } } + /** + * Parses next element in the 'in head' insertion mode. + * + * this internal function performs the 'in select' insertion mode + * logic for the generalized wp_html_processor::step() function. + * + * @since 6.7.0 + * + * @throws wp_html_unsupported_exception when encountering unsupported html input. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselect + * @see wp_html_processor::step + * + * @return bool whether an element was found. + */ + private function step_in_head() { + $this->last_error = self::ERROR_UNSUPPORTED; + throw new WP_HTML_Unsupported_Exception( "No support for parsing in the '{$this->state->insertion_mode}' state." ); + } + /** * Parses next element in the 'in select' insertion mode. * From f6d686d396535fb754de9f0078f79ca645a342c1 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 21:29:35 +0200 Subject: [PATCH 19/22] Multi-line comments should be block comments --- .../html-api/class-wp-html-processor.php | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) 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 6ba85262f3a1d..1ecce27b9487d 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1351,15 +1351,17 @@ private function step_in_body() { $this->insert_html_element( $this->state->current_token ); $this->state->frameset_ok = false; - // If the insertion mode is one of - // - "in table" - // - "in caption" - // - "in table body" - // - "in row" - // - "in cell" - // then switch the insertion mode to "in select in table" - // - // Otherwise, switch the insertion mode to "in select". + /* + * If the insertion mode is one of + * - "in table" + * - "in caption" + * - "in table body" + * - "in row" + * - "in cell" + * then switch the insertion mode to "in select in table" + * + * Otherwise, switch the insertion mode to "in select". + */ switch ( $this->state->insertion_mode ) { case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE: case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION: From 783cef63fe13c3b31a3ecdbe2f060f8761593e96 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 21:32:40 +0200 Subject: [PATCH 20/22] fixup! Update several cases to use current_node_is --- src/wp-includes/html-api/class-wp-html-processor.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 1ecce27b9487d..e6ed1decea45c 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1381,8 +1381,7 @@ private function step_in_body() { */ case '+OPTGROUP': case '+OPTION': - $current_node = $this->state->stack_of_open_elements->current_node(); - if ( $current_node && 'OPTION' === $current_node->node_name ) { + if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { $this->state->stack_of_open_elements->pop(); } $this->reconstruct_active_formatting_elements(); From 3222275610c5770bcddd3535a659b5dc6149e633 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 4 Jul 2024 21:35:25 +0200 Subject: [PATCH 21/22] Fix step_in_head documentation block --- .../html-api/class-wp-html-processor.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 e6ed1decea45c..49634d8571f12 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1496,20 +1496,20 @@ private function step_in_body() { } } - /** + /* * Parses next element in the 'in head' insertion mode. * - * this internal function performs the 'in select' insertion mode - * logic for the generalized wp_html_processor::step() function. + * This internal function performs the 'in head' insertion mode + * logic for the generalized WP_HTML_Processor::step() function. * * @since 6.7.0 * - * @throws wp_html_unsupported_exception when encountering unsupported html input. + * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * - * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselect - * @see wp_html_processor::step + * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhead + * @see WP_HTML_Processor::step * - * @return bool whether an element was found. + * @return bool Whether an element was found. */ private function step_in_head() { $this->last_error = self::ERROR_UNSUPPORTED; From f2e1d036c730ac81127caa9044b0bb874f5e78c8 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 4 Jul 2024 17:03:36 -0700 Subject: [PATCH 22/22] Apply feedback requests. --- .../html-api/class-wp-html-open-elements.php | 21 +++-- .../html-api/class-wp-html-processor.php | 76 ++++++++++++++----- 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php index f0b0fcb595f56..d1585cdea5bf5 100644 --- a/src/wp-includes/html-api/class-wp-html-open-elements.php +++ b/src/wp-includes/html-api/class-wp-html-open-elements.php @@ -189,11 +189,6 @@ public function current_node_is( string $identity ): bool { /** * Returns whether an element is in a specific scope. * - * ## HTML Support - * - * This function skips checking for the termination list because there - * are no supported elements which appear in the termination list. - * * @since 6.4.0 * * @see https://html.spec.whatwg.org/#has-an-element-in-the-specific-scope @@ -311,18 +306,22 @@ public function has_element_in_table_scope( $tag_name ) { /** * Returns whether a particular element is in select scope. * + * This test differs from the others like it, in that its rules are inverted. + * Instead of arriving at a match when one of any tag in a termination group + * is reached, this one terminates if any other tag is reached. + * + * > The stack of open elements is said to have a particular element in select scope when it has + * > that element in the specific scope consisting of all element types except the following: + * > - optgroup in the HTML namespace + * > - option in the HTML namespace + * * @since 6.4.0 Stub implementation (throws). * @since 6.7.0 Full implementation. * * @see https://html.spec.whatwg.org/#has-an-element-in-select-scope * - * > The stack of open elements is said to have a particular element in select scope when it has - * > that element in the specific scope consisting of all element types except the following: - * > - optgroup in the HTML namespace - * > - option in the HTML namespace - * * @param string $tag_name Name of tag to check. - * @return bool Whether given element is in scope. + * @return bool Whether the given element is in SELECT scope. */ public function has_element_in_select_scope( $tag_name ) { foreach ( $this->walk_up() as $node ) { 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 2c2baadd2ce26..40538491152ad 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1351,18 +1351,11 @@ private function step_in_body() { $this->insert_html_element( $this->state->current_token ); $this->state->frameset_ok = false; - /* - * If the insertion mode is one of - * - "in table" - * - "in caption" - * - "in table body" - * - "in row" - * - "in cell" - * then switch the insertion mode to "in select in table" - * - * Otherwise, switch the insertion mode to "in select". - */ switch ( $this->state->insertion_mode ) { + /* + * > If the insertion mode is one of "in table", "in caption", "in table body", "in row", + * > or "in cell", then switch the insertion mode to "in select in table". + */ case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE: case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION: case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY: @@ -1370,6 +1363,10 @@ private function step_in_body() { case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL: $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE; break; + + /* + * > Otherwise, switch the insertion mode to "in select". + */ default: $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT; break; @@ -1496,13 +1493,13 @@ private function step_in_body() { } } - /* + /** * Parses next element in the 'in head' insertion mode. * * This internal function performs the 'in head' insertion mode * logic for the generalized WP_HTML_Processor::step() function. * - * @since 6.7.0 + * @since 6.7.0 Stub implementation. * * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input. * @@ -1542,6 +1539,23 @@ private function step_in_select() { * > Any other character token */ case '#text': + $current_token = $this->bookmarks[ $this->state->current_token->bookmark_name ]; + + /* + * > A character token that is U+0000 NULL + * + * If a text node only comprises null bytes then it should be + * entirely ignored and should not return to calling code. + */ + if ( + 1 <= $current_token->length && + "\x00" === $this->html[ $current_token->start ] && + strspn( $this->html, "\x00", $current_token->start, $current_token->length ) === $current_token->length + ) { + // Parse error: ignore the token. + return $this->step(); + } + $this->insert_html_element( $this->state->current_token ); return true; @@ -1558,7 +1572,7 @@ private function step_in_select() { * > A DOCTYPE token */ case 'html': - // Parse error. Ignore the token. + // Parse error: ignore the token. return $this->step(); /* @@ -1615,6 +1629,7 @@ private function step_in_select() { $this->state->stack_of_open_elements->pop(); return true; } + // Parse error: ignore the token. return $this->step(); @@ -1626,31 +1641,36 @@ private function step_in_select() { $this->state->stack_of_open_elements->pop(); return true; } + // Parse error: ignore the token. return $this->step(); /* * > An end tag whose tag name is "select" * > A start tag whose tag name is "select" + * + * > It just gets treated like an end tag. */ case '-SELECT': case '+SELECT': if ( ! $this->state->stack_of_open_elements->has_element_in_select_scope( 'SELECT' ) ) { + // Parse error: ignore the token. return $this->step(); } $this->state->stack_of_open_elements->pop_until( 'SELECT' ); - $this->state->stack_of_open_elements->pop(); $this->reset_insertion_mode(); return true; /* * > A start tag whose tag name is one of: "input", "keygen", "textarea" + * + * All three of these tags are considered a parse error when found in this insertion mode. */ case '+INPUT': case '+KEYGEN': case '+TEXTAREA': - // Parse error. if ( ! $this->state->stack_of_open_elements->has_element_in_select_scope( 'SELECT' ) ) { + // Ignore the token. return $this->step(); } $this->state->stack_of_open_elements->pop_until( 'SELECT' ); @@ -2262,7 +2282,7 @@ private function close_a_p_element() { * Closes elements that have implied end tags. * * @since 6.4.0 - * @since 6.7.0 Support "option" and "optgroup". + * @since 6.7.0 Full spec support. * * @see https://html.spec.whatwg.org/#generate-implied-end-tags * @@ -2276,11 +2296,16 @@ private function generate_implied_end_tags( $except_for_this_element = null ) { 'OPTGROUP', 'OPTION', 'P', + 'RB', + 'RP', + 'RT', + 'RTC', ); - $current_node = $this->state->stack_of_open_elements->current_node(); + $no_exclusions = ! isset( $except_for_this_element ); + while ( - $current_node && $current_node->node_name !== $except_for_this_element && + ( $no_exclusions || ! $this->state->stack_of_open_elements->current_node_is( $except_for_this_element ) ) && in_array( $this->state->stack_of_open_elements->current_node(), $elements_with_implied_end_tags, true ) ) { $this->state->stack_of_open_elements->pop(); @@ -2294,18 +2319,31 @@ private function generate_implied_end_tags( $except_for_this_element = null ) { * different from generating end tags in the normal sense. * * @since 6.4.0 + * @since 6.7.0 Full spec support. * * @see WP_HTML_Processor::generate_implied_end_tags * @see https://html.spec.whatwg.org/#generate-implied-end-tags */ private function generate_implied_end_tags_thoroughly() { $elements_with_implied_end_tags = array( + 'CAPTION', + 'COLGROUP', 'DD', 'DT', 'LI', 'OPTGROUP', 'OPTION', 'P', + 'RB', + 'RP', + 'RT', + 'RTC', + 'TBODY', + 'TD', + 'TFOOT', + 'TH', + 'THEAD', + 'TR', ); while ( in_array( $this->state->stack_of_open_elements->current_node(), $elements_with_implied_end_tags, true ) ) {