diff --git a/wp-includes/html-api/class-wp-html-open-elements.php b/wp-includes/html-api/class-wp-html-open-elements.php
index c760009ce0..5ce1f8feb5 100644
--- a/wp-includes/html-api/class-wp-html-open-elements.php
+++ b/wp-includes/html-api/class-wp-html-open-elements.php
@@ -113,13 +113,13 @@ class WP_HTML_Open_Elements {
*
* @param int $nth Retrieve the nth item on the stack, with 1 being
* the top element, 2 being the second, etc...
- * @return string|null Name of the node on the stack at the given location,
- * or `null` if the location isn't on the stack.
+ * @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 ): ?string {
+ public function at( int $nth ): ?WP_HTML_Token {
foreach ( $this->walk_down() as $item ) {
if ( 0 === --$nth ) {
- return $item->node_name;
+ return $item;
}
}
@@ -242,18 +242,22 @@ class WP_HTML_Open_Elements {
*/
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;
}
- if ( in_array( $node->node_name, $termination_list, true ) ) {
+ if ( in_array( $namespaced_name, $termination_list, true ) ) {
return false;
}
}
@@ -288,7 +292,7 @@ class WP_HTML_Open_Elements {
* > - SVG title
*
* @since 6.4.0
- * @since 6.7.0 Supports all required HTML elements.
+ * @since 6.7.0 Full support.
*
* @see https://html.spec.whatwg.org/#has-an-element-in-scope
*
@@ -309,19 +313,16 @@ class WP_HTML_Open_Elements {
'OBJECT',
'TEMPLATE',
- /*
- * @todo Support SVG and MathML nodes when support for foreign content is added.
- *
- * - MathML mi
- * - MathML mo
- * - MathML mn
- * - MathML ms
- * - MathML mtext
- * - MathML annotation-xml
- * - SVG foreignObject
- * - SVG desc
- * - SVG title
- */
+ 'math MI',
+ 'math MO',
+ 'math MN',
+ 'math MS',
+ 'math MTEXT',
+ 'math ANNOTATION-XML',
+
+ 'svg FOREIGNOBJECT',
+ 'svg DESC',
+ 'svg TITLE',
)
);
}
@@ -363,19 +364,16 @@ class WP_HTML_Open_Elements {
'TEMPLATE',
'UL',
- /*
- * @todo Support SVG and MathML nodes when support for foreign content is added.
- *
- * - MathML mi
- * - MathML mo
- * - MathML mn
- * - MathML ms
- * - MathML mtext
- * - MathML annotation-xml
- * - SVG foreignObject
- * - SVG desc
- * - SVG title
- */
+ 'math MI',
+ 'math MO',
+ 'math MN',
+ 'math MS',
+ 'math MTEXT',
+ 'math ANNOTATION-XML',
+
+ 'svg FOREIGNOBJECT',
+ 'svg DESC',
+ 'svg TITLE',
)
);
}
@@ -413,19 +411,16 @@ class WP_HTML_Open_Elements {
'OBJECT',
'TEMPLATE',
- /*
- * @todo Support SVG and MathML nodes when support for foreign content is added.
- *
- * - MathML mi
- * - MathML mo
- * - MathML mn
- * - MathML ms
- * - MathML mtext
- * - MathML annotation-xml
- * - SVG foreignObject
- * - SVG desc
- * - SVG title
- */
+ 'math MI',
+ 'math MO',
+ 'math MN',
+ 'math MS',
+ 'math MTEXT',
+ 'math ANNOTATION-XML',
+
+ 'svg FOREIGNOBJECT',
+ 'svg DESC',
+ 'svg TITLE',
)
);
}
@@ -692,11 +687,15 @@ class WP_HTML_Open_Elements {
* @param WP_HTML_Token $item Element that was added to the stack of open elements.
*/
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':
@@ -707,6 +706,15 @@ class WP_HTML_Open_Elements {
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;
@@ -750,6 +758,15 @@ class WP_HTML_Open_Elements {
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;
}
diff --git a/wp-includes/html-api/class-wp-html-processor-state.php b/wp-includes/html-api/class-wp-html-processor-state.php
index 97f6da95a0..16875c4ac1 100644
--- a/wp-includes/html-api/class-wp-html-processor-state.php
+++ b/wp-includes/html-api/class-wp-html-processor-state.php
@@ -299,18 +299,6 @@ class WP_HTML_Processor_State {
*/
const INSERTION_MODE_AFTER_AFTER_FRAMESET = 'insertion-mode-after-after-frameset';
- /**
- * In foreign content insertion mode for full HTML parser.
- *
- * @since 6.7.0
- *
- * @see https://html.spec.whatwg.org/#parsing-main-inforeign
- * @see WP_HTML_Processor_State::$insertion_mode
- *
- * @var string
- */
- const INSERTION_MODE_IN_FOREIGN_CONTENT = 'insertion-mode-in-foreign-content';
-
/**
* No-quirks mode document compatability mode.
*
diff --git a/wp-includes/html-api/class-wp-html-processor.php b/wp-includes/html-api/class-wp-html-processor.php
index 39ba43e467..3820fe0277 100644
--- a/wp-includes/html-api/class-wp-html-processor.php
+++ b/wp-includes/html-api/class-wp-html-processor.php
@@ -307,14 +307,14 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
$processor->bookmarks['root-node'] = new WP_HTML_Span( 0, 0 );
$processor->bookmarks['context-node'] = new WP_HTML_Span( 0, 0 );
- $processor->state->stack_of_open_elements->push(
- new WP_HTML_Token(
- 'root-node',
- 'HTML',
- false
- )
+ $root_node = new WP_HTML_Token(
+ 'root-node',
+ 'HTML',
+ false
);
+ $processor->state->stack_of_open_elements->push( $root_node );
+
$context_node = new WP_HTML_Token(
'context-node',
$processor->state->context_node[0],
@@ -392,6 +392,8 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
$same_node = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name;
$provenance = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real';
$this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::PUSH, $provenance );
+
+ $this->change_parsing_namespace( $token->namespace );
}
);
@@ -401,6 +403,12 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
$same_node = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name;
$provenance = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real';
$this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::POP, $provenance );
+ $adjusted_current_node = $this->get_adjusted_current_node();
+ $this->change_parsing_namespace(
+ $adjusted_current_node
+ ? $adjusted_current_node->namespace
+ : 'html'
+ );
}
);
@@ -767,19 +775,20 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
* foreign content will also act just like a void tag, immediately
* closing as soon as the processor advances to the next token.
*
- * @since 6.6.0
+ * @todo Review the self-closing logic when no node is present, ensure it
+ * matches the expectations in `step()`.
*
- * @todo When adding support for foreign content, ensure that
- * this returns false for self-closing elements in the
- * SVG and MathML namespace.
+ * @since 6.6.0
*
* @param WP_HTML_Token|null $node Optional. Node to examine, if provided.
* Default is to examine current node.
* @return bool|null Whether to expect a closer for the currently-matched node,
* or `null` if not matched on any token.
*/
- public function expects_closer( $node = null ): ?bool {
- $token_name = $node->node_name ?? $this->get_token_name();
+ public function expects_closer( WP_HTML_Token $node = null ): ?bool {
+ $token_name = $node->node_name ?? $this->get_token_name();
+ $token_namespace = $node->namespace ?? $this->get_namespace();
+
if ( ! isset( $token_name ) ) {
return null;
}
@@ -792,7 +801,9 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
// Void elements.
self::is_void( $token_name ) ||
// Special atomic elements.
- in_array( $token_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true )
+ ( 'html' === $token_namespace && in_array( $token_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true ) ) ||
+ // Self-closing elements in foreign content.
+ ( isset( $node ) && 'html' !== $node->namespace && $node->has_self_closing_flag )
);
}
@@ -824,14 +835,9 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
*
* When moving on to the next node, therefore, if the bottom-most element
* on the stack is a void element, it must be closed.
- *
- * @todo Once self-closing foreign elements and BGSOUND are supported,
- * they must also be implicitly closed here too. BGSOUND is
- * special since it's only self-closing if the self-closing flag
- * is provided in the opening tag, otherwise it expects a tag closer.
*/
$top_node = $this->state->stack_of_open_elements->current_node();
- if ( isset( $top_node ) && ! static::expects_closer( $top_node ) ) {
+ if ( isset( $top_node ) && ! $this->expects_closer( $top_node ) ) {
$this->state->stack_of_open_elements->pop();
}
}
@@ -848,14 +854,46 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
return false;
}
- $this->state->current_token = new WP_HTML_Token(
- $this->bookmark_token(),
- $this->get_token_name(),
- $this->has_self_closing_flag(),
- $this->release_internal_bookmark_on_destruct
+ $adjusted_current_node = $this->get_adjusted_current_node();
+ $is_closer = $this->is_tag_closer();
+ $is_start_tag = WP_HTML_Tag_Processor::STATE_MATCHED_TAG === $this->parser_state && ! $is_closer;
+ $token_name = $this->get_token_name();
+
+ if ( self::REPROCESS_CURRENT_NODE !== $node_to_process ) {
+ $this->state->current_token = new WP_HTML_Token(
+ $this->bookmark_token(),
+ $token_name,
+ $this->has_self_closing_flag(),
+ $this->release_internal_bookmark_on_destruct
+ );
+ }
+
+ $parse_in_current_insertion_mode = (
+ 0 === $this->state->stack_of_open_elements->count() ||
+ 'html' === $adjusted_current_node->namespace ||
+ (
+ 'math' === $adjusted_current_node->integration_node_type &&
+ (
+ ( $is_start_tag && ! in_array( $token_name, array( 'MGLYPH', 'MALIGNMARK' ), true ) ) ||
+ '#text' === $token_name
+ )
+ ) ||
+ (
+ 'math' === $adjusted_current_node->namespace &&
+ 'ANNOTATION-XML' === $adjusted_current_node->node_name &&
+ $is_start_tag && 'SVG' === $token_name
+ ) ||
+ (
+ 'html' === $adjusted_current_node->integration_node_type &&
+ ( $is_start_tag || '#text' === $token_name )
+ )
);
try {
+ if ( ! $parse_in_current_insertion_mode ) {
+ return $this->step_in_foreign_content();
+ }
+
switch ( $this->state->insertion_mode ) {
case WP_HTML_Processor_State::INSERTION_MODE_INITIAL:
return $this->step_initial();
@@ -923,9 +961,6 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET:
return $this->step_after_after_frameset();
- case WP_HTML_Processor_State::INSERTION_MODE_IN_FOREIGN_CONTENT:
- return $this->step_in_foreign_content();
-
// This should be unreachable but PHP doesn't have total type checking on switch.
default:
$this->bail( "Unaware of the requested parsing mode: '{$this->state->insertion_mode}'." );
@@ -1853,7 +1888,7 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
case '+BODY':
if (
1 === $this->state->stack_of_open_elements->count() ||
- 'BODY' !== $this->state->stack_of_open_elements->at( 2 ) ||
+ 'BODY' !== ( $this->state->stack_of_open_elements->at( 2 )->node_name ?? null ) ||
$this->state->stack_of_open_elements->contains( 'TEMPLATE' )
) {
// Ignore the token.
@@ -1879,7 +1914,7 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
case '+FRAMESET':
if (
1 === $this->state->stack_of_open_elements->count() ||
- 'BODY' !== $this->state->stack_of_open_elements->at( 2 ) ||
+ 'BODY' !== ( $this->state->stack_of_open_elements->at( 2 )->node_name ?? null ) ||
false === $this->state->frameset_ok
) {
// Ignore the token.
@@ -2075,7 +2110,7 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
'ADDRESS' !== $node->node_name &&
'DIV' !== $node->node_name &&
'P' !== $node->node_name &&
- $this->is_special( $node->node_name )
+ self::is_special( $node )
) {
/*
* > If node is in the special category, but is not an address, div,
@@ -2136,11 +2171,6 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
* > "button", "center", "details", "dialog", "dir", "div", "dl", "fieldset",
* > "figcaption", "figure", "footer", "header", "hgroup", "listing", "main",
* > "menu", "nav", "ol", "pre", "search", "section", "summary", "ul"
- *
- * @todo This needs to check if the element in scope is an HTML element, meaning that
- * when SVG and MathML support is added, this needs to differentiate between an
- * HTML element of the given name, such as `
`, and a foreign element of
- * the same given name.
*/
case '-ADDRESS':
case '-ARTICLE':
@@ -2411,11 +2441,6 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
/*
* > A end tag token whose tag name is one of: "applet", "marquee", "object"
- *
- * @todo This needs to check if the element in scope is an HTML element, meaning that
- * when SVG and MathML support is added, this needs to differentiate between an
- * HTML element of the given name, such as `