<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[59099] trunk: HTML API: Switch to HTML namespace when entering Integration Points.</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/59099">59099</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/59099","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>dmsnell</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-09-27 00:42:47 +0000 (Fri, 27 Sep 2024)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>HTML API: Switch to HTML namespace when entering Integration Points.
When encountering inline SVG and MathML content in an HTML document, there are certain "integration points" which transition back into the HTML parsing ruleset. Previously, the HTML API was incorrectly switching into the namespace of the element transitioning into that ruleset.
In this patch, the correct transition is made, where all integration points refer to HTML rules, while non-integration points refer to the rules of the namespace corresponding to the token itself.
Developed in https://github.com/wordpress/wordpress-develop/pull/7425
Discussed in https://core.trac.wordpress.org/ticket/61576
Props dmsnell, jonsurrell.
See <a href="https://core.trac.wordpress.org/ticket/61576">#61576</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmlprocessorphp">trunk/src/wp-includes/html-api/class-wp-html-processor.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlProcessorphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludeshtmlapiclasswphtmlprocessorphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/html-api/class-wp-html-processor.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/html-api/class-wp-html-processor.php 2024-09-26 19:35:26 UTC (rev 59098)
+++ trunk/src/wp-includes/html-api/class-wp-html-processor.php 2024-09-27 00:42:47 UTC (rev 59099)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -393,7 +393,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $provenance = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real';
</span><span class="cx" style="display: block; padding: 0 10px"> $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::PUSH, $provenance );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $this->change_parsing_namespace( $token->namespace );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $this->change_parsing_namespace( $token->integration_node_type ? 'html' : $token->namespace );
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -403,12 +403,14 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $same_node = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name;
</span><span class="cx" style="display: block; padding: 0 10px"> $provenance = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real';
</span><span class="cx" style="display: block; padding: 0 10px"> $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::POP, $provenance );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px"> $adjusted_current_node = $this->get_adjusted_current_node();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $this->change_parsing_namespace(
- $adjusted_current_node
- ? $adjusted_current_node->namespace
- : 'html'
- );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+ if ( $adjusted_current_node ) {
+ $this->change_parsing_namespace( $adjusted_current_node->integration_node_type ? 'html' : $adjusted_current_node->namespace );
+ } else {
+ $this->change_parsing_namespace( 'html' );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px">
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlProcessorphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php 2024-09-26 19:35:26 UTC (rev 59098)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php 2024-09-27 00:42:47 UTC (rev 59099)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -745,4 +745,122 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $class_list
</span><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+ /**
+ * Ensures that the processor correctly adjusts the namespace
+ * for elements inside HTML integration points.
+ *
+ * @ticket 61576
+ */
+ public function test_adjusts_for_html_integration_points_in_svg() {
+ $processor = WP_HTML_Processor::create_full_parser(
+ '<svg><foreignobject><image /><svg /><image />'
+ );
+
+ // At the foreignObject, the processor is in the SVG namespace.
+ $this->assertTrue(
+ $processor->next_tag( 'foreignObject' ),
+ 'Failed to find "foreignObject" under test: check test setup.'
+ );
+
+ $this->assertSame(
+ 'svg',
+ $processor->get_namespace(),
+ 'Found the wrong namespace for the "foreignObject" element.'
+ );
+
+ /*
+ * The IMAGE tag should be handled according to HTML processing rules
+ * and transformted to an IMG tag because `foreignObject` is an HTML
+ * integration point. At this point, the processor is entering the HTML
+ * integration point.
+ */
+ $this->assertTrue(
+ $processor->next_tag( 'IMG' ),
+ 'Failed to find expected "IMG" tag from "<IMAGE>" source tag.'
+ );
+
+ $this->assertSame(
+ 'html',
+ $processor->get_namespace(),
+ 'Found the wrong namespace for the transformed "IMAGE"/"IMG" element.'
+ );
+
+ /*
+ * Again, the IMAGE tag should be handled according to HTML processing
+ * rules and transformted to an IMG tag because `foreignObject` is an
+ * HTML integration point. At this point, the processor is has entered
+ * SVG and is returning to an HTML integration point.
+ */
+ $this->assertTrue(
+ $processor->next_tag( 'IMG' ),
+ 'Failed to find expected "IMG" tag from "<IMAGE>" source tag.'
+ );
+
+ $this->assertSame(
+ 'html',
+ $processor->get_namespace(),
+ 'Found the wrong namespace for the transformed "IMAGE"/"IMG" element.'
+ );
+ }
+
+ /**
+ * Ensures that the processor correctly adjusts the namespace
+ * for elements inside MathML integration points.
+ *
+ * @ticket 61576
+ */
+ public function test_adjusts_for_mathml_integration_points() {
+ $processor = WP_HTML_Processor::create_fragment(
+ '<mo><image /></mo><math><image /><mo><image /></mo></math>'
+ );
+
+ // Advance token-by-token to ensure matching the right raw "<image />" token.
+ $processor->next_token(); // Advance past the +MO.
+ $processor->next_token(); // Advance into the +IMG.
+
+ $this->assertSame(
+ 'IMG',
+ $processor->get_tag(),
+ 'Failed to find expected "IMG" tag from "<IMAGE>" source tag.'
+ );
+
+ $this->assertSame(
+ 'html',
+ $processor->get_namespace(),
+ 'Found the wrong namespace for the transformed "IMAGE"/"IMG" element.'
+ );
+
+ // Advance token-by-token to ensure matching the right raw "<image />" token.
+ $processor->next_token(); // Advance past the -MO.
+ $processor->next_token(); // Advance past the +MATH.
+ $processor->next_token(); // Advance into the +IMAGE.
+
+ $this->assertSame(
+ 'IMAGE',
+ $processor->get_tag(),
+ 'Failed to find the un-transformed "<image />" tag.'
+ );
+
+ $this->assertSame(
+ 'math',
+ $processor->get_namespace(),
+ 'Found the wrong namespace for the transformed "IMAGE"/"IMG" element.'
+ );
+
+ $processor->next_token(); // Advance past the +MO.
+ $processor->next_token(); // Advance into the +IMG.
+
+ $this->assertSame(
+ 'IMG',
+ $processor->get_tag(),
+ 'Failed to find expected "IMG" tag from "<IMAGE>" source tag.'
+ );
+
+ $this->assertSame(
+ 'html',
+ $processor->get_namespace(),
+ 'Found the wrong namespace for the transformed "IMAGE"/"IMG" element.'
+ );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre>
</div>
</div>
</body>
</html>