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 210492ab9af08..36631f74800d0 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 @@ -280,6 +280,7 @@ public function has_element_in_specific_scope( string $tag_name, $termination_li * > - th * > - marquee * > - object + * > - select * > - template * > - MathML mi * > - MathML mo @@ -311,6 +312,7 @@ public function has_element_in_scope( string $tag_name ): bool { 'TH', 'MARQUEE', 'OBJECT', + 'SELECT', 'TEMPLATE', 'math MI', @@ -470,12 +472,14 @@ public function has_element_in_table_scope( string $tag_name ): bool { * @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 + * @deprecated {WP_VERSION} This method is no longer part of the HTML standard. * * @param string $tag_name Name of tag to check. * @return bool Whether the given element is in SELECT scope. */ public function has_element_in_select_scope( string $tag_name ): bool { + _deprecated_function( __METHOD__, '{WP_VERSION}' ); + foreach ( $this->walk_up() as $node ) { if ( $node->node_name === $tag_name ) { return true; diff --git a/src/wp-includes/html-api/class-wp-html-processor-state.php b/src/wp-includes/html-api/class-wp-html-processor-state.php index b257aa809da75..0c8c705fba74f 100644 --- a/src/wp-includes/html-api/class-wp-html-processor-state.php +++ b/src/wp-includes/html-api/class-wp-html-processor-state.php @@ -208,7 +208,8 @@ class WP_HTML_Processor_State { * * @since 6.7.0 * - * @see https://html.spec.whatwg.org/#parsing-main-inselect + * @deprecated {WP_VERSION} The "in select" insertion mode was removed from the standard. + * * @see WP_HTML_Processor_State::$insertion_mode * * @var string @@ -220,7 +221,8 @@ class WP_HTML_Processor_State { * * @since 6.7.0 * - * @see https://html.spec.whatwg.org/#parsing-main-inselectintable + * @deprecated {WP_VERSION} The "in select in table" insertion mode was removed from the standard. + * * @see WP_HTML_Processor_State::$insertion_mode * * @var string 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 d64574e2a8db2..653c2bcfa62c2 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1100,12 +1100,6 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ): bool { case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL: return $this->step_in_cell(); - case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT: - return $this->step_in_select(); - - case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE: - return $this->step_in_select_in_table(); - case WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE: return $this->step_in_template(); @@ -2546,7 +2540,7 @@ private function step_in_body(): bool { * > An end tag whose tag name is one of: "address", "article", "aside", "blockquote", * > "button", "center", "details", "dialog", "dir", "div", "dl", "fieldset", * > "figcaption", "figure", "footer", "header", "hgroup", "listing", "main", - * > "menu", "nav", "ol", "pre", "search", "section", "summary", "ul" + * > "menu", "nav", "ol", "pre", "search", "section", "select", "summary", "ul" */ case '-ADDRESS': case '-ARTICLE': @@ -2573,6 +2567,7 @@ private function step_in_body(): bool { case '-PRE': case '-SEARCH': case '-SECTION': + case '-SELECT': case '-SUMMARY': case '-UL': if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $token_name ) ) { @@ -2882,6 +2877,28 @@ private function step_in_body(): bool { * > A start tag whose tag name is "input" */ case '+INPUT': + /* + * > If the parser was created as part of the HTML fragment parsing algorithm + * > (fragment case) and the context element passed to that algorithm is a + * > select element: + * > 1. Parse error. + * > 2. Ignore the token. + * > 3. Return. + */ + if ( null !== $this->context_node && 'SELECT' === $this->context_node->node_name ) { + return $this->step(); + } + + /* + * > If the stack of open elements has a select element in scope: + * > 1. Parse error. + * > 2. Pop elements from the stack of open elements until a select element + * > has been popped from the stack. + */ + if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) { + $this->state->stack_of_open_elements->pop_until( 'SELECT' ); + } + $this->reconstruct_active_formatting_elements(); $this->insert_html_element( $this->state->current_token ); @@ -2913,6 +2930,17 @@ private function step_in_body(): bool { if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) { $this->close_a_p_element(); } + + if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) { + $this->generate_implied_end_tags(); + /* + * > If the stack of open elements has an option element in scope or has + * > an optgroup element in scope, then this is a parse error. + * + * @todo Indicate a parse error once it's possible. + */ + } + $this->insert_html_element( $this->state->current_token ); $this->state->frameset_ok = false; return true; @@ -2999,44 +3027,79 @@ private function step_in_body(): bool { * > A start tag whose tag name is "select" */ case '+SELECT': + /* + * > If the parser was created as part of the HTML fragment parsing algorithm + * > (fragment case) and the context element passed to that algorithm is a + * > select element: + * > 1. Parse error. + * > 2. Ignore the token. + */ + if ( null !== $this->context_node && 'SELECT' === $this->context_node->node_name ) { + // @todo Indicate a parse error once it's possible. + return $this->step(); + } + /* + * > Otherwise, if the stack of open elements has a select element in scope: + * > 1. Parse error. + * > 2. Ignore the token. + * > 3. Pop elements from the stack of open elements until a select element + * > has been popped from the stack. + */ + if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) { + // @todo Indicate a parse error once it's possible. + $this->state->stack_of_open_elements->pop_until( 'SELECT' ); + return $this->step(); + } + $this->reconstruct_active_formatting_elements(); $this->insert_html_element( $this->state->current_token ); $this->state->frameset_ok = false; + return true; - 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: - 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; - + /* + * > A start tag whose tag name is "option" + */ + case '+OPTION': + if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) { + $this->generate_implied_end_tags( 'OPTGROUP' ); /* - * > Otherwise, switch the insertion mode to "in select". + * > If the stack of open elements has an option element in scope, then this + * > is a parse error. + * @todo Indicate a parse error once it's possible. */ - default: - $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT; - break; + } elseif ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { + $this->state->stack_of_open_elements->pop(); } + + $this->reconstruct_active_formatting_elements(); + $this->insert_html_element( $this->state->current_token ); return true; /* - * > A start tag whose tag name is one of: "optgroup", "option" + * > A start tag whose tag name is "optgroup" */ case '+OPTGROUP': - case '+OPTION': - if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { + if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) { + $this->generate_implied_end_tags(); + /* + * > If the stack of open elements has an option element in scope or has an + * > optgroup element in scope, then this is a parse error. + * @todo Indicate a parse error once it's possible. + */ + } elseif ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { $this->state->stack_of_open_elements->pop(); } + $this->reconstruct_active_formatting_elements(); $this->insert_html_element( $this->state->current_token ); return true; + /* + * > An end tag whose tag name is "option" + * + * The "option" end tag is handled in the any other end tag section below. + */ + /* * > A start tag whose tag name is one of: "rb", "rtc" */ @@ -3131,12 +3194,36 @@ private function step_in_body(): bool { /* * > Any other start tag */ + + /* + * SELECT > BUTTON > SELECTEDCONTENT requires special handling, cloning the + * selected option. This is unsupported. + */ + if ( 'SELECTEDCONTENT' === $token_name ) { + $walker = $this->state->stack_of_open_elements->walk_up(); + if ( null !== $walker->current() && $walker->current()->node_name === 'BUTTON' ) { + $walker->next(); + if ( null !== $walker->current() && $walker->current()->node_name === 'SELECT' ) { + $this->bail( 'Cannot process SELECTEDCONTENT where cloning may be necessary.' ); + } + } + } $this->reconstruct_active_formatting_elements(); $this->insert_html_element( $this->state->current_token ); return true; } else { /* * > Any other end tag + * + * OPTION end tags are handled here as well: + * + * > An end tag whose tag name is "option" + * > - Let option be the first option element in the stack of open elements. + * > - Run the steps for "any other end tag." + * > - If option is no longer in the stack of open elements, then run maybe clone + * > an option into selectedcontent given option. + * + * The "maybe clone an option into selectedcontent" algorithm is not implemented. */ /* @@ -3957,245 +4044,6 @@ private function step_in_cell(): bool { return $this->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.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_select(): bool { - $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': - /* - * > 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 ( parent::TEXT_IS_NULL_SEQUENCE === $this->text_node_classification ) { - // Parse error: ignore the token. - return $this->step(); - } - - $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" - */ - case '+HTML': - return $this->step_in_body(); - - /* - * > A start tag whose tag name is "option" - */ - case '+OPTION': - 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 ); - return true; - - /* - * > 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': - if ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) { - $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(); - } - - $this->insert_html_element( $this->state->current_token ); - return true; - - /* - * > An end tag whose tag name is "optgroup" - */ - case '-OPTGROUP': - $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(); - } - } - - 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(); - - /* - * > An end tag whose tag name is "option" - */ - case '-OPTION': - 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(); - - /* - * > 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->reset_insertion_mode_appropriately(); - 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': - 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' ); - $this->reset_insertion_mode_appropriately(); - 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(); - } - - /* - * > Anything else - * > Parse error: ignore the token. - */ - return $this->step(); - } - - /** - * Parses next element in the 'in select in table' insertion mode. - * - * This internal function performs the 'in select in table' 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/#parsing-main-inselectintable - * @see WP_HTML_Processor::step - * - * @return bool Whether an element was found. - */ - private function step_in_select_in_table(): bool { - $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 ) { - /* - * > A start tag whose tag name is one of: "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th" - */ - case '+CAPTION': - case '+TABLE': - case '+TBODY': - case '+TFOOT': - case '+THEAD': - case '+TR': - case '+TD': - case '+TH': - // @todo Indicate a parse error once it's possible. - $this->state->stack_of_open_elements->pop_until( 'SELECT' ); - $this->reset_insertion_mode_appropriately(); - return $this->step( self::REPROCESS_CURRENT_NODE ); - - /* - * > An end tag whose tag name is one of: "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th" - */ - case '-CAPTION': - case '-TABLE': - case '-TBODY': - case '-TFOOT': - case '-THEAD': - case '-TR': - case '-TD': - case '-TH': - // @todo Indicate a parse error once it's possible. - if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $token_name ) ) { - return $this->step(); - } - $this->state->stack_of_open_elements->pop_until( 'SELECT' ); - $this->reset_insertion_mode_appropriately(); - return $this->step( self::REPROCESS_CURRENT_NODE ); - } - - /* - * > Anything else - */ - return $this->step_in_select(); - } - /** * Parses next element in the 'in template' insertion mode. * @@ -5033,12 +4881,6 @@ private function step_in_foreign_content(): bool { case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL: return $this->step_in_cell(); - case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT: - return $this->step_in_select(); - - case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE: - return $this->step_in_select_in_table(); - case WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE: return $this->step_in_template(); @@ -5905,46 +5747,7 @@ private function reset_insertion_mode_appropriately(): void { switch ( $node->node_name ) { /* - * > 4. If node is a `select` element, run these substeps: - * > 1. If _last_ is true, jump to the step below labeled done. - * > 2. Let _ancestor_ be _node_. - * > 3. _Loop_: If _ancestor_ is the first node in the stack of open elements, - * > jump to the step below labeled done. - * > 4. Let ancestor be the node before ancestor in the stack of open elements. - * > … - * > 7. Jump back to the step labeled _loop_. - * > 8. _Done_: Switch the insertion mode to "in select" and return. - */ - case 'SELECT': - if ( ! $last ) { - foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $ancestor ) { - if ( 'html' !== $ancestor->namespace ) { - continue; - } - - switch ( $ancestor->node_name ) { - /* - * > 5. If _ancestor_ is a `template` node, jump to the step below - * > labeled _done_. - */ - case 'TEMPLATE': - break 2; - - /* - * > 6. If _ancestor_ is a `table` node, switch the insertion mode to - * > "in select in table" and return. - */ - case 'TABLE': - $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE; - return; - } - } - } - $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT; - return; - - /* - * > 5. If _node_ is a `td` or `th` element and _last_ is false, then switch the + * > 4. If _node_ is a `td` or `th` element and _last_ is false, then switch the * > insertion mode to "in cell" and return. */ case 'TD': @@ -5955,16 +5758,16 @@ private function reset_insertion_mode_appropriately(): void { } break; - /* - * > 6. If _node_ is a `tr` element, then switch the insertion mode to "in row" - * > and return. - */ + /* + * > 5. If _node_ is a `tr` element, then switch the insertion mode to "in row" + * > and return. + */ case 'TR': $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW; return; /* - * > 7. If _node_ is a `tbody`, `thead`, or `tfoot` element, then switch the + * > 6. If _node_ is a `tbody`, `thead`, or `tfoot` element, then switch the * > insertion mode to "in table body" and return. */ case 'TBODY': @@ -5974,7 +5777,7 @@ private function reset_insertion_mode_appropriately(): void { return; /* - * > 8. If _node_ is a `caption` element, then switch the insertion mode to + * > 7. If _node_ is a `caption` element, then switch the insertion mode to * > "in caption" and return. */ case 'CAPTION': @@ -5982,7 +5785,7 @@ private function reset_insertion_mode_appropriately(): void { return; /* - * > 9. If _node_ is a `colgroup` element, then switch the insertion mode to + * > 8. If _node_ is a `colgroup` element, then switch the insertion mode to * > "in column group" and return. */ case 'COLGROUP': @@ -5990,15 +5793,15 @@ private function reset_insertion_mode_appropriately(): void { return; /* - * > 10. If _node_ is a `table` element, then switch the insertion mode to - * > "in table" and return. + * > 9. If _node_ is a `table` element, then switch the insertion mode to + * > "in table" and return. */ case 'TABLE': $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE; return; /* - * > 11. If _node_ is a `template` element, then switch the insertion mode to the + * > 10. If _node_ is a `template` element, then switch the insertion mode to the * > current template insertion mode and return. */ case 'TEMPLATE': @@ -6006,7 +5809,7 @@ private function reset_insertion_mode_appropriately(): void { return; /* - * > 12. If _node_ is a `head` element and _last_ is false, then switch the + * > 11. If _node_ is a `head` element and _last_ is false, then switch the * > insertion mode to "in head" and return. */ case 'HEAD': @@ -6017,7 +5820,7 @@ private function reset_insertion_mode_appropriately(): void { break; /* - * > 13. If _node_ is a `body` element, then switch the insertion mode to "in body" + * > 12. If _node_ is a `body` element, then switch the insertion mode to "in body" * > and return. */ case 'BODY': @@ -6025,7 +5828,7 @@ private function reset_insertion_mode_appropriately(): void { return; /* - * > 14. If _node_ is a `frameset` element, then switch the insertion mode to + * > 13. If _node_ is a `frameset` element, then switch the insertion mode to * > "in frameset" and return. (fragment case) */ case 'FRAMESET': @@ -6033,7 +5836,7 @@ private function reset_insertion_mode_appropriately(): void { return; /* - * > 15. If _node_ is an `html` element, run these substeps: + * > 14. If _node_ is an `html` element, run these substeps: * > 1. If the head element pointer is null, switch the insertion mode to * > "before head" and return. (fragment case) * > 2. Otherwise, the head element pointer is not null, switch the insertion @@ -6048,7 +5851,7 @@ private function reset_insertion_mode_appropriately(): void { } /* - * > 16. If _last_ is true, then switch the insertion mode to "in body" + * > 15. If _last_ is true, then switch the insertion mode to "in body" * > and return. (fragment case) * * This is only reachable if `$last` is true, as per the fragment parsing case. diff --git a/tests/phpunit/data/html5lib-tests/tree-construction/menuitem-element.dat b/tests/phpunit/data/html5lib-tests/tree-construction/menuitem-element.dat index fb13c3c33b0ab..f7c8e2c3ca74e 100644 --- a/tests/phpunit/data/html5lib-tests/tree-construction/menuitem-element.dat +++ b/tests/phpunit/data/html5lib-tests/tree-construction/menuitem-element.dat @@ -161,13 +161,14 @@ #data #errors -33: Stray start tag “menuitem”. +1:34: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body, select, menuitem. #document | | | | |