diff -r 48c4eec2b7e6 -r 8c2e4d02f4ef wp/wp-includes/html-api/class-wp-html-open-elements.php --- a/wp/wp-includes/html-api/class-wp-html-open-elements.php Fri Sep 05 18:40:08 2025 +0200 +++ b/wp/wp-includes/html-api/class-wp-html-open-elements.php Fri Sep 05 18:52:52 2025 +0200 @@ -58,7 +58,7 @@ * * @since 6.6.0 * - * @var Closure + * @var Closure|null */ private $pop_handler = null; @@ -69,7 +69,7 @@ * * @since 6.6.0 * - * @var Closure + * @var Closure|null */ private $push_handler = null; @@ -83,7 +83,7 @@ * * @param Closure $handler The handler function. */ - public function set_pop_handler( Closure $handler ) { + public function set_pop_handler( Closure $handler ): void { $this->pop_handler = $handler; } @@ -97,11 +97,54 @@ * * @param Closure $handler The handler function. */ - public function set_push_handler( Closure $handler ) { + public function set_push_handler( Closure $handler ): void { $this->push_handler = $handler; } /** + * Returns the name of the node at the nth position on the stack + * of open elements, or `null` if no such position exists. + * + * Note that this uses a 1-based index, which represents the + * "nth item" on the stack, counting from the top, where the + * top-most element is the 1st, the second is the 2nd, etc... + * + * @since 6.7.0 + * + * @param int $nth Retrieve the nth item on the stack, with 1 being + * the top element, 2 being the second, etc... + * @return WP_HTML_Token|null Name of the node on the stack at the given location, + * or `null` if the location isn't on the stack. + */ + public function at( int $nth ): ?WP_HTML_Token { + foreach ( $this->walk_down() as $item ) { + if ( 0 === --$nth ) { + return $item; + } + } + + return null; + } + + /** + * Reports if a node of a given name is in the stack of open elements. + * + * @since 6.7.0 + * + * @param string $node_name Name of node for which to check. + * @return bool Whether a node of the given name is in the stack of open elements. + */ + public function contains( string $node_name ): bool { + foreach ( $this->walk_up() as $item ) { + if ( $node_name === $item->node_name ) { + return true; + } + } + + return false; + } + + /** * Reports if a specific node is in the stack of open elements. * * @since 6.4.0 @@ -109,9 +152,9 @@ * @param WP_HTML_Token $token Look for this node in the stack. * @return bool Whether the referenced node is in the stack of open elements. */ - public function contains_node( $token ) { + public function contains_node( WP_HTML_Token $token ): bool { foreach ( $this->walk_up() as $item ) { - if ( $token->bookmark_name === $item->bookmark_name ) { + if ( $token === $item ) { return true; } } @@ -126,7 +169,7 @@ * * @return int How many node are in the stack of open elements. */ - public function count() { + public function count(): int { return count( $this->stack ); } @@ -138,19 +181,56 @@ * * @return WP_HTML_Token|null Last node in the stack of open elements, if one exists, otherwise null. */ - public function current_node() { + public function current_node(): ?WP_HTML_Token { $current_node = end( $this->stack ); return $current_node ? $current_node : null; } /** - * Returns whether an element is in a specific scope. + * Indicates if the current node is of a given type or name. + * + * It's possible to pass either a node type or a node name to this function. + * In the case there is no current element it will always return `false`. + * + * 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' ); + * + * // Is the current node any element/tag? + * $stack->current_node_is( '#tag' ); + * + * @see WP_HTML_Tag_Processor::get_token_type + * @see WP_HTML_Tag_Processor::get_token_name + * + * @since 6.7.0 * - * ## HTML Support + * @access private * - * This function skips checking for the termination list because there - * are no supported elements which appear in the termination list. + * @param string $identity Check if the current node has this name or type (depending on what is provided). + * @return bool Whether there is a current element that matches the given identity, whether a token name or type. + */ + public function current_node_is( string $identity ): bool { + $current_node = end( $this->stack ); + if ( false === $current_node ) { + return false; + } + + $current_node_name = $current_node->node_name; + + return ( + $current_node_name === $identity || + ( '#doctype' === $identity && 'html' === $current_node_name ) || + ( '#tag' === $identity && ctype_upper( $current_node_name ) ) + ); + } + + /** + * Returns whether an element is in a specific scope. * * @since 6.4.0 * @@ -160,25 +240,24 @@ * @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 ) { + public function has_element_in_specific_scope( string $tag_name, $termination_list ): bool { foreach ( $this->walk_up() as $node ) { - if ( $node->node_name === $tag_name ) { + $namespaced_name = 'html' === $node->namespace + ? $node->node_name + : "{$node->namespace} {$node->node_name}"; + + if ( $namespaced_name === $tag_name ) { return true; } if ( '(internal: H1 through H6 - do not use)' === $tag_name && - in_array( $node->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) + in_array( $namespaced_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) { return true; } - switch ( $node->node_name ) { - case 'HTML': - return false; - } - - if ( in_array( $node->node_name, $termination_list, true ) ) { + if ( in_array( $namespaced_name, $termination_list, true ) ) { return false; } } @@ -189,25 +268,61 @@ /** * Returns whether a particular element is in scope. * + * > The stack of open elements is said to have a particular element in + * > scope when it has that element in the specific scope consisting of + * > the following element types: + * > + * > - applet + * > - caption + * > - html + * > - table + * > - td + * > - th + * > - marquee + * > - object + * > - template + * > - MathML mi + * > - MathML mo + * > - MathML mn + * > - MathML ms + * > - MathML mtext + * > - MathML annotation-xml + * > - SVG foreignObject + * > - SVG desc + * > - SVG title + * * @since 6.4.0 + * @since 6.7.0 Full support. * * @see https://html.spec.whatwg.org/#has-an-element-in-scope * * @param string $tag_name Name of tag to check. * @return bool Whether given element is in scope. */ - public function has_element_in_scope( $tag_name ) { + public function has_element_in_scope( string $tag_name ): bool { return $this->has_element_in_specific_scope( $tag_name, array( + 'APPLET', + 'CAPTION', + 'HTML', + 'TABLE', + 'TD', + 'TH', + 'MARQUEE', + 'OBJECT', + 'TEMPLATE', - /* - * Because it's not currently possible to encounter - * one of the termination elements, they don't need - * to be listed here. If they were, they would be - * unreachable and only waste CPU cycles while - * scanning through HTML. - */ + 'math MI', + 'math MO', + 'math MN', + 'math MS', + 'math MTEXT', + 'math ANNOTATION-XML', + + 'svg FOREIGNOBJECT', + 'svg DESC', + 'svg TITLE', ) ); } @@ -215,21 +330,50 @@ /** * Returns whether a particular element is in list item scope. * + * > The stack of open elements is said to have a particular element + * > in list item scope when it has that element in the specific scope + * > consisting of the following element types: + * > + * > - All the element types listed above for the has an element in scope algorithm. + * > - ol in the HTML namespace + * > - ul in the HTML namespace + * * @since 6.4.0 * @since 6.5.0 Implemented: no longer throws on every invocation. + * @since 6.7.0 Supports all required HTML elements. * * @see https://html.spec.whatwg.org/#has-an-element-in-list-item-scope * * @param string $tag_name Name of tag to check. * @return bool Whether given element is in scope. */ - public function has_element_in_list_item_scope( $tag_name ) { + public function has_element_in_list_item_scope( string $tag_name ): bool { return $this->has_element_in_specific_scope( $tag_name, array( - // There are more elements that belong here which aren't currently supported. + 'APPLET', + 'BUTTON', + 'CAPTION', + 'HTML', + 'TABLE', + 'TD', + 'TH', + 'MARQUEE', + 'OBJECT', 'OL', + 'TEMPLATE', 'UL', + + 'math MI', + 'math MO', + 'math MN', + 'math MS', + 'math MTEXT', + 'math ANNOTATION-XML', + + 'svg FOREIGNOBJECT', + 'svg DESC', + 'svg TITLE', ) ); } @@ -237,51 +381,115 @@ /** * Returns whether a particular element is in button scope. * + * > The stack of open elements is said to have a particular element + * > in button scope when it has that element in the specific scope + * > consisting of the following element types: + * > + * > - All the element types listed above for the has an element in scope algorithm. + * > - button in the HTML namespace + * * @since 6.4.0 + * @since 6.7.0 Supports all required HTML elements. * * @see https://html.spec.whatwg.org/#has-an-element-in-button-scope * * @param string $tag_name Name of tag to check. * @return bool Whether given element is in scope. */ - public function has_element_in_button_scope( $tag_name ) { - return $this->has_element_in_specific_scope( $tag_name, array( 'BUTTON' ) ); + public function has_element_in_button_scope( string $tag_name ): bool { + return $this->has_element_in_specific_scope( + $tag_name, + array( + 'APPLET', + 'BUTTON', + 'CAPTION', + 'HTML', + 'TABLE', + 'TD', + 'TH', + 'MARQUEE', + 'OBJECT', + 'TEMPLATE', + + 'math MI', + 'math MO', + 'math MN', + 'math MS', + 'math MTEXT', + 'math ANNOTATION-XML', + + 'svg FOREIGNOBJECT', + 'svg DESC', + 'svg TITLE', + ) + ); } /** * Returns whether a particular element is in table scope. * + * > The stack of open elements is said to have a particular element + * > in table scope when it has that element in the specific scope + * > consisting of the following element types: + * > + * > - html in the HTML namespace + * > - table in the HTML namespace + * > - template in the HTML namespace + * * @since 6.4.0 + * @since 6.7.0 Full implementation. * * @see https://html.spec.whatwg.org/#has-an-element-in-table-scope * - * @throws WP_HTML_Unsupported_Exception Always until this function is implemented. - * * @param string $tag_name Name of tag to check. * @return bool Whether given element is in scope. */ - public function has_element_in_table_scope( $tag_name ) { - throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on table scope.' ); - - return false; // The linter requires this unreachable code until the function is implemented and can return. + public function has_element_in_table_scope( string $tag_name ): bool { + return $this->has_element_in_specific_scope( + $tag_name, + array( + 'HTML', + 'TABLE', + 'TEMPLATE', + ) + ); } /** * Returns whether a particular element is in select scope. * - * @since 6.4.0 + * 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 * - * @throws WP_HTML_Unsupported_Exception Always until this function is implemented. - * * @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 ) { - throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on select scope.' ); + public function has_element_in_select_scope( string $tag_name ): bool { + 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; } /** @@ -293,7 +501,7 @@ * * @return bool Whether a P is in BUTTON scope. */ - public function has_p_in_button_scope() { + public function has_p_in_button_scope(): bool { return $this->has_p_in_button_scope; } @@ -306,47 +514,42 @@ * * @return bool Whether a node was popped off of the stack. */ - public function pop() { + public function pop(): bool { $item = array_pop( $this->stack ); if ( null === $item ) { return false; } - if ( 'context-node' === $item->bookmark_name ) { - $this->stack[] = $item; - return false; - } - $this->after_element_pop( $item ); return true; } /** - * Pops nodes off of the stack of open elements until one with the given tag name has been popped. + * Pops nodes off of the stack of open elements until an HTML tag with the given name has been popped. * * @since 6.4.0 * * @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 $html_tag_name Name of tag that needs to be popped off of the stack of open elements. * @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 ) { + public function pop_until( string $html_tag_name ): bool { foreach ( $this->walk_up() as $item ) { - if ( 'context-node' === $item->bookmark_name ) { - return true; + $this->pop(); + + if ( 'html' !== $item->namespace ) { + continue; } - $this->pop(); - if ( - '(internal: H1 through H6 - do not use)' === $tag_name && + '(internal: H1 through H6 - do not use)' === $html_tag_name && in_array( $item->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) { return true; } - if ( $tag_name === $item->node_name ) { + if ( $html_tag_name === $item->node_name ) { return true; } } @@ -363,7 +566,7 @@ * * @param WP_HTML_Token $stack_item Item to add onto stack. */ - public function push( $stack_item ) { + public function push( WP_HTML_Token $stack_item ): void { $this->stack[] = $stack_item; $this->after_element_push( $stack_item ); } @@ -376,11 +579,7 @@ * @param WP_HTML_Token $token The node to remove from the stack of open elements. * @return bool Whether the node was found and removed from the stack of open elements. */ - public function remove_node( $token ) { - if ( 'context-node' === $token->bookmark_name ) { - return false; - } - + public function remove_node( WP_HTML_Token $token ): bool { foreach ( $this->walk_up() as $position_from_end => $item ) { if ( $token->bookmark_name !== $item->bookmark_name ) { continue; @@ -443,9 +642,10 @@ * @since 6.4.0 * @since 6.5.0 Accepts $above_this_node to start traversal above a given node, if it exists. * - * @param ?WP_HTML_Token $above_this_node Start traversing above this node, if provided and if the node exists. + * @param WP_HTML_Token|null $above_this_node Optional. Start traversing above this node, + * if provided and if the node exists. */ - public function walk_up( $above_this_node = null ) { + public function walk_up( ?WP_HTML_Token $above_this_node = null ) { $has_found_node = null === $above_this_node; for ( $i = count( $this->stack ) - 1; $i >= 0; $i-- ) { @@ -477,13 +677,35 @@ * * @param WP_HTML_Token $item Element that was added to the stack of open elements. */ - public function after_element_push( $item ) { + public function after_element_push( WP_HTML_Token $item ): void { + $namespaced_name = 'html' === $item->namespace + ? $item->node_name + : "{$item->namespace} {$item->node_name}"; + /* * When adding support for new elements, expand this switch to trap * cases where the precalculated value needs to change. */ - switch ( $item->node_name ) { + switch ( $namespaced_name ) { + case 'APPLET': case 'BUTTON': + case 'CAPTION': + case 'HTML': + case 'TABLE': + case 'TD': + case 'TH': + case 'MARQUEE': + case 'OBJECT': + case 'TEMPLATE': + case 'math MI': + case 'math MO': + case 'math MN': + case 'math MS': + case 'math MTEXT': + case 'math ANNOTATION-XML': + case 'svg FOREIGNOBJECT': + case 'svg DESC': + case 'svg TITLE': $this->has_p_in_button_scope = false; break; @@ -510,17 +732,32 @@ * * @param WP_HTML_Token $item Element that was removed from the stack of open elements. */ - public function after_element_pop( $item ) { + public function after_element_pop( WP_HTML_Token $item ): void { /* * When adding support for new elements, expand this switch to trap * cases where the precalculated value needs to change. */ switch ( $item->node_name ) { + case 'APPLET': case 'BUTTON': - $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' ); - break; - + case 'CAPTION': + case 'HTML': case 'P': + case 'TABLE': + case 'TD': + case 'TH': + case 'MARQUEE': + case 'OBJECT': + case 'TEMPLATE': + case 'math MI': + case 'math MO': + case 'math MN': + case 'math MS': + case 'math MTEXT': + case 'math ANNOTATION-XML': + case 'svg FOREIGNOBJECT': + case 'svg DESC': + case 'svg TITLE': $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' ); break; } @@ -531,6 +768,80 @@ } /** + * Clear the stack back to a table context. + * + * > When the steps above require the UA to clear the stack back to a table context, it means + * > that the UA must, while the current node is not a table, template, or html element, pop + * > elements from the stack of open elements. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-context + * + * @since 6.7.0 + */ + public function clear_to_table_context(): void { + foreach ( $this->walk_up() as $item ) { + if ( + 'TABLE' === $item->node_name || + 'TEMPLATE' === $item->node_name || + 'HTML' === $item->node_name + ) { + break; + } + $this->pop(); + } + } + + /** + * Clear the stack back to a table body context. + * + * > When the steps above require the UA to clear the stack back to a table body context, it + * > means that the UA must, while the current node is not a tbody, tfoot, thead, template, or + * > html element, pop elements from the stack of open elements. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-body-context + * + * @since 6.7.0 + */ + public function clear_to_table_body_context(): void { + foreach ( $this->walk_up() as $item ) { + if ( + 'TBODY' === $item->node_name || + 'TFOOT' === $item->node_name || + 'THEAD' === $item->node_name || + 'TEMPLATE' === $item->node_name || + 'HTML' === $item->node_name + ) { + break; + } + $this->pop(); + } + } + + /** + * Clear the stack back to a table row context. + * + * > When the steps above require the UA to clear the stack back to a table row context, it + * > means that the UA must, while the current node is not a tr, template, or html element, pop + * > elements from the stack of open elements. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-row-context + * + * @since 6.7.0 + */ + public function clear_to_table_row_context(): void { + foreach ( $this->walk_up() as $item ) { + if ( + 'TR' === $item->node_name || + 'TEMPLATE' === $item->node_name || + 'HTML' === $item->node_name + ) { + break; + } + $this->pop(); + } + } + + /** * Wakeup magic method. * * @since 6.6.0