<!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>[58925] trunk: HTML API: Parse DOCTYPE tokens and set HTML parser mode accordingly.</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/58925">58925</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/58925","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-23 14:53:59 +0000 (Fri, 23 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: Parse DOCTYPE tokens and set HTML parser mode accordingly.

This patch adds until-now missing code to parse the structure of HTML DOCTYPE declarations. The DOCTYPE is mostly unused but can dictate the document compatability mode, which governs whether CSS class names match in a ASCII-case-insensitive way or not, and whether TABLE elements close an open P element.

The DOCTYPE information is made available through a new method on the Tag Processor, `get_doctype_info()`.

Developed in https://github.com/wordpress/wordpress-develop/pull/7195
Discussed in https://core.trac.wordpress.org/ticket/61576

Props dmsnell, jonsurrell.
See <a href="https://core.trac.wordpress.org/ticket/61576">#61576</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkphpcsxmldist">trunk/phpcs.xml.dist</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="#trunksrcwpsettingsphp">trunk/src/wp-settings.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlProcessorHtml5libphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlTagProcessorphp">trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmldoctypeinfophp">trunk/src/wp-includes/html-api/class-wp-html-doctype-info.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlDoctypeInfophp">trunk/tests/phpunit/tests/html-api/wpHtmlDoctypeInfo.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkphpcsxmldist"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/phpcs.xml.dist</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/phpcs.xml.dist      2024-08-22 23:45:49 UTC (rev 58924)
+++ trunk/phpcs.xml.dist        2024-08-23 14:53:59 UTC (rev 58925)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -262,6 +262,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">             in the parsing, and distance the code from its standard. -->
</span><span class="cx" style="display: block; padding: 0 10px">        <rule ref="Generic.PHP.DiscourageGoto.Found">
</span><span class="cx" style="display: block; padding: 0 10px">                <exclude-pattern>/wp-includes/html-api/class-wp-html-processor\.php</exclude-pattern>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <exclude-pattern>/wp-includes/html-api/class-wp-html-doctype-info\.php</exclude-pattern>
</ins><span class="cx" style="display: block; padding: 0 10px">         </rule>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        <!-- Exclude sample config from modernization to prevent breaking CI workflows based on WP-CLI scaffold.
</span></span></pre></div>
<a id="trunksrcwpincludeshtmlapiclasswphtmldoctypeinfophp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/html-api/class-wp-html-doctype-info.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-doctype-info.php                             (rev 0)
+++ trunk/src/wp-includes/html-api/class-wp-html-doctype-info.php       2024-08-23 14:53:59 UTC (rev 58925)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,616 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * HTML API: WP_HTML_Doctype_Info class
+ *
+ * @package WordPress
+ * @subpackage HTML-API
+ * @since 6.7.0
+ */
+
+/**
+ * Core class used by the HTML API to represent a DOCTYPE declaration.
+ *
+ * This class parses DOCTYPE tokens for the full parser in the HTML Processor.
+ * Most code interacting with HTML won't need to parse DOCTYPE declarations;
+ * the HTML Processor is one exception. Consult the HTML Processor for proper
+ * parsing of an HTML document.
+ *
+ * A DOCTYPE declaration may indicate its document compatibility mode, which impacts
+ * the structure of the following HTML as well as the behavior of CSS class selectors.
+ * There are three possible modes:
+ *
+ *  - "no-quirks" and "limited-quirks" modes (also called "standards mode").
+ *  - "quirks" mode.
+ *
+ * These modes mostly determine whether CSS class name selectors match values in the
+ * HTML `class` attribute in an ASCII-case-insensitive way (quirks mode), or whether
+ * they match only when byte-for-byte identical (no-quirks mode).
+ *
+ * All HTML documents should start with the standard HTML5 DOCTYPE: `<!DOCTYPE html>`.
+ *
+ * > DOCTYPEs are required for legacy reasons. When omitted, browsers tend to use a different
+ * > rendering mode that is incompatible with some specifications. Including the DOCTYPE in a
+ * > document ensures that the browser makes a best-effort attempt at following the
+ * > relevant specifications.
+ *
+ * @see https://html.spec.whatwg.org/#the-doctype
+ *
+ * DOCTYPE declarations comprise four properties: a name, public identifier, system identifier,
+ * and an indication of which document compatability mode they would imply if an HTML parser
+ * hadn't already determined it from other information.
+ *
+ * @see https://html.spec.whatwg.org/#the-initial-insertion-mode
+ *
+ * Historically, the DOCTYPE declaration was used in SGML documents to instruct a parser how
+ * to interpret the various tags and entities within a document. Its role in HTML diverged
+ * from how it was used in SGML and no meaning should be back-read into HTML based on how it
+ * is used in SGML, XML, or XHTML documents.
+ *
+ * @see https://www.iso.org/standard/16387.html
+ *
+ * @since 6.7.0
+ *
+ * @see WP_HTML_Processor
+ */
+class WP_HTML_Doctype_Info {
+       /**
+        * Name of the DOCTYPE: should be "html" for HTML documents.
+        *
+        * This value should be considered "read only" and not modified.
+        *
+        * Historically the DOCTYPE name indicates name of the document's root element.
+        *
+        *     <!DOCTYPE html>
+        *               ╰──┴── name is "html".
+        *
+        * @see https://html.spec.whatwg.org/#tokenization
+        *
+        * @since 6.7.0
+        *
+        * @var string|null
+        */
+       public $name = null;
+
+       /**
+        * Public identifier of the DOCTYPE.
+        *
+        * This value should be considered "read only" and not modified.
+        *
+        * The public identifier is optional and should not appear in HTML documents.
+        * A `null` value indicates that no public identifier was present in the DOCTYPE.
+        *
+        * Historically the presence of the public identifier indicated that a document
+        * was meant to be shared between computer systems and the value indicated to a
+        * knowledgeable parser how to find the relevant document type definition (DTD).
+        *
+        *     <!DOCTYPE html PUBLIC "public id goes here in quotes">
+        *               │  │         ╰─── public identifier ─────╯
+        *               ╰──┴── name is "html".
+        *
+        * @see https://html.spec.whatwg.org/#tokenization
+        *
+        * @since 6.7.0
+        *
+        * @var string|null
+        */
+       public $public_identifier = null;
+
+       /**
+        * System identifier of the DOCTYPE.
+        *
+        * This value should be considered "read only" and not modified.
+        *
+        * The system identifier is optional and should not appear in HTML documents.
+        * A `null` value indicates that no system identifier was present in the DOCTYPE.
+        *
+        * Historically the system identifier specified where a relevant document type
+        * declaration for the given document is stored and may be retrieved.
+        *
+        *     <!DOCTYPE html SYSTEM "system id goes here in quotes">
+        *               │  │         ╰──── system identifier ────╯
+        *               ╰──┴── name is "html".
+        *
+        * If a public identifier were provided it would indicate to a knowledgeable
+        * parser how to interpret the system identifier.
+        *
+        *     <!DOCTYPE html PUBLIC "public id goes here in quotes" "system id goes here in quotes">
+        *               │  │         ╰─── public identifier ─────╯   ╰──── system identifier ────╯
+        *               ╰──┴── name is "html".
+        *
+        * @see https://html.spec.whatwg.org/#tokenization
+        *
+        * @since 6.7.0
+        *
+        * @var string|null
+        */
+       public $system_identifier = null;
+
+       /**
+        * Which document compatability mode this DOCTYPE declaration indicates.
+        *
+        * This value should be considered "read only" and not modified.
+        *
+        * When an HTML parser has not already set the document compatability mode,
+        * (e.g. "quirks" or "no-quirks" mode), it will infer if from the properties
+        * of the appropriate DOCTYPE declaration, if one exists. The DOCTYPE can
+        * indicate one of three possible document compatability modes:
+        *
+        *  - "no-quirks" and "limited-quirks" modes (also called "standards" mode).
+        *  - "quirks" mode (also called `CSS1Compat` mode).
+        *
+        * An appropriate DOCTYPE is one encountered in the "initial" insertion mode,
+        * before the HTML element has been opened and before finding any other
+        * DOCTYPE declaration tokens.
+        *
+        * @see https://html.spec.whatwg.org/#the-initial-insertion-mode
+        *
+        * @since 6.7.0
+        *
+        * @var string One of "no-quirks", "limited-quirks", or "quirks".
+        */
+       public $indicated_compatability_mode;
+
+       /**
+        * Constructor.
+        *
+        * This class should not be instantiated directly.
+        * Use the static {@see self::from_doctype_token} method instead.
+        *
+        * The arguments to this constructor correspond to the "DOCTYPE token"
+        * as defined in the HTML specification.
+        *
+        * > DOCTYPE tokens have a name, a public identifier, a system identifier,
+        * > and a force-quirks flag. When a DOCTYPE token is created, its name, public identifier,
+        * > and system identifier must be marked as missing (which is a distinct state from the
+        * > empty string), and the force-quirks flag must be set to off (its other state is on).
+        *
+        * @see https://html.spec.whatwg.org/multipage/parsing.html#tokenization
+        *
+        * @since 6.7.0
+        *
+        * @param string|null $name              Name of the DOCTYPE.
+        * @param string|null $public_identifier Public identifier of the DOCTYPE.
+        * @param string|null $system_identifier System identifier of the DOCTYPE.
+        * @param bool        $force_quirks_flag Whether the force-quirks flag is set for the token.
+        */
+       private function __construct(
+               ?string $name,
+               ?string $public_identifier,
+               ?string $system_identifier,
+               bool $force_quirks_flag
+       ) {
+               $this->name              = $name;
+               $this->public_identifier = $public_identifier;
+               $this->system_identifier = $system_identifier;
+
+               /*
+                * > If the DOCTYPE token matches one of the conditions in the following list,
+                * > then set the Document to quirks mode:
+                */
+
+               /*
+                * > The force-quirks flag is set to on.
+                */
+               if ( $force_quirks_flag ) {
+                       $this->indicated_compatability_mode = 'quirks';
+                       return;
+               }
+
+               /*
+                * Normative documents will contain the literal `<!DOCTYPE html>` with no
+                * public or system identifiers; short-circuit to avoid extra parsing.
+                */
+               if ( 'html' === $name && null === $public_identifier && null === $system_identifier ) {
+                       $this->indicated_compatability_mode = 'no-quirks';
+                       return;
+               }
+
+               /*
+                * > The name is not "html".
+                *
+                * The tokenizer must report the name in lower case even if provided in
+                * the document in upper case; thus no conversion is required here.
+                */
+               if ( 'html' !== $name ) {
+                       $this->indicated_compatability_mode = 'quirks';
+                       return;
+               }
+
+               /*
+                * Set up some variables to handle the rest of the conditions.
+                *
+                * > set...the public identifier...to...the empty string if the public identifier was missing.
+                * > set...the system identifier...to...the empty string if the system identifier was missing.
+                * >
+                * > The system identifier and public identifier strings must be compared...
+                * > in an ASCII case-insensitive manner.
+                * >
+                * > A system identifier whose value is the empty string is not considered missing
+                * > for the purposes of the conditions above.
+                */
+               $system_identifier_is_missing = null === $system_identifier;
+               $public_identifier            = null === $public_identifier ? '' : strtolower( $public_identifier );
+               $system_identifier            = null === $system_identifier ? '' : strtolower( $system_identifier );
+
+               /*
+                * > The public identifier is set to…
+                */
+               if (
+                       '-//w3o//dtd w3 html strict 3.0//en//' === $public_identifier ||
+                       '-/w3c/dtd html 4.0 transitional/en' === $public_identifier ||
+                       'html' === $public_identifier
+               ) {
+                       $this->indicated_compatability_mode = 'quirks';
+                       return;
+               }
+
+               /*
+                * > The system identifier is set to…
+                */
+               if ( 'http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd' === $system_identifier ) {
+                       $this->indicated_compatability_mode = 'quirks';
+                       return;
+               }
+
+               /*
+                * All of the following conditions depend on matching the public identifier.
+                * If the public identifier is empty, none of the following conditions will match.
+                */
+               if ( '' === $public_identifier ) {
+                       $this->indicated_compatability_mode = 'no-quirks';
+                       return;
+               }
+
+               /*
+                * > The public identifier starts with…
+                *
+                * @todo Optimize this matching. It shouldn't be a large overall performance issue,
+                *       however, as only a single DOCTYPE declaration token should ever be parsed,
+                *       and normative documents will have exited before reaching this condition.
+                */
+               if (
+                       str_starts_with( $public_identifier, '+//silmaril//dtd html pro v0r11 19970101//' ) ||
+                       str_starts_with( $public_identifier, '-//as//dtd html 3.0 aswedit + extensions//' ) ||
+                       str_starts_with( $public_identifier, '-//advasoft ltd//dtd html 3.0 aswedit + extensions//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 level 1//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 level 2//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 strict level 1//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 strict level 2//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 strict//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html 2.0//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html 2.1e//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html 3.0//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html 3.2 final//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html 3.2//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html 3//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html level 0//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html level 1//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html level 2//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html level 3//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html strict level 0//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html strict level 1//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html strict level 2//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html strict level 3//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html strict//' ) ||
+                       str_starts_with( $public_identifier, '-//ietf//dtd html//' ) ||
+                       str_starts_with( $public_identifier, '-//metrius//dtd metrius presentational//' ) ||
+                       str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 2.0 html strict//' ) ||
+                       str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 2.0 html//' ) ||
+                       str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 2.0 tables//' ) ||
+                       str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 3.0 html strict//' ) ||
+                       str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 3.0 html//' ) ||
+                       str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 3.0 tables//' ) ||
+                       str_starts_with( $public_identifier, '-//netscape comm. corp.//dtd html//' ) ||
+                       str_starts_with( $public_identifier, '-//netscape comm. corp.//dtd strict html//' ) ||
+                       str_starts_with( $public_identifier, "-//o'reilly and associates//dtd html 2.0//" ) ||
+                       str_starts_with( $public_identifier, "-//o'reilly and associates//dtd html extended 1.0//" ) ||
+                       str_starts_with( $public_identifier, "-//o'reilly and associates//dtd html extended relaxed 1.0//" ) ||
+                       str_starts_with( $public_identifier, '-//sq//dtd html 2.0 hotmetal + extensions//' ) ||
+                       str_starts_with( $public_identifier, '-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//' ) ||
+                       str_starts_with( $public_identifier, '-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//' ) ||
+                       str_starts_with( $public_identifier, '-//spyglass//dtd html 2.0 extended//' ) ||
+                       str_starts_with( $public_identifier, '-//sun microsystems corp.//dtd hotjava html//' ) ||
+                       str_starts_with( $public_identifier, '-//sun microsystems corp.//dtd hotjava strict html//' ) ||
+                       str_starts_with( $public_identifier, '-//w3c//dtd html 3 1995-03-24//' ) ||
+                       str_starts_with( $public_identifier, '-//w3c//dtd html 3.2 draft//' ) ||
+                       str_starts_with( $public_identifier, '-//w3c//dtd html 3.2 final//' ) ||
+                       str_starts_with( $public_identifier, '-//w3c//dtd html 3.2//' ) ||
+                       str_starts_with( $public_identifier, '-//w3c//dtd html 3.2s draft//' ) ||
+                       str_starts_with( $public_identifier, '-//w3c//dtd html 4.0 frameset//' ) ||
+                       str_starts_with( $public_identifier, '-//w3c//dtd html 4.0 transitional//' ) ||
+                       str_starts_with( $public_identifier, '-//w3c//dtd html experimental 19960712//' ) ||
+                       str_starts_with( $public_identifier, '-//w3c//dtd html experimental 970421//' ) ||
+                       str_starts_with( $public_identifier, '-//w3c//dtd w3 html//' ) ||
+                       str_starts_with( $public_identifier, '-//w3o//dtd w3 html 3.0//' ) ||
+                       str_starts_with( $public_identifier, '-//webtechs//dtd mozilla html 2.0//' ) ||
+                       str_starts_with( $public_identifier, '-//webtechs//dtd mozilla html//' )
+               ) {
+                       $this->indicated_compatability_mode = 'quirks';
+                       return;
+               }
+
+               /*
+                * > The system identifier is missing and the public identifier starts with…
+                */
+               if (
+                       $system_identifier_is_missing && (
+                               str_starts_with( $public_identifier, '-//w3c//dtd html 4.01 frameset//' ) ||
+                               str_starts_with( $public_identifier, '-//w3c//dtd html 4.01 transitional//' )
+                       )
+               ) {
+                       $this->indicated_compatability_mode = 'quirks';
+                       return;
+               }
+
+               /*
+                * > Otherwise, if the DOCTYPE token matches one of the conditions in
+                * > the following list, then set the Document to limited-quirks mode.
+                */
+
+               /*
+                * > The public identifier starts with…
+                */
+               if (
+                       str_starts_with( $public_identifier, '-//w3c//dtd xhtml 1.0 frameset//' ) ||
+                       str_starts_with( $public_identifier, '-//w3c//dtd xhtml 1.0 transitional//' )
+               ) {
+                       $this->indicated_compatability_mode = 'limited-quirks';
+                       return;
+               }
+
+               /*
+                * > The system identifier is not missing and the public identifier starts with…
+                */
+               if (
+                       ! $system_identifier_is_missing && (
+                               str_starts_with( $public_identifier, '-//w3c//dtd html 4.01 frameset//' ) ||
+                               str_starts_with( $public_identifier, '-//w3c//dtd html 4.01 transitional//' )
+                       )
+               ) {
+                       $this->indicated_compatability_mode = 'limited-quirks';
+                       return;
+               }
+
+               $this->indicated_compatability_mode = 'no-quirks';
+       }
+
+       /**
+        * Creates a WP_HTML_Doctype_Info instance by parsing a raw DOCTYPE declaration token.
+        *
+        * Use this method to parse a DOCTYPE declaration token and get access to its properties
+        * via the returned WP_HTML_Doctype_Info class instance. The provided input must parse
+        * properly as a DOCTYPE declaration, though it must not represent a valid DOCTYPE.
+        *
+        * Example:
+        *
+        *     // Normative HTML DOCTYPE declaration.
+        *     $doctype = WP_HTML_Doctype_Info::from_doctype_token( '<!DOCTYPE html>' );
+        *     'no-quirks' === $doctype->indicated_compatability_mode;
+        *
+        *     // A nonsensical DOCTYPE is still valid, and will indicate "quirks" mode.
+        *     $doctype = WP_HTML_Doctype_Info::from_doctype_token( '<!doctypeJSON SILLY "nonsense\'>' );
+        *     'quirks' === $doctype->indicated_compatability_mode;
+        *
+        *     // Textual quirks present in raw HTML are handled appropriately.
+        *     $doctype = WP_HTML_Doctype_Info::from_doctype_token( "<!DOCTYPE\nhtml\n>" );
+        *     'no-quirks' === $doctype->indicated_compatability_mode;
+        *
+        *     // Anything other than a proper DOCTYPE declaration token fails to parse.
+        *     null === WP_HTML_Doctype_Info::from_doctype_token( ' <!DOCTYPE>' );
+        *     null === WP_HTML_Doctype_Info::from_doctype_token( '<!DOCTYPE ><p>' );
+        *     null === WP_HTML_Doctype_Info::from_doctype_token( '<!TYPEDOC>' );
+        *     null === WP_HTML_Doctype_Info::from_doctype_token( 'html' );
+        *     null === WP_HTML_Doctype_Info::from_doctype_token( '<?xml version="1.0" encoding="UTF-8" ?>' );
+        *
+        * @since 6.7.0
+        *
+        * @param string $doctype_html The complete raw DOCTYPE HTML string, e.g. `<!DOCTYPE html>`.
+        *
+        * @return WP_HTML_Doctype_Info|null A WP_HTML_Doctype_Info instance will be returned if the
+        *                                   provided DOCTYPE HTML is a valid DOCTYPE. Otherwise, null.
+        */
+       public static function from_doctype_token( string $doctype_html ): ?self {
+               $doctype_name      = null;
+               $doctype_public_id = null;
+               $doctype_system_id = null;
+
+               $end = strlen( $doctype_html ) - 1;
+
+               /*
+                * This parser combines the rules for parsing DOCTYPE tokens found in the HTML
+                * specification for the DOCTYPE related tokenizer states.
+                *
+                * @see https://html.spec.whatwg.org/#doctype-state
+                */
+
+               /*
+                * - Valid DOCTYPE HTML token must be at least `<!DOCTYPE>` assuming a complete token not
+                *   ending in end-of-file.
+                * - It must start with an ASCII case-insensitive match for `<!DOCTYPE`.
+                * - The only occurrence of `>` must be the final byte in the HTML string.
+                */
+               if (
+                       $end < 9 ||
+                       0 !== substr_compare( $doctype_html, '<!DOCTYPE', 0, 9, true )
+               ) {
+                       return null;
+               }
+
+               $at = 9;
+               // Is there one and only one `>`?
+               if ( '>' !== $doctype_html[ $end ] || ( strcspn( $doctype_html, '>', $at ) + $at ) < $end ) {
+                       return null;
+               }
+
+               /*
+                * Perform newline normalization and ensure the $end value is correct after normalization.
+                *
+                * @see https://html.spec.whatwg.org/#preprocessing-the-input-stream
+                * @see https://infra.spec.whatwg.org/#normalize-newlines
+                */
+               $doctype_html = str_replace( "\r\n", "\n", $doctype_html );
+               $doctype_html = str_replace( "\r", "\n", $doctype_html );
+               $end          = strlen( $doctype_html ) - 1;
+
+               /*
+                * In this state, the doctype token has been found and its "content" optionally including the
+                * name, public identifier, and system identifier is between the current position and the end.
+                *
+                *     "<!DOCTYPE...declaration...>"
+                *               ╰─ $at           ╰─ $end
+                *
+                * It's also possible that the declaration part is empty.
+                *
+                *               ╭─ $at
+                *     "<!DOCTYPE>"
+                *               ╰─ $end
+                *
+                * Rules for parsing ">" which terminates the DOCTYPE do not need to be considered as they
+                * have been handled above in the condition that the provided DOCTYPE HTML must contain
+                * exactly one ">" character in the final position.
+                */
+
+               /*
+                *
+                * Parsing effectively begins in "Before DOCTYPE name state". Ignore whitespace and
+                * proceed to the next state.
+                *
+                * @see https://html.spec.whatwg.org/#before-doctype-name-state
+                */
+               $at += strspn( $doctype_html, " \t\n\f\r", $at );
+
+               if ( $at >= $end ) {
+                       return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
+               }
+
+               $name_length  = strcspn( $doctype_html, " \t\n\f\r", $at, $end - $at );
+               $doctype_name = str_replace( "\0", "\u{FFFD}", strtolower( substr( $doctype_html, $at, $name_length ) ) );
+
+               $at += $name_length;
+               $at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at );
+               if ( $at >= $end ) {
+                       return new self( $doctype_name, $doctype_public_id, $doctype_system_id, false );
+               }
+
+               /*
+                * "After DOCTYPE name state"
+                *
+                * Find a case-insensitive match for "PUBLIC" or "SYSTEM" at this point.
+                * Otherwise, set force-quirks and enter bogus DOCTYPE state (skip the rest of the doctype).
+                *
+                * @see https://html.spec.whatwg.org/#after-doctype-name-state
+                */
+               if ( $at + 6 >= $end ) {
+                       return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
+               }
+
+               /*
+                * > If the six characters starting from the current input character are an ASCII
+                * > case-insensitive match for the word "PUBLIC", then consume those characters
+                * > and switch to the after DOCTYPE public keyword state.
+                */
+               if ( 0 === substr_compare( $doctype_html, 'PUBLIC', $at, 6, true ) ) {
+                       $at += 6;
+                       $at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at );
+                       if ( $at >= $end ) {
+                               return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
+                       }
+                       goto parse_doctype_public_identifier;
+               }
+
+               /*
+                * > Otherwise, if the six characters starting from the current input character are an ASCII
+                * > case-insensitive match for the word "SYSTEM", then consume those characters and switch
+                * > to the after DOCTYPE system keyword state.
+                */
+               if ( 0 === substr_compare( $doctype_html, 'SYSTEM', $at, 6, true ) ) {
+                       $at += 6;
+                       $at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at );
+                       if ( $at >= $end ) {
+                               return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
+                       }
+                       goto parse_doctype_system_identifier;
+               }
+
+               /*
+                * > Otherwise, this is an invalid-character-sequence-after-doctype-name parse error.
+                * > Set the current DOCTYPE token's force-quirks flag to on. Reconsume in the bogus
+                * > DOCTYPE state.
+                */
+               return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
+
+               parse_doctype_public_identifier:
+               /*
+                * The parser should enter "DOCTYPE public identifier (double-quoted) state" or
+                * "DOCTYPE public identifier (single-quoted) state" by finding one of the valid quotes.
+                * Anything else forces quirks mode and ignores the rest of the contents.
+                *
+                * @see https://html.spec.whatwg.org/#doctype-public-identifier-(double-quoted)-state
+                * @see https://html.spec.whatwg.org/#doctype-public-identifier-(single-quoted)-state
+                */
+               $closer_quote = $doctype_html[ $at ];
+
+               /*
+                * > This is a missing-quote-before-doctype-public-identifier parse error. Set the
+                * > current DOCTYPE token's force-quirks flag to on. Reconsume in the bogus DOCTYPE state.
+                */
+               if ( '"' !== $closer_quote && "'" !== $closer_quote ) {
+                       return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
+               }
+
+               ++$at;
+
+               $identifier_length = strcspn( $doctype_html, $closer_quote, $at, $end - $at );
+               $doctype_public_id = str_replace( "\0", "\u{FFFD}", substr( $doctype_html, $at, $identifier_length ) );
+
+               $at += $identifier_length;
+               if ( $at >= $end || $closer_quote !== $doctype_html[ $at ] ) {
+                       return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
+               }
+
+               ++$at;
+
+               /*
+                * "Between DOCTYPE public and system identifiers state"
+                *
+                * Advance through whitespace between public and system identifiers.
+                *
+                * @see https://html.spec.whatwg.org/#between-doctype-public-and-system-identifiers-state
+                */
+               $at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at );
+               if ( $at >= $end ) {
+                       return new self( $doctype_name, $doctype_public_id, $doctype_system_id, false );
+               }
+
+               parse_doctype_system_identifier:
+               /*
+                * The parser should enter "DOCTYPE system identifier (double-quoted) state" or
+                * "DOCTYPE system identifier (single-quoted) state" by finding one of the valid quotes.
+                * Anything else forces quirks mode and ignores the rest of the contents.
+                *
+                * @see https://html.spec.whatwg.org/#doctype-system-identifier-(double-quoted)-state
+                * @see https://html.spec.whatwg.org/#doctype-system-identifier-(single-quoted)-state
+                */
+               $closer_quote = $doctype_html[ $at ];
+
+               /*
+                * > This is a missing-quote-before-doctype-system-identifier parse error. Set the
+                * > current DOCTYPE token's force-quirks flag to on. Reconsume in the bogus DOCTYPE state.
+                */
+               if ( '"' !== $closer_quote && "'" !== $closer_quote ) {
+                       return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
+               }
+
+               ++$at;
+
+               $identifier_length = strcspn( $doctype_html, $closer_quote, $at, $end - $at );
+               $doctype_system_id = str_replace( "\0", "\u{FFFD}", substr( $doctype_html, $at, $identifier_length ) );
+
+               $at += $identifier_length;
+               if ( $at >= $end || $closer_quote !== $doctype_html[ $at ] ) {
+                       return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
+               }
+
+               return new self( $doctype_name, $doctype_public_id, $doctype_system_id, false );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/html-api/class-wp-html-doctype-info.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><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-22 23:45:49 UTC (rev 58924)
+++ trunk/src/wp-includes/html-api/class-wp-html-processor.php  2024-08-23 14:53:59 UTC (rev 58925)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1076,13 +1076,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                         * > A DOCTYPE token
</span><span class="cx" style="display: block; padding: 0 10px">                         */
</span><span class="cx" style="display: block; padding: 0 10px">                        case 'html':
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $contents = $this->get_modifiable_text();
-                               if ( ' html' !== $contents ) {
-                                       /*
-                                        * @todo When the HTML Tag Processor fully parses the DOCTYPE declaration,
-                                        *       this code should examine the contents to set the compatability mode.
-                                        */
-                                       $this->bail( 'Cannot process any DOCTYPE other than a normative HTML5 doctype.' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $doctype = $this->get_doctype_info();
+                               if ( null !== $doctype && 'quirks' === $doctype->indicated_compatability_mode ) {
+                                       $this->state->document_mode = WP_HTML_Processor_State::QUIRKS_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">@@ -1089,6 +1085,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                 * > Then, switch the insertion mode to "before html".
</span><span class="cx" style="display: block; padding: 0 10px">                                 */
</span><span class="cx" style="display: block; padding: 0 10px">                                $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                $this->insert_html_element( $this->state->current_token );
</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="lines" style="display: block; padding: 0 10px; color: #888">@@ -1096,6 +1093,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * > Anything else
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                initial_anything_else:
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->state->document_mode  = WP_HTML_Processor_State::QUIRKS_MODE;
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML;
</span><span class="cx" style="display: block; padding: 0 10px">                return $this->step( self::REPROCESS_CURRENT_NODE );
</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-22 23:45:49 UTC (rev 58924)
+++ trunk/src/wp-includes/html-api/class-wp-html-tag-processor.php      2024-08-23 14:53:59 UTC (rev 58925)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4027,6 +4027,27 @@
</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">+         * Gets DOCTYPE declaration info from a DOCTYPE token.
+        *
+        * DOCTYPE tokens may appear in many places in an HTML document. In most places, they are
+        * simply ignored. The main parsing functions find the basic shape of DOCTYPE tokens but
+        * do not perform detailed parsing.
+        *
+        * This method can be called to perform a full parse of the DOCTYPE token and retrieve
+        * its information.
+        *
+        * @return WP_HTML_Doctype_Info|null The DOCTYPE declaration information or `null` if not
+        *                                   currently at a DOCTYPE node.
+        */
+       public function get_doctype_info(): ?WP_HTML_Doctype_Info {
+               if ( self::STATE_DOCTYPE !== $this->parser_state ) {
+                       return null;
+               }
+
+               return WP_HTML_Doctype_Info::from_doctype_token( substr( $this->html, $this->token_starts_at, $this->token_length ) );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Parser Ready State.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * Indicates that the parser is ready to run and waiting for a state transition.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4117,7 +4138,7 @@
</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">         * Indicates that the parser has found a DOCTYPE node and it's
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * possible to read and modify its modifiable text.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * possible to read its DOCTYPE information via `get_doctype_info()`.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 6.5.0
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span></span></pre></div>
<a id="trunksrcwpsettingsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-settings.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-settings.php 2024-08-22 23:45:49 UTC (rev 58924)
+++ trunk/src/wp-settings.php   2024-08-23 14:53:59 UTC (rev 58925)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -252,6 +252,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/html-api/html5-named-character-references.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/html-api/class-wp-html-attribute-token.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/html-api/class-wp-html-span.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require ABSPATH . WPINC . '/html-api/class-wp-html-doctype-info.php';
</ins><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/html-api/class-wp-html-text-replacement.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/html-api/class-wp-html-decoder.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/html-api/class-wp-html-tag-processor.php';
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlDoctypeInfophp"></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/wpHtmlDoctypeInfo.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlDoctypeInfo.php                          (rev 0)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlDoctypeInfo.php    2024-08-23 14:53:59 UTC (rev 58925)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,118 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_HTML_Doctype_Info functionality.
+ *
+ * @package WordPress
+ * @subpackage HTML-API
+ */
+
+/**
+ * @group html-api
+ *
+ * @coversDefaultClass WP_HTML_Doctype_Info
+ */
+class Tests_HtmlApi_WpHtmlDoctypeInfo extends WP_UnitTestCase {
+       /**
+        * Test DOCTYPE handling.
+        *
+        * @ticket 61576
+        *
+        * @dataProvider data_parseable_raw_doctypes
+        */
+       public function test_doctype_doc_info(
+               string $html,
+               string $expected_compat_mode,
+               ?string $expected_name = null,
+               ?string $expected_public_id = null,
+               ?string $expected_system_id = null
+       ) {
+               $doctype = WP_HTML_Doctype_Info::from_doctype_token( $html );
+               $this->assertNotNull(
+                       $doctype,
+                       "Should have parsed the following doctype declaration: {$html}"
+               );
+
+               $this->assertSame(
+                       $expected_compat_mode,
+                       $doctype->indicated_compatability_mode,
+                       'Failed to infer the expected document compatability mode.'
+               );
+
+               $this->assertSame(
+                       $expected_name,
+                       $doctype->name,
+                       'Failed to parse the expected DOCTYPE name.'
+               );
+
+               $this->assertSame(
+                       $expected_public_id,
+                       $doctype->public_identifier,
+                       'Failed to parse the expected DOCTYPE public identifier.'
+               );
+
+               $this->assertSame(
+                       $expected_system_id,
+                       $doctype->system_identifier,
+                       'Failed to parse the expected DOCTYPE system identifier.'
+               );
+       }
+
+       /**
+        * Data provider.
+        *
+        * @return array[]
+        */
+       public static function data_parseable_raw_doctypes(): array {
+               return array(
+                       'Missing doctype name'                      => array( '<!DOCTYPE>', 'quirks' ),
+                       'HTML5 doctype'                             => array( '<!DOCTYPE html>', 'no-quirks', 'html' ),
+                       'HTML5 doctype no whitespace before name'   => array( '<!DOCTYPEhtml>', 'no-quirks', 'html' ),
+                       'XHTML doctype'                             => array( '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">', 'no-quirks', 'html', '-//W3C//DTD HTML 4.01//EN', 'http://www.w3.org/TR/html4/strict.dtd' ),
+                       'SVG doctype'                               => array( '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">', 'quirks', 'svg', '-//W3C//DTD SVG 1.1//EN', 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' ),
+                       'MathML doctype'                            => array( '<!DOCTYPE math PUBLIC "-//W3C//DTD MathML 2.0//EN" "http://www.w3.org/Math/DTD/mathml2/mathml2.dtd">', 'quirks', 'math', '-//W3C//DTD MathML 2.0//EN', 'http://www.w3.org/Math/DTD/mathml2/mathml2.dtd' ),
+                       'Doctype with null byte replacement'        => array( "<!DOCTYPE null-\0 PUBLIC '\0' '\0\0'>", 'quirks', "null-\u{FFFD}", "\u{FFFD}", "\u{FFFD}\u{FFFD}" ),
+                       'Uppercase doctype'                         => array( '<!DOCTYPE UPPERCASE>', 'quirks', 'uppercase' ),
+                       'Lowercase doctype'                         => array( '<!doctype lowercase>', 'quirks', 'lowercase' ),
+                       'Doctype with whitespace'                   => array( "<!DOCTYPE\n\thtml\f\rPUBLIC\r\n''\t''>", 'no-quirks', 'html', '', '' ),
+                       'Doctype trailing characters'               => array( "<!DOCTYPE html PUBLIC '' '' Anything (except closing angle bracket) is just fine here !!!>", 'no-quirks', 'html', '', '' ),
+                       'An ugly no-quirks doctype'                 => array( "<!dOcTyPehtml\tPublIC\"pub-id\"'sysid'>", 'no-quirks', 'html', 'pub-id', 'sysid' ),
+                       'Missing public ID'                         => array( '<!DOCTYPE html PUBLIC>', 'quirks', 'html' ),
+                       'Missing system ID'                         => array( '<!DOCTYPE html SYSTEM>', 'quirks', 'html' ),
+                       'Missing close quote public ID'             => array( "<!DOCTYPE html PUBLIC 'xyz>", 'quirks', 'html', 'xyz' ),
+                       'Missing close quote system ID'             => array( "<!DOCTYPE html SYSTEM 'xyz>", 'quirks', 'html', null, 'xyz' ),
+                       'Missing close quote system ID with public' => array( "<!DOCTYPE html PUBLIC 'abc' 'xyz>", 'quirks', 'html', 'abc', 'xyz' ),
+                       'Bogus characters instead of system/public' => array( '<!DOCTYPE html FOOBAR>', 'quirks', 'html' ),
+                       'Bogus characters instead of PUBLIC quote'  => array( "<!DOCTYPE html PUBLIC x ''''>", 'quirks', 'html' ),
+                       'Bogus characters instead of SYSTEM quote ' => array( "<!DOCTYPE html SYSTEM x ''>", 'quirks', 'html' ),
+                       'Emoji'                                     => array( '<!DOCTYPE 🏴󠁧󠁢󠁥󠁮󠁧󠁿 PUBLIC "🔥" "😈">', 'quirks', "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}", '🔥', '😈' ),
+                       'Bogus characters instead of SYSTEM quote after public' => array( "<!DOCTYPE html PUBLIC ''x''>", 'quirks', 'html', '' ),
+                       'Special quirks mode if system unset'       => array( '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//">', 'quirks', 'html', '-//W3C//DTD HTML 4.01 Frameset//' ),
+                       'Special limited-quirks mode if system set' => array( '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//" "">', 'limited-quirks', 'html', '-//W3C//DTD HTML 4.01 Frameset//', '' ),
+               );
+       }
+
+       /**
+        * @dataProvider invalid_inputs
+        *
+        * @ticket 61576
+        */
+       public function test_invalid_inputs_return_null( string $html ) {
+               $this->assertNull( WP_HTML_Doctype_Info::from_doctype_token( $html ) );
+       }
+
+       /**
+        * Data provider.
+        *
+        * @return array[]
+        */
+       public static function invalid_inputs(): array {
+               return array(
+                       'Empty string'                  => array( '' ),
+                       'Other HTML'                    => array( '<div>' ),
+                       'DOCTYPE after HTML'            => array( 'x<!DOCTYPE>' ),
+                       'DOCTYPE before HTML'           => array( '<!DOCTYPE>x' ),
+                       'Incomplete DOCTYPE'            => array( '<!DOCTYPE' ),
+                       'Pseudo DOCTYPE containing ">"' => array( '<!DOCTYPE html PUBLIC ">">' ),
+               );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/html-api/wpHtmlDoctypeInfo.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><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-22 23:45:49 UTC (rev 58924)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php      2024-08-23 14:53:59 UTC (rev 58925)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -27,6 +27,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        const SKIP_TESTS = array(
</span><span class="cx" style="display: block; padding: 0 10px">                'comments01/line0155'    => 'Unimplemented: Need to access raw comment text on non-normative comments.',
</span><span class="cx" style="display: block; padding: 0 10px">                'comments01/line0169'    => 'Unimplemented: Need to access raw comment text on non-normative comments.',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                'doctype01/line0380'     => 'Bug: Mixed whitespace, non-whitespace text in head not split correctly',
</ins><span class="cx" style="display: block; padding: 0 10px">                 'html5test-com/line0129' => 'Unimplemented: Need to access raw comment text on non-normative comments.',
</span><span class="cx" style="display: block; padding: 0 10px">                'noscript01/line0014'    => 'Unimplemented: This parser does not add missing attributes to existing HTML or BODY tags.',
</span><span class="cx" style="display: block; padding: 0 10px">                'tests1/line0692'        => 'Bug: Mixed whitespace, non-whitespace text in head not split correctly',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -115,7 +116,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                $test_context_element = $test[1];
</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::should_skip_test( $test_context_element, $test_name, $test[3] ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( self::should_skip_test( $test_context_element, $test_name ) ) {
</ins><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><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -133,7 +134,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return bool True if the test case should be skipped. False otherwise.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        private static function should_skip_test( ?string $test_context_element, string $test_name, string $expected_tree ): bool {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ private static function should_skip_test( ?string $test_context_element, string $test_name ): bool {
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( null !== $test_context_element && 'body' !== $test_context_element ) {
</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="lines" style="display: block; padding: 0 10px; color: #888">@@ -189,6 +190,15 @@
</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">                        switch ( $token_type ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                case '#doctype':
+                                       $doctype = $processor->get_doctype_info();
+                                       $output .= "<!DOCTYPE {$doctype->name}";
+                                       if ( null !== $doctype->public_identifier || null !== $doctype->system_identifier ) {
+                                               $output .= " \"{$doctype->public_identifier}\" \"{$doctype->system_identifier}\"";
+                                       }
+                                       $output .= ">\n";
+                                       break;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 case '#tag':
</span><span class="cx" style="display: block; padding: 0 10px">                                        $namespace = $processor->get_namespace();
</span><span class="cx" style="display: block; padding: 0 10px">                                        $tag_name  = 'html' === $namespace
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -450,15 +460,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                 */
</span><span class="cx" style="display: block; padding: 0 10px">                                case 'document':
</span><span class="cx" style="display: block; padding: 0 10px">                                        if ( '|' === $line[0] ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                /*
-                                                * The next_token() method these tests rely on do not stop
-                                                * at doctype nodes. Strip doctypes from output.
-                                                * @todo Restore this line if and when the processor
-                                                * exposes doctypes.
-                                                */
-                                               if ( '| <!DOCTYPE ' !== substr( $line, 0, 12 ) ) {
-                                                       $test_dom .= substr( $line, 2 );
-                                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         $test_dom .= substr( $line, 2 );
</ins><span class="cx" style="display: block; padding: 0 10px">                                         } else {
</span><span class="cx" style="display: block; padding: 0 10px">                                                // This is a text node that includes unescaped newlines.
</span><span class="cx" style="display: block; padding: 0 10px">                                                // Everything else should be singles lines starting with "| ".
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlTagProcessorphp"></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.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php 2024-08-22 23:45:49 UTC (rev 58924)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php   2024-08-23 14:53:59 UTC (rev 58925)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2939,4 +2939,20 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertFalse( $processor->next_tag() );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertTrue( $processor->paused_at_incomplete_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">+
+       /**
+        * Test basic DOCTYPE handling.
+        *
+        * @ticket 61576
+        */
+       public function test_doctype_doc_name() {
+               $processor = new WP_HTML_Tag_Processor( '<!DOCTYPE html>' );
+               $this->assertTrue( $processor->next_token() );
+               $doctype = $processor->get_doctype_info();
+               $this->assertNotNull( $doctype );
+               $this->assertSame( 'html', $doctype->name );
+               $this->assertSame( 'no-quirks', $doctype->indicated_compatability_mode );
+               $this->assertNull( $doctype->public_identifier );
+               $this->assertNull( $doctype->system_identifier );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre>
</div>
</div>

</body>
</html>