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 c50032829f63f..5bf69bab3ebbe 100644
--- a/src/wp-includes/html-api/class-wp-html-processor.php
+++ b/src/wp-includes/html-api/class-wp-html-processor.php
@@ -422,6 +422,61 @@ function ( WP_HTML_Token $token ): void {
};
}
+ /**
+ * Creates a fragment processor with the current node as its context element.
+ *
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-parsing-algorithm
+ *
+ * @param string $html Input HTML fragment to process.
+ * @return static|null The created processor if successful, otherwise null.
+ */
+ public function spawn_fragment_parser( string $html ): ?self {
+ if ( $this->get_token_type() !== '#tag' ) {
+ return null;
+ }
+
+ $namespace = $this->get_namespace();
+
+ /*
+ * Prevent creating fragments at "self-contained" nodes.
+ *
+ * @see https://github.com/WordPress/wordpress-develop/pull/7141
+ * @see https://github.com/WordPress/wordpress-develop/pull/7198
+ */
+ if (
+ 'html' === $namespace &&
+ in_array( $this->get_tag(), array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true )
+ ) {
+ return null;
+ }
+
+ $fragment_processor = self::create_fragment( $html );
+ $fragment_processor->compat_mode = $this->compat_mode;
+
+ $fragment_processor->context_node = clone $this->state->current_token;
+ $fragment_processor->context_node->bookmark_name = 'context-node';
+ $fragment_processor->context_node->on_destroy = null;
+
+ $context_element = array( $fragment_processor->context_node->node_name, array() );
+ foreach ( $this->get_attribute_names_with_prefix( '' ) as $name => $value ) {
+ $context_element[1][ $name ] = $value;
+ }
+
+ $fragment_processor->breadcrumbs = array();
+
+ if ( 'TEMPLATE' === $context_element[0] ) {
+ $fragment_processor->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE;
+ }
+
+ $fragment_processor->reset_insertion_mode_appropriately();
+
+ // @todo Set the parser's form element pointer.
+
+ $fragment_processor->state->encoding_confidence = 'irrelevant';
+
+ return $fragment_processor;
+ }
+
/**
* Stops the parser and terminates its execution when encountering unsupported markup.
*
@@ -4501,7 +4556,7 @@ private function step_in_foreign_content(): bool {
$this->state->stack_of_open_elements->pop();
}
- return $this->step( self::REPROCESS_CURRENT_NODE );
+ goto in_foreign_content_process_in_current_insertion_mode;
}
/*
@@ -4577,6 +4632,7 @@ private function step_in_foreign_content(): bool {
goto in_foreign_content_end_tag_loop;
}
+ in_foreign_content_process_in_current_insertion_mode:
switch ( $this->state->insertion_mode ) {
case WP_HTML_Processor_State::INSERTION_MODE_INITIAL:
return $this->step_initial();
diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php b/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php
index 54d60f8c78a66..f6b518b96c940 100644
--- a/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php
@@ -21,6 +21,8 @@
* @group html-api-html5lib-tests
*/
class Tests_HtmlApi_Html5lib extends WP_UnitTestCase {
+ const TREE_INDENT = ' ';
+
/**
* Skip specific tests that may not be supported or have known issues.
*/
@@ -139,10 +141,6 @@ public function data_external_html5lib_tests() {
* @return bool True if the test case should be skipped. False otherwise.
*/
private static function should_skip_test( ?string $test_context_element, string $test_name ): bool {
- if ( null !== $test_context_element && 'body' !== $test_context_element ) {
- return true;
- }
-
if ( array_key_exists( $test_name, self::SKIP_TESTS ) ) {
return true;
}
@@ -158,11 +156,77 @@ private static function should_skip_test( ?string $test_context_element, string
* @return string|null Tree structure of parsed HTML, if supported, else null.
*/
private static function build_tree_representation( ?string $fragment_context, string $html ) {
- $processor = $fragment_context
- ? WP_HTML_Processor::create_fragment( $html, "<{$fragment_context}>" )
- : WP_HTML_Processor::create_full_parser( $html );
- if ( null === $processor ) {
- throw new WP_HTML_Unsupported_Exception( "Could not create a parser with the given fragment context: {$fragment_context}.", '', 0, '', array(), array() );
+ $processor = null;
+ if ( $fragment_context ) {
+ if ( 'body' === $fragment_context ) {
+ $processor = WP_HTML_Processor::create_fragment( $html );
+ } else {
+
+ /*
+ * If the string of characters starts with "svg ", the context
+ * element is in the SVG namespace and the substring after
+ * "svg " is the local name. If the string of characters starts
+ * with "math ", the context element is in the MathML namespace
+ * and the substring after "math " is the local name.
+ * Otherwise, the context element is in the HTML namespace and
+ * the string is the local name.
+ */
+ if ( str_starts_with( $fragment_context, 'svg ' ) ) {
+ $tag_name = substr( $fragment_context, 4 );
+ if ( 'svg' === $tag_name ) {
+ $parent_processor = WP_HTML_Processor::create_full_parser( '