<!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>[56331] trunk: HTML API: Add support for SPAN element.</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/56331">56331</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/56331","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>Bernhard Reiter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2023-08-01 07:54:54 +0000 (Tue, 01 Aug 2023)</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 SPAN element.
In this patch we're introducing support for the SPAN element, which is the first
in the class of "any other tag" in the "in body" insertion mode.
This patch introduces the mechanisms required to handle that class of tags but
only introduces SPAN to keep the change focused. With the tests and mechanisms
in place it will be possible to follow-up and add another limited set of tags.
It's important that this not use the default catch-all in the switch handling
`step_in_body` because that would catch tags that have specific rules in previous
case statements that aren't yet added. For example, we don't want to treat the
`TABLE` element as "any other tag".
Props dmsnell.
Fixes <a href="https://core.trac.wordpress.org/ticket/58907">#58907</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmlprocessorphp">trunk/src/wp-includes/html-api/class-wp-html-processor.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlProcessorBreadcrumbsphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlProcessorSemanticRulesphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlSupportRequiredHtmlProcessorphp">trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludeshtmlapiclasswphtmlprocessorphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/html-api/class-wp-html-processor.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/html-api/class-wp-html-processor.php 2023-08-01 04:22:42 UTC (rev 56330)
+++ trunk/src/wp-includes/html-api/class-wp-html-processor.php 2023-08-01 07:54:54 UTC (rev 56331)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -626,6 +626,37 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $this->insert_html_element( $this->current_token );
</span><span class="cx" style="display: block; padding: 0 10px"> return true;
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /*
+ * > Any other start tag
+ */
+ case '+SPAN':
+ $this->reconstruct_active_formatting_elements();
+ $this->insert_html_element( $this->current_token );
+ return true;
+
+ /*
+ * Any other end tag
+ */
+ case '-SPAN':
+ foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
+ // > If node is an HTML element with the same tag name as the token, then:
+ if ( $item->node_name === $tag_name ) {
+ $this->generate_implied_end_tags( $tag_name );
+
+ // > If node is not the current node, then this is a parse error.
+
+ $this->state->stack_of_open_elements->pop_until( $tag_name );
+ return true;
+ }
+
+ // > Otherwise, if node is in the special category, then this is a parse error; ignore the token, and return.
+ if ( self::is_special( $item->node_name ) ) {
+ return $this->step();
+ }
+ }
+ // Execution should not reach here; if it does then something went wrong.
+ return false;
+
</ins><span class="cx" style="display: block; padding: 0 10px"> default:
</span><span class="cx" style="display: block; padding: 0 10px"> $this->last_error = self::ERROR_UNSUPPORTED;
</span><span class="cx" style="display: block; padding: 0 10px"> throw new WP_HTML_Unsupported_Exception( "Cannot process {$tag_name} element." );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -873,7 +904,7 @@
</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="cx" style="display: block; padding: 0 10px"> *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @throws Exception
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @throws WP_HTML_Unsupported_Exception
</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/#generate-implied-end-tags
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -893,6 +924,26 @@
</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">+ /*
+ * Closes elements that have implied end tags, thoroughly.
+ *
+ * See the HTML specification for an explanation why this is
+ * different from {@see WP_HTML_Processor::generate_implied_end_tags}.
+ *
+ * @since 6.4.0
+ *
+ * @see https://html.spec.whatwg.org/#generate-implied-end-tags
+ */
+ private function generate_implied_end_tags_thoroughly() {
+ $elements_with_implied_end_tags = array(
+ 'P',
+ );
+
+ while ( in_array( $this->state->stack_of_open_elements->current_node(), $elements_with_implied_end_tags, true ) ) {
+ $this->state->stack_of_open_elements->pop();
+ }
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /**
</span><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></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 2023-08-01 04:22:42 UTC (rev 56330)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php 2023-08-01 07:54:54 UTC (rev 56331)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -49,6 +49,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'IMG',
</span><span class="cx" style="display: block; padding: 0 10px"> 'P',
</span><span class="cx" style="display: block; padding: 0 10px"> 'SMALL',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'SPAN',
</ins><span class="cx" style="display: block; padding: 0 10px"> 'STRIKE',
</span><span class="cx" style="display: block; padding: 0 10px"> 'STRONG',
</span><span class="cx" style="display: block; padding: 0 10px"> 'TT',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -191,7 +192,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'SLOT',
</span><span class="cx" style="display: block; padding: 0 10px"> 'SOURCE',
</span><span class="cx" style="display: block; padding: 0 10px"> 'SPACER', // Deprecated
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'SPAN',
</del><span class="cx" style="display: block; padding: 0 10px"> 'STYLE',
</span><span class="cx" style="display: block; padding: 0 10px"> 'SUB',
</span><span class="cx" style="display: block; padding: 0 10px"> 'SUMMARY',
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlProcessorSemanticRulesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php (rev 0)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php 2023-08-01 07:54:54 UTC (rev 56331)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,68 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_HTML_Processor compliance with HTML5 semantic parsing rules.
+ *
+ * @package WordPress
+ * @subpackage HTML-API
+ *
+ * @since 6.4.0
+ *
+ * @group html-api
+ *
+ * @coversDefaultClass WP_HTML_Processor
+ */
+class Tests_HtmlApi_WpHtmlProcessorSemanticRules extends WP_UnitTestCase {
+ /*******************************************************************
+ * RULES FOR "IN BODY" MODE
+ *******************************************************************/
+
+ /*
+ * Verifies that when "in body" and encountering "any other end tag"
+ * that the HTML processor ignores the end tag if there's a special
+ * element on the stack of open elements before the matching opening.
+ *
+ * @ticket 58907
+ *
+ * @since 6.4.0
+ *
+ * @covers WP_HTML_Processor::step_in_body
+ */
+ public function test_in_body_any_other_end_tag_with_unclosed_special_element() {
+ $p = WP_HTML_Processor::createFragment( '<div><span><p></span><div>' );
+
+ $p->next_tag( 'P' );
+ $this->assertSame( 'P', $p->get_tag(), "Expected to start test on P element but found {$p->get_tag()} instead." );
+ $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'P' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting.' );
+
+ $this->assertTrue( $p->next_tag(), 'Failed to advance past P tag to expected DIV opener.' );
+ $this->assertSame( 'DIV', $p->get_tag(), "Expected to find DIV element, but found {$p->get_tag()} instead." );
+ $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'DIV' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting: SPAN should still be open and DIV should be its child.' );
+ }
+
+ /*
+ * Verifies that when "in body" and encountering "any other end tag"
+ * that the HTML processor closes appropriate elements on the stack of
+ * open elements up to the matching opening.
+ *
+ * @ticket 58907
+ *
+ * @since 6.4.0
+ *
+ * @covers WP_HTML_Processor::step_in_body
+ */
+ public function test_in_body_any_other_end_tag_with_unclosed_non_special_element() {
+ $p = WP_HTML_Processor::createFragment( '<div><span><code></span><div>' );
+
+ $p->next_tag( 'CODE' );
+ $this->assertSame( 'CODE', $p->get_tag(), "Expected to start test on CODE element but found {$p->get_tag()} instead." );
+ $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'CODE' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting.' );
+
+ $this->assertTrue( $p->next_tag(), 'Failed to advance past CODE tag to expected SPAN closer.' );
+ $this->assertTrue( $p->is_tag_closer(), 'Expected to find closing SPAN, but found opener instead.' );
+ $this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $p->get_breadcrumbs(), 'Failed to advance past CODE tag to expected DIV opener.' );
+
+ $this->assertTrue( $p->next_tag(), 'Failed to advance past SPAN closer to expected DIV opener.' );
+ $this->assertSame( 'DIV', $p->get_tag(), "Expected to find DIV element, but found {$p->get_tag()} instead." );
+ $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'DIV' ), $p->get_breadcrumbs(), 'Failed to produce expected DOM nesting: SPAN should be closed and DIV should be its sibling.' );
+ }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlSupportRequiredHtmlProcessorphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php (rev 0)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlSupportRequiredHtmlProcessor.php 2023-08-01 07:54:54 UTC (rev 56331)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,101 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests for the HTML API indicating that changes are needed to the
+ * WP_HTML_Processor 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 LI element it's necessary to update the function which
+ * generates implied end tags. This is because each element might bring with it semantic
+ * rules that impact the way the document should be parsed.
+ *
+ * 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_WpHtmlSupportRequiredHtmlProcessor 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 ) {
+ $p = WP_HTML_Processor::createFragment( "<$tag_name>" );
+
+ $this->assertFalse( $p->step(), "Must support terminating elements in specific scope check before adding support for the {$tag_name} element." );
+ }
+
+ /**
+ * Generating implied end tags walks up the stack of open elements
+ * as long as any of the following missing elements is the current node.
+ *
+ * @since 6.4.0
+ *
+ * @ticket 58907
+ *
+ * @covers WP_HTML_Processor::generate_implied_end_tags
+ */
+ public function test_generate_implied_end_tags_needs_support() {
+ $this->ensure_support_is_added_everywhere( 'DD' );
+ $this->ensure_support_is_added_everywhere( 'DT' );
+ $this->ensure_support_is_added_everywhere( 'LI' );
+ $this->ensure_support_is_added_everywhere( 'OPTGROUP' );
+ $this->ensure_support_is_added_everywhere( 'OPTION' );
+ $this->ensure_support_is_added_everywhere( 'RB' );
+ $this->ensure_support_is_added_everywhere( 'RP' );
+ $this->ensure_support_is_added_everywhere( 'RT' );
+ $this->ensure_support_is_added_everywhere( 'RTC' );
+ }
+
+ /**
+ * Generating implied end tags thoroughly walks up the stack of open elements
+ * as long as any of the following missing elements is the current node.
+ *
+ * @since 6.4.0
+ *
+ * @ticket 58907
+ *
+ * @covers WP_HTML_Processor::generate_implied_end_tags_thoroughly
+ */
+ public function test_generate_implied_end_tags_thoroughly_needs_support() {
+ $this->ensure_support_is_added_everywhere( 'CAPTION' );
+ $this->ensure_support_is_added_everywhere( 'COLGROUP' );
+ $this->ensure_support_is_added_everywhere( 'DD' );
+ $this->ensure_support_is_added_everywhere( 'DT' );
+ $this->ensure_support_is_added_everywhere( 'LI' );
+ $this->ensure_support_is_added_everywhere( 'OPTGROUP' );
+ $this->ensure_support_is_added_everywhere( 'OPTION' );
+ $this->ensure_support_is_added_everywhere( 'RB' );
+ $this->ensure_support_is_added_everywhere( 'RP' );
+ $this->ensure_support_is_added_everywhere( 'RT' );
+ $this->ensure_support_is_added_everywhere( 'RTC' );
+ $this->ensure_support_is_added_everywhere( 'TBODY' );
+ $this->ensure_support_is_added_everywhere( 'TD' );
+ $this->ensure_support_is_added_everywhere( 'TFOOT' );
+ $this->ensure_support_is_added_everywhere( 'TH' );
+ $this->ensure_support_is_added_everywhere( 'HEAD' );
+ $this->ensure_support_is_added_everywhere( 'TR' );
+ }
+}
</ins></span></pre>
</div>
</div>
</body>
</html>