[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/html-api/ -> class-wp-html-doctype-info.php (source)

   1  <?php
   2  /**
   3   * HTML API: WP_HTML_Doctype_Info class
   4   *
   5   * @package WordPress
   6   * @subpackage HTML-API
   7   * @since 6.7.0
   8   */
   9  
  10  /**
  11   * Core class used by the HTML API to represent a DOCTYPE declaration.
  12   *
  13   * This class parses DOCTYPE tokens for the full parser in the HTML Processor.
  14   * Most code interacting with HTML won't need to parse DOCTYPE declarations;
  15   * the HTML Processor is one exception. Consult the HTML Processor for proper
  16   * parsing of an HTML document.
  17   *
  18   * A DOCTYPE declaration may indicate its document compatibility mode, which impacts
  19   * the structure of the following HTML as well as the behavior of CSS class selectors.
  20   * There are three possible modes:
  21   *
  22   *  - "no-quirks" and "limited-quirks" modes (also called "standards mode").
  23   *  - "quirks" mode.
  24   *
  25   * These modes mostly determine whether CSS class name selectors match values in the
  26   * HTML `class` attribute in an ASCII-case-insensitive way (quirks mode), or whether
  27   * they match only when byte-for-byte identical (no-quirks mode).
  28   *
  29   * All HTML documents should start with the standard HTML5 DOCTYPE: `<!DOCTYPE html>`.
  30   *
  31   * > DOCTYPEs are required for legacy reasons. When omitted, browsers tend to use a different
  32   * > rendering mode that is incompatible with some specifications. Including the DOCTYPE in a
  33   * > document ensures that the browser makes a best-effort attempt at following the
  34   * > relevant specifications.
  35   *
  36   * @see https://html.spec.whatwg.org/#the-doctype
  37   *
  38   * DOCTYPE declarations comprise four properties: a name, public identifier, system identifier,
  39   * and an indication of which document compatibility mode they would imply if an HTML parser
  40   * hadn't already determined it from other information.
  41   *
  42   * @see https://html.spec.whatwg.org/#the-initial-insertion-mode
  43   *
  44   * Historically, the DOCTYPE declaration was used in SGML documents to instruct a parser how
  45   * to interpret the various tags and entities within a document. Its role in HTML diverged
  46   * from how it was used in SGML and no meaning should be back-read into HTML based on how it
  47   * is used in SGML, XML, or XHTML documents.
  48   *
  49   * @see https://www.iso.org/standard/16387.html
  50   *
  51   * @since 6.7.0
  52   * @since 7.1.0 Spec update: missing and empty SYSTEM identifiers are handled
  53   *              the same for determining the document mode.
  54   *
  55   * @access private
  56   *
  57   * @see WP_HTML_Processor
  58   */
  59  class WP_HTML_Doctype_Info {
  60      /**
  61       * Name of the DOCTYPE: should be "html" for HTML documents.
  62       *
  63       * This value should be considered "read only" and not modified.
  64       *
  65       * Historically the DOCTYPE name indicates name of the document's root element.
  66       *
  67       *     <!DOCTYPE html>
  68       *               ╰──┴── name is "html".
  69       *
  70       * @see https://html.spec.whatwg.org/#tokenization
  71       *
  72       * @since 6.7.0
  73       *
  74       * @var string|null
  75       */
  76      public $name = null;
  77  
  78      /**
  79       * Public identifier of the DOCTYPE.
  80       *
  81       * This value should be considered "read only" and not modified.
  82       *
  83       * The public identifier is optional and should not appear in HTML documents.
  84       * A `null` value indicates that no public identifier was present in the DOCTYPE.
  85       *
  86       * Historically the presence of the public identifier indicated that a document
  87       * was meant to be shared between computer systems and the value indicated to a
  88       * knowledgeable parser how to find the relevant document type definition (DTD).
  89       *
  90       *     <!DOCTYPE html PUBLIC "public id goes here in quotes">
  91       *               │  │         ╰─── public identifier ─────╯
  92       *               ╰──┴── name is "html".
  93       *
  94       * @see https://html.spec.whatwg.org/#tokenization
  95       *
  96       * @since 6.7.0
  97       *
  98       * @var string|null
  99       */
 100      public $public_identifier = null;
 101  
 102      /**
 103       * System identifier of the DOCTYPE.
 104       *
 105       * This value should be considered "read only" and not modified.
 106       *
 107       * The system identifier is optional and should not appear in HTML documents.
 108       * A `null` value indicates that no system identifier was present in the DOCTYPE.
 109       *
 110       * Historically the system identifier specified where a relevant document type
 111       * declaration for the given document is stored and may be retrieved.
 112       *
 113       *     <!DOCTYPE html SYSTEM "system id goes here in quotes">
 114       *               │  │         ╰──── system identifier ────╯
 115       *               ╰──┴── name is "html".
 116       *
 117       * If a public identifier were provided it would indicate to a knowledgeable
 118       * parser how to interpret the system identifier.
 119       *
 120       *     <!DOCTYPE html PUBLIC "public id goes here in quotes" "system id goes here in quotes">
 121       *               │  │         ╰─── public identifier ─────╯   ╰──── system identifier ────╯
 122       *               ╰──┴── name is "html".
 123       *
 124       * @see https://html.spec.whatwg.org/#tokenization
 125       *
 126       * @since 6.7.0
 127       *
 128       * @var string|null
 129       */
 130      public $system_identifier = null;
 131  
 132      /**
 133       * Which document compatibility mode this DOCTYPE declaration indicates.
 134       *
 135       * This value should be considered "read only" and not modified.
 136       *
 137       * When an HTML parser has not already set the document compatibility mode,
 138       * (e.g. "quirks" or "no-quirks" mode), it will be inferred from the properties
 139       * of the appropriate DOCTYPE declaration, if one exists. The DOCTYPE can
 140       * indicate one of three possible document compatibility modes:
 141       * "no-quirks", "limited-quirks", or "quirks".
 142       *
 143       * Browsers expose the resulting document mode via `document.compatMode`:
 144       * - "BackCompat" indicates "quirks" mode.
 145       * - "CSS1Compat" indicates "no-quirks" or "limited-quirks" (these modes are not
 146       *   distinguished by `document.compatMode`).
 147       *
 148       * An appropriate DOCTYPE is one encountered in the "initial" insertion mode,
 149       * before the HTML element has been opened and before finding any other
 150       * DOCTYPE declaration tokens.
 151       *
 152       * @see https://html.spec.whatwg.org/#the-initial-insertion-mode
 153       *
 154       * @since 6.7.0
 155       *
 156       * @var string One of "no-quirks", "limited-quirks", or "quirks".
 157       */
 158      public $indicated_compatibility_mode;
 159  
 160      /**
 161       * Constructor.
 162       *
 163       * This class should not be instantiated directly.
 164       * Use the static {@see self::from_doctype_token} method instead.
 165       *
 166       * The arguments to this constructor correspond to the "DOCTYPE token"
 167       * as defined in the HTML specification.
 168       *
 169       * > DOCTYPE tokens have a name, a public identifier, a system identifier,
 170       * > and a force-quirks flag. When a DOCTYPE token is created, its name, public identifier,
 171       * > and system identifier must be marked as missing (which is a distinct state from the
 172       * > empty string), and the force-quirks flag must be set to off (its other state is on).
 173       *
 174       * @see https://html.spec.whatwg.org/multipage/parsing.html#tokenization
 175       *
 176       * @since 6.7.0
 177       *
 178       * @param string|null $name              Name of the DOCTYPE.
 179       * @param string|null $public_identifier Public identifier of the DOCTYPE.
 180       * @param string|null $system_identifier System identifier of the DOCTYPE.
 181       * @param bool        $force_quirks_flag Whether the force-quirks flag is set for the token.
 182       */
 183  	private function __construct(
 184          ?string $name,
 185          ?string $public_identifier,
 186          ?string $system_identifier,
 187          bool $force_quirks_flag
 188      ) {
 189          $this->name              = $name;
 190          $this->public_identifier = $public_identifier;
 191          $this->system_identifier = $system_identifier;
 192  
 193          /*
 194           * > If the DOCTYPE token matches one of the conditions in the following list,
 195           * > then set the Document to quirks mode:
 196           */
 197  
 198          /*
 199           * > The force-quirks flag is set to on.
 200           */
 201          if ( $force_quirks_flag ) {
 202              $this->indicated_compatibility_mode = 'quirks';
 203              return;
 204          }
 205  
 206          /*
 207           * Normative documents will contain the literal `<!DOCTYPE html>` with no
 208           * public or system identifiers; short-circuit to avoid extra parsing.
 209           */
 210          if ( 'html' === $name && null === $public_identifier && null === $system_identifier ) {
 211              $this->indicated_compatibility_mode = 'no-quirks';
 212              return;
 213          }
 214  
 215          /*
 216           * > The name is not "html".
 217           *
 218           * The tokenizer must report the name in lower case even if provided in
 219           * the document in upper case; thus no conversion is required here.
 220           */
 221          if ( 'html' !== $name ) {
 222              $this->indicated_compatibility_mode = 'quirks';
 223              return;
 224          }
 225  
 226          /*
 227           * Set up some variables to handle the rest of the conditions.
 228           *
 229           * > set...the public identifier...to...the empty string if the public identifier was missing.
 230           * > set...the system identifier...to...the empty string if the system identifier was missing.
 231           * >
 232           * > The system identifier and public identifier strings must be compared...
 233           * > in an ASCII case-insensitive manner.
 234           */
 235          $public_identifier = null === $public_identifier ? '' : strtolower( $public_identifier );
 236          $system_identifier = null === $system_identifier ? '' : strtolower( $system_identifier );
 237  
 238          /*
 239           * > The public identifier is set to…
 240           */
 241          if (
 242              '-//w3o//dtd w3 html strict 3.0//en//' === $public_identifier ||
 243              '-/w3c/dtd html 4.0 transitional/en' === $public_identifier ||
 244              'html' === $public_identifier
 245          ) {
 246              $this->indicated_compatibility_mode = 'quirks';
 247              return;
 248          }
 249  
 250          /*
 251           * > The system identifier is set to…
 252           */
 253          if ( 'http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd' === $system_identifier ) {
 254              $this->indicated_compatibility_mode = 'quirks';
 255              return;
 256          }
 257  
 258          /*
 259           * All of the following conditions depend on matching the public identifier.
 260           * If the public identifier is empty, none of the following conditions will match.
 261           */
 262          if ( '' === $public_identifier ) {
 263              $this->indicated_compatibility_mode = 'no-quirks';
 264              return;
 265          }
 266  
 267          /*
 268           * > The public identifier starts with…
 269           *
 270           * @todo Optimize this matching. It shouldn't be a large overall performance issue,
 271           *       however, as only a single DOCTYPE declaration token should ever be parsed,
 272           *       and normative documents will have exited before reaching this condition.
 273           */
 274          if (
 275              str_starts_with( $public_identifier, '+//silmaril//dtd html pro v0r11 19970101//' ) ||
 276              str_starts_with( $public_identifier, '-//as//dtd html 3.0 aswedit + extensions//' ) ||
 277              str_starts_with( $public_identifier, '-//advasoft ltd//dtd html 3.0 aswedit + extensions//' ) ||
 278              str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 level 1//' ) ||
 279              str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 level 2//' ) ||
 280              str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 strict level 1//' ) ||
 281              str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 strict level 2//' ) ||
 282              str_starts_with( $public_identifier, '-//ietf//dtd html 2.0 strict//' ) ||
 283              str_starts_with( $public_identifier, '-//ietf//dtd html 2.0//' ) ||
 284              str_starts_with( $public_identifier, '-//ietf//dtd html 2.1e//' ) ||
 285              str_starts_with( $public_identifier, '-//ietf//dtd html 3.0//' ) ||
 286              str_starts_with( $public_identifier, '-//ietf//dtd html 3.2 final//' ) ||
 287              str_starts_with( $public_identifier, '-//ietf//dtd html 3.2//' ) ||
 288              str_starts_with( $public_identifier, '-//ietf//dtd html 3//' ) ||
 289              str_starts_with( $public_identifier, '-//ietf//dtd html level 0//' ) ||
 290              str_starts_with( $public_identifier, '-//ietf//dtd html level 1//' ) ||
 291              str_starts_with( $public_identifier, '-//ietf//dtd html level 2//' ) ||
 292              str_starts_with( $public_identifier, '-//ietf//dtd html level 3//' ) ||
 293              str_starts_with( $public_identifier, '-//ietf//dtd html strict level 0//' ) ||
 294              str_starts_with( $public_identifier, '-//ietf//dtd html strict level 1//' ) ||
 295              str_starts_with( $public_identifier, '-//ietf//dtd html strict level 2//' ) ||
 296              str_starts_with( $public_identifier, '-//ietf//dtd html strict level 3//' ) ||
 297              str_starts_with( $public_identifier, '-//ietf//dtd html strict//' ) ||
 298              str_starts_with( $public_identifier, '-//ietf//dtd html//' ) ||
 299              str_starts_with( $public_identifier, '-//metrius//dtd metrius presentational//' ) ||
 300              str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 2.0 html strict//' ) ||
 301              str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 2.0 html//' ) ||
 302              str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 2.0 tables//' ) ||
 303              str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 3.0 html strict//' ) ||
 304              str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 3.0 html//' ) ||
 305              str_starts_with( $public_identifier, '-//microsoft//dtd internet explorer 3.0 tables//' ) ||
 306              str_starts_with( $public_identifier, '-//netscape comm. corp.//dtd html//' ) ||
 307              str_starts_with( $public_identifier, '-//netscape comm. corp.//dtd strict html//' ) ||
 308              str_starts_with( $public_identifier, "-//o'reilly and associates//dtd html 2.0//" ) ||
 309              str_starts_with( $public_identifier, "-//o'reilly and associates//dtd html extended 1.0//" ) ||
 310              str_starts_with( $public_identifier, "-//o'reilly and associates//dtd html extended relaxed 1.0//" ) ||
 311              str_starts_with( $public_identifier, '-//sq//dtd html 2.0 hotmetal + extensions//' ) ||
 312              str_starts_with( $public_identifier, '-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//' ) ||
 313              str_starts_with( $public_identifier, '-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//' ) ||
 314              str_starts_with( $public_identifier, '-//spyglass//dtd html 2.0 extended//' ) ||
 315              str_starts_with( $public_identifier, '-//sun microsystems corp.//dtd hotjava html//' ) ||
 316              str_starts_with( $public_identifier, '-//sun microsystems corp.//dtd hotjava strict html//' ) ||
 317              str_starts_with( $public_identifier, '-//w3c//dtd html 3 1995-03-24//' ) ||
 318              str_starts_with( $public_identifier, '-//w3c//dtd html 3.2 draft//' ) ||
 319              str_starts_with( $public_identifier, '-//w3c//dtd html 3.2 final//' ) ||
 320              str_starts_with( $public_identifier, '-//w3c//dtd html 3.2//' ) ||
 321              str_starts_with( $public_identifier, '-//w3c//dtd html 3.2s draft//' ) ||
 322              str_starts_with( $public_identifier, '-//w3c//dtd html 4.0 frameset//' ) ||
 323              str_starts_with( $public_identifier, '-//w3c//dtd html 4.0 transitional//' ) ||
 324              str_starts_with( $public_identifier, '-//w3c//dtd html experimental 19960712//' ) ||
 325              str_starts_with( $public_identifier, '-//w3c//dtd html experimental 970421//' ) ||
 326              str_starts_with( $public_identifier, '-//w3c//dtd w3 html//' ) ||
 327              str_starts_with( $public_identifier, '-//w3o//dtd w3 html 3.0//' ) ||
 328              str_starts_with( $public_identifier, '-//webtechs//dtd mozilla html 2.0//' ) ||
 329              str_starts_with( $public_identifier, '-//webtechs//dtd mozilla html//' )
 330          ) {
 331              $this->indicated_compatibility_mode = 'quirks';
 332              return;
 333          }
 334  
 335          /*
 336           * > The system identifier is missing or the empty string, and the
 337           * > public identifier starts with…
 338           */
 339          if (
 340              '' === $system_identifier && (
 341                  str_starts_with( $public_identifier, '-//w3c//dtd html 4.01 frameset//' ) ||
 342                  str_starts_with( $public_identifier, '-//w3c//dtd html 4.01 transitional//' )
 343              )
 344          ) {
 345              $this->indicated_compatibility_mode = 'quirks';
 346              return;
 347          }
 348  
 349          /*
 350           * > Otherwise, if the DOCTYPE token matches one of the conditions in
 351           * > the following list, then set the Document to limited-quirks mode.
 352           */
 353  
 354          /*
 355           * > The public identifier starts with…
 356           */
 357          if (
 358              str_starts_with( $public_identifier, '-//w3c//dtd xhtml 1.0 frameset//' ) ||
 359              str_starts_with( $public_identifier, '-//w3c//dtd xhtml 1.0 transitional//' )
 360          ) {
 361              $this->indicated_compatibility_mode = 'limited-quirks';
 362              return;
 363          }
 364  
 365          /*
 366           * > The system identifier is neither missing nor the empty string, and the
 367           * > public identifier starts with…
 368           */
 369          if (
 370              '' !== $system_identifier && (
 371                  str_starts_with( $public_identifier, '-//w3c//dtd html 4.01 frameset//' ) ||
 372                  str_starts_with( $public_identifier, '-//w3c//dtd html 4.01 transitional//' )
 373              )
 374          ) {
 375              $this->indicated_compatibility_mode = 'limited-quirks';
 376              return;
 377          }
 378  
 379          $this->indicated_compatibility_mode = 'no-quirks';
 380      }
 381  
 382      /**
 383       * Creates a WP_HTML_Doctype_Info instance by parsing a raw DOCTYPE declaration token.
 384       *
 385       * Use this method to parse a DOCTYPE declaration token and get access to its properties
 386       * via the returned WP_HTML_Doctype_Info class instance. The provided input must parse
 387       * properly as a DOCTYPE declaration, though it must not represent a valid DOCTYPE.
 388       *
 389       * Example:
 390       *
 391       *     // Normative HTML DOCTYPE declaration.
 392       *     $doctype = WP_HTML_Doctype_Info::from_doctype_token( '<!DOCTYPE html>' );
 393       *     'no-quirks' === $doctype->indicated_compatibility_mode;
 394       *
 395       *     // A nonsensical DOCTYPE is still valid, and will indicate "quirks" mode.
 396       *     $doctype = WP_HTML_Doctype_Info::from_doctype_token( '<!doctypeJSON SILLY "nonsense\'>' );
 397       *     'quirks' === $doctype->indicated_compatibility_mode;
 398       *
 399       *     // Textual quirks present in raw HTML are handled appropriately.
 400       *     $doctype = WP_HTML_Doctype_Info::from_doctype_token( "<!DOCTYPE\nhtml\n>" );
 401       *     'no-quirks' === $doctype->indicated_compatibility_mode;
 402       *
 403       *     // Anything other than a proper DOCTYPE declaration token fails to parse.
 404       *     null === WP_HTML_Doctype_Info::from_doctype_token( ' <!DOCTYPE>' );
 405       *     null === WP_HTML_Doctype_Info::from_doctype_token( '<!DOCTYPE ><p>' );
 406       *     null === WP_HTML_Doctype_Info::from_doctype_token( '<!TYPEDOC>' );
 407       *     null === WP_HTML_Doctype_Info::from_doctype_token( 'html' );
 408       *     null === WP_HTML_Doctype_Info::from_doctype_token( '<?xml version="1.0" encoding="UTF-8" ?>' );
 409       *
 410       * @since 6.7.0
 411       *
 412       * @param string $doctype_html The complete raw DOCTYPE HTML string, e.g. `<!DOCTYPE html>`.
 413       * @return WP_HTML_Doctype_Info|null A WP_HTML_Doctype_Info instance will be returned if the
 414       *                                   provided DOCTYPE HTML is a valid DOCTYPE. Otherwise, null.
 415       */
 416  	public static function from_doctype_token( string $doctype_html ): ?self {
 417          $doctype_name      = null;
 418          $doctype_public_id = null;
 419          $doctype_system_id = null;
 420  
 421          $end = strlen( $doctype_html ) - 1;
 422  
 423          /*
 424           * This parser combines the rules for parsing DOCTYPE tokens found in the HTML
 425           * specification for the DOCTYPE related tokenizer states.
 426           *
 427           * @see https://html.spec.whatwg.org/#doctype-state
 428           */
 429  
 430          /*
 431           * - Valid DOCTYPE HTML token must be at least `<!DOCTYPE>` assuming a complete token not
 432           *   ending in end-of-file.
 433           * - It must start with an ASCII case-insensitive match for `<!DOCTYPE`.
 434           * - The only occurrence of `>` must be the final byte in the HTML string.
 435           */
 436          if (
 437              $end < 9 ||
 438              0 !== substr_compare( $doctype_html, '<!DOCTYPE', 0, 9, true )
 439          ) {
 440              return null;
 441          }
 442  
 443          $at = 9;
 444          // Is there one and only one `>`?
 445          if ( '>' !== $doctype_html[ $end ] || ( strcspn( $doctype_html, '>', $at ) + $at ) < $end ) {
 446              return null;
 447          }
 448  
 449          /*
 450           * Perform newline normalization and ensure the $end value is correct after normalization.
 451           *
 452           * @see https://html.spec.whatwg.org/#preprocessing-the-input-stream
 453           * @see https://infra.spec.whatwg.org/#normalize-newlines
 454           */
 455          $doctype_html = str_replace( "\r\n", "\n", $doctype_html );
 456          $doctype_html = str_replace( "\r", "\n", $doctype_html );
 457          $end          = strlen( $doctype_html ) - 1;
 458  
 459          /*
 460           * In this state, the doctype token has been found and its "content" optionally including the
 461           * name, public identifier, and system identifier is between the current position and the end.
 462           *
 463           *     "<!DOCTYPE...declaration...>"
 464           *               ╰─ $at           ╰─ $end
 465           *
 466           * It's also possible that the declaration part is empty.
 467           *
 468           *               ╭─ $at
 469           *     "<!DOCTYPE>"
 470           *               ╰─ $end
 471           *
 472           * Rules for parsing ">" which terminates the DOCTYPE do not need to be considered as they
 473           * have been handled above in the condition that the provided DOCTYPE HTML must contain
 474           * exactly one ">" character in the final position.
 475           */
 476  
 477          /*
 478           *
 479           * Parsing effectively begins in "Before DOCTYPE name state". Ignore whitespace and
 480           * proceed to the next state.
 481           *
 482           * @see https://html.spec.whatwg.org/#before-doctype-name-state
 483           */
 484          $at += strspn( $doctype_html, " \t\n\f\r", $at );
 485  
 486          if ( $at >= $end ) {
 487              return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
 488          }
 489  
 490          $name_length  = strcspn( $doctype_html, " \t\n\f\r", $at, $end - $at );
 491          $doctype_name = str_replace( "\0", "\u{FFFD}", strtolower( substr( $doctype_html, $at, $name_length ) ) );
 492  
 493          $at += $name_length;
 494          $at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at );
 495          if ( $at >= $end ) {
 496              return new self( $doctype_name, $doctype_public_id, $doctype_system_id, false );
 497          }
 498  
 499          /*
 500           * "After DOCTYPE name state"
 501           *
 502           * Find a case-insensitive match for "PUBLIC" or "SYSTEM" at this point.
 503           * Otherwise, set force-quirks and enter bogus DOCTYPE state (skip the rest of the doctype).
 504           *
 505           * @see https://html.spec.whatwg.org/#after-doctype-name-state
 506           */
 507          if ( $at + 6 >= $end ) {
 508              return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
 509          }
 510  
 511          /*
 512           * > If the six characters starting from the current input character are an ASCII
 513           * > case-insensitive match for the word "PUBLIC", then consume those characters
 514           * > and switch to the after DOCTYPE public keyword state.
 515           */
 516          if ( 0 === substr_compare( $doctype_html, 'PUBLIC', $at, 6, true ) ) {
 517              $at += 6;
 518              $at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at );
 519              if ( $at >= $end ) {
 520                  return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
 521              }
 522              goto parse_doctype_public_identifier;
 523          }
 524  
 525          /*
 526           * > Otherwise, if the six characters starting from the current input character are an ASCII
 527           * > case-insensitive match for the word "SYSTEM", then consume those characters and switch
 528           * > to the after DOCTYPE system keyword state.
 529           */
 530          if ( 0 === substr_compare( $doctype_html, 'SYSTEM', $at, 6, true ) ) {
 531              $at += 6;
 532              $at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at );
 533              if ( $at >= $end ) {
 534                  return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
 535              }
 536              goto parse_doctype_system_identifier;
 537          }
 538  
 539          /*
 540           * > Otherwise, this is an invalid-character-sequence-after-doctype-name parse error.
 541           * > Set the current DOCTYPE token's force-quirks flag to on. Reconsume in the bogus
 542           * > DOCTYPE state.
 543           */
 544          return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
 545  
 546          parse_doctype_public_identifier:
 547          /*
 548           * The parser should enter "DOCTYPE public identifier (double-quoted) state" or
 549           * "DOCTYPE public identifier (single-quoted) state" by finding one of the valid quotes.
 550           * Anything else forces quirks mode and ignores the rest of the contents.
 551           *
 552           * @see https://html.spec.whatwg.org/#doctype-public-identifier-(double-quoted)-state
 553           * @see https://html.spec.whatwg.org/#doctype-public-identifier-(single-quoted)-state
 554           */
 555          $closer_quote = $doctype_html[ $at ];
 556  
 557          /*
 558           * > This is a missing-quote-before-doctype-public-identifier parse error. Set the
 559           * > current DOCTYPE token's force-quirks flag to on. Reconsume in the bogus DOCTYPE state.
 560           */
 561          if ( '"' !== $closer_quote && "'" !== $closer_quote ) {
 562              return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
 563          }
 564  
 565          ++$at;
 566  
 567          $identifier_length = strcspn( $doctype_html, $closer_quote, $at, $end - $at );
 568          $doctype_public_id = str_replace( "\0", "\u{FFFD}", substr( $doctype_html, $at, $identifier_length ) );
 569  
 570          $at += $identifier_length;
 571          if ( $at >= $end || $closer_quote !== $doctype_html[ $at ] ) {
 572              return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
 573          }
 574  
 575          ++$at;
 576  
 577          /*
 578           * "Between DOCTYPE public and system identifiers state"
 579           *
 580           * Advance through whitespace between public and system identifiers.
 581           *
 582           * @see https://html.spec.whatwg.org/#between-doctype-public-and-system-identifiers-state
 583           */
 584          $at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at );
 585          if ( $at >= $end ) {
 586              return new self( $doctype_name, $doctype_public_id, $doctype_system_id, false );
 587          }
 588  
 589          parse_doctype_system_identifier:
 590          /*
 591           * The parser should enter "DOCTYPE system identifier (double-quoted) state" or
 592           * "DOCTYPE system identifier (single-quoted) state" by finding one of the valid quotes.
 593           * Anything else forces quirks mode and ignores the rest of the contents.
 594           *
 595           * @see https://html.spec.whatwg.org/#doctype-system-identifier-(double-quoted)-state
 596           * @see https://html.spec.whatwg.org/#doctype-system-identifier-(single-quoted)-state
 597           */
 598          $closer_quote = $doctype_html[ $at ];
 599  
 600          /*
 601           * > This is a missing-quote-before-doctype-system-identifier parse error. Set the
 602           * > current DOCTYPE token's force-quirks flag to on. Reconsume in the bogus DOCTYPE state.
 603           */
 604          if ( '"' !== $closer_quote && "'" !== $closer_quote ) {
 605              return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
 606          }
 607  
 608          ++$at;
 609  
 610          $identifier_length = strcspn( $doctype_html, $closer_quote, $at, $end - $at );
 611          $doctype_system_id = str_replace( "\0", "\u{FFFD}", substr( $doctype_html, $at, $identifier_length ) );
 612  
 613          $at += $identifier_length;
 614          if ( $at >= $end || $closer_quote !== $doctype_html[ $at ] ) {
 615              return new self( $doctype_name, $doctype_public_id, $doctype_system_id, true );
 616          }
 617  
 618          return new self( $doctype_name, $doctype_public_id, $doctype_system_id, false );
 619      }
 620  }


Generated : Thu Aug 20 08:20:25 2026 Cross-referenced by PHPXref