--- 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