<!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>[58867] trunk: HTML API: Add support for SVG and MathML (Foreign content)</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/58867">58867</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/58867","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-08-08 07:23:53 +0000 (Thu, 08 Aug 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: Add support for SVG and MathML (Foreign content)
As part of work to add more spec support to the HTML API, this patch adds
support for SVG and MathML elements, or more generally, "foreign content."
The rules in foreign content are a mix of XML and HTML parsing rules and
introduce additional complexity into the processor, but is important in
order to avoid getting lost when inside these elements.
Developed in https://github.com/wordpress/wordpress-develop/pull/6006
Discussed in https://core.trac.wordpress.org/ticket/61576
Props: dmsnell, jonsurrell, westonruter.
See <a href="https://core.trac.wordpress.org/ticket/61576">#61576</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmlopenelementsphp">trunk/src/wp-includes/html-api/class-wp-html-open-elements.php</a></li>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmlprocessorstatephp">trunk/src/wp-includes/html-api/class-wp-html-processor-state.php</a></li>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmlprocessorphp">trunk/src/wp-includes/html-api/class-wp-html-processor.php</a></li>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmltagprocessorphp">trunk/src/wp-includes/html-api/class-wp-html-tag-processor.php</a></li>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmltokenphp">trunk/src/wp-includes/html-api/class-wp-html-token.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlProcessorphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlProcessorBreadcrumbsphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlProcessorHtml5libphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlSupportRequiredOpenElementsphp">trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlTagProcessortokenscanningphp">trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor-token-scanning.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludeshtmlapiclasswphtmlopenelementsphp"></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-open-elements.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-open-elements.php 2024-08-08 04:24:03 UTC (rev 58866)
+++ trunk/src/wp-includes/html-api/class-wp-html-open-elements.php 2024-08-08 07:23:53 UTC (rev 58867)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -113,13 +113,13 @@
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @param int $nth Retrieve the nth item on the stack, with 1 being
</span><span class="cx" style="display: block; padding: 0 10px"> * the top element, 2 being the second, etc...
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @return string|null Name of the node on the stack at the given location,
- * or `null` if the location isn't on the stack.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @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.
</ins><span class="cx" style="display: block; padding: 0 10px"> */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- public function at( int $nth ): ?string {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function at( int $nth ): ?WP_HTML_Token {
</ins><span class="cx" style="display: block; padding: 0 10px"> foreach ( $this->walk_down() as $item ) {
</span><span class="cx" style="display: block; padding: 0 10px"> if ( 0 === --$nth ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- return $item->node_name;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ return $item;
</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">@@ -242,18 +242,22 @@
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> public function has_element_in_specific_scope( string $tag_name, $termination_list ): bool {
</span><span class="cx" style="display: block; padding: 0 10px"> foreach ( $this->walk_up() as $node ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( $node->node_name === $tag_name ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $namespaced_name = 'html' === $node->namespace
+ ? $node->node_name
+ : "{$node->namespace} {$node->node_name}";
+
+ if ( $namespaced_name === $tag_name ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> return true;
</span><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"> if (
</span><span class="cx" style="display: block; padding: 0 10px"> '(internal: H1 through H6 - do not use)' === $tag_name &&
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- in_array( $node->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ in_array( $namespaced_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true )
</ins><span class="cx" style="display: block; padding: 0 10px"> ) {
</span><span class="cx" style="display: block; padding: 0 10px"> return true;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( in_array( $node->node_name, $termination_list, true ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( in_array( $namespaced_name, $termination_list, true ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> return false;
</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">@@ -288,7 +292,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * > - SVG title
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 6.4.0
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @since 6.7.0 Supports all required HTML elements.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 6.7.0 Full support.
</ins><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @see https://html.spec.whatwg.org/#has-an-element-in-scope
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -309,19 +313,16 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'OBJECT',
</span><span class="cx" style="display: block; padding: 0 10px"> 'TEMPLATE',
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- /*
- * @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
- */
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'math MI',
+ 'math MO',
+ 'math MN',
+ 'math MS',
+ 'math MTEXT',
+ 'math ANNOTATION-XML',
+
+ 'svg FOREIGNOBJECT',
+ 'svg DESC',
+ 'svg TITLE',
</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">@@ -363,19 +364,16 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'TEMPLATE',
</span><span class="cx" style="display: block; padding: 0 10px"> 'UL',
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- /*
- * @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
- */
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'math MI',
+ 'math MO',
+ 'math MN',
+ 'math MS',
+ 'math MTEXT',
+ 'math ANNOTATION-XML',
+
+ 'svg FOREIGNOBJECT',
+ 'svg DESC',
+ 'svg TITLE',
</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">@@ -413,19 +411,16 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'OBJECT',
</span><span class="cx" style="display: block; padding: 0 10px"> 'TEMPLATE',
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- /*
- * @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
- */
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'math MI',
+ 'math MO',
+ 'math MN',
+ 'math MS',
+ 'math MTEXT',
+ 'math ANNOTATION-XML',
+
+ 'svg FOREIGNOBJECT',
+ 'svg DESC',
+ 'svg TITLE',
</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">@@ -692,11 +687,15 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * @param WP_HTML_Token $item Element that was added to the stack of open elements.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> public function after_element_push( WP_HTML_Token $item ): void {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $namespaced_name = 'html' === $item->namespace
+ ? $item->node_name
+ : "{$item->namespace} {$item->node_name}";
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /*
</span><span class="cx" style="display: block; padding: 0 10px"> * When adding support for new elements, expand this switch to trap
</span><span class="cx" style="display: block; padding: 0 10px"> * cases where the precalculated value needs to change.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- switch ( $item->node_name ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ switch ( $namespaced_name ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> case 'APPLET':
</span><span class="cx" style="display: block; padding: 0 10px"> case 'BUTTON':
</span><span class="cx" style="display: block; padding: 0 10px"> case 'CAPTION':
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -707,6 +706,15 @@
</span><span class="cx" style="display: block; padding: 0 10px"> case 'MARQUEE':
</span><span class="cx" style="display: block; padding: 0 10px"> case 'OBJECT':
</span><span class="cx" style="display: block; padding: 0 10px"> case 'TEMPLATE':
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 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':
</ins><span class="cx" style="display: block; padding: 0 10px"> $this->has_p_in_button_scope = false;
</span><span class="cx" style="display: block; padding: 0 10px"> break;
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -750,6 +758,15 @@
</span><span class="cx" style="display: block; padding: 0 10px"> case 'MARQUEE':
</span><span class="cx" style="display: block; padding: 0 10px"> case 'OBJECT':
</span><span class="cx" style="display: block; padding: 0 10px"> case 'TEMPLATE':
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 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':
</ins><span class="cx" style="display: block; padding: 0 10px"> $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' );
</span><span class="cx" style="display: block; padding: 0 10px"> break;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunksrcwpincludeshtmlapiclasswphtmlprocessorstatephp"></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-state.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-state.php 2024-08-08 04:24:03 UTC (rev 58866)
+++ trunk/src/wp-includes/html-api/class-wp-html-processor-state.php 2024-08-08 07:23:53 UTC (rev 58867)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -300,18 +300,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> const INSERTION_MODE_AFTER_AFTER_FRAMESET = 'insertion-mode-after-after-frameset';
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * 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';
-
- /**
</del><span class="cx" style="display: block; padding: 0 10px"> * No-quirks mode document compatability mode.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * > In no-quirks mode, the behavior is (hopefully) the desired behavior
</span></span></pre></div>
<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-08-08 04:24:03 UTC (rev 58866)
+++ trunk/src/wp-includes/html-api/class-wp-html-processor.php 2024-08-08 07:23:53 UTC (rev 58867)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -307,14 +307,14 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $processor->bookmarks['root-node'] = new WP_HTML_Span( 0, 0 );
</span><span class="cx" style="display: block; padding: 0 10px"> $processor->bookmarks['context-node'] = new WP_HTML_Span( 0, 0 );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $processor->state->stack_of_open_elements->push(
- new WP_HTML_Token(
- 'root-node',
- 'HTML',
- false
- )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $root_node = new WP_HTML_Token(
+ 'root-node',
+ 'HTML',
+ false
</ins><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">+ $processor->state->stack_of_open_elements->push( $root_node );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> $context_node = new WP_HTML_Token(
</span><span class="cx" style="display: block; padding: 0 10px"> 'context-node',
</span><span class="cx" style="display: block; padding: 0 10px"> $processor->state->context_node[0],
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -392,6 +392,8 @@
</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::PUSH, $provenance );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+ $this->change_parsing_namespace( $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">@@ -401,6 +403,12 @@
</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">+ $adjusted_current_node = $this->get_adjusted_current_node();
+ $this->change_parsing_namespace(
+ $adjusted_current_node
+ ? $adjusted_current_node->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 class="lines" style="display: block; padding: 0 10px; color: #888">@@ -767,19 +775,20 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * foreign content will also act just like a void tag, immediately
</span><span class="cx" style="display: block; padding: 0 10px"> * closing as soon as the processor advances to the next token.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @todo Review the self-closing logic when no node is present, ensure it
+ * matches the expectations in `step()`.
+ *
</ins><span class="cx" style="display: block; padding: 0 10px"> * @since 6.6.0
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @todo When adding support for foreign content, ensure that
- * this returns false for self-closing elements in the
- * SVG and MathML namespace.
- *
</del><span class="cx" style="display: block; padding: 0 10px"> * @param WP_HTML_Token|null $node Optional. Node to examine, if provided.
</span><span class="cx" style="display: block; padding: 0 10px"> * Default is to examine current node.
</span><span class="cx" style="display: block; padding: 0 10px"> * @return bool|null Whether to expect a closer for the currently-matched node,
</span><span class="cx" style="display: block; padding: 0 10px"> * or `null` if not matched on any token.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- public function expects_closer( $node = null ): ?bool {
- $token_name = $node->node_name ?? $this->get_token_name();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 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();
+
</ins><span class="cx" style="display: block; padding: 0 10px"> if ( ! isset( $token_name ) ) {
</span><span class="cx" style="display: block; padding: 0 10px"> return null;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -792,7 +801,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> // Void elements.
</span><span class="cx" style="display: block; padding: 0 10px"> self::is_void( $token_name ) ||
</span><span class="cx" style="display: block; padding: 0 10px"> // Special atomic elements.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- in_array( $token_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ ( '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 )
</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">@@ -824,14 +835,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * When moving on to the next node, therefore, if the bottom-most element
</span><span class="cx" style="display: block; padding: 0 10px"> * on the stack is a void element, it must be closed.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- *
- * @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.
</del><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> $top_node = $this->state->stack_of_open_elements->current_node();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( isset( $top_node ) && ! static::expects_closer( $top_node ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( isset( $top_node ) && ! $this->expects_closer( $top_node ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> $this->state->stack_of_open_elements->pop();
</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">@@ -848,14 +854,46 @@
</span><span class="cx" style="display: block; padding: 0 10px"> return false;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</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->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
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $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 )
+ )
</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"> try {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( ! $parse_in_current_insertion_mode ) {
+ return $this->step_in_foreign_content();
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> switch ( $this->state->insertion_mode ) {
</span><span class="cx" style="display: block; padding: 0 10px"> case WP_HTML_Processor_State::INSERTION_MODE_INITIAL:
</span><span class="cx" style="display: block; padding: 0 10px"> return $this->step_initial();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -923,9 +961,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET:
</span><span class="cx" style="display: block; padding: 0 10px"> return $this->step_after_after_frameset();
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- case WP_HTML_Processor_State::INSERTION_MODE_IN_FOREIGN_CONTENT:
- return $this->step_in_foreign_content();
-
</del><span class="cx" style="display: block; padding: 0 10px"> // This should be unreachable but PHP doesn't have total type checking on switch.
</span><span class="cx" style="display: block; padding: 0 10px"> default:
</span><span class="cx" style="display: block; padding: 0 10px"> $this->bail( "Unaware of the requested parsing mode: '{$this->state->insertion_mode}'." );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1853,7 +1888,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> case '+BODY':
</span><span class="cx" style="display: block; padding: 0 10px"> if (
</span><span class="cx" style="display: block; padding: 0 10px"> 1 === $this->state->stack_of_open_elements->count() ||
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'BODY' !== $this->state->stack_of_open_elements->at( 2 ) ||
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'BODY' !== ( $this->state->stack_of_open_elements->at( 2 )->node_name ?? null ) ||
</ins><span class="cx" style="display: block; padding: 0 10px"> $this->state->stack_of_open_elements->contains( 'TEMPLATE' )
</span><span class="cx" style="display: block; padding: 0 10px"> ) {
</span><span class="cx" style="display: block; padding: 0 10px"> // Ignore the token.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1879,7 +1914,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> case '+FRAMESET':
</span><span class="cx" style="display: block; padding: 0 10px"> if (
</span><span class="cx" style="display: block; padding: 0 10px"> 1 === $this->state->stack_of_open_elements->count() ||
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'BODY' !== $this->state->stack_of_open_elements->at( 2 ) ||
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'BODY' !== ( $this->state->stack_of_open_elements->at( 2 )->node_name ?? null ) ||
</ins><span class="cx" style="display: block; padding: 0 10px"> false === $this->state->frameset_ok
</span><span class="cx" style="display: block; padding: 0 10px"> ) {
</span><span class="cx" style="display: block; padding: 0 10px"> // Ignore the token.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2075,7 +2110,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'ADDRESS' !== $node->node_name &&
</span><span class="cx" style="display: block; padding: 0 10px"> 'DIV' !== $node->node_name &&
</span><span class="cx" style="display: block; padding: 0 10px"> 'P' !== $node->node_name &&
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $this->is_special( $node->node_name )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ self::is_special( $node )
</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"> * > If node is in the special category, but is not an address, div,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2136,11 +2171,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * > "button", "center", "details", "dialog", "dir", "div", "dl", "fieldset",
</span><span class="cx" style="display: block; padding: 0 10px"> * > "figcaption", "figure", "footer", "header", "hgroup", "listing", "main",
</span><span class="cx" style="display: block; padding: 0 10px"> * > "menu", "nav", "ol", "pre", "search", "section", "summary", "ul"
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- *
- * @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 `<center>`, and a foreign element of
- * the same given name.
</del><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> case '-ADDRESS':
</span><span class="cx" style="display: block; padding: 0 10px"> case '-ARTICLE':
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2411,11 +2441,6 @@
</span><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"> * > A end tag token whose tag name is one of: "applet", "marquee", "object"
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- *
- * @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 `<object>`, and a foreign element of
- * the same given name.
</del><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> case '-APPLET':
</span><span class="cx" style="display: block; padding: 0 10px"> case '-MARQUEE':
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2679,10 +2704,13 @@
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * These ought to be handled in the attribute methods.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $this->state->current_token->namespace = 'math';
+ $this->insert_html_element( $this->state->current_token );
+ if ( $this->state->current_token->has_self_closing_flag ) {
+ $this->state->stack_of_open_elements->pop();
+ }
+ return true;
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $this->bail( 'Cannot process MATH element, opening foreign content.' );
- break;
-
</del><span class="cx" style="display: block; padding: 0 10px"> /*
</span><span class="cx" style="display: block; padding: 0 10px"> * > A start tag whose tag name is "svg"
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2695,10 +2723,13 @@
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * These ought to be handled in the attribute methods.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $this->state->current_token->namespace = 'svg';
+ $this->insert_html_element( $this->state->current_token );
+ if ( $this->state->current_token->has_self_closing_flag ) {
+ $this->state->stack_of_open_elements->pop();
+ }
+ return true;
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $this->bail( 'Cannot process SVG element, opening foreign content.' );
- break;
-
</del><span class="cx" style="display: block; padding: 0 10px"> /*
</span><span class="cx" style="display: block; padding: 0 10px"> * > A start tag whose tag name is one of: "caption", "col", "colgroup",
</span><span class="cx" style="display: block; padding: 0 10px"> * > "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr"
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2737,17 +2768,11 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * close anything beyond its containing `P` or `DIV` element.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- /*
- * @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 `<object>`, and a foreign element of
- * the same given name.
- */
- if ( $token_name === $node->node_name ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( 'html' === $node->namespace && $token_name === $node->node_name ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> break;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( self::is_special( $node->node_name ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( self::is_special( $node ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> // This is a parse error, ignore the token.
</span><span class="cx" style="display: block; padding: 0 10px"> return $this->step();
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4069,7 +4094,284 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * @return bool Whether an element was found.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> private function step_in_foreign_content(): bool {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_FOREIGN_CONTENT . ' state.' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $tag_name = $this->get_token_name();
+ $token_type = $this->get_token_type();
+ $op_sigil = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : '';
+ $op = "{$op_sigil}{$tag_name}";
+
+ /*
+ * > A start tag whose name is "font", if the token has any attributes named "color", "face", or "size"
+ *
+ * This section drawn out above the switch to more easily incorporate
+ * the additional rules based on the presence of the attributes.
+ */
+ if (
+ '+FONT' === $op &&
+ (
+ null !== $this->get_attribute( 'color' ) ||
+ null !== $this->get_attribute( 'face' ) ||
+ null !== $this->get_attribute( 'size' )
+ )
+ ) {
+ $op = '+FONT with attributes';
+ }
+
+ switch ( $op ) {
+ case '#text':
+ /*
+ * > A character token that is U+0000 NULL
+ *
+ * This is handled by `get_modifiable_text()`.
+ */
+
+ /*
+ * Whitespace-only text does not affect the frameset-ok flag.
+ * It is probably inter-element whitespace, but it may also
+ * contain character references which decode only to whitespace.
+ */
+ $text = $this->get_modifiable_text();
+ if ( strlen( $text ) !== strspn( $text, " \t\n\f\r" ) ) {
+ $this->state->frameset_ok = false;
+ }
+
+ $this->insert_foreign_element( $this->state->current_token, false );
+ return true;
+
+ /*
+ * > A comment token
+ */
+ case '#cdata-section':
+ case '#comment':
+ case '#funky_comment':
+ $this->insert_foreign_element( $this->state->current_token, false );
+ return true;
+
+ /*
+ * > A DOCTYPE token
+ */
+ case 'html':
+ // Parse error: ignore the token.
+ return $this->step();
+
+ /*
+ * > A start tag whose tag name is "b", "big", "blockquote", "body", "br", "center",
+ * > "code", "dd", "div", "dl", "dt", "em", "embed", "h1", "h2", "h3", "h4", "h5",
+ * > "h6", "head", "hr", "i", "img", "li", "listing", "menu", "meta", "nobr", "ol",
+ * > "p", "pre", "ruby", "s", "small", "span", "strong", "strike", "sub", "sup",
+ * > "table", "tt", "u", "ul", "var"
+ *
+ * > A start tag whose name is "font", if the token has any attributes named "color", "face", or "size"
+ *
+ * > An end tag whose tag name is "br", "p"
+ *
+ * Closing BR tags are always reported by the Tag Processor as opening tags.
+ */
+ case '+B':
+ case '+BIG':
+ case '+BLOCKQUOTE':
+ case '+BODY':
+ case '+BR':
+ case '+CENTER':
+ case '+CODE':
+ case '+DD':
+ case '+DIV':
+ case '+DL':
+ case '+DT':
+ case '+EM':
+ case '+EMBED':
+ case '+H1':
+ case '+H2':
+ case '+H3':
+ case '+H4':
+ case '+H5':
+ case '+H6':
+ case '+HEAD':
+ case '+HR':
+ case '+I':
+ case '+IMG':
+ case '+LI':
+ case '+LISTING':
+ case '+MENU':
+ case '+META':
+ case '+NOBR':
+ case '+OL':
+ case '+P':
+ case '+PRE':
+ case '+RUBY':
+ case '+S':
+ case '+SMALL':
+ case '+SPAN':
+ case '+STRONG':
+ case '+STRIKE':
+ case '+SUB':
+ case '+SUP':
+ case '+TABLE':
+ case '+TT':
+ case '+U':
+ case '+UL':
+ case '+VAR':
+ case '+FONT with attributes':
+ case '-BR':
+ case '-P':
+ // @todo Indicate a parse error once it's possible.
+ foreach ( $this->state->stack_of_open_elements->walk_up() as $current_node ) {
+ if (
+ 'math' === $current_node->integration_node_type ||
+ 'html' === $current_node->integration_node_type ||
+ 'html' === $current_node->namespace
+ ) {
+ break;
+ }
+
+ $this->state->stack_of_open_elements->pop();
+ }
+ return $this->step( self::REPROCESS_CURRENT_NODE );
+ }
+
+ /*
+ * > Any other start tag
+ */
+ if ( ! $this->is_tag_closer() ) {
+ $this->insert_foreign_element( $this->state->current_token, false );
+
+ /*
+ * > If the token has its self-closing flag set, then run
+ * > the appropriate steps from the following list:
+ */
+ if ( $this->state->current_token->has_self_closing_flag ) {
+ if ( 'SCRIPT' === $this->state->current_token->node_name && 'svg' === $this->state->current_token->namespace ) {
+ /*
+ * > Acknowledge the token's self-closing flag, and then act as
+ * > described in the steps for a "script" end tag below.
+ *
+ * @todo Verify that this shouldn't be handled by the rule for
+ * "An end tag whose name is 'script', if the current node
+ * is an SVG script element."
+ */
+ goto in_foreign_content_any_other_end_tag;
+ } else {
+ $this->state->stack_of_open_elements->pop();
+ }
+ }
+ return true;
+ }
+
+ /*
+ * > An end tag whose name is "script", if the current node is an SVG script element.
+ */
+ if ( $this->is_tag_closer() && 'SCRIPT' === $this->state->current_token->node_name && 'svg' === $this->state->current_token->namespace ) {
+ $this->state->stack_of_open_elements->pop();
+ }
+
+ /*
+ * > Any other end tag
+ */
+ if ( $this->is_tag_closer() ) {
+ in_foreign_content_any_other_end_tag:
+ $node = $this->state->stack_of_open_elements->current_node();
+ if ( $tag_name !== $node->node_name ) {
+ // @todo Indicate a parse error once it's possible.
+ }
+ in_foreign_content_end_tag_loop:
+ if ( $node === $this->state->stack_of_open_elements->at( 1 ) ) {
+ return true;
+ }
+
+ /*
+ * > If node's tag name, converted to ASCII lowercase, is the same as the tag name
+ * > of the token, pop elements from the stack of open elements until node has
+ * > been popped from the stack, and then return.
+ */
+ if ( 0 === strcasecmp( $node->node_name, $tag_name ) ) {
+ foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
+ $this->state->stack_of_open_elements->pop();
+ if ( $node === $item ) {
+ return true;
+ }
+ }
+ }
+
+ foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $item ) {
+ $node = $item;
+ break;
+ }
+
+ if ( 'html' !== $node->namespace ) {
+ goto in_foreign_content_end_tag_loop;
+ }
+
+ switch ( $this->state->insertion_mode ) {
+ case WP_HTML_Processor_State::INSERTION_MODE_INITIAL:
+ return $this->step_initial();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML:
+ return $this->step_before_html();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD:
+ return $this->step_before_head();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD:
+ return $this->step_in_head();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD_NOSCRIPT:
+ return $this->step_in_head_noscript();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD:
+ return $this->step_after_head();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_BODY:
+ return $this->step_in_body();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE:
+ return $this->step_in_table();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_TEXT:
+ return $this->step_in_table_text();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION:
+ return $this->step_in_caption();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP:
+ return $this->step_in_column_group();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY:
+ return $this->step_in_table_body();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_ROW:
+ return $this->step_in_row();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL:
+ return $this->step_in_cell();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT:
+ return $this->step_in_select();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_SELECT_IN_TABLE:
+ return $this->step_in_select_in_table();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE:
+ return $this->step_in_template();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_AFTER_BODY:
+ return $this->step_after_body();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_IN_FRAMESET:
+ return $this->step_in_frameset();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_AFTER_FRAMESET:
+ return $this->step_after_frameset();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_BODY:
+ return $this->step_after_after_body();
+
+ case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET:
+ return $this->step_after_after_frameset();
+
+ // 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}'." );
+ }
+ }
</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">@@ -4100,6 +4402,19 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Indicates the namespace of the current token, or "html" if there is none.
+ *
+ * @return string One of "html", "math", or "svg".
+ */
+ public function get_namespace(): string {
+ if ( ! isset( $this->current_element ) ) {
+ return 'html';
+ }
+
+ return $this->current_element->token->namespace;
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Returns the uppercase name of the matched tag.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * The semantic rules for HTML specify that certain tags be reprocessed
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4735,6 +5050,28 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Returns the adjusted current node.
+ *
+ * > The adjusted current node is the context element if the parser was created as
+ * > part of the HTML fragment parsing algorithm and the stack of open elements
+ * > has only one element in it (fragment case); otherwise, the adjusted current
+ * > node is the current node.
+ *
+ * @see https://html.spec.whatwg.org/#adjusted-current-node
+ *
+ * @since 6.7.0
+ *
+ * @return WP_HTML_Token|null The adjusted current node.
+ */
+ private function get_adjusted_current_node(): ?WP_HTML_Token {
+ if ( isset( $this->context_node ) && 1 === $this->state->stack_of_open_elements->count() ) {
+ return $this->context_node;
+ }
+
+ return $this->state->stack_of_open_elements->current_node();
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Reconstructs the active formatting elements.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * > This has the effect of reopening all the formatting elements that were opened
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5043,7 +5380,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> continue;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( self::is_special( $item->node_name ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( self::is_special( $item ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> $furthest_block = $item;
</span><span class="cx" style="display: block; padding: 0 10px"> break;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5112,6 +5449,45 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Inserts a foreign element on to the stack of open elements.
+ *
+ * @since 6.7.0
+ *
+ * @see https://html.spec.whatwg.org/#insert-a-foreign-element
+ *
+ * @param WP_HTML_Token $token Insert this token. The token's namespace and
+ * insertion point will be updated correctly.
+ * @param bool $only_add_to_element_stack Whether to skip the "insert an element at the adjusted
+ * insertion location" algorithm when adding this element.
+ */
+ private function insert_foreign_element( WP_HTML_Token $token, bool $only_add_to_element_stack ): void {
+ $adjusted_current_node = $this->get_adjusted_current_node();
+
+ $token->namespace = $adjusted_current_node ? $adjusted_current_node->namespace : 'html';
+
+ if ( $this->is_mathml_integration_point() ) {
+ $token->integration_node_type = 'math';
+ } elseif ( $this->is_html_integration_point() ) {
+ $token->integration_node_type = 'html';
+ }
+
+ if ( false === $only_add_to_element_stack ) {
+ /*
+ * @todo Implement the "appropriate place for inserting a node" and the
+ * "insert an element at the adjusted insertion location" algorithms.
+ *
+ * These algorithms mostly impacts DOM tree construction and not the HTML API.
+ * Here, there's no DOM node onto which the element will be appended, so the
+ * parser will skip this step.
+ *
+ * @see https://html.spec.whatwg.org/#insert-an-element-at-the-adjusted-insertion-location
+ */
+ }
+
+ $this->insert_html_element( $token );
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Inserts a virtual element on the stack of open elements.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 6.7.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5137,6 +5513,88 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Indicates if the current token is a MathML integration point.
+ *
+ * @since 6.7.0
+ *
+ * @see https://html.spec.whatwg.org/#mathml-text-integration-point
+ *
+ * @return bool Whether the current token is a MathML integration point.
+ */
+ private function is_mathml_integration_point(): bool {
+ $current_token = $this->state->current_token;
+ if ( ! isset( $current_token ) ) {
+ return false;
+ }
+
+ if ( 'math' !== $current_token->namespace || 'M' !== $current_token->node_name[0] ) {
+ return false;
+ }
+
+ $tag_name = $current_token->node_name;
+
+ return (
+ 'MI' === $tag_name ||
+ 'MO' === $tag_name ||
+ 'MN' === $tag_name ||
+ 'MS' === $tag_name ||
+ 'MTEXT' === $tag_name
+ );
+ }
+
+ /**
+ * Indicates if the current token is an HTML integration point.
+ *
+ * Note that this method must be an instance method with access
+ * to the current token, since it needs to examine the attributes
+ * of the currently-matched tag, if it's in the MathML namespace.
+ * Otherwise it would be required to scan the HTML and ensure that
+ * no other accounting is overlooked.
+ *
+ * @since 6.7.0
+ *
+ * @see https://html.spec.whatwg.org/#html-integration-point
+ *
+ * @return bool Whether the current token is an HTML integration point.
+ */
+ private function is_html_integration_point(): bool {
+ $current_token = $this->state->current_token;
+ if ( ! isset( $current_token ) ) {
+ return false;
+ }
+
+ if ( 'html' === $current_token->namespace ) {
+ return false;
+ }
+
+ $tag_name = $current_token->node_name;
+
+ if ( 'svg' === $current_token->namespace ) {
+ return (
+ 'DESC' === $tag_name ||
+ 'FOREIGNOBJECT' === $tag_name ||
+ 'TITLE' === $tag_name
+ );
+ }
+
+ if ( 'math' === $current_token->namespace ) {
+ if ( 'ANNOTATION-XML' !== $tag_name ) {
+ return false;
+ }
+
+ $encoding = $this->get_attribute( 'encoding' );
+
+ return (
+ is_string( $encoding ) &&
+ (
+ 0 === strcasecmp( $encoding, 'application/xhtml+xml' ) ||
+ 0 === strcasecmp( $encoding, 'text/html' )
+ )
+ );
+ }
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Returns whether an element of a given name is in the HTML special category.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 6.4.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5143,11 +5601,17 @@
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @see https://html.spec.whatwg.org/#special
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @param string $tag_name Name of element to check.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @param WP_HTML_Token|string $tag_name Node to check, or only its name if in the HTML namespace.
</ins><span class="cx" style="display: block; padding: 0 10px"> * @return bool Whether the element of the given name is in the special category.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> public static function is_special( $tag_name ): bool {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $tag_name = strtoupper( $tag_name );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( is_string( $tag_name ) ) {
+ $tag_name = strtoupper( $tag_name );
+ } else {
+ $tag_name = 'html' === $tag_name->namespace
+ ? strtoupper( $tag_name->node_name )
+ : "{$tag_name->namespace} {$tag_name->node_name}";
+ }
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> return (
</span><span class="cx" style="display: block; padding: 0 10px"> 'ADDRESS' === $tag_name ||
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5235,17 +5699,17 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'XMP' === $tag_name ||
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> // MathML.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'MI' === $tag_name ||
- 'MO' === $tag_name ||
- 'MN' === $tag_name ||
- 'MS' === $tag_name ||
- 'MTEXT' === $tag_name ||
- 'ANNOTATION-XML' === $tag_name ||
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'math MI' === $tag_name ||
+ 'math MO' === $tag_name ||
+ 'math MN' === $tag_name ||
+ 'math MS' === $tag_name ||
+ 'math MTEXT' === $tag_name ||
+ 'math ANNOTATION-XML' === $tag_name ||
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> // SVG.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'FOREIGNOBJECT' === $tag_name ||
- 'DESC' === $tag_name ||
- 'TITLE' === $tag_name
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'svg DESC' === $tag_name ||
+ 'svg FOREIGNOBJECT' === $tag_name ||
+ 'svg TITLE' === $tag_name
</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="trunksrcwpincludeshtmlapiclasswphtmltagprocessorphp"></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-tag-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-tag-processor.php 2024-08-08 04:24:03 UTC (rev 58866)
+++ trunk/src/wp-includes/html-api/class-wp-html-tag-processor.php 2024-08-08 07:23:53 UTC (rev 58867)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -512,6 +512,23 @@
</span><span class="cx" style="display: block; padding: 0 10px"> protected $parser_state = self::STATE_READY;
</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">+ * Indicates whether the parser is inside foreign content,
+ * e.g. inside an SVG or MathML element.
+ *
+ * One of 'html', 'svg', or 'math'.
+ *
+ * Several parsing rules change based on whether the parser
+ * is inside foreign content, including whether CDATA sections
+ * are allowed and whether a self-closing flag indicates that
+ * an element has no content.
+ *
+ * @since 6.7.0
+ *
+ * @var string
+ */
+ private $parsing_namespace = 'html';
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * What kind of syntax token became an HTML comment.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * Since there are many ways in which HTML syntax can create an HTML comment,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -781,6 +798,25 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Switches parsing mode into a new namespace, such as when
+ * encountering an SVG tag and entering foreign content.
+ *
+ * @since 6.7.0
+ *
+ * @param string $new_namespace One of 'html', 'svg', or 'math' indicating into what
+ * namespace the next tokens will be processed.
+ * @return bool Whether the namespace was valid and changed.
+ */
+ public function change_parsing_namespace( string $new_namespace ): bool {
+ if ( ! in_array( $new_namespace, array( 'html', 'math', 'svg' ), true ) ) {
+ return false;
+ }
+
+ $this->parsing_namespace = $new_namespace;
+ return true;
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Finds the next tag matching the $query.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 6.2.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -843,6 +879,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * The Tag Processor currently only supports the tag token.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 6.5.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 6.7.0 Recognizes CDATA sections within foreign content.
</ins><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @return bool Whether a token was parsed.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -956,6 +993,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> if (
</span><span class="cx" style="display: block; padding: 0 10px"> $this->is_closing_tag ||
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'html' !== $this->parsing_namespace ||
</ins><span class="cx" style="display: block; padding: 0 10px"> 1 !== strspn( $this->html, 'iIlLnNpPsStTxX', $this->tag_name_starts_at, 1 )
</span><span class="cx" style="display: block; padding: 0 10px"> ) {
</span><span class="cx" style="display: block; padding: 0 10px"> return true;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -996,7 +1034,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $duplicate_attributes = $this->duplicate_attributes;
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> // Find the closing tag if necessary.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $found_closer = false;
</del><span class="cx" style="display: block; padding: 0 10px"> switch ( $tag_name ) {
</span><span class="cx" style="display: block; padding: 0 10px"> case 'SCRIPT':
</span><span class="cx" style="display: block; padding: 0 10px"> $found_closer = $this->skip_script_data();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1759,6 +1796,32 @@
</span><span class="cx" style="display: block; padding: 0 10px"> return true;
</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">+ if (
+ 'html' !== $this->parsing_namespace &&
+ strlen( $html ) > $at + 8 &&
+ '[' === $html[ $at + 2 ] &&
+ 'C' === $html[ $at + 3 ] &&
+ 'D' === $html[ $at + 4 ] &&
+ 'A' === $html[ $at + 5 ] &&
+ 'T' === $html[ $at + 6 ] &&
+ 'A' === $html[ $at + 7 ] &&
+ '[' === $html[ $at + 8 ]
+ ) {
+ $closer_at = strpos( $html, ']]>', $at + 9 );
+ if ( false === $closer_at ) {
+ $this->parser_state = self::STATE_INCOMPLETE_INPUT;
+
+ return false;
+ }
+
+ $this->parser_state = self::STATE_CDATA_NODE;
+ $this->text_starts_at = $at + 9;
+ $this->text_length = $closer_at - $this->text_starts_at;
+ $this->token_length = $closer_at + 3 - $this->token_starts_at;
+ $this->bytes_already_parsed = $closer_at + 3;
+ return true;
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /*
</span><span class="cx" style="display: block; padding: 0 10px"> * Anything else here is an incorrectly-opened comment and transitions
</span><span class="cx" style="display: block; padding: 0 10px"> * to the bogus comment state - skip to the nearest >. If no closer is
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2654,6 +2717,17 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Returns the namespace of the matched token.
+ *
+ * @since 6.7.0
+ *
+ * @return string One of 'html', 'math', or 'svg'.
+ */
+ public function get_namespace(): string {
+ return $this->parsing_namespace;
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Returns the uppercase name of the matched tag.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * Example:
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2691,6 +2765,388 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Returns the adjusted tag name for a given token, taking into
+ * account the current parsing context, whether HTML, SVG, or MathML.
+ *
+ * @since 6.7.0
+ *
+ * @return string|null Name of current tag name.
+ */
+ public function get_qualified_tag_name(): ?string {
+ $tag_name = $this->get_tag();
+ if ( null === $tag_name ) {
+ return null;
+ }
+
+ if ( 'html' === $this->get_namespace() ) {
+ return $tag_name;
+ }
+
+ $lower_tag_name = strtolower( $tag_name );
+ if ( 'math' === $this->get_namespace() ) {
+ return $lower_tag_name;
+ }
+
+ if ( 'svg' === $this->get_namespace() ) {
+ switch ( $lower_tag_name ) {
+ case 'altglyph':
+ return 'altGlyph';
+
+ case 'altglyphdef':
+ return 'altGlyphDef';
+
+ case 'altglyphitem':
+ return 'altGlyphItem';
+
+ case 'animatecolor':
+ return 'animateColor';
+
+ case 'animatemotion':
+ return 'animateMotion';
+
+ case 'animatetransform':
+ return 'animateTransform';
+
+ case 'clippath':
+ return 'clipPath';
+
+ case 'feblend':
+ return 'feBlend';
+
+ case 'fecolormatrix':
+ return 'feColorMatrix';
+
+ case 'fecomponenttransfer':
+ return 'feComponentTransfer';
+
+ case 'fecomposite':
+ return 'feComposite';
+
+ case 'feconvolvematrix':
+ return 'feConvolveMatrix';
+
+ case 'fediffuselighting':
+ return 'feDiffuseLighting';
+
+ case 'fedisplacementmap':
+ return 'feDisplacementMap';
+
+ case 'fedistantlight':
+ return 'feDistantLight';
+
+ case 'fedropshadow':
+ return 'feDropShadow';
+
+ case 'feflood':
+ return 'feFlood';
+
+ case 'fefunca':
+ return 'feFuncA';
+
+ case 'fefuncb':
+ return 'feFuncB';
+
+ case 'fefuncg':
+ return 'feFuncG';
+
+ case 'fefuncr':
+ return 'feFuncR';
+
+ case 'fegaussianblur':
+ return 'feGaussianBlur';
+
+ case 'feimage':
+ return 'feImage';
+
+ case 'femerge':
+ return 'feMerge';
+
+ case 'femergenode':
+ return 'feMergeNode';
+
+ case 'femorphology':
+ return 'feMorphology';
+
+ case 'feoffset':
+ return 'feOffset';
+
+ case 'fepointlight':
+ return 'fePointLight';
+
+ case 'fespecularlighting':
+ return 'feSpecularLighting';
+
+ case 'fespotlight':
+ return 'feSpotLight';
+
+ case 'fetile':
+ return 'feTile';
+
+ case 'feturbulence':
+ return 'feTurbulence';
+
+ case 'foreignobject':
+ return 'foreignObject';
+
+ case 'glyphref':
+ return 'glyphRef';
+
+ case 'lineargradient':
+ return 'linearGradient';
+
+ case 'radialgradient':
+ return 'radialGradient';
+
+ case 'textpath':
+ return 'textPath';
+
+ default:
+ return $lower_tag_name;
+ }
+ }
+ }
+
+ /**
+ * Returns the adjusted attribute name for a given attribute, taking into
+ * account the current parsing context, whether HTML, SVG, or MathML.
+ *
+ * @since 6.7.0
+ *
+ * @param string $attribute_name Which attribute to adjust.
+ *
+ * @return string|null
+ */
+ public function get_qualified_attribute_name( $attribute_name ): ?string {
+ if ( self::STATE_MATCHED_TAG !== $this->parser_state ) {
+ return null;
+ }
+
+ $namespace = $this->get_namespace();
+ $lower_name = strtolower( $attribute_name );
+
+ if ( 'math' === $namespace && 'definitionurl' === $lower_name ) {
+ return 'definitionURL';
+ }
+
+ if ( 'svg' === $this->get_namespace() ) {
+ switch ( $lower_name ) {
+ case 'attributename':
+ return 'attributeName';
+
+ case 'attributetype':
+ return 'attributeType';
+
+ case 'basefrequency':
+ return 'baseFrequency';
+
+ case 'baseprofile':
+ return 'baseProfile';
+
+ case 'calcmode':
+ return 'calcMode';
+
+ case 'clippathunits':
+ return 'clipPathUnits';
+
+ case 'diffuseconstant':
+ return 'diffuseConstant';
+
+ case 'edgemode':
+ return 'edgeMode';
+
+ case 'filterunits':
+ return 'filterUnits';
+
+ case 'glyphref':
+ return 'glyphRef';
+
+ case 'gradienttransform':
+ return 'gradientTransform';
+
+ case 'gradientunits':
+ return 'gradientUnits';
+
+ case 'kernelmatrix':
+ return 'kernelMatrix';
+
+ case 'kernelunitlength':
+ return 'kernelUnitLength';
+
+ case 'keypoints':
+ return 'keyPoints';
+
+ case 'keysplines':
+ return 'keySplines';
+
+ case 'keytimes':
+ return 'keyTimes';
+
+ case 'lengthadjust':
+ return 'lengthAdjust';
+
+ case 'limitingconeangle':
+ return 'limitingConeAngle';
+
+ case 'markerheight':
+ return 'markerHeight';
+
+ case 'markerunits':
+ return 'markerUnits';
+
+ case 'markerwidth':
+ return 'markerWidth';
+
+ case 'maskcontentunits':
+ return 'maskContentUnits';
+
+ case 'maskunits':
+ return 'maskUnits';
+
+ case 'numoctaves':
+ return 'numOctaves';
+
+ case 'pathlength':
+ return 'pathLength';
+
+ case 'patterncontentunits':
+ return 'patternContentUnits';
+
+ case 'patterntransform':
+ return 'patternTransform';
+
+ case 'patternunits':
+ return 'patternUnits';
+
+ case 'pointsatx':
+ return 'pointsAtX';
+
+ case 'pointsaty':
+ return 'pointsAtY';
+
+ case 'pointsatz':
+ return 'pointsAtZ';
+
+ case 'preservealpha':
+ return 'preserveAlpha';
+
+ case 'preserveaspectratio':
+ return 'preserveAspectRatio';
+
+ case 'primitiveunits':
+ return 'primitiveUnits';
+
+ case 'refx':
+ return 'refX';
+
+ case 'refy':
+ return 'refY';
+
+ case 'repeatcount':
+ return 'repeatCount';
+
+ case 'repeatdur':
+ return 'repeatDur';
+
+ case 'requiredextensions':
+ return 'requiredExtensions';
+
+ case 'requiredfeatures':
+ return 'requiredFeatures';
+
+ case 'specularconstant':
+ return 'specularConstant';
+
+ case 'specularexponent':
+ return 'specularExponent';
+
+ case 'spreadmethod':
+ return 'spreadMethod';
+
+ case 'startoffset':
+ return 'startOffset';
+
+ case 'stddeviation':
+ return 'stdDeviation';
+
+ case 'stitchtiles':
+ return 'stitchTiles';
+
+ case 'surfacescale':
+ return 'surfaceScale';
+
+ case 'systemlanguage':
+ return 'systemLanguage';
+
+ case 'tablevalues':
+ return 'tableValues';
+
+ case 'targetx':
+ return 'targetX';
+
+ case 'targety':
+ return 'targetY';
+
+ case 'textlength':
+ return 'textLength';
+
+ case 'viewbox':
+ return 'viewBox';
+
+ case 'viewtarget':
+ return 'viewTarget';
+
+ case 'xchannelselector':
+ return 'xChannelSelector';
+
+ case 'ychannelselector':
+ return 'yChannelSelector';
+
+ case 'zoomandpan':
+ return 'zoomAndPan';
+ }
+ }
+
+ if ( 'html' !== $namespace ) {
+ switch ( $lower_name ) {
+ case 'xlink:actuate':
+ return 'xlink actuate';
+
+ case 'xlink:arcrole':
+ return 'xlink arcrole';
+
+ case 'xlink:href':
+ return 'xlink href';
+
+ case 'xlink:role':
+ return 'xlink role';
+
+ case 'xlink:show':
+ return 'xlink show';
+
+ case 'xlink:title':
+ return 'xlink title';
+
+ case 'xlink:type':
+ return 'xlink type';
+
+ case 'xml:lang':
+ return 'xml lang';
+
+ case 'xml:space':
+ return 'xml space';
+
+ case 'xmlns':
+ return 'xmlns';
+
+ case 'xmlns:xlink':
+ return 'xmlns xlink';
+ }
+ }
+
+ return $attribute_name;
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Indicates if the currently matched tag contains the self-closing flag.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * No HTML elements ought to have the self-closing flag and for those, the self-closing
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2963,8 +3419,12 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * In all other contexts it's replaced by the replacement character (U+FFFD)
</span><span class="cx" style="display: block; padding: 0 10px"> * for security reasons (to avoid joining together strings that were safe
</span><span class="cx" style="display: block; padding: 0 10px"> * when separated, but not when joined).
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ *
+ * @todo Inside HTML integration points and MathML integration points, the
+ * text is processed according to the insertion mode, not according
+ * to the foreign content rules. This should strip the NULL bytes.
</ins><span class="cx" style="display: block; padding: 0 10px"> */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- return '#text' === $tag_name
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ return ( '#text' === $tag_name && 'html' === $this->get_namespace() )
</ins><span class="cx" style="display: block; padding: 0 10px"> ? str_replace( "\x00", '', $decoded )
</span><span class="cx" style="display: block; padding: 0 10px"> : str_replace( "\x00", "\u{FFFD}", $decoded );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunksrcwpincludeshtmlapiclasswphtmltokenphp"></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-token.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-token.php 2024-08-08 04:24:03 UTC (rev 58866)
+++ trunk/src/wp-includes/html-api/class-wp-html-token.php 2024-08-08 07:23:53 UTC (rev 58867)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -61,6 +61,24 @@
</span><span class="cx" style="display: block; padding: 0 10px"> public $has_self_closing_flag = false;
</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">+ * Indicates if the element is an HTML element or if it's inside foreign content.
+ *
+ * @since 6.7.0
+ *
+ * @var string 'html', 'svg', or 'math'.
+ */
+ public $namespace = 'html';
+
+ /**
+ * Indicates which kind of integration point the element is, if any.
+ *
+ * @since 6.7.0
+ *
+ * @var string|null 'math', 'html', or null if not an integration point.
+ */
+ public $integration_node_type = null;
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Called when token is garbage-collected or otherwise destroyed.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @var callable|null
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -80,6 +98,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> public function __construct( ?string $bookmark_name, string $node_name, bool $has_self_closing_flag, ?callable $on_destroy = null ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $this->bookmark_name = $bookmark_name;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $this->namespace = 'html';
</ins><span class="cx" style="display: block; padding: 0 10px"> $this->node_name = $node_name;
</span><span class="cx" style="display: block; padding: 0 10px"> $this->has_self_closing_flag = $has_self_closing_flag;
</span><span class="cx" style="display: block; padding: 0 10px"> $this->on_destroy = $on_destroy;
</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-08-08 04:24:03 UTC (rev 58866)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php 2024-08-08 07:23:53 UTC (rev 58867)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -359,37 +359,6 @@
</span><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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * Ensures that special handling of unsupported tags is cleaned up
- * as handling is implemented. Otherwise there's risk of leaving special
- * handling (that is never reached) when tag handling is implemented.
- *
- * @ticket 60092
- *
- * @dataProvider data_unsupported_special_in_body_tags
- *
- * @covers WP_HTML_Processor::step_in_body
- *
- * @param string $tag_name Name of the tag to test.
- */
- public function test_step_in_body_fails_on_unsupported_tags( $tag_name ) {
- $fragment = WP_HTML_Processor::create_fragment( '<' . $tag_name . '></' . $tag_name . '>' );
- $this->assertFalse( $fragment->next_tag(), 'Should fail to find tag: ' . $tag_name . '.' );
- $this->assertEquals( $fragment->get_last_error(), WP_HTML_Processor::ERROR_UNSUPPORTED, 'Should have unsupported last error.' );
- }
-
- /**
- * Data provider.
- *
- * @return array[]
- */
- public static function data_unsupported_special_in_body_tags() {
- return array(
- 'MATH' => array( 'MATH' ),
- 'SVG' => array( 'SVG' ),
- );
- }
-
- /**
</del><span class="cx" style="display: block; padding: 0 10px"> * Ensures that the HTML Processor properly reports the depth of a given element.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @ticket 61255
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlProcessorBreadcrumbsphp"></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/wpHtmlProcessorBreadcrumbs.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php 2024-08-08 04:24:03 UTC (rev 58866)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php 2024-08-08 07:23:53 UTC (rev 58867)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -165,51 +165,8 @@
</span><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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * Ensures that no new HTML elements are accidentally partially-supported.
- *
- * When introducing support for new HTML elements, there are multiple places
- * in the HTML Processor that need to be updated, until the time that the class
- * has full HTML5 support. Because of this, these tests lock down the interface
- * to ensure that support isn't accidentally updated in one place for a new
- * element while overlooked in another.
- *
</del><span class="cx" style="display: block; padding: 0 10px"> * @ticket 58517
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @covers WP_HTML_Processor::step
- *
- * @dataProvider data_unsupported_elements
- *
- * @param string $html HTML string containing unsupported elements.
- */
- public function test_fails_when_encountering_unsupported_tag( $html ) {
- $processor = WP_HTML_Processor::create_fragment( $html );
-
- $this->assertFalse( $processor->step(), "Should not have stepped into unsupported {$processor->get_tag()} element." );
- }
-
- /**
- * Data provider.
- *
- * @return array[]
- */
- public static function data_unsupported_elements() {
- $unsupported_elements = array(
- 'MATH',
- 'PLAINTEXT', // Neutralized.
- 'SVG',
- );
-
- $data = array();
- foreach ( $unsupported_elements as $tag_name ) {
- $data[ $tag_name ] = array( "<{$tag_name}>" );
- }
-
- return $data;
- }
-
- /**
- * @ticket 58517
- *
</del><span class="cx" style="display: block; padding: 0 10px"> * @dataProvider data_unsupported_markup
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @param string $html HTML containing unsupported markup.
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlProcessorHtml5libphp"></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/wpHtmlProcessorHtml5lib.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php 2024-08-08 04:24:03 UTC (rev 58866)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php 2024-08-08 07:23:53 UTC (rev 58867)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -181,7 +181,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $is_closer = $processor->is_tag_closer();
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $was_text && '#text' !== $token_name ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $output .= "{$text_node}\"\n";
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( '' !== $text_node ) {
+ $output .= "{$text_node}\"\n";
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> $was_text = false;
</span><span class="cx" style="display: block; padding: 0 10px"> $text_node = '';
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -188,12 +190,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> switch ( $token_type ) {
</span><span class="cx" style="display: block; padding: 0 10px"> case '#tag':
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $tag_name = strtolower( $token_name );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $namespace = $processor->get_namespace();
+ $tag_name = 'html' === $namespace
+ ? strtolower( $processor->get_tag() )
+ : "{$namespace} {$processor->get_qualified_tag_name()}";
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $is_closer ) {
</span><span class="cx" style="display: block; padding: 0 10px"> --$indent_level;
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( 'TEMPLATE' === $token_name ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( 'html' === $namespace && 'TEMPLATE' === $token_name ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> --$indent_level;
</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">@@ -202,7 +207,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $tag_indent = $indent_level;
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( ! WP_HTML_Processor::is_void( $tag_name ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( 'html' !== $namespace ) {
+ if ( ! $processor->has_self_closing_flag() ) {
+ ++$indent_level;
+ }
+ } elseif ( ! WP_HTML_Processor::is_void( $tag_name ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> ++$indent_level;
</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">@@ -210,9 +219,47 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $attribute_names = $processor->get_attribute_names_with_prefix( '' );
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $attribute_names ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- sort( $attribute_names, SORT_STRING );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $sorted_attributes = array();
+ foreach ( $attribute_names as $attribute_name ) {
+ $sorted_attributes[ $attribute_name ] = $processor->get_qualified_attribute_name( $attribute_name );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- foreach ( $attribute_names as $attribute_name ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /*
+ * Sorts attributes to match html5lib sort order.
+ *
+ * - First comes normal HTML attributes.
+ * - Then come adjusted foreign attributes; these have spaces in their names.
+ * - Finally come non-adjusted foreign attributes; these have a colon in their names.
+ *
+ * Example:
+ *
+ * From: <math xlink:author definitionurl xlink:title xlink:show>
+ * Sorted: 'definitionURL', 'xlink show', 'xlink title', 'xlink:author'
+ */
+ uasort(
+ $sorted_attributes,
+ static function ( $a, $b ) {
+ $a_has_ns = str_contains( $a, ':' );
+ $b_has_ns = str_contains( $b, ':' );
+
+ // Attributes with `:` should follow all other attributes.
+ if ( $a_has_ns !== $b_has_ns ) {
+ return $a_has_ns ? 1 : -1;
+ }
+
+ $a_has_sp = str_contains( $a, ' ' );
+ $b_has_sp = str_contains( $b, ' ' );
+
+ // Attributes with a namespace ' ' should come after those without.
+ if ( $a_has_sp !== $b_has_sp ) {
+ return $a_has_sp ? 1 : -1;
+ }
+
+ return $a <=> $b;
+ }
+ );
+
+ foreach ( $sorted_attributes as $attribute_name => $display_name ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> $val = $processor->get_attribute( $attribute_name );
</span><span class="cx" style="display: block; padding: 0 10px"> /*
</span><span class="cx" style="display: block; padding: 0 10px"> * Attributes with no value are `true` with the HTML API,
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -221,7 +268,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> if ( true === $val ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $val = '';
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $output .= str_repeat( $indent, $tag_indent + 1 ) . "{$attribute_name}=\"{$val}\"\n";
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $output .= str_repeat( $indent, $tag_indent + 1 ) . "{$display_name}=\"{$val}\"\n";
</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">@@ -231,7 +278,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $output .= str_repeat( $indent, $indent_level ) . "\"{$modifiable_text}\"\n";
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( 'TEMPLATE' === $token_name ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( 'html' === $namespace && 'TEMPLATE' === $token_name ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> $output .= str_repeat( $indent, $indent_level ) . "content\n";
</span><span class="cx" style="display: block; padding: 0 10px"> ++$indent_level;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -242,12 +289,17 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> break;
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ case '#cdata-section':
</ins><span class="cx" style="display: block; padding: 0 10px"> case '#text':
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $text_content = $processor->get_modifiable_text();
+ if ( '' === $text_content ) {
+ break;
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> $was_text = true;
</span><span class="cx" style="display: block; padding: 0 10px"> if ( '' === $text_node ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $text_node .= str_repeat( $indent, $indent_level ) . '"';
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $text_node .= $processor->get_modifiable_text();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $text_node .= $text_content;
</ins><span class="cx" style="display: block; padding: 0 10px"> break;
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> case '#funky-comment':
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlSupportRequiredOpenElementsphp"></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/wpHtmlSupportRequiredOpenElements.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php 2024-08-08 04:24:03 UTC (rev 58866)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredOpenElements.php 2024-08-08 07:23:53 UTC (rev 58867)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,219 +0,0 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-<?php
-/**
- * Unit tests for the HTML API indicating that changes are needed to the
- * WP_HTML_Open_Elements class before specific features are added to the API.
- *
- * Note! Duplication of test cases and the helper function in this file are intentional.
- * This test file exists to warn developers of related areas of code that need to update
- * together when adding support for new elements to the HTML Processor. For example,
- * when adding support for the BUTTON element it's necessary to update multiple methods
- * in the class governing the stack of open elements as well as the HTML Processor class
- * itself. This is because each element might bring with it semantic rules that impact
- * the way the document should be parsed. BUTTON creates a kind of boundary in the
- * DOM tree and implicitly closes existing open BUTTON elements.
- *
- * Without these tests a developer needs to investigate all possible places they
- * might need to update when adding support for more elements and risks overlooking
- * important parts that, in the absence of the related support, will lead to errors.
- *
- * @package WordPress
- * @subpackage HTML-API
- *
- * @since 6.4.0
- *
- * @group html-api
- *
- * @coversDefaultClass WP_HTML_Processor
- */
-class Tests_HtmlApi_WpHtmlSupportRequiredOpenElements extends WP_UnitTestCase {
- /**
- * Fails to assert if the HTML Processor handles the given tag.
- *
- * This test helper is used throughout this test file for one purpose only: to
- * fail a test if the HTML Processor handles the given tag. In other words, it
- * ensures that the HTML Processor aborts when encountering the given tag.
- *
- * This is used to ensure that when support for a new tag is added to the
- * HTML Processor it receives full support and not partial support, which
- * could lead to a variety of issues.
- *
- * Do not remove this helper function as it provides semantic meaning to the
- * assertions in the tests in this file and its behavior is incredibly specific
- * and limited and doesn't warrant adding a new abstraction into WP_UnitTestCase.
- *
- * @param string $tag_name the HTML Processor should abort when encountering this tag, e.g. "BUTTON".
- */
- private function ensure_support_is_added_everywhere( $tag_name ) {
- $processor = WP_HTML_Processor::create_fragment( "<$tag_name>" );
-
- $this->assertFalse( $processor->step(), "Must support terminating elements in specific scope check before adding support for the {$tag_name} element." );
- }
-
- /**
- * The check for whether an element is in a scope depends on
- * looking for a number of terminating elements in the stack of open
- * elements. Until the listed elements are supported in the HTML
- * processor, there are no terminating elements and there's no
- * point in taking the time to look for them.
- *
- * @since 6.4.0
- *
- * @ticket 58517
- */
- public function test_has_element_in_scope_needs_support() {
- // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
- $this->ensure_support_is_added_everywhere( 'MATH' );
-
- /*
- * SVG elements: note that TITLE is both an HTML element and an SVG element
- * so care must be taken when adding support for either one.
- *
- * FOREIGNOBJECT, DESC, TITLE.
- */
- $this->ensure_support_is_added_everywhere( 'SVG' );
- }
-
- /**
- * The check for whether an element is in list item scope depends on
- * the elements for any scope, plus UL and OL.
- *
- * The method for asserting list item scope doesn't currently exist
- * because the LI element isn't yet supported and the LI element is
- * the only element that needs to know about list item scope.
- *
- * @since 6.4.0
- *
- * @ticket 58517
- *
- * @covers WP_HTML_Open_Elements::has_element_in_list_item_scope
- */
- public function test_has_element_in_list_item_scope_needs_support() {
- // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
- $this->ensure_support_is_added_everywhere( 'MATH' );
-
- /*
- * SVG elements: note that TITLE is both an HTML element and an SVG element
- * so care must be taken when adding support for either one.
- *
- * FOREIGNOBJECT, DESC, TITLE.
- */
- $this->ensure_support_is_added_everywhere( 'SVG' );
- }
-
- /**
- * The check for whether an element is in BUTTON scope depends on
- * the elements for any scope, plus BUTTON.
- *
- * @since 6.4.0
- *
- * @ticket 58517
- *
- * @covers WP_HTML_Open_Elements::has_element_in_button_scope
- */
- public function test_has_element_in_button_scope_needs_support() {
- // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
- $this->ensure_support_is_added_everywhere( 'MATH' );
-
- /*
- * SVG elements: note that TITLE is both an HTML element and an SVG element
- * so care must be taken when adding support for either one.
- *
- * FOREIGNOBJECT, DESC, TITLE.
- */
- $this->ensure_support_is_added_everywhere( 'SVG' );
- }
-
- /**
- * The optimization maintaining a flag for "P is in BUTTON scope" requires
- * updating that flag every time an element is popped from the stack of
- * open elements.
- *
- * @since 6.4.0
- *
- * @ticket 58517
- *
- * @covers WP_HTML_Open_Elements::after_element_pop
- */
- public function test_after_element_pop_must_maintain_p_in_button_scope_flag() {
- // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
- $this->ensure_support_is_added_everywhere( 'MATH' );
-
- /*
- * SVG elements: note that TITLE is both an HTML element and an SVG element
- * so care must be taken when adding support for either one.
- *
- * FOREIGNOBJECT, DESC, TITLE.
- */
- $this->ensure_support_is_added_everywhere( 'SVG' );
- }
-
- /**
- * The optimization maintaining a flag for "P is in BUTTON scope" requires
- * updating that flag every time an element is pushed onto the stack of
- * open elements.
- *
- * @since 6.4.0
- *
- * @ticket 58517
- *
- * @covers WP_HTML_Open_Elements::after_element_push
- */
- public function test_after_element_push_must_maintain_p_in_button_scope_flag() {
- // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
- $this->ensure_support_is_added_everywhere( 'MATH' );
-
- /*
- * SVG elements: note that TITLE is both an HTML element and an SVG element
- * so care must be taken when adding support for either one.
- *
- * FOREIGNOBJECT, DESC, TITLE.
- */
- $this->ensure_support_is_added_everywhere( 'SVG' );
- }
-
- /**
- * The check for whether an element is in TABLE scope depends on
- * the HTML, TABLE, and TEMPLATE elements.
- *
- * @since 6.4.0
- *
- * @ticket 58517
- *
- * @covers WP_HTML_Open_Elements::has_element_in_table_scope
- */
- public function test_has_element_in_table_scope_needs_support() {
- // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
- $this->ensure_support_is_added_everywhere( 'MATH' );
-
- /*
- * SVG elements: note that TITLE is both an HTML element and an SVG element
- * so care must be taken when adding support for either one.
- *
- * FOREIGNOBJECT, DESC, TITLE.
- */
- $this->ensure_support_is_added_everywhere( 'SVG' );
- }
-
- /**
- * The check for whether an element is in SELECT scope depends on
- * the OPTGROUP and OPTION elements.
- *
- * @since 6.4.0
- *
- * @ticket 58517
- *
- * @covers WP_HTML_Open_Elements::has_element_in_select_scope
- */
- public function test_has_element_in_select_scope_needs_support() {
- // MathML Elements: MI, MO, MN, MS, MTEXT, ANNOTATION-XML.
- $this->ensure_support_is_added_everywhere( 'MATH' );
-
- /*
- * SVG elements: note that TITLE is both an HTML element and an SVG element
- * so care must be taken when adding support for either one.
- *
- * FOREIGNOBJECT, DESC, TITLE.
- */
- $this->ensure_support_is_added_everywhere( 'SVG' );
- }
-}
</del></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlTagProcessortokenscanningphp"></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/wpHtmlTagProcessor-token-scanning.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor-token-scanning.php 2024-08-08 04:24:03 UTC (rev 58866)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor-token-scanning.php 2024-08-08 07:23:53 UTC (rev 58867)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -513,6 +513,67 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Ensures that basic CDATA sections inside foreign content are detected.
+ *
+ * @ticket 61576
+ */
+ public function test_basic_cdata_in_foreign_content() {
+ $processor = new WP_HTML_Tag_Processor( '<svg><![CDATA[this is >> real CDATA]]></svg>' );
+ $processor->next_token();
+
+ // Artificially change namespace; this should be done in the HTML Processor.
+ $processor->change_parsing_namespace( 'svg' );
+ $processor->next_token();
+
+ $this->assertSame(
+ '#cdata-section',
+ $processor->get_token_name(),
+ "Should have found a CDATA section but found {$processor->get_token_name()} instead."
+ );
+
+ $this->assertNull(
+ $processor->get_tag(),
+ 'Should not have been able to query tag name on non-element token.'
+ );
+
+ $this->assertNull(
+ $processor->get_attribute( 'type' ),
+ 'Should not have been able to query attributes on non-element token.'
+ );
+
+ $this->assertSame(
+ 'this is >> real CDATA',
+ $processor->get_modifiable_text(),
+ 'Found incorrect modifiable text.'
+ );
+ }
+
+ /**
+ * Ensures that empty CDATA sections inside foreign content are detected.
+ *
+ * @ticket 61576
+ */
+ public function test_empty_cdata_in_foreign_content() {
+ $processor = new WP_HTML_Tag_Processor( '<svg><![CDATA[]]></svg>' );
+ $processor->next_token();
+
+ // Artificially change namespace; this should be done in the HTML Processor.
+ $processor->change_parsing_namespace( 'svg' );
+ $processor->next_token();
+
+ $this->assertSame(
+ '#cdata-section',
+ $processor->get_token_name(),
+ "Should have found a CDATA section but found {$processor->get_token_name()} instead."
+ );
+
+ $this->assertEmpty(
+ $processor->get_modifiable_text(),
+ 'Found non-empty modifiable text.'
+ );
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Ensures that normative Processing Instruction nodes are properly parsed.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @ticket 60170
</span></span></pre>
</div>
</div>
</body>
</html>