[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

   1  <?php
   2  /**
   3   * HTML API: WP_HTML_Processor class
   4   *
   5   * @package WordPress
   6   * @subpackage HTML-API
   7   * @since 6.4.0
   8   */
   9  
  10  /**
  11   * Core class used to safely parse and modify an HTML document.
  12   *
  13   * The HTML Processor class properly parses and modifies HTML5 documents.
  14   *
  15   * It supports a subset of the HTML5 specification, and when it encounters
  16   * unsupported markup, it aborts early to avoid unintentionally breaking
  17   * the document. The HTML Processor should never break an HTML document.
  18   *
  19   * While the `WP_HTML_Tag_Processor` is a valuable tool for modifying
  20   * attributes on individual HTML tags, the HTML Processor is more capable
  21   * and useful for the following operations:
  22   *
  23   *  - Querying based on nested HTML structure.
  24   *
  25   * Eventually the HTML Processor will also support:
  26   *  - Wrapping a tag in surrounding HTML.
  27   *  - Unwrapping a tag by removing its parent.
  28   *  - Inserting and removing nodes.
  29   *  - Reading and changing inner content.
  30   *  - Navigating up or around HTML structure.
  31   *
  32   * ## Usage
  33   *
  34   * Use of this class requires three steps:
  35   *
  36   *   1. Call a static creator method with your input HTML document.
  37   *   2. Find the location in the document you are looking for.
  38   *   3. Request changes to the document at that location.
  39   *
  40   * Example:
  41   *
  42   *     $processor = WP_HTML_Processor::create_fragment( $html );
  43   *     if ( $processor->next_tag( array( 'breadcrumbs' => array( 'DIV', 'FIGURE', 'IMG' ) ) ) ) {
  44   *         $processor->add_class( 'responsive-image' );
  45   *     }
  46   *
  47   * #### Breadcrumbs
  48   *
  49   * Breadcrumbs represent the stack of open elements from the root
  50   * of the document or fragment down to the currently-matched node,
  51   * if one is currently selected. Call WP_HTML_Processor::get_breadcrumbs()
  52   * to inspect the breadcrumbs for a matched tag.
  53   *
  54   * Breadcrumbs can specify nested HTML structure and are equivalent
  55   * to a CSS selector comprising tag names separated by the child
  56   * combinator, such as "DIV > FIGURE > IMG".
  57   *
  58   * Since all elements find themselves inside a full HTML document
  59   * when parsed, the return value from `get_breadcrumbs()` will always
  60   * contain any implicit outermost elements. For example, when parsing
  61   * with `create_fragment()` in the `BODY` context (the default), any
  62   * tag in the given HTML document will contain `array( 'HTML', 'BODY', … )`
  63   * in its breadcrumbs.
  64   *
  65   * Despite containing the implied outermost elements in their breadcrumbs,
  66   * tags may be found with the shortest-matching breadcrumb query. That is,
  67   * `array( 'IMG' )` matches all IMG elements and `array( 'P', 'IMG' )`
  68   * matches all IMG elements directly inside a P element. To ensure that no
  69   * partial matches erroneously match it's possible to specify in a query
  70   * the full breadcrumb match all the way down from the root HTML element.
  71   *
  72   * Example:
  73   *
  74   *     $html = '<figure><img><figcaption>A <em>lovely</em> day outside</figcaption></figure>';
  75   *     //               ----- Matches here.
  76   *     $processor->next_tag( array( 'breadcrumbs' => array( 'FIGURE', 'IMG' ) ) );
  77   *
  78   *     $html = '<figure><img><figcaption>A <em>lovely</em> day outside</figcaption></figure>';
  79   *     //                                  ---- Matches here.
  80   *     $processor->next_tag( array( 'breadcrumbs' => array( 'FIGURE', 'FIGCAPTION', 'EM' ) ) );
  81   *
  82   *     $html = '<div><img></div><img>';
  83   *     //                       ----- Matches here, because IMG must be a direct child of the implicit BODY.
  84   *     $processor->next_tag( array( 'breadcrumbs' => array( 'BODY', 'IMG' ) ) );
  85   *
  86   * ## HTML Support
  87   *
  88   * This class implements a small part of the HTML5 specification.
  89   * It's designed to operate within its support and abort early whenever
  90   * encountering circumstances it can't properly handle. This is
  91   * the principle way in which this class remains as simple as possible
  92   * without cutting corners and breaking compliance.
  93   *
  94   * ### Supported elements
  95   *
  96   * If any unsupported element appears in the HTML input the HTML Processor
  97   * will abort early and stop all processing. This draconian measure ensures
  98   * that the HTML Processor won't break any HTML it doesn't fully understand.
  99   *
 100   * The HTML Processor supports all elements other than a specific set:
 101   *
 102   *  - Any element inside a TABLE.
 103   *  - Any element inside foreign content, including SVG and MATH.
 104   *  - Any element outside the IN BODY insertion mode, e.g. doctype declarations, meta, links.
 105   *
 106   * ### Supported markup
 107   *
 108   * Some kinds of non-normative HTML involve reconstruction of formatting elements and
 109   * re-parenting of mis-nested elements. For example, a DIV tag found inside a TABLE
 110   * may in fact belong _before_ the table in the DOM. If the HTML Processor encounters
 111   * such a case it will stop processing.
 112   *
 113   * The following list illustrates some common examples of unexpected HTML inputs that
 114   * the HTML Processor properly parses and represents:
 115   *
 116   *  - HTML with optional tags omitted, e.g. `<p>one<p>two`.
 117   *  - HTML with unexpected tag closers, e.g. `<p>one </span> more</p>`.
 118   *  - Non-void tags with self-closing flag, e.g. `<div/>the DIV is still open.</div>`.
 119   *  - Heading elements which close open heading elements of another level, e.g. `<h1>Closed by </h2>`.
 120   *  - Elements containing text that looks like other tags but isn't, e.g. `<title>The <img> is plaintext</title>`.
 121   *  - SCRIPT and STYLE tags containing text that looks like HTML but isn't, e.g. `<script>document.write('<p>Hi</p>');</script>`.
 122   *  - SCRIPT content which has been escaped, e.g. `<script><!-- document.write('<script>console.log("hi")</script>') --></script>`.
 123   *
 124   * ### Unsupported Features
 125   *
 126   * This parser does not report parse errors.
 127   *
 128   * Normally, when additional HTML or BODY tags are encountered in a document, if there
 129   * are any additional attributes on them that aren't found on the previous elements,
 130   * the existing HTML and BODY elements adopt those missing attribute values. This
 131   * parser does not add those additional attributes.
 132   *
 133   * In certain situations, elements are moved to a different part of the document in
 134   * a process called "adoption" and "fostering." Because the nodes move to a location
 135   * in the document that the parser had already processed, this parser does not support
 136   * these situations and will bail.
 137   *
 138   * The parser does not implement the "maybe clone an option into selectedcontent" algorithm.
 139   * SELECTEDCONTENT elements may not reflect the actual selected content.
 140   *
 141   * @since 6.4.0
 142   *
 143   * @see WP_HTML_Tag_Processor
 144   * @link https://html.spec.whatwg.org/
 145   * @phpstan-consistent-constructor
 146   */
 147  class WP_HTML_Processor extends WP_HTML_Tag_Processor {
 148      /**
 149       * The maximum number of bookmarks allowed to exist at any given time.
 150       *
 151       * HTML processing requires more bookmarks than basic tag processing,
 152       * so this class constant from the Tag Processor is overwritten.
 153       *
 154       * @since 6.4.0
 155       * @since 7.0.0 Increased from 100 to 10,000
 156       *
 157       * @var int
 158       */
 159      const MAX_BOOKMARKS = 10_000;
 160  
 161      /**
 162       * Holds the working state of the parser, including the stack of
 163       * open elements and the stack of active formatting elements.
 164       *
 165       * Initialized in the constructor.
 166       *
 167       * @since 6.4.0
 168       *
 169       * @var WP_HTML_Processor_State
 170       */
 171      private $state;
 172  
 173      /**
 174       * Used to create unique bookmark names.
 175       *
 176       * This class sets a bookmark for every tag in the HTML document that it encounters.
 177       * The bookmark name is auto-generated and increments, starting with `1`. These are
 178       * internal bookmarks and are automatically released when the referring WP_HTML_Token
 179       * goes out of scope and is garbage-collected.
 180       *
 181       * @since 6.4.0
 182       *
 183       * @see WP_HTML_Processor::$release_internal_bookmark_on_destruct
 184       *
 185       * @var int
 186       */
 187      private $bookmark_counter = 0;
 188  
 189      /**
 190       * Stores an explanation for why something failed, if it did.
 191       *
 192       * @see self::get_last_error
 193       *
 194       * @since 6.4.0
 195       *
 196       * @var string|null
 197       */
 198      private $last_error = null;
 199  
 200      /**
 201       * Stores context for why the parser bailed on unsupported HTML, if it did.
 202       *
 203       * @see self::get_unsupported_exception
 204       *
 205       * @since 6.7.0
 206       *
 207       * @var WP_HTML_Unsupported_Exception|null
 208       */
 209      private $unsupported_exception = null;
 210  
 211      /**
 212       * Releases a bookmark when PHP garbage-collects its wrapping WP_HTML_Token instance.
 213       *
 214       * This function is created inside the class constructor so that it can be passed to
 215       * the stack of open elements and the stack of active formatting elements without
 216       * exposing it as a public method on the class.
 217       *
 218       * @since 6.4.0
 219       *
 220       * @var Closure|null
 221       */
 222      private $release_internal_bookmark_on_destruct = null;
 223  
 224      /**
 225       * Stores stack events which arise during parsing of the
 226       * HTML document, which will then supply the "match" events.
 227       *
 228       * @since 6.6.0
 229       *
 230       * @var WP_HTML_Stack_Event[]
 231       */
 232      private $element_queue = array();
 233  
 234      /**
 235       * Stores the current breadcrumbs.
 236       *
 237       * @since 6.7.0
 238       *
 239       * @var string[]
 240       */
 241      private $breadcrumbs = array();
 242  
 243      /**
 244       * Current stack event, if set, representing a matched token.
 245       *
 246       * Because the parser may internally point to a place further along in a document
 247       * than the nodes which have already been processed (some "virtual" nodes may have
 248       * appeared while scanning the HTML document), this will point at the "current" node
 249       * being processed. It comes from the front of the element queue.
 250       *
 251       * @since 6.6.0
 252       *
 253       * @var WP_HTML_Stack_Event|null
 254       */
 255      private $current_element = null;
 256  
 257      /**
 258       * Context node if created as a fragment parser.
 259       *
 260       * @since 6.6.0
 261       * @var WP_HTML_Token|null
 262       */
 263      private $context_node = null;
 264  
 265      /*
 266       * Public Interface Functions
 267       */
 268  
 269      /**
 270       * Creates an HTML processor in the fragment parsing mode.
 271       *
 272       * Use this for cases where you are processing chunks of HTML that
 273       * will be found within a bigger HTML document, such as rendered
 274       * block output that exists within a post, `the_content` inside a
 275       * rendered site layout.
 276       *
 277       * Fragment parsing occurs within a context, which is an HTML element
 278       * that the document will eventually be placed in. It becomes important
 279       * when special elements have different rules than others, such as inside
 280       * a TEXTAREA or a TITLE tag where things that look like tags are text,
 281       * or inside a SCRIPT tag where things that look like HTML syntax are JS.
 282       *
 283       * The context value should be a representation of the tag into which the
 284       * HTML is found. For most cases this will be the body element. The HTML
 285       * form is provided because a context element may have attributes that
 286       * impact the parse, such as with a SCRIPT tag and its `type` attribute.
 287       *
 288       * ## Current HTML Support
 289       *
 290       *  - The only supported context is `<body>`, which is the default value.
 291       *  - The only supported document encoding is `UTF-8`, which is the default value.
 292       *
 293       * @since 6.4.0
 294       * @since 6.6.0 Returns `static` instead of `self` so it can create subclass instances.
 295       *
 296       * @param string $html     Input HTML fragment to process.
 297       * @param string $context  Context element for the fragment, must be default of `<body>`.
 298       * @param string $encoding Text encoding of the document; must be default of 'UTF-8'.
 299       * @return static|null The created processor if successful, otherwise null.
 300       */
 301  	public static function create_fragment( $html, $context = '<body>', $encoding = 'UTF-8' ) {
 302          if ( '<body>' !== $context || 'UTF-8' !== $encoding ) {
 303              return null;
 304          }
 305  
 306          if ( ! is_string( $html ) ) {
 307              _doing_it_wrong(
 308                  __METHOD__,
 309                  __( 'The HTML parameter must be a string.' ),
 310                  '6.9.0'
 311              );
 312              return null;
 313          }
 314  
 315          $context_processor = static::create_full_parser( "<!DOCTYPE html>{$context}", $encoding );
 316          if ( null === $context_processor ) {
 317              return null;
 318          }
 319  
 320          while ( $context_processor->next_tag() ) {
 321              if ( ! $context_processor->is_virtual() ) {
 322                  $context_processor->set_bookmark( 'final_node' );
 323              }
 324          }
 325  
 326          if (
 327              ! $context_processor->has_bookmark( 'final_node' ) ||
 328              ! $context_processor->seek( 'final_node' )
 329          ) {
 330              _doing_it_wrong( __METHOD__, __( 'No valid context element was detected.' ), '6.8.0' );
 331              return null;
 332          }
 333  
 334          return $context_processor->create_fragment_at_current_node( $html );
 335      }
 336  
 337      /**
 338       * Creates an HTML processor in the full parsing mode.
 339       *
 340       * It's likely that a fragment parser is more appropriate, unless sending an
 341       * entire HTML document from start to finish. Consider a fragment parser with
 342       * a context node of `<body>`.
 343       *
 344       * UTF-8 is the only allowed encoding. If working with a document that
 345       * isn't UTF-8, first convert the document to UTF-8, then pass in the
 346       * converted HTML.
 347       *
 348       * @since 6.7.0
 349       *
 350       * @param string      $html                    Input HTML document to process.
 351       * @param string|null $known_definite_encoding Optional. If provided, specifies the charset used
 352       *                                             in the input byte stream. Currently must be UTF-8.
 353       * @return static|null The created processor if successful, otherwise null.
 354       */
 355  	public static function create_full_parser( $html, $known_definite_encoding = 'UTF-8' ) {
 356          if ( 'UTF-8' !== $known_definite_encoding ) {
 357              return null;
 358          }
 359          if ( ! is_string( $html ) ) {
 360              _doing_it_wrong(
 361                  __METHOD__,
 362                  __( 'The HTML parameter must be a string.' ),
 363                  '6.9.0'
 364              );
 365              return null;
 366          }
 367  
 368          $processor                             = new static( $html, self::CONSTRUCTOR_UNLOCK_CODE );
 369          $processor->state->encoding            = $known_definite_encoding;
 370          $processor->state->encoding_confidence = 'certain';
 371  
 372          return $processor;
 373      }
 374  
 375      /**
 376       * Constructor.
 377       *
 378       * Do not use this method. Use the static creator methods instead.
 379       *
 380       * @access private
 381       *
 382       * @since 6.4.0
 383       *
 384       * @see WP_HTML_Processor::create_fragment()
 385       *
 386       * @param string      $html                                  HTML to process.
 387       * @param string|null $use_the_static_create_methods_instead This constructor should not be called manually.
 388       */
 389  	public function __construct( $html, $use_the_static_create_methods_instead = null ) {
 390          parent::__construct( $html );
 391  
 392          if ( self::CONSTRUCTOR_UNLOCK_CODE !== $use_the_static_create_methods_instead ) {
 393              _doing_it_wrong(
 394                  __METHOD__,
 395                  sprintf(
 396                      /* translators: %s: WP_HTML_Processor::create_fragment(). */
 397                      __( 'Call %s to create an HTML Processor instead of calling the constructor directly.' ),
 398                      '<code>WP_HTML_Processor::create_fragment()</code>'
 399                  ),
 400                  '6.4.0'
 401              );
 402          }
 403  
 404          $this->state = new WP_HTML_Processor_State();
 405  
 406          $this->state->stack_of_open_elements->set_push_handler(
 407              function ( WP_HTML_Token $token ): void {
 408                  $is_virtual            = ! isset( $this->state->current_token ) || $this->is_tag_closer();
 409                  $same_node             = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name;
 410                  $provenance            = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real';
 411                  $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::PUSH, $provenance );
 412  
 413                  $this->change_parsing_namespace( $token->integration_node_type ? 'html' : $token->namespace );
 414              }
 415          );
 416  
 417          $this->state->stack_of_open_elements->set_pop_handler(
 418              function ( WP_HTML_Token $token ): void {
 419                  $is_virtual            = ! isset( $this->state->current_token ) || ! $this->is_tag_closer();
 420                  $same_node             = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name;
 421                  $provenance            = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real';
 422                  $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::POP, $provenance );
 423  
 424                  $adjusted_current_node = $this->get_adjusted_current_node();
 425  
 426                  if ( $adjusted_current_node ) {
 427                      $this->change_parsing_namespace( $adjusted_current_node->integration_node_type ? 'html' : $adjusted_current_node->namespace );
 428                  } else {
 429                      $this->change_parsing_namespace( 'html' );
 430                  }
 431              }
 432          );
 433  
 434          /*
 435           * Create this wrapper so that it's possible to pass
 436           * a private method into WP_HTML_Token classes without
 437           * exposing it to any public API.
 438           */
 439          $this->release_internal_bookmark_on_destruct = function ( string $name ): void {
 440              parent::release_bookmark( $name );
 441          };
 442      }
 443  
 444      /**
 445       * Creates a fragment processor at the current node.
 446       *
 447       * HTML Fragment parsing always happens with a context node. HTML Fragment Processors can be
 448       * instantiated with a `BODY` context node via `WP_HTML_Processor::create_fragment( $html )`.
 449       *
 450       * The context node may impact how a fragment of HTML is parsed. For example, consider the HTML
 451       * fragment `<td />Inside TD?</td>`.
 452       *
 453       * A BODY context node will produce the following tree:
 454       *
 455       *     └─#text Inside TD?
 456       *
 457       * Notice that the `<td>` tags are completely ignored.
 458       *
 459       * Compare that with an SVG context node that produces the following tree:
 460       *
 461       *     ├─svg:td
 462       *     └─#text Inside TD?
 463       *
 464       * Here, a `td` node in the `svg` namespace is created, and its self-closing flag is respected.
 465       * This is a peculiarity of parsing HTML in foreign content like SVG.
 466       *
 467       * Finally, consider the tree produced with a TABLE context node:
 468       *
 469       *     └─TBODY
 470       *       └─TR
 471       *         └─TD
 472       *           └─#text Inside TD?
 473       *
 474       * These examples demonstrate how important the context node may be when processing an HTML
 475       * fragment. Special care must be taken when processing fragments that are expected to appear
 476       * in specific contexts. SVG and TABLE are good examples, but there are others.
 477       *
 478       * @see https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-parsing-algorithm
 479       *
 480       * @since 6.8.0
 481       *
 482       * @param string $html Input HTML fragment to process.
 483       * @return static|null The created processor if successful, otherwise null.
 484       */
 485  	private function create_fragment_at_current_node( string $html ) {
 486          if ( $this->get_token_type() !== '#tag' || $this->is_tag_closer() ) {
 487              _doing_it_wrong(
 488                  __METHOD__,
 489                  __( 'The context element must be a start tag.' ),
 490                  '6.8.0'
 491              );
 492              return null;
 493          }
 494  
 495          $tag_name  = $this->current_element->token->node_name;
 496          $namespace = $this->current_element->token->namespace;
 497  
 498          if ( 'html' === $namespace && self::is_void( $tag_name ) ) {
 499              _doing_it_wrong(
 500                  __METHOD__,
 501                  sprintf(
 502                      // translators: %s: A tag name like INPUT or BR.
 503                      __( 'The context element cannot be a void element, found "%s".' ),
 504                      $tag_name
 505                  ),
 506                  '6.8.0'
 507              );
 508              return null;
 509          }
 510  
 511          /*
 512           * Prevent creating fragments at nodes that require a special tokenizer state.
 513           * This is unsupported by the HTML Processor.
 514           */
 515          if (
 516              'html' === $namespace &&
 517              in_array( $tag_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP', 'PLAINTEXT' ), true )
 518          ) {
 519              _doing_it_wrong(
 520                  __METHOD__,
 521                  sprintf(
 522                      // translators: %s: A tag name like IFRAME or TEXTAREA.
 523                      __( 'The context element "%s" is not supported.' ),
 524                      $tag_name
 525                  ),
 526                  '6.8.0'
 527              );
 528              return null;
 529          }
 530  
 531          $fragment_processor = new static( $html, self::CONSTRUCTOR_UNLOCK_CODE );
 532  
 533          $fragment_processor->compat_mode = $this->compat_mode;
 534  
 535          // @todo Create "fake" bookmarks for non-existent but implied nodes.
 536          $fragment_processor->bookmarks['root-node'] = new WP_HTML_Span( 0, 0 );
 537          $root_node                                  = new WP_HTML_Token(
 538              'root-node',
 539              'HTML',
 540              false
 541          );
 542          $fragment_processor->state->stack_of_open_elements->push( $root_node );
 543  
 544          $fragment_processor->bookmarks['context-node']   = new WP_HTML_Span( 0, 0 );
 545          $fragment_processor->context_node                = clone $this->current_element->token;
 546          $fragment_processor->context_node->bookmark_name = 'context-node';
 547          $fragment_processor->context_node->on_destroy    = null;
 548  
 549          $fragment_processor->breadcrumbs = array( 'HTML', $fragment_processor->context_node->node_name );
 550  
 551          if ( 'TEMPLATE' === $fragment_processor->context_node->node_name ) {
 552              $fragment_processor->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE;
 553          }
 554  
 555          $fragment_processor->reset_insertion_mode_appropriately();
 556  
 557          /*
 558           * > Set the parser's form element pointer to the nearest node to the context element that
 559           * > is a form element (going straight up the ancestor chain, and including the element
 560           * > itself, if it is a form element), if any. (If there is no such form element, the
 561           * > form element pointer keeps its initial value, null.)
 562           */
 563          foreach ( $this->state->stack_of_open_elements->walk_up() as $element ) {
 564              if ( 'FORM' === $element->node_name && 'html' === $element->namespace ) {
 565                  $fragment_processor->state->form_element                = clone $element;
 566                  $fragment_processor->state->form_element->bookmark_name = null;
 567                  $fragment_processor->state->form_element->on_destroy    = null;
 568                  break;
 569              }
 570          }
 571  
 572          $fragment_processor->state->encoding_confidence = 'irrelevant';
 573  
 574          /*
 575           * Update the parsing namespace near the end of the process.
 576           * This is important so that any push/pop from the stack of open
 577           * elements does not change the parsing namespace.
 578           */
 579          $fragment_processor->change_parsing_namespace(
 580              $this->current_element->token->integration_node_type ? 'html' : $namespace
 581          );
 582  
 583          return $fragment_processor;
 584      }
 585  
 586      /**
 587       * Stops the parser and terminates its execution when encountering unsupported markup.
 588       *
 589       * @throws WP_HTML_Unsupported_Exception Halts execution of the parser.
 590       *
 591       * @since 6.7.0
 592       * @ignore
 593       *
 594       * @param string $message Explains support is missing in order to parse the current node.
 595       * @return never
 596       */
 597  	private function bail( string $message ) {
 598          $here  = $this->bookmarks[ $this->state->current_token->bookmark_name ];
 599          $token = substr( $this->html, $here->start, $here->length );
 600  
 601          $open_elements = array();
 602          foreach ( $this->state->stack_of_open_elements->stack as $item ) {
 603              $open_elements[] = $item->node_name;
 604          }
 605  
 606          $active_formats = array();
 607          foreach ( $this->state->active_formatting_elements->walk_down() as $item ) {
 608              $active_formats[] = $item->node_name;
 609          }
 610  
 611          $this->last_error = self::ERROR_UNSUPPORTED;
 612  
 613          $this->unsupported_exception = new WP_HTML_Unsupported_Exception(
 614              $message,
 615              $this->state->current_token->node_name,
 616              $here->start,
 617              $token,
 618              $open_elements,
 619              $active_formats
 620          );
 621  
 622          throw $this->unsupported_exception;
 623      }
 624  
 625      /**
 626       * Returns the last error, if any.
 627       *
 628       * Various situations lead to parsing failure but this class will
 629       * return `false` in all those cases. To determine why something
 630       * failed it's possible to request the last error. This can be
 631       * helpful to know to distinguish whether a given tag couldn't
 632       * be found or if content in the document caused the processor
 633       * to give up and abort processing.
 634       *
 635       * Example
 636       *
 637       *     $processor = WP_HTML_Processor::create_fragment( '<template><strong><button><em><p><em>' );
 638       *     false === $processor->next_tag();
 639       *     WP_HTML_Processor::ERROR_UNSUPPORTED === $processor->get_last_error();
 640       *
 641       * @since 6.4.0
 642       *
 643       * @see self::ERROR_UNSUPPORTED
 644       * @see self::ERROR_EXCEEDED_MAX_BOOKMARKS
 645       *
 646       * @return string|null The last error, if one exists, otherwise null.
 647       */
 648  	public function get_last_error(): ?string {
 649          return $this->last_error;
 650      }
 651  
 652      /**
 653       * Returns context for why the parser aborted due to unsupported HTML, if it did.
 654       *
 655       * This is meant for debugging purposes, not for production use.
 656       *
 657       * @since 6.7.0
 658       *
 659       * @see self::$unsupported_exception
 660       *
 661       * @return WP_HTML_Unsupported_Exception|null
 662       */
 663  	public function get_unsupported_exception() {
 664          return $this->unsupported_exception;
 665      }
 666  
 667      /**
 668       * Finds the next tag matching the $query.
 669       *
 670       * @todo Support matching the class name and tag name.
 671       *
 672       * @since 6.4.0
 673       * @since 6.6.0 Visits all tokens, including virtual ones.
 674       *
 675       * @throws Exception When unable to allocate a bookmark for the next token in the input HTML document.
 676       *
 677       * @param array|string|null $query {
 678       *     Optional. Which tag name to find, having which class, etc. Default is to find any tag.
 679       *
 680       *     @type string|null $tag_name     Which tag to find, or `null` for "any tag."
 681       *     @type string      $tag_closers  'visit' to pause at tag closers, 'skip' or unset to only visit openers.
 682       *     @type int|null    $match_offset Find the Nth tag matching all search criteria.
 683       *                                     1 for "first" tag, 3 for "third," etc.
 684       *                                     Defaults to first tag.
 685       *     @type string|null $class_name   Tag must contain this whole class name to match.
 686       *     @type string[]    $breadcrumbs  DOM sub-path at which element is found, e.g. `array( 'FIGURE', 'IMG' )`.
 687       *                                     May also contain the wildcard `*` which matches a single element, e.g. `array( 'SECTION', '*' )`.
 688       * }
 689       * @return bool Whether a tag was matched.
 690       */
 691  	public function next_tag( $query = null ): bool {
 692          $visit_closers = isset( $query['tag_closers'] ) && 'visit' === $query['tag_closers'];
 693  
 694          if ( null === $query ) {
 695              while ( $this->next_token() ) {
 696                  if ( '#tag' !== $this->get_token_type() ) {
 697                      continue;
 698                  }
 699  
 700                  if ( ! $this->is_tag_closer() || $visit_closers ) {
 701                      return true;
 702                  }
 703              }
 704  
 705              return false;
 706          }
 707  
 708          if ( is_string( $query ) ) {
 709              $query = array( 'breadcrumbs' => array( $query ) );
 710          }
 711  
 712          if ( ! is_array( $query ) ) {
 713              _doing_it_wrong(
 714                  __METHOD__,
 715                  __( 'Please pass a query array to this function.' ),
 716                  '6.4.0'
 717              );
 718              return false;
 719          }
 720  
 721          if ( isset( $query['tag_name'] ) ) {
 722              $query['tag_name'] = strtoupper( $query['tag_name'] );
 723          }
 724  
 725          $needs_class = ( isset( $query['class_name'] ) && is_string( $query['class_name'] ) )
 726              ? $query['class_name']
 727              : null;
 728  
 729          if ( ! ( array_key_exists( 'breadcrumbs', $query ) && is_array( $query['breadcrumbs'] ) ) ) {
 730              while ( $this->next_token() ) {
 731                  if ( '#tag' !== $this->get_token_type() ) {
 732                      continue;
 733                  }
 734  
 735                  if ( isset( $query['tag_name'] ) && $query['tag_name'] !== $this->get_token_name() ) {
 736                      continue;
 737                  }
 738  
 739                  if ( isset( $needs_class ) && ! $this->has_class( $needs_class ) ) {
 740                      continue;
 741                  }
 742  
 743                  if ( ! $this->is_tag_closer() || $visit_closers ) {
 744                      return true;
 745                  }
 746              }
 747  
 748              return false;
 749          }
 750  
 751          $breadcrumbs  = $query['breadcrumbs'];
 752          $match_offset = isset( $query['match_offset'] ) ? (int) $query['match_offset'] : 1;
 753  
 754          while ( $match_offset > 0 && $this->next_token() ) {
 755              if ( '#tag' !== $this->get_token_type() || $this->is_tag_closer() ) {
 756                  continue;
 757              }
 758  
 759              if ( isset( $needs_class ) && ! $this->has_class( $needs_class ) ) {
 760                  continue;
 761              }
 762  
 763              if ( $this->matches_breadcrumbs( $breadcrumbs ) && 0 === --$match_offset ) {
 764                  return true;
 765              }
 766          }
 767  
 768          return false;
 769      }
 770  
 771      /**
 772       * Finds the next token in the HTML document.
 773       *
 774       * This doesn't currently have a way to represent non-tags and doesn't process
 775       * semantic rules for text nodes. For access to the raw tokens consider using
 776       * WP_HTML_Tag_Processor instead.
 777       *
 778       * @since 6.5.0 Added for internal support; do not use.
 779       * @since 6.7.2 Refactored so subclasses may extend.
 780       *
 781       * @return bool Whether a token was parsed.
 782       *
 783       * @phpstan-impure
 784       */
 785  	public function next_token(): bool {
 786          return $this->next_visitable_token();
 787      }
 788  
 789      /**
 790       * Ensures internal accounting is maintained for HTML semantic rules while
 791       * the underlying Tag Processor class is seeking to a bookmark.
 792       *
 793       * This doesn't currently have a way to represent non-tags and doesn't process
 794       * semantic rules for text nodes. For access to the raw tokens consider using
 795       * WP_HTML_Tag_Processor instead.
 796       *
 797       * Note that this method may call itself recursively. This is why it is not
 798       * implemented as {@see WP_HTML_Processor::next_token()}, which instead calls
 799       * this method similarly to how {@see WP_HTML_Tag_Processor::next_token()}
 800       * calls the {@see WP_HTML_Tag_Processor::base_class_next_token()} method.
 801       *
 802       * @since 6.7.2 Added for internal support.
 803       * @ignore
 804       *
 805       * @return bool
 806       */
 807  	private function next_visitable_token(): bool {
 808          $this->current_element = null;
 809  
 810          if ( isset( $this->last_error ) ) {
 811              return false;
 812          }
 813  
 814          /*
 815           * Prime the events if there are none.
 816           *
 817           * @todo In some cases, probably related to the adoption agency
 818           *       algorithm, this call to step() doesn't create any new
 819           *       events. Calling it again creates them. Figure out why
 820           *       this is and if it's inherent or if it's a bug. Looping
 821           *       until there are events or until there are no more
 822           *       tokens works in the meantime and isn't obviously wrong.
 823           */
 824          if ( empty( $this->element_queue ) ) {
 825              if ( $this->step() ) {
 826                  return $this->next_visitable_token();
 827              }
 828  
 829              if ( isset( $this->last_error ) ) {
 830                  return false;
 831              }
 832          }
 833  
 834          // Process the next event on the queue.
 835          $this->current_element = array_shift( $this->element_queue );
 836          if ( ! isset( $this->current_element ) ) {
 837              // There are no tokens left, so close all remaining open elements.
 838              while ( $this->state->stack_of_open_elements->pop() ) {
 839                  continue;
 840              }
 841  
 842              return empty( $this->element_queue ) ? false : $this->next_visitable_token();
 843          }
 844  
 845          $is_pop = WP_HTML_Stack_Event::POP === $this->current_element->operation;
 846  
 847          /*
 848           * The root node only exists in the fragment parser, and closing it
 849           * indicates that the parse is complete. Stop before popping it from
 850           * the breadcrumbs.
 851           */
 852          if ( 'root-node' === $this->current_element->token->bookmark_name ) {
 853              return $this->next_visitable_token();
 854          }
 855  
 856          // Adjust the breadcrumbs for this event.
 857          if ( $is_pop ) {
 858              array_pop( $this->breadcrumbs );
 859          } else {
 860              $this->breadcrumbs[] = $this->current_element->token->node_name;
 861          }
 862  
 863          // Avoid sending close events for elements which don't expect a closing.
 864          if ( $is_pop && ! $this->expects_closer( $this->current_element->token ) ) {
 865              return $this->next_visitable_token();
 866          }
 867  
 868          return true;
 869      }
 870  
 871      /**
 872       * Indicates if the current tag token is a tag closer.
 873       *
 874       * Example:
 875       *
 876       *     $p = WP_HTML_Processor::create_fragment( '<div></div>' );
 877       *     $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) );
 878       *     $p->is_tag_closer() === false;
 879       *
 880       *     $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) );
 881       *     $p->is_tag_closer() === true;
 882       *
 883       * @since 6.6.0 Subclassed for HTML Processor.
 884       *
 885       * @return bool Whether the current tag is a tag closer.
 886       */
 887  	public function is_tag_closer(): bool {
 888          return $this->is_virtual()
 889              ? ( WP_HTML_Stack_Event::POP === $this->current_element->operation && '#tag' === $this->get_token_type() )
 890              : parent::is_tag_closer();
 891      }
 892  
 893      /**
 894       * Indicates if the currently-matched token is virtual, created by a stack operation
 895       * while processing HTML, rather than a token found in the HTML text itself.
 896       *
 897       * @since 6.6.0
 898       * @ignore
 899       *
 900       * @return bool Whether the current token is virtual.
 901       */
 902  	private function is_virtual(): bool {
 903          return (
 904              isset( $this->current_element->provenance ) &&
 905              'virtual' === $this->current_element->provenance
 906          );
 907      }
 908  
 909      /**
 910       * Indicates if the currently-matched tag matches the given breadcrumbs.
 911       *
 912       * A "*" represents a single tag wildcard, where any tag matches, but not no tags.
 913       *
 914       * At some point this function _may_ support a `**` syntax for matching any number
 915       * of unspecified tags in the breadcrumb stack. This has been intentionally left
 916       * out, however, to keep this function simple and to avoid introducing backtracking,
 917       * which could open up surprising performance breakdowns.
 918       *
 919       * Example:
 920       *
 921       *     $processor = WP_HTML_Processor::create_fragment( '<div><span><figure><img></figure></span></div>' );
 922       *     $processor->next_tag( 'img' );
 923       *     true  === $processor->matches_breadcrumbs( array( 'figure', 'img' ) );
 924       *     true  === $processor->matches_breadcrumbs( array( 'span', 'figure', 'img' ) );
 925       *     false === $processor->matches_breadcrumbs( array( 'span', 'img' ) );
 926       *     true  === $processor->matches_breadcrumbs( array( 'span', '*', 'img' ) );
 927       *
 928       * @since 6.4.0
 929       *
 930       * @param string[] $breadcrumbs DOM sub-path at which element is found, e.g. `array( 'FIGURE', 'IMG' )`.
 931       *                              May also contain the wildcard `*` which matches a single element, e.g. `array( 'SECTION', '*' )`.
 932       * @return bool Whether the currently-matched tag is found at the given nested structure.
 933       */
 934  	public function matches_breadcrumbs( $breadcrumbs ): bool {
 935          // Everything matches when there are zero constraints.
 936          if ( 0 === count( $breadcrumbs ) ) {
 937              return true;
 938          }
 939  
 940          // Start at the last crumb.
 941          $crumb = end( $breadcrumbs );
 942  
 943          if ( '*' !== $crumb && $this->get_tag() !== strtoupper( $crumb ) ) {
 944              return false;
 945          }
 946  
 947          for ( $i = count( $this->breadcrumbs ) - 1; $i >= 0; $i-- ) {
 948              $node  = $this->breadcrumbs[ $i ];
 949              $crumb = strtoupper( current( $breadcrumbs ) );
 950  
 951              if ( '*' !== $crumb && $node !== $crumb ) {
 952                  return false;
 953              }
 954  
 955              if ( false === prev( $breadcrumbs ) ) {
 956                  return true;
 957              }
 958          }
 959  
 960          return false;
 961      }
 962  
 963      /**
 964       * Indicates if the currently-matched node expects a closing
 965       * token, or if it will self-close on the next step.
 966       *
 967       * Most HTML elements expect a closer, such as a P element or
 968       * a DIV element. Others, like an IMG element, are void and don't
 969       * have a closing tag. Special elements, such as SCRIPT and STYLE,
 970       * are treated just like void tags. Text nodes and self-closing
 971       * foreign content will also act just like a void tag, immediately
 972       * closing as soon as the processor advances to the next token.
 973       *
 974       * @since 6.6.0
 975       *
 976       * @param WP_HTML_Token|null $node Optional. Node to examine, if provided.
 977       *                                 Default is to examine current node.
 978       * @return bool|null Whether to expect a closer for the currently-matched node,
 979       *                   or `null` if not matched on any token.
 980       */
 981  	public function expects_closer( ?WP_HTML_Token $node = null ): ?bool {
 982          $token_name = $node->node_name ?? $this->get_token_name();
 983  
 984          if ( ! isset( $token_name ) ) {
 985              return null;
 986          }
 987  
 988          $token_namespace        = $node->namespace ?? $this->get_namespace();
 989          $token_has_self_closing = $node->has_self_closing_flag ?? $this->has_self_closing_flag();
 990  
 991          return ! (
 992              // Comments, text nodes, and other atomic tokens.
 993              '#' === $token_name[0] ||
 994              // Doctype declarations.
 995              'html' === $token_name ||
 996              // Void elements.
 997              ( 'html' === $token_namespace && self::is_void( $token_name ) ) ||
 998              // Special atomic elements.
 999              ( 'html' === $token_namespace && in_array( $token_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true ) ) ||
1000              // Self-closing elements in foreign content.
1001              ( 'html' !== $token_namespace && $token_has_self_closing )
1002          );
1003      }
1004  
1005      /**
1006       * Steps through the HTML document and stop at the next tag, if any.
1007       *
1008       * This is an internal method. The relevant public methods are
1009       * {@see WP_HTML_Processor::next_tag()} and {@see WP_HTML_Processor::next_token()}.
1010       *
1011       * @since 6.4.0
1012       *
1013       * @access private
1014       *
1015       * @throws Exception When unable to allocate a bookmark for the next token in the input HTML document.
1016       *
1017       * @see self::PROCESS_NEXT_NODE
1018       * @see self::REPROCESS_CURRENT_NODE
1019       *
1020       * @param string $node_to_process Whether to parse the next node or reprocess the current node.
1021       * @return bool Whether a tag was matched.
1022       */
1023  	public function step( $node_to_process = self::PROCESS_NEXT_NODE ): bool {
1024          // Refuse to proceed if there was a previous error.
1025          if ( null !== $this->last_error ) {
1026              return false;
1027          }
1028  
1029          if ( self::REPROCESS_CURRENT_NODE !== $node_to_process ) {
1030              /*
1031               * Void elements still hop onto the stack of open elements even though
1032               * there's no corresponding closing tag. This is important for managing
1033               * stack-based operations such as "navigate to parent node" or checking
1034               * on an element's breadcrumbs.
1035               *
1036               * When moving on to the next node, therefore, if the bottom-most element
1037               * on the stack is a void element, it must be closed.
1038               */
1039              $top_node = $this->state->stack_of_open_elements->current_node();
1040              if ( isset( $top_node ) && ! $this->expects_closer( $top_node ) ) {
1041                  $this->state->stack_of_open_elements->pop();
1042              }
1043          }
1044  
1045          if ( self::PROCESS_NEXT_NODE === $node_to_process ) {
1046              parent::next_token();
1047              if ( WP_HTML_Tag_Processor::STATE_TEXT_NODE === $this->parser_state ) {
1048                  parent::subdivide_text_appropriately();
1049              }
1050          }
1051  
1052          // Finish stepping when there are no more tokens in the document.
1053          if (
1054              WP_HTML_Tag_Processor::STATE_INCOMPLETE_INPUT === $this->parser_state ||
1055              WP_HTML_Tag_Processor::STATE_COMPLETE === $this->parser_state
1056          ) {
1057              return false;
1058          }
1059  
1060          $adjusted_current_node = $this->get_adjusted_current_node();
1061          $is_closer             = $this->is_tag_closer();
1062          $is_start_tag          = WP_HTML_Tag_Processor::STATE_MATCHED_TAG === $this->parser_state && ! $is_closer;
1063          $token_name            = $this->get_token_name();
1064  
1065          if ( self::REPROCESS_CURRENT_NODE !== $node_to_process ) {
1066              try {
1067                  $bookmark_name = $this->bookmark_token();
1068              } catch ( Exception $e ) {
1069                  if ( self::ERROR_EXCEEDED_MAX_BOOKMARKS === $this->last_error ) {
1070                      return false;
1071                  }
1072                  throw $e;
1073              }
1074  
1075              $this->state->current_token = new WP_HTML_Token(
1076                  $bookmark_name,
1077                  $token_name,
1078                  $this->has_self_closing_flag(),
1079                  $this->release_internal_bookmark_on_destruct
1080              );
1081          }
1082  
1083          $parse_in_current_insertion_mode = (
1084              0 === $this->state->stack_of_open_elements->count() ||
1085              'html' === $adjusted_current_node->namespace ||
1086              (
1087                  'math' === $adjusted_current_node->integration_node_type &&
1088                  (
1089                      ( $is_start_tag && ! in_array( $token_name, array( 'MGLYPH', 'MALIGNMARK' ), true ) ) ||
1090                      '#text' === $token_name
1091                  )
1092              ) ||
1093              (
1094                  'math' === $adjusted_current_node->namespace &&
1095                  'ANNOTATION-XML' === $adjusted_current_node->node_name &&
1096                  $is_start_tag && 'SVG' === $token_name
1097              ) ||
1098              (
1099                  'html' === $adjusted_current_node->integration_node_type &&
1100                  ( $is_start_tag || '#text' === $token_name )
1101              )
1102          );
1103  
1104          try {
1105              if ( ! $parse_in_current_insertion_mode ) {
1106                  return $this->step_in_foreign_content();
1107              }
1108  
1109              switch ( $this->state->insertion_mode ) {
1110                  case WP_HTML_Processor_State::INSERTION_MODE_INITIAL:
1111                      return $this->step_initial();
1112  
1113                  case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML:
1114                      return $this->step_before_html();
1115  
1116                  case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD:
1117                      return $this->step_before_head();
1118  
1119                  case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD:
1120                      return $this->step_in_head();
1121  
1122                  case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD_NOSCRIPT:
1123                      return $this->step_in_head_noscript();
1124  
1125                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD:
1126                      return $this->step_after_head();
1127  
1128                  case WP_HTML_Processor_State::INSERTION_MODE_IN_BODY:
1129                      return $this->step_in_body();
1130  
1131                  case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE:
1132                      return $this->step_in_table();
1133  
1134                  case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_TEXT:
1135                      return $this->step_in_table_text();
1136  
1137                  case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION:
1138                      return $this->step_in_caption();
1139  
1140                  case WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP:
1141                      return $this->step_in_column_group();
1142  
1143                  case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY:
1144                      return $this->step_in_table_body();
1145  
1146                  case WP_HTML_Processor_State::INSERTION_MODE_IN_ROW:
1147                      return $this->step_in_row();
1148  
1149                  case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL:
1150                      return $this->step_in_cell();
1151  
1152                  case WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE:
1153                      return $this->step_in_template();
1154  
1155                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_BODY:
1156                      return $this->step_after_body();
1157  
1158                  case WP_HTML_Processor_State::INSERTION_MODE_IN_FRAMESET:
1159                      return $this->step_in_frameset();
1160  
1161                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_FRAMESET:
1162                      return $this->step_after_frameset();
1163  
1164                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_BODY:
1165                      return $this->step_after_after_body();
1166  
1167                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET:
1168                      return $this->step_after_after_frameset();
1169  
1170                  // This should be unreachable but PHP doesn't have total type checking on switch.
1171                  default:
1172                      $this->bail( "Unaware of the requested parsing mode: '{$this->state->insertion_mode}'." );
1173              }
1174          } catch ( WP_HTML_Unsupported_Exception $e ) {
1175              /*
1176               * Exceptions are used in this class to escape deep call stacks that
1177               * otherwise might involve messier calling and return conventions.
1178               */
1179              return false;
1180          } catch ( Exception $e ) {
1181              if ( self::ERROR_EXCEEDED_MAX_BOOKMARKS === $this->last_error ) {
1182                  return false;
1183              }
1184              // Rethrow any other exceptions for higher-level handling.
1185              throw $e;
1186          }
1187      }
1188  
1189      /**
1190       * Computes the HTML breadcrumbs for the currently-matched node, if matched.
1191       *
1192       * Breadcrumbs start at the outermost parent and descend toward the matched element.
1193       * They always include the entire path from the root HTML node to the matched element.
1194       *
1195       * Example:
1196       *
1197       *     $processor = WP_HTML_Processor::create_fragment( '<p><strong><em><img></em></strong></p>' );
1198       *     $processor->next_tag( 'IMG' );
1199       *     $processor->get_breadcrumbs() === array( 'HTML', 'BODY', 'P', 'STRONG', 'EM', 'IMG' );
1200       *
1201       * @since 6.4.0
1202       *
1203       * @return string[] Array of tag names representing path to matched node.
1204       */
1205  	public function get_breadcrumbs(): array {
1206          return $this->breadcrumbs;
1207      }
1208  
1209      /**
1210       * Returns the nesting depth of the current location in the document.
1211       *
1212       * Example:
1213       *
1214       *     $processor = WP_HTML_Processor::create_fragment( '<div><p></p></div>' );
1215       *     // The processor starts in the BODY context, meaning it has depth from the start: HTML > BODY.
1216       *     2 === $processor->get_current_depth();
1217       *
1218       *     // Opening the DIV element increases the depth.
1219       *     $processor->next_token();
1220       *     3 === $processor->get_current_depth();
1221       *
1222       *     // Opening the P element increases the depth.
1223       *     $processor->next_token();
1224       *     4 === $processor->get_current_depth();
1225       *
1226       *     // The P element is closed during `next_token()` so the depth is decreased to reflect that.
1227       *     $processor->next_token();
1228       *     3 === $processor->get_current_depth();
1229       *
1230       * @since 6.6.0
1231       *
1232       * @return int Nesting-depth of current location in the document.
1233       */
1234  	public function get_current_depth(): int {
1235          return count( $this->breadcrumbs );
1236      }
1237  
1238      /**
1239       * Normalizes an HTML fragment by serializing it.
1240       *
1241       * This method assumes that the given HTML snippet is found in BODY context.
1242       * For normalizing full documents or fragments found in other contexts, create
1243       * a new processor using {@see WP_HTML_Processor::create_fragment} or
1244       * {@see WP_HTML_Processor::create_full_parser} and call {@see WP_HTML_Processor::serialize}
1245       * on the created instances.
1246       *
1247       * Many aspects of an input HTML fragment may be changed during normalization.
1248       *
1249       *  - Attribute values will be double-quoted.
1250       *  - Duplicate attributes will be removed.
1251       *  - Omitted tags will be added.
1252       *  - Tag and attribute name casing will be lower-cased,
1253       *    except for specific SVG and MathML tags or attributes.
1254       *  - Text will be re-encoded, null bytes handled,
1255       *    and invalid UTF-8 replaced with U+FFFD.
1256       *  - Any incomplete syntax trailing at the end will be omitted,
1257       *    for example, an unclosed comment opener will be removed.
1258       *
1259       * Example:
1260       *
1261       *     echo WP_HTML_Processor::normalize( '<a href=#anchor v=5 href="/" enabled>One</a another v=5><!--' );
1262       *     // <a href="#anchor" v="5" enabled>One</a>
1263       *
1264       *     echo WP_HTML_Processor::normalize( '<div></p>fun<table><td>cell</div>' );
1265       *     // <div><p></p>fun<table><tbody><tr><td>cell</td></tr></tbody></table></div>
1266       *
1267       *     echo WP_HTML_Processor::normalize( '<![CDATA[invalid comment]]> syntax < <> "oddities"' );
1268       *     // <!--[CDATA[invalid comment]]--> syntax &lt; &lt;&gt; &quot;oddities&quot;
1269       *
1270       * @since 6.7.0
1271       *
1272       * @param string $html Input HTML to normalize.
1273       * @return string|null Normalized output, or `null` if unable to normalize.
1274       */
1275  	public static function normalize( string $html ): ?string {
1276          return static::create_fragment( $html )->serialize();
1277      }
1278  
1279      /**
1280       * Returns normalized HTML for a fragment by serializing it.
1281       *
1282       * This differs from {@see WP_HTML_Processor::normalize} in that it starts with
1283       * a specific HTML Processor, which _must_ not have already started scanning;
1284       * it must be in the initial ready state and will be in the completed state once
1285       * serialization is complete.
1286       *
1287       * Many aspects of an input HTML fragment may be changed during normalization.
1288       *
1289       *  - Attribute values will be double-quoted.
1290       *  - Duplicate attributes will be removed.
1291       *  - Omitted tags will be added.
1292       *  - Tag and attribute name casing will be lower-cased,
1293       *    except for specific SVG and MathML tags or attributes.
1294       *  - Text will be re-encoded, null bytes handled,
1295       *    and invalid UTF-8 replaced with U+FFFD.
1296       *  - Any incomplete syntax trailing at the end will be omitted,
1297       *    for example, an unclosed comment opener will be removed.
1298       *
1299       * Example:
1300       *
1301       *     $processor = WP_HTML_Processor::create_fragment( '<a href=#anchor v=5 href="/" enabled>One</a another v=5><!--' );
1302       *     echo $processor->serialize();
1303       *     // <a href="#anchor" v="5" enabled>One</a>
1304       *
1305       *     $processor = WP_HTML_Processor::create_fragment( '<div></p>fun<table><td>cell</div>' );
1306       *     echo $processor->serialize();
1307       *     // <div><p></p>fun<table><tbody><tr><td>cell</td></tr></tbody></table></div>
1308       *
1309       *     $processor = WP_HTML_Processor::create_fragment( '<![CDATA[invalid comment]]> syntax < <> "oddities"' );
1310       *     echo $processor->serialize();
1311       *     // <!--[CDATA[invalid comment]]--> syntax &lt; &lt;&gt; &quot;oddities&quot;
1312       *
1313       * @since 6.7.0
1314       *
1315       * @return string|null Normalized HTML markup represented by processor,
1316       *                     or `null` if unable to generate serialization.
1317       */
1318  	public function serialize(): ?string {
1319          if ( WP_HTML_Tag_Processor::STATE_READY !== $this->parser_state ) {
1320              wp_trigger_error(
1321                  __METHOD__,
1322                  'An HTML Processor which has already started processing cannot serialize its contents. Serialize immediately after creating the instance.',
1323                  E_USER_WARNING
1324              );
1325              return null;
1326          }
1327  
1328          $html = '';
1329          while ( $this->next_token() ) {
1330              $html .= $this->serialize_token();
1331          }
1332  
1333          if ( null !== $this->get_last_error() ) {
1334              wp_trigger_error(
1335                  __METHOD__,
1336                  "Cannot serialize HTML Processor with parsing error: {$this->get_last_error()}.",
1337                  E_USER_WARNING
1338              );
1339              return null;
1340          }
1341  
1342          return $html;
1343      }
1344  
1345      /**
1346       * Serializes the currently-matched token.
1347       *
1348       * This method produces a fully-normative HTML string for the currently-matched token,
1349       * if able. If not matched at any token or if the token doesn't correspond to any HTML
1350       * it will return an empty string (for example, presumptuous end tags are ignored).
1351       *
1352       * @see static::serialize()
1353       *
1354       * @since 6.7.0
1355       * @since 6.9.0 Converted from protected to public method.
1356       *
1357       * @return string Serialization of token, or empty string if no serialization exists.
1358       */
1359  	public function serialize_token(): string {
1360          $html       = '';
1361          $token_type = $this->get_token_type();
1362  
1363          switch ( $token_type ) {
1364              case '#doctype':
1365                  $doctype = $this->get_doctype_info();
1366                  if ( null === $doctype ) {
1367                      break;
1368                  }
1369  
1370                  $html .= '<!DOCTYPE';
1371  
1372                  if ( $doctype->name ) {
1373                      $html .= " {$doctype->name}";
1374                  }
1375  
1376                  if ( null !== $doctype->public_identifier ) {
1377                      $quote = str_contains( $doctype->public_identifier, '"' ) ? "'" : '"';
1378                      $html .= " PUBLIC {$quote}{$doctype->public_identifier}{$quote}";
1379                  }
1380                  if ( null !== $doctype->system_identifier ) {
1381                      if ( null === $doctype->public_identifier ) {
1382                          $html .= ' SYSTEM';
1383                      }
1384                      $quote = str_contains( $doctype->system_identifier, '"' ) ? "'" : '"';
1385                      $html .= " {$quote}{$doctype->system_identifier}{$quote}";
1386                  }
1387  
1388                  $html .= '>';
1389                  break;
1390  
1391              case '#text':
1392                  $html .= self::escape_text_for_serialization( $this->get_modifiable_text() );
1393                  break;
1394  
1395              // Unlike the `<>` which is interpreted as plaintext, this is ignored entirely.
1396              case '#presumptuous-tag':
1397                  break;
1398  
1399              case '#funky-comment':
1400              case '#comment':
1401                  $html .= "<!--{$this->get_full_comment_text()}-->";
1402                  break;
1403  
1404              /**
1405               * Processing instructions are serialized as `"<?" target " " data "?>"`.
1406               *
1407               * @link https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments
1408               */
1409              case '#processing-instruction':
1410                  $html .= "<?{$this->get_tag()} {$this->get_modifiable_text()}?>";
1411                  break;
1412  
1413              case '#cdata-section':
1414                  $html .= "<![CDATA[{$this->get_modifiable_text()}]]>";
1415                  break;
1416          }
1417  
1418          if ( '#tag' !== $token_type ) {
1419              return $html;
1420          }
1421  
1422          $tag_name       = $this->get_tag();
1423          $in_html        = 'html' === $this->get_namespace();
1424          $qualified_name = $in_html ? strtolower( $tag_name ) : $this->get_qualified_tag_name();
1425  
1426          if ( $this->is_tag_closer() ) {
1427              $html .= "</{$qualified_name}>";
1428              return $html;
1429          }
1430  
1431          $attribute_names = $this->get_attribute_names_with_prefix( '' );
1432          if ( ! isset( $attribute_names ) ) {
1433              $html .= "<{$qualified_name}>";
1434              return $html;
1435          }
1436  
1437          $html .= "<{$qualified_name}";
1438  
1439          $previous_attribute_was_true = false;
1440          $seen_attribute_names        = array();
1441          foreach ( $attribute_names as $attribute_name ) {
1442              $qualified_attribute_name = $this->get_qualified_attribute_name( $attribute_name );
1443              $qualified_attribute_name = wp_scrub_utf8( $qualified_attribute_name );
1444              /**
1445               * Spaces only appear via the foreign attribute adjustment table.
1446               * @see WP_HTML_Tag_Processor::get_qualified_attribute_name()
1447               */
1448              $serialized_attribute_name = str_replace( ' ', ':', $qualified_attribute_name );
1449              if ( isset( $seen_attribute_names[ $qualified_attribute_name ] ) ) {
1450                  continue;
1451              } else {
1452                  $seen_attribute_names[ $qualified_attribute_name ] = true;
1453              }
1454  
1455              if (
1456                  $previous_attribute_was_true &&
1457                  isset( $serialized_attribute_name[0] ) &&
1458                  '=' === $serialized_attribute_name[0]
1459              ) {
1460                  $html .= '=""';
1461              }
1462  
1463              $html .= " {$serialized_attribute_name}";
1464              $value = $this->get_attribute( $attribute_name );
1465  
1466              if ( is_string( $value ) ) {
1467                  $html .= '="' . self::escape_text_for_serialization( $value ) . '"';
1468              }
1469  
1470              $previous_attribute_was_true = true === $value;
1471          }
1472  
1473          if ( ! $in_html && $this->has_self_closing_flag() ) {
1474              $html .= ' /';
1475          }
1476  
1477          $html .= '>';
1478  
1479          /*
1480           * The HTML parser strips a leading newline immediately after the start
1481           * tag of TEXTAREA, PRE, and LISTING elements in HTML content. When serializing,
1482           * prepend a leading newline to ensure the semantic HTML content is preserved.
1483           *
1484           * For example, `<pre>\n\nX</pre>` must not become `<pre>\nX</pre>` because its content
1485           * has changed. However, `<pre>X</pre>` and `<pre>\nX</pre>` are _equivalent_.
1486           *
1487           * > A start tag whose tag name is "textarea"
1488           * >   …
1489           * >   If the next token is a U+000A LINE FEED (LF) character token, then ignore
1490           * >   that token and move on to the next one. (Newlines at the start of textarea
1491           * >   elements are ignored as an authoring convenience.)
1492           *
1493           * > A start tag whose tag name is one of: "pre", "listing"
1494           * >   …
1495           * >   If the next token is a U+000A LINE FEED (LF) character token, then ignore
1496           * >   that token and move on to the next one. (Newlines at the start of pre blocks
1497           * >   are ignored as an authoring convenience.)
1498           *
1499           * @see https://html.spec.whatwg.org/multipage/parsing.html
1500           */
1501          if ( $in_html && ( 'TEXTAREA' === $tag_name || 'PRE' === $tag_name || 'LISTING' === $tag_name ) ) {
1502              $html .= "\n";
1503          }
1504  
1505          // Flush out self-contained elements.
1506          if ( $in_html && in_array( $tag_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true ) ) {
1507              $text = $this->get_modifiable_text();
1508  
1509              switch ( $tag_name ) {
1510                  case 'IFRAME':
1511                  case 'NOEMBED':
1512                  case 'NOFRAMES':
1513                  case 'SCRIPT':
1514                  case 'STYLE':
1515                  case 'XMP':
1516                      break;
1517  
1518                  default:
1519                      $text = self::escape_text_for_serialization( $text );
1520              }
1521  
1522              $html .= "{$text}</{$qualified_name}>";
1523          }
1524  
1525          return $html;
1526      }
1527  
1528      /**
1529       * Escapes decoded text for HTML serialization.
1530       *
1531       * Use for:
1532       * - Attribute values.
1533       * - Text in ordinary (data-state) elements and in the RCDATA elements
1534       *   (TITLE and TEXTAREA).
1535       * - Text in foreign content (elements not in the HTML namespace).
1536       *
1537       * Do not use for text in the RAWTEXT elements (STYLE, XMP, IFRAME,
1538       * NOEMBED, NOFRAMES), HTML SCRIPT elements, or PLAINTEXT elements,
1539       * whose contents serialize without escaping.
1540       *
1541       * @since 7.1.0
1542       * @ignore
1543       *
1544       * @param string $text Decoded text to escape.
1545       * @return string Escaped text.
1546       */
1547  	private static function escape_text_for_serialization( string $text ): string {
1548          $text = htmlspecialchars( $text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8' );
1549  
1550          $text = str_replace( "\r", '&#xD;', $text );
1551  
1552          return str_replace( "\x00", "\u{FFFD}", $text );
1553      }
1554  
1555      /**
1556       * Parses next element in the 'initial' insertion mode.
1557       *
1558       * This internal function performs the 'initial' insertion mode
1559       * logic for the generalized WP_HTML_Processor::step() function.
1560       *
1561       * @since 6.7.0
1562       * @ignore
1563       *
1564       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
1565       *
1566       * @see https://html.spec.whatwg.org/#the-initial-insertion-mode
1567       * @see WP_HTML_Processor::step
1568       *
1569       * @return bool Whether an element was found.
1570       */
1571  	private function step_initial(): bool {
1572          $token_name = $this->get_token_name();
1573          $token_type = $this->get_token_type();
1574          $op_sigil   = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : '';
1575          $op         = "{$op_sigil}{$token_name}";
1576  
1577          switch ( $op ) {
1578              /*
1579               * > A character token that is one of U+0009 CHARACTER TABULATION,
1580               * > U+000A LINE FEED (LF), U+000C FORM FEED (FF),
1581               * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
1582               *
1583               * Parse error: ignore the token.
1584               */
1585              case '#text':
1586                  if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
1587                      return $this->step();
1588                  }
1589                  goto initial_anything_else;
1590  
1591              /*
1592               * > A comment token
1593               * > A processing instruction token
1594               */
1595              case '#comment':
1596              case '#funky-comment':
1597              case '#presumptuous-tag':
1598              case '#processing-instruction':
1599                  $this->insert_html_element( $this->state->current_token );
1600                  return true;
1601  
1602              /*
1603               * > A DOCTYPE token
1604               */
1605              case 'html':
1606                  $doctype = $this->get_doctype_info();
1607                  if ( null !== $doctype && 'quirks' === $doctype->indicated_compatibility_mode ) {
1608                      $this->compat_mode = WP_HTML_Tag_Processor::QUIRKS_MODE;
1609                  }
1610  
1611                  /*
1612                   * > Then, switch the insertion mode to "before html".
1613                   */
1614                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML;
1615                  $this->insert_html_element( $this->state->current_token );
1616                  return true;
1617          }
1618  
1619          /*
1620           * > Anything else
1621           */
1622          initial_anything_else:
1623          $this->compat_mode           = WP_HTML_Tag_Processor::QUIRKS_MODE;
1624          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML;
1625          return $this->step( self::REPROCESS_CURRENT_NODE );
1626      }
1627  
1628      /**
1629       * Parses next element in the 'before html' insertion mode.
1630       *
1631       * This internal function performs the 'before html' insertion mode
1632       * logic for the generalized WP_HTML_Processor::step() function.
1633       *
1634       * @since 6.7.0
1635       * @ignore
1636       *
1637       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
1638       *
1639       * @see https://html.spec.whatwg.org/#the-before-html-insertion-mode
1640       * @see WP_HTML_Processor::step
1641       *
1642       * @return bool Whether an element was found.
1643       */
1644  	private function step_before_html(): bool {
1645          $token_name = $this->get_token_name();
1646          $token_type = $this->get_token_type();
1647          $is_closer  = parent::is_tag_closer();
1648          $op_sigil   = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : '';
1649          $op         = "{$op_sigil}{$token_name}";
1650  
1651          switch ( $op ) {
1652              /*
1653               * > A DOCTYPE token
1654               */
1655              case 'html':
1656                  // Parse error: ignore the token.
1657                  return $this->step();
1658  
1659              /*
1660               * > A comment token
1661               * > A processing instruction token
1662               */
1663              case '#comment':
1664              case '#funky-comment':
1665              case '#presumptuous-tag':
1666              case '#processing-instruction':
1667                  $this->insert_html_element( $this->state->current_token );
1668                  return true;
1669  
1670              /*
1671               * > A character token that is one of U+0009 CHARACTER TABULATION,
1672               * > U+000A LINE FEED (LF), U+000C FORM FEED (FF),
1673               * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
1674               *
1675               * Parse error: ignore the token.
1676               */
1677              case '#text':
1678                  if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
1679                      return $this->step();
1680                  }
1681                  goto before_html_anything_else;
1682  
1683              /*
1684               * > A start tag whose tag name is "html"
1685               */
1686              case '+HTML':
1687                  $this->insert_html_element( $this->state->current_token );
1688                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD;
1689                  return true;
1690  
1691              /*
1692               * > An end tag whose tag name is one of: "head", "body", "html", "br"
1693               *
1694               * Closing BR tags are always reported by the Tag Processor as opening tags.
1695               */
1696              case '-HEAD':
1697              case '-BODY':
1698              case '-HTML':
1699                  /*
1700                   * > Act as described in the "anything else" entry below.
1701                   */
1702                  goto before_html_anything_else;
1703          }
1704  
1705          /*
1706           * > Any other end tag
1707           */
1708          if ( $is_closer ) {
1709              // Parse error: ignore the token.
1710              return $this->step();
1711          }
1712  
1713          /*
1714           * > Anything else.
1715           *
1716           * > Create an html element whose node document is the Document object.
1717           * > Append it to the Document object. Put this element in the stack of open elements.
1718           * > Switch the insertion mode to "before head", then reprocess the token.
1719           */
1720          before_html_anything_else:
1721          $this->insert_virtual_node( 'HTML' );
1722          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD;
1723          return $this->step( self::REPROCESS_CURRENT_NODE );
1724      }
1725  
1726      /**
1727       * Parses next element in the 'before head' insertion mode.
1728       *
1729       * This internal function performs the 'before head' insertion mode
1730       * logic for the generalized WP_HTML_Processor::step() function.
1731       *
1732       * @since 6.7.0
1733       * @ignore
1734       *
1735       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
1736       *
1737       * @see https://html.spec.whatwg.org/#the-before-head-insertion-mode
1738       * @see WP_HTML_Processor::step
1739       *
1740       * @return bool Whether an element was found.
1741       */
1742  	private function step_before_head(): bool {
1743          $token_name = $this->get_token_name();
1744          $token_type = $this->get_token_type();
1745          $is_closer  = parent::is_tag_closer();
1746          $op_sigil   = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : '';
1747          $op         = "{$op_sigil}{$token_name}";
1748  
1749          switch ( $op ) {
1750              /*
1751               * > A character token that is one of U+0009 CHARACTER TABULATION,
1752               * > U+000A LINE FEED (LF), U+000C FORM FEED (FF),
1753               * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
1754               *
1755               * Parse error: ignore the token.
1756               */
1757              case '#text':
1758                  if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
1759                      return $this->step();
1760                  }
1761                  goto before_head_anything_else;
1762  
1763              /*
1764               * > A comment token
1765               * > A processing instruction token
1766               */
1767              case '#comment':
1768              case '#funky-comment':
1769              case '#presumptuous-tag':
1770              case '#processing-instruction':
1771                  $this->insert_html_element( $this->state->current_token );
1772                  return true;
1773  
1774              /*
1775               * > A DOCTYPE token
1776               */
1777              case 'html':
1778                  // Parse error: ignore the token.
1779                  return $this->step();
1780  
1781              /*
1782               * > A start tag whose tag name is "html"
1783               */
1784              case '+HTML':
1785                  return $this->step_in_body();
1786  
1787              /*
1788               * > A start tag whose tag name is "head"
1789               */
1790              case '+HEAD':
1791                  $this->insert_html_element( $this->state->current_token );
1792                  $this->state->head_element   = $this->state->current_token;
1793                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD;
1794                  return true;
1795  
1796              /*
1797               * > An end tag whose tag name is one of: "head", "body", "html", "br"
1798               * > Act as described in the "anything else" entry below.
1799               *
1800               * Closing BR tags are always reported by the Tag Processor as opening tags.
1801               */
1802              case '-HEAD':
1803              case '-BODY':
1804              case '-HTML':
1805                  goto before_head_anything_else;
1806          }
1807  
1808          if ( $is_closer ) {
1809              // Parse error: ignore the token.
1810              return $this->step();
1811          }
1812  
1813          /*
1814           * > Anything else
1815           *
1816           * > Insert an HTML element for a "head" start tag token with no attributes.
1817           */
1818          before_head_anything_else:
1819          $this->state->head_element   = $this->insert_virtual_node( 'HEAD' );
1820          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD;
1821          return $this->step( self::REPROCESS_CURRENT_NODE );
1822      }
1823  
1824      /**
1825       * Parses next element in the 'in head' insertion mode.
1826       *
1827       * This internal function performs the 'in head' insertion mode
1828       * logic for the generalized WP_HTML_Processor::step() function.
1829       *
1830       * @since 6.7.0
1831       * @ignore
1832       *
1833       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
1834       *
1835       * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhead
1836       * @see WP_HTML_Processor::step
1837       *
1838       * @return bool Whether an element was found.
1839       */
1840  	private function step_in_head(): bool {
1841          $token_name = $this->get_token_name();
1842          $token_type = $this->get_token_type();
1843          $is_closer  = parent::is_tag_closer();
1844          $op_sigil   = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : '';
1845          $op         = "{$op_sigil}{$token_name}";
1846  
1847          switch ( $op ) {
1848              case '#text':
1849                  /*
1850                   * > A character token that is one of U+0009 CHARACTER TABULATION,
1851                   * > U+000A LINE FEED (LF), U+000C FORM FEED (FF),
1852                   * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
1853                   */
1854                  if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
1855                      // Insert the character.
1856                      $this->insert_html_element( $this->state->current_token );
1857                      return true;
1858                  }
1859  
1860                  goto in_head_anything_else;
1861  
1862              /*
1863               * > A comment token
1864               * > A processing instruction token
1865               */
1866              case '#comment':
1867              case '#funky-comment':
1868              case '#presumptuous-tag':
1869              case '#processing-instruction':
1870                  $this->insert_html_element( $this->state->current_token );
1871                  return true;
1872  
1873              /*
1874               * > A DOCTYPE token
1875               */
1876              case 'html':
1877                  // Parse error: ignore the token.
1878                  return $this->step();
1879  
1880              /*
1881               * > A start tag whose tag name is "html"
1882               */
1883              case '+HTML':
1884                  return $this->step_in_body();
1885  
1886              /*
1887               * > A start tag whose tag name is one of: "base", "basefont", "bgsound", "link"
1888               */
1889              case '+BASE':
1890              case '+BASEFONT':
1891              case '+BGSOUND':
1892              case '+LINK':
1893                  $this->insert_html_element( $this->state->current_token );
1894                  return true;
1895  
1896              /*
1897               * > A start tag whose tag name is "meta"
1898               */
1899              case '+META':
1900                  $this->insert_html_element( $this->state->current_token );
1901  
1902                  // All following conditions depend on "tentative" encoding confidence.
1903                  if ( 'tentative' !== $this->state->encoding_confidence ) {
1904                      return true;
1905                  }
1906  
1907                  /*
1908                   * > If the active speculative HTML parser is null, then:
1909                   * >   - If the element has a charset attribute, and getting an encoding from
1910                   * >     its value results in an encoding, and the confidence is currently
1911                   * >     tentative, then change the encoding to the resulting encoding.
1912                   */
1913                  $charset = $this->get_attribute( 'charset' );
1914                  if ( is_string( $charset ) ) {
1915                      $this->bail( 'Cannot yet process META tags with charset to determine encoding.' );
1916                  }
1917  
1918                  /*
1919                   * >   - Otherwise, if the element has an http-equiv attribute whose value is
1920                   * >     an ASCII case-insensitive match for the string "Content-Type", and
1921                   * >     the element has a content attribute, and applying the algorithm for
1922                   * >     extracting a character encoding from a meta element to that attribute's
1923                   * >     value returns an encoding, and the confidence is currently tentative,
1924                   * >     then change the encoding to the extracted encoding.
1925                   */
1926                  $http_equiv = $this->get_attribute( 'http-equiv' );
1927                  $content    = $this->get_attribute( 'content' );
1928                  if (
1929                      is_string( $http_equiv ) &&
1930                      is_string( $content ) &&
1931                      0 === strcasecmp( $http_equiv, 'Content-Type' )
1932                  ) {
1933                      $this->bail( 'Cannot yet process META tags with http-equiv Content-Type to determine encoding.' );
1934                  }
1935  
1936                  return true;
1937  
1938              /*
1939               * > A start tag whose tag name is "title"
1940               */
1941              case '+TITLE':
1942                  $this->insert_html_element( $this->state->current_token );
1943                  return true;
1944  
1945              /*
1946               * > A start tag whose tag name is "noscript", if the scripting flag is enabled
1947               * > A start tag whose tag name is one of: "noframes", "style"
1948               *
1949               * The scripting flag is never enabled in this parser.
1950               */
1951              case '+NOFRAMES':
1952              case '+STYLE':
1953                  $this->insert_html_element( $this->state->current_token );
1954                  return true;
1955  
1956              /*
1957               * > A start tag whose tag name is "noscript", if the scripting flag is disabled
1958               */
1959              case '+NOSCRIPT':
1960                  $this->insert_html_element( $this->state->current_token );
1961                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD_NOSCRIPT;
1962                  return true;
1963  
1964              /*
1965               * > A start tag whose tag name is "script"
1966               *
1967               * @todo Could the adjusted insertion location be anything other than the current location?
1968               */
1969              case '+SCRIPT':
1970                  $this->insert_html_element( $this->state->current_token );
1971                  return true;
1972  
1973              /*
1974               * > An end tag whose tag name is "head"
1975               */
1976              case '-HEAD':
1977                  $this->state->stack_of_open_elements->pop();
1978                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD;
1979                  return true;
1980  
1981              /*
1982               * > An end tag whose tag name is one of: "body", "html", "br"
1983               *
1984               * BR tags are always reported by the Tag Processor as opening tags.
1985               */
1986              case '-BODY':
1987              case '-HTML':
1988                  /*
1989                   * > Act as described in the "anything else" entry below.
1990                   */
1991                  goto in_head_anything_else;
1992  
1993              /*
1994               * > A start tag whose tag name is "template"
1995               *
1996               * @todo Could the adjusted insertion location be anything other than the current location?
1997               */
1998              case '+TEMPLATE':
1999                  $this->state->active_formatting_elements->insert_marker();
2000                  $this->state->frameset_ok = false;
2001  
2002                  $this->state->insertion_mode                      = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE;
2003                  $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE;
2004  
2005                  $this->insert_html_element( $this->state->current_token );
2006                  return true;
2007  
2008              /*
2009               * > An end tag whose tag name is "template"
2010               */
2011              case '-TEMPLATE':
2012                  if ( ! $this->state->stack_of_open_elements->contains( 'TEMPLATE' ) ) {
2013                      // @todo Indicate a parse error once it's possible.
2014                      return $this->step();
2015                  }
2016  
2017                  $this->generate_implied_end_tags_thoroughly();
2018                  if ( ! $this->state->stack_of_open_elements->current_node_is( 'TEMPLATE' ) ) {
2019                      // @todo Indicate a parse error once it's possible.
2020                  }
2021  
2022                  $this->state->stack_of_open_elements->pop_until( 'TEMPLATE' );
2023                  $this->state->active_formatting_elements->clear_up_to_last_marker();
2024                  array_pop( $this->state->stack_of_template_insertion_modes );
2025                  $this->reset_insertion_mode_appropriately();
2026                  return true;
2027          }
2028  
2029          /*
2030           * > A start tag whose tag name is "head"
2031           * > Any other end tag
2032           */
2033          if ( '+HEAD' === $op || $is_closer ) {
2034              // Parse error: ignore the token.
2035              return $this->step();
2036          }
2037  
2038          /*
2039           * > Anything else
2040           */
2041          in_head_anything_else:
2042          $this->state->stack_of_open_elements->pop();
2043          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD;
2044          return $this->step( self::REPROCESS_CURRENT_NODE );
2045      }
2046  
2047      /**
2048       * Parses next element in the 'in head noscript' insertion mode.
2049       *
2050       * This internal function performs the 'in head noscript' insertion mode
2051       * logic for the generalized WP_HTML_Processor::step() function.
2052       *
2053       * @since 6.7.0
2054       * @ignore
2055       *
2056       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
2057       *
2058       * @see https://html.spec.whatwg.org/#parsing-main-inheadnoscript
2059       * @see WP_HTML_Processor::step
2060       *
2061       * @return bool Whether an element was found.
2062       */
2063  	private function step_in_head_noscript(): bool {
2064          $token_name = $this->get_token_name();
2065          $token_type = $this->get_token_type();
2066          $is_closer  = parent::is_tag_closer();
2067          $op_sigil   = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : '';
2068          $op         = "{$op_sigil}{$token_name}";
2069  
2070          switch ( $op ) {
2071              /*
2072               * > A character token that is one of U+0009 CHARACTER TABULATION,
2073               * > U+000A LINE FEED (LF), U+000C FORM FEED (FF),
2074               * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
2075               *
2076               * Parse error: ignore the token.
2077               */
2078              case '#text':
2079                  if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
2080                      return $this->step_in_head();
2081                  }
2082  
2083                  goto in_head_noscript_anything_else;
2084  
2085              /*
2086               * > A DOCTYPE token
2087               */
2088              case 'html':
2089                  // Parse error: ignore the token.
2090                  return $this->step();
2091  
2092              /*
2093               * > A start tag whose tag name is "html"
2094               */
2095              case '+HTML':
2096                  return $this->step_in_body();
2097  
2098              /*
2099               * > An end tag whose tag name is "noscript"
2100               */
2101              case '-NOSCRIPT':
2102                  $this->state->stack_of_open_elements->pop();
2103                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD;
2104                  return true;
2105  
2106              /*
2107               * > A comment token
2108               * > A processing instruction token
2109               * > A start tag whose tag name is one of: "basefont", "bgsound",
2110               * > "link", "meta", "noframes", "style"
2111               */
2112              case '#comment':
2113              case '#funky-comment':
2114              case '#presumptuous-tag':
2115              case '#processing-instruction':
2116              case '+BASEFONT':
2117              case '+BGSOUND':
2118              case '+LINK':
2119              case '+META':
2120              case '+NOFRAMES':
2121              case '+STYLE':
2122                  return $this->step_in_head();
2123  
2124              /*
2125               * > An end tag whose tag name is "br"
2126               *
2127               * This should never happen, as the Tag Processor prevents showing a BR closing tag.
2128               */
2129          }
2130  
2131          /*
2132           * > A start tag whose tag name is one of: "head", "noscript"
2133           * > Any other end tag
2134           */
2135          if ( '+HEAD' === $op || '+NOSCRIPT' === $op || $is_closer ) {
2136              // Parse error: ignore the token.
2137              return $this->step();
2138          }
2139  
2140          /*
2141           * > Anything else
2142           *
2143           * Anything here is a parse error.
2144           */
2145          in_head_noscript_anything_else:
2146          $this->state->stack_of_open_elements->pop();
2147          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD;
2148          return $this->step( self::REPROCESS_CURRENT_NODE );
2149      }
2150  
2151      /**
2152       * Parses next element in the 'after head' insertion mode.
2153       *
2154       * This internal function performs the 'after head' insertion mode
2155       * logic for the generalized WP_HTML_Processor::step() function.
2156       *
2157       * @since 6.7.0
2158       * @ignore
2159       *
2160       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
2161       *
2162       * @see https://html.spec.whatwg.org/#the-after-head-insertion-mode
2163       * @see WP_HTML_Processor::step
2164       *
2165       * @return bool Whether an element was found.
2166       */
2167  	private function step_after_head(): bool {
2168          $token_name = $this->get_token_name();
2169          $token_type = $this->get_token_type();
2170          $is_closer  = parent::is_tag_closer();
2171          $op_sigil   = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : '';
2172          $op         = "{$op_sigil}{$token_name}";
2173  
2174          switch ( $op ) {
2175              /*
2176               * > A character token that is one of U+0009 CHARACTER TABULATION,
2177               * > U+000A LINE FEED (LF), U+000C FORM FEED (FF),
2178               * > U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
2179               */
2180              case '#text':
2181                  if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
2182                      // Insert the character.
2183                      $this->insert_html_element( $this->state->current_token );
2184                      return true;
2185                  }
2186                  goto after_head_anything_else;
2187  
2188              /*
2189               * > A comment token
2190               * > A processing instruction token
2191               */
2192              case '#comment':
2193              case '#funky-comment':
2194              case '#presumptuous-tag':
2195              case '#processing-instruction':
2196                  $this->insert_html_element( $this->state->current_token );
2197                  return true;
2198  
2199              /*
2200               * > A DOCTYPE token
2201               */
2202              case 'html':
2203                  // Parse error: ignore the token.
2204                  return $this->step();
2205  
2206              /*
2207               * > A start tag whose tag name is "html"
2208               */
2209              case '+HTML':
2210                  return $this->step_in_body();
2211  
2212              /*
2213               * > A start tag whose tag name is "body"
2214               */
2215              case '+BODY':
2216                  $this->insert_html_element( $this->state->current_token );
2217                  $this->state->frameset_ok    = false;
2218                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
2219                  return true;
2220  
2221              /*
2222               * > A start tag whose tag name is "frameset"
2223               */
2224              case '+FRAMESET':
2225                  $this->insert_html_element( $this->state->current_token );
2226                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_FRAMESET;
2227                  return true;
2228  
2229              /*
2230               * > A start tag whose tag name is one of: "base", "basefont", "bgsound",
2231               * > "link", "meta", "noframes", "script", "style", "template", "title"
2232               *
2233               * Anything here is a parse error.
2234               */
2235              case '+BASE':
2236              case '+BASEFONT':
2237              case '+BGSOUND':
2238              case '+LINK':
2239              case '+META':
2240              case '+NOFRAMES':
2241              case '+SCRIPT':
2242              case '+STYLE':
2243              case '+TEMPLATE':
2244              case '+TITLE':
2245                  /*
2246                   * > Push the node pointed to by the head element pointer onto the stack of open elements.
2247                   * > Process the token using the rules for the "in head" insertion mode.
2248                   * > Remove the node pointed to by the head element pointer from the stack of open elements. (It might not be the current node at this point.)
2249                   */
2250                  $this->bail( 'Cannot process elements after HEAD which reopen the HEAD element.' );
2251                  /*
2252                   * Do not leave this break in when adding support; it's here to prevent
2253                   * WPCS from getting confused at the switch structure without a return,
2254                   * because it doesn't know that `bail()` always throws.
2255                   */
2256                  break;
2257  
2258              /*
2259               * > An end tag whose tag name is "template"
2260               */
2261              case '-TEMPLATE':
2262                  return $this->step_in_head();
2263  
2264              /*
2265               * > An end tag whose tag name is one of: "body", "html", "br"
2266               *
2267               * Closing BR tags are always reported by the Tag Processor as opening tags.
2268               */
2269              case '-BODY':
2270              case '-HTML':
2271                  /*
2272                   * > Act as described in the "anything else" entry below.
2273                   */
2274                  goto after_head_anything_else;
2275          }
2276  
2277          /*
2278           * > A start tag whose tag name is "head"
2279           * > Any other end tag
2280           */
2281          if ( '+HEAD' === $op || $is_closer ) {
2282              // Parse error: ignore the token.
2283              return $this->step();
2284          }
2285  
2286          /*
2287           * > Anything else
2288           * > Insert an HTML element for a "body" start tag token with no attributes.
2289           */
2290          after_head_anything_else:
2291          $this->insert_virtual_node( 'BODY' );
2292          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
2293          return $this->step( self::REPROCESS_CURRENT_NODE );
2294      }
2295  
2296      /**
2297       * Parses next element in the 'in body' insertion mode.
2298       *
2299       * This internal function performs the 'in body' insertion mode
2300       * logic for the generalized WP_HTML_Processor::step() function.
2301       *
2302       * @since 6.4.0
2303       * @ignore
2304       *
2305       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
2306       *
2307       * @see https://html.spec.whatwg.org/#parsing-main-inbody
2308       * @see WP_HTML_Processor::step
2309       *
2310       * @return bool Whether an element was found.
2311       */
2312  	private function step_in_body(): bool {
2313          $token_name = $this->get_token_name();
2314          $token_type = $this->get_token_type();
2315          $op_sigil   = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : '';
2316          $op         = "{$op_sigil}{$token_name}";
2317  
2318          switch ( $op ) {
2319              case '#text':
2320                  /*
2321                   * > A character token that is U+0000 NULL
2322                   *
2323                   * Any successive sequence of NULL bytes is ignored and won't
2324                   * trigger active format reconstruction. Therefore, if the text
2325                   * only comprises NULL bytes then the token should be ignored
2326                   * here, but if there are any other characters in the stream
2327                   * the active formats should be reconstructed.
2328                   */
2329                  if ( parent::TEXT_IS_NULL_SEQUENCE === $this->text_node_classification ) {
2330                      // Parse error: ignore the token.
2331                      return $this->step();
2332                  }
2333  
2334                  $this->reconstruct_active_formatting_elements();
2335  
2336                  /*
2337                   * Whitespace-only text does not affect the frameset-ok flag.
2338                   * It is probably inter-element whitespace, but it may also
2339                   * contain character references which decode only to whitespace.
2340                   */
2341                  if ( parent::TEXT_IS_GENERIC === $this->text_node_classification ) {
2342                      $this->state->frameset_ok = false;
2343                  }
2344  
2345                  $this->insert_html_element( $this->state->current_token );
2346                  return true;
2347  
2348              case '#comment':
2349              case '#funky-comment':
2350              case '#presumptuous-tag':
2351              case '#processing-instruction':
2352                  $this->insert_html_element( $this->state->current_token );
2353                  return true;
2354  
2355              /*
2356               * > A DOCTYPE token
2357               * > Parse error. Ignore the token.
2358               */
2359              case 'html':
2360                  return $this->step();
2361  
2362              /*
2363               * > A start tag whose tag name is "html"
2364               */
2365              case '+HTML':
2366                  if ( ! $this->state->stack_of_open_elements->contains( 'TEMPLATE' ) ) {
2367                      /*
2368                       * > Otherwise, for each attribute on the token, check to see if the attribute
2369                       * > is already present on the top element of the stack of open elements. If
2370                       * > it is not, add the attribute and its corresponding value to that element.
2371                       *
2372                       * This parser does not currently support this behavior: ignore the token.
2373                       */
2374                  }
2375  
2376                  // Ignore the token.
2377                  return $this->step();
2378  
2379              /*
2380               * > A start tag whose tag name is one of: "base", "basefont", "bgsound", "link",
2381               * > "meta", "noframes", "script", "style", "template", "title"
2382               * >
2383               * > An end tag whose tag name is "template"
2384               */
2385              case '+BASE':
2386              case '+BASEFONT':
2387              case '+BGSOUND':
2388              case '+LINK':
2389              case '+META':
2390              case '+NOFRAMES':
2391              case '+SCRIPT':
2392              case '+STYLE':
2393              case '+TEMPLATE':
2394              case '+TITLE':
2395              case '-TEMPLATE':
2396                  return $this->step_in_head();
2397  
2398              /*
2399               * > A start tag whose tag name is "body"
2400               *
2401               * This tag in the IN BODY insertion mode is a parse error.
2402               */
2403              case '+BODY':
2404                  if (
2405                      1 === $this->state->stack_of_open_elements->count() ||
2406                      'BODY' !== ( $this->state->stack_of_open_elements->at( 2 )->node_name ?? null ) ||
2407                      $this->state->stack_of_open_elements->contains( 'TEMPLATE' )
2408                  ) {
2409                      // Ignore the token.
2410                      return $this->step();
2411                  }
2412  
2413                  /*
2414                   * > Otherwise, set the frameset-ok flag to "not ok"; then, for each attribute
2415                   * > on the token, check to see if the attribute is already present on the body
2416                   * > element (the second element) on the stack of open elements, and if it is
2417                   * > not, add the attribute and its corresponding value to that element.
2418                   *
2419                   * This parser does not currently support this behavior: ignore the token.
2420                   */
2421                  $this->state->frameset_ok = false;
2422                  return $this->step();
2423  
2424              /*
2425               * > A start tag whose tag name is "frameset"
2426               *
2427               * This tag in the IN BODY insertion mode is a parse error.
2428               */
2429              case '+FRAMESET':
2430                  if (
2431                      1 === $this->state->stack_of_open_elements->count() ||
2432                      'BODY' !== ( $this->state->stack_of_open_elements->at( 2 )->node_name ?? null ) ||
2433                      false === $this->state->frameset_ok
2434                  ) {
2435                      // Ignore the token.
2436                      return $this->step();
2437                  }
2438  
2439                  /*
2440                   * > Otherwise, run the following steps:
2441                   */
2442                  $this->bail( 'Cannot process non-ignored FRAMESET tags.' );
2443                  break;
2444  
2445              /*
2446               * > An end tag whose tag name is "body"
2447               */
2448              case '-BODY':
2449                  if ( ! $this->state->stack_of_open_elements->has_element_in_scope( 'BODY' ) ) {
2450                      // Parse error: ignore the token.
2451                      return $this->step();
2452                  }
2453  
2454                  /*
2455                   * > Otherwise, if there is a node in the stack of open elements that is not either a
2456                   * > dd element, a dt element, an li element, an optgroup element, an option element,
2457                   * > a p element, an rb element, an rp element, an rt element, an rtc element, a tbody
2458                   * > element, a td element, a tfoot element, a th element, a thread element, a tr
2459                   * > element, the body element, or the html element, then this is a parse error.
2460                   *
2461                   * There is nothing to do for this parse error, so don't check for it.
2462                   */
2463  
2464                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_BODY;
2465                  /*
2466                   * The BODY element is not removed from the stack of open elements.
2467                   * Only internal state has changed, this does not qualify as a "step"
2468                   * in terms of advancing through the document to another token.
2469                   * Nothing has been pushed or popped.
2470                   * Proceed to parse the next item.
2471                   */
2472                  return $this->step();
2473  
2474              /*
2475               * > An end tag whose tag name is "html"
2476               */
2477              case '-HTML':
2478                  if ( ! $this->state->stack_of_open_elements->has_element_in_scope( 'BODY' ) ) {
2479                      // Parse error: ignore the token.
2480                      return $this->step();
2481                  }
2482  
2483                  /*
2484                   * > Otherwise, if there is a node in the stack of open elements that is not either a
2485                   * > dd element, a dt element, an li element, an optgroup element, an option element,
2486                   * > a p element, an rb element, an rp element, an rt element, an rtc element, a tbody
2487                   * > element, a td element, a tfoot element, a th element, a thread element, a tr
2488                   * > element, the body element, or the html element, then this is a parse error.
2489                   *
2490                   * There is nothing to do for this parse error, so don't check for it.
2491                   */
2492  
2493                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_BODY;
2494                  return $this->step( self::REPROCESS_CURRENT_NODE );
2495  
2496              /*
2497               * > A start tag whose tag name is one of: "address", "article", "aside",
2498               * > "blockquote", "center", "details", "dialog", "dir", "div", "dl",
2499               * > "fieldset", "figcaption", "figure", "footer", "header", "hgroup",
2500               * > "main", "menu", "nav", "ol", "p", "search", "section", "summary", "ul"
2501               */
2502              case '+ADDRESS':
2503              case '+ARTICLE':
2504              case '+ASIDE':
2505              case '+BLOCKQUOTE':
2506              case '+CENTER':
2507              case '+DETAILS':
2508              case '+DIALOG':
2509              case '+DIR':
2510              case '+DIV':
2511              case '+DL':
2512              case '+FIELDSET':
2513              case '+FIGCAPTION':
2514              case '+FIGURE':
2515              case '+FOOTER':
2516              case '+HEADER':
2517              case '+HGROUP':
2518              case '+MAIN':
2519              case '+MENU':
2520              case '+NAV':
2521              case '+OL':
2522              case '+P':
2523              case '+SEARCH':
2524              case '+SECTION':
2525              case '+SUMMARY':
2526              case '+UL':
2527                  if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
2528                      $this->close_a_p_element();
2529                  }
2530  
2531                  $this->insert_html_element( $this->state->current_token );
2532                  return true;
2533  
2534              /*
2535               * > A start tag whose tag name is one of: "h1", "h2", "h3", "h4", "h5", "h6"
2536               */
2537              case '+H1':
2538              case '+H2':
2539              case '+H3':
2540              case '+H4':
2541              case '+H5':
2542              case '+H6':
2543                  if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
2544                      $this->close_a_p_element();
2545                  }
2546  
2547                  if (
2548                      in_array(
2549                          $this->state->stack_of_open_elements->current_node()->node_name,
2550                          array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ),
2551                          true
2552                      )
2553                  ) {
2554                      // @todo Indicate a parse error once it's possible.
2555                      $this->state->stack_of_open_elements->pop();
2556                  }
2557  
2558                  $this->insert_html_element( $this->state->current_token );
2559                  return true;
2560  
2561              /*
2562               * > A start tag whose tag name is one of: "pre", "listing"
2563               */
2564              case '+PRE':
2565              case '+LISTING':
2566                  if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
2567                      $this->close_a_p_element();
2568                  }
2569  
2570                  /*
2571                   * > If the next token is a U+000A LINE FEED (LF) character token,
2572                   * > then ignore that token and move on to the next one. (Newlines
2573                   * > at the start of pre blocks are ignored as an authoring convenience.)
2574                   *
2575                   * This is handled in `get_modifiable_text()`.
2576                   */
2577  
2578                  $this->insert_html_element( $this->state->current_token );
2579                  $this->state->frameset_ok = false;
2580                  return true;
2581  
2582              /*
2583               * > A start tag whose tag name is "form"
2584               */
2585              case '+FORM':
2586                  $stack_contains_template = $this->state->stack_of_open_elements->contains( 'TEMPLATE' );
2587  
2588                  if ( isset( $this->state->form_element ) && ! $stack_contains_template ) {
2589                      // Parse error: ignore the token.
2590                      return $this->step();
2591                  }
2592  
2593                  if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
2594                      $this->close_a_p_element();
2595                  }
2596  
2597                  $this->insert_html_element( $this->state->current_token );
2598                  if ( ! $stack_contains_template ) {
2599                      $this->state->form_element = $this->state->current_token;
2600                  }
2601  
2602                  return true;
2603  
2604              /*
2605               * > A start tag whose tag name is "li"
2606               * > A start tag whose tag name is one of: "dd", "dt"
2607               */
2608              case '+DD':
2609              case '+DT':
2610              case '+LI':
2611                  $this->state->frameset_ok = false;
2612                  $node                     = $this->state->stack_of_open_elements->current_node();
2613                  $is_li                    = 'LI' === $token_name;
2614  
2615                  in_body_list_loop:
2616                  /*
2617                   * The logic for LI and DT/DD is the same except for one point: LI elements _only_
2618                   * close other LI elements, but a DT or DD element closes _any_ open DT or DD element.
2619                   */
2620                  if ( $is_li ? 'LI' === $node->node_name : ( 'DD' === $node->node_name || 'DT' === $node->node_name ) ) {
2621                      $node_name = $is_li ? 'LI' : $node->node_name;
2622                      $this->generate_implied_end_tags( $node_name );
2623                      if ( ! $this->state->stack_of_open_elements->current_node_is( $node_name ) ) {
2624                          // @todo Indicate a parse error once it's possible. This error does not impact the logic here.
2625                      }
2626  
2627                      $this->state->stack_of_open_elements->pop_until( $node_name );
2628                      goto in_body_list_done;
2629                  }
2630  
2631                  if (
2632                      'ADDRESS' !== $node->node_name &&
2633                      'DIV' !== $node->node_name &&
2634                      'P' !== $node->node_name &&
2635                      self::is_special( $node )
2636                  ) {
2637                      /*
2638                       * > If node is in the special category, but is not an address, div,
2639                       * > or p element, then jump to the step labeled done below.
2640                       */
2641                      goto in_body_list_done;
2642                  } else {
2643                      /*
2644                       * > Otherwise, set node to the previous entry in the stack of open elements
2645                       * > and return to the step labeled loop.
2646                       */
2647                      foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $item ) {
2648                          $node = $item;
2649                          break;
2650                      }
2651                      goto in_body_list_loop;
2652                  }
2653  
2654                  in_body_list_done:
2655                  if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
2656                      $this->close_a_p_element();
2657                  }
2658  
2659                  $this->insert_html_element( $this->state->current_token );
2660                  return true;
2661  
2662              case '+PLAINTEXT':
2663                  if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
2664                      $this->close_a_p_element();
2665                  }
2666  
2667                  /*
2668                   * @todo This may need to be handled in the Tag Processor and turn into
2669                   *       a single self-contained tag like TEXTAREA, whose modifiable text
2670                   *       is the rest of the input document as plaintext.
2671                   */
2672                  $this->bail( 'Cannot process PLAINTEXT elements.' );
2673                  break;
2674  
2675              /*
2676               * > A start tag whose tag name is "button"
2677               */
2678              case '+BUTTON':
2679                  if ( $this->state->stack_of_open_elements->has_element_in_scope( 'BUTTON' ) ) {
2680                      // @todo Indicate a parse error once it's possible. This error does not impact the logic here.
2681                      $this->generate_implied_end_tags();
2682                      $this->state->stack_of_open_elements->pop_until( 'BUTTON' );
2683                  }
2684  
2685                  $this->reconstruct_active_formatting_elements();
2686                  $this->insert_html_element( $this->state->current_token );
2687                  $this->state->frameset_ok = false;
2688  
2689                  return true;
2690  
2691              /*
2692               * > An end tag whose tag name is one of: "address", "article", "aside", "blockquote",
2693               * > "button", "center", "details", "dialog", "dir", "div", "dl", "fieldset",
2694               * > "figcaption", "figure", "footer", "header", "hgroup", "listing", "main",
2695               * > "menu", "nav", "ol", "pre", "search", "section", "select", "summary", "ul"
2696               */
2697              case '-ADDRESS':
2698              case '-ARTICLE':
2699              case '-ASIDE':
2700              case '-BLOCKQUOTE':
2701              case '-BUTTON':
2702              case '-CENTER':
2703              case '-DETAILS':
2704              case '-DIALOG':
2705              case '-DIR':
2706              case '-DIV':
2707              case '-DL':
2708              case '-FIELDSET':
2709              case '-FIGCAPTION':
2710              case '-FIGURE':
2711              case '-FOOTER':
2712              case '-HEADER':
2713              case '-HGROUP':
2714              case '-LISTING':
2715              case '-MAIN':
2716              case '-MENU':
2717              case '-NAV':
2718              case '-OL':
2719              case '-PRE':
2720              case '-SEARCH':
2721              case '-SECTION':
2722              case '-SELECT':
2723              case '-SUMMARY':
2724              case '-UL':
2725                  if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $token_name ) ) {
2726                      // @todo Report parse error.
2727                      // Ignore the token.
2728                      return $this->step();
2729                  }
2730  
2731                  $this->generate_implied_end_tags();
2732                  if ( ! $this->state->stack_of_open_elements->current_node_is( $token_name ) ) {
2733                      // @todo Record parse error: this error doesn't impact parsing.
2734                  }
2735                  $this->state->stack_of_open_elements->pop_until( $token_name );
2736                  return true;
2737  
2738              /*
2739               * > An end tag whose tag name is "form"
2740               */
2741              case '-FORM':
2742                  if ( ! $this->state->stack_of_open_elements->contains( 'TEMPLATE' ) ) {
2743                      $node = $this->state->form_element;
2744  
2745                      /*
2746                       * > If node is null or if the stack of open elements does not have node
2747                       * > in scope, then this is a parse error; return and ignore the token.
2748                       *
2749                       * @todo It's necessary to check if the form token itself is in scope, not
2750                       *       simply whether any FORM is in scope.
2751                       */
2752                      if (
2753                          null === $node ||
2754                          ! $this->state->stack_of_open_elements->has_element_in_scope( 'FORM' )
2755                      ) {
2756                          /*
2757                           * Parse error: ignore the token.
2758                           *
2759                           * Keep the form pointer intact when the end tag is ignored, such as
2760                           * when a FORM closing tag appears inside an SVG TITLE integration
2761                           * point. Otherwise the ignored token changes parser state in a way
2762                           * that serialization cannot represent, allowing a later FORM opener
2763                           * to appear in the first normalization pass and disappear on the second.
2764                           */
2765                          return $this->step();
2766                      }
2767  
2768                      $this->state->form_element = null;
2769  
2770                      $this->generate_implied_end_tags();
2771                      if ( $node !== $this->state->stack_of_open_elements->current_node() ) {
2772                          // @todo Indicate a parse error once it's possible. This error does not impact the logic here.
2773                          $this->bail( 'Cannot close a FORM when other elements remain open as this would throw off the breadcrumbs for the following tokens.' );
2774                      }
2775  
2776                      $this->state->stack_of_open_elements->remove_node( $node );
2777                      return true;
2778                  } else {
2779                      /*
2780                       * > If the stack of open elements does not have a form element in scope,
2781                       * > then this is a parse error; return and ignore the token.
2782                       *
2783                       * Note that unlike in the clause above, this is checking for any FORM in scope.
2784                       */
2785                      if ( ! $this->state->stack_of_open_elements->has_element_in_scope( 'FORM' ) ) {
2786                          // Parse error: ignore the token.
2787                          return $this->step();
2788                      }
2789  
2790                      $this->generate_implied_end_tags();
2791  
2792                      if ( ! $this->state->stack_of_open_elements->current_node_is( 'FORM' ) ) {
2793                          // @todo Indicate a parse error once it's possible. This error does not impact the logic here.
2794                      }
2795  
2796                      $this->state->stack_of_open_elements->pop_until( 'FORM' );
2797                      return true;
2798                  }
2799                  break;
2800  
2801              /*
2802               * > An end tag whose tag name is "p"
2803               */
2804              case '-P':
2805                  if ( ! $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
2806                      $this->insert_html_element( $this->state->current_token );
2807                  }
2808  
2809                  $this->close_a_p_element();
2810                  return true;
2811  
2812              /*
2813               * > An end tag whose tag name is "li"
2814               * > An end tag whose tag name is one of: "dd", "dt"
2815               */
2816              case '-DD':
2817              case '-DT':
2818              case '-LI':
2819                  if (
2820                      /*
2821                       * An end tag whose tag name is "li":
2822                       * If the stack of open elements does not have an li element in list item scope,
2823                       * then this is a parse error; ignore the token.
2824                       */
2825                      (
2826                          'LI' === $token_name &&
2827                          ! $this->state->stack_of_open_elements->has_element_in_list_item_scope( 'LI' )
2828                      ) ||
2829                      /*
2830                       * An end tag whose tag name is one of: "dd", "dt":
2831                       * If the stack of open elements does not have an element in scope that is an
2832                       * HTML element with the same tag name as that of the token, then this is a
2833                       * parse error; ignore the token.
2834                       */
2835                      (
2836                          'LI' !== $token_name &&
2837                          ! $this->state->stack_of_open_elements->has_element_in_scope( $token_name )
2838                      )
2839                  ) {
2840                      /*
2841                       * This is a parse error, ignore the token.
2842                       *
2843                       * @todo Indicate a parse error once it's possible.
2844                       */
2845                      return $this->step();
2846                  }
2847  
2848                  $this->generate_implied_end_tags( $token_name );
2849  
2850                  if ( ! $this->state->stack_of_open_elements->current_node_is( $token_name ) ) {
2851                      // @todo Indicate a parse error once it's possible. This error does not impact the logic here.
2852                  }
2853  
2854                  $this->state->stack_of_open_elements->pop_until( $token_name );
2855                  return true;
2856  
2857              /*
2858               * > An end tag whose tag name is one of: "h1", "h2", "h3", "h4", "h5", "h6"
2859               */
2860              case '-H1':
2861              case '-H2':
2862              case '-H3':
2863              case '-H4':
2864              case '-H5':
2865              case '-H6':
2866                  if ( ! $this->state->stack_of_open_elements->has_element_in_scope( '(internal: H1 through H6 - do not use)' ) ) {
2867                      /*
2868                       * This is a parse error; ignore the token.
2869                       *
2870                       * @todo Indicate a parse error once it's possible.
2871                       */
2872                      return $this->step();
2873                  }
2874  
2875                  $this->generate_implied_end_tags();
2876  
2877                  if ( ! $this->state->stack_of_open_elements->current_node_is( $token_name ) ) {
2878                      // @todo Record parse error: this error doesn't impact parsing.
2879                  }
2880  
2881                  $this->state->stack_of_open_elements->pop_until( '(internal: H1 through H6 - do not use)' );
2882                  return true;
2883  
2884              /*
2885               * > A start tag whose tag name is "a"
2886               */
2887              case '+A':
2888                  foreach ( $this->state->active_formatting_elements->walk_up() as $item ) {
2889                      switch ( $item->node_name ) {
2890                          case 'marker':
2891                              break 2;
2892  
2893                          case 'A':
2894                              $this->run_adoption_agency_algorithm();
2895                              $this->state->active_formatting_elements->remove_node( $item );
2896                              $this->state->stack_of_open_elements->remove_node( $item );
2897                              break 2;
2898                      }
2899                  }
2900  
2901                  $this->reconstruct_active_formatting_elements();
2902                  $this->insert_html_element( $this->state->current_token );
2903                  $this->state->active_formatting_elements->push( $this->state->current_token );
2904                  return true;
2905  
2906              /*
2907               * > A start tag whose tag name is one of: "b", "big", "code", "em", "font", "i",
2908               * > "s", "small", "strike", "strong", "tt", "u"
2909               */
2910              case '+B':
2911              case '+BIG':
2912              case '+CODE':
2913              case '+EM':
2914              case '+FONT':
2915              case '+I':
2916              case '+S':
2917              case '+SMALL':
2918              case '+STRIKE':
2919              case '+STRONG':
2920              case '+TT':
2921              case '+U':
2922                  $this->reconstruct_active_formatting_elements();
2923                  $this->insert_html_element( $this->state->current_token );
2924                  $this->state->active_formatting_elements->push( $this->state->current_token );
2925                  return true;
2926  
2927              /*
2928               * > A start tag whose tag name is "nobr"
2929               */
2930              case '+NOBR':
2931                  $this->reconstruct_active_formatting_elements();
2932  
2933                  if ( $this->state->stack_of_open_elements->has_element_in_scope( 'NOBR' ) ) {
2934                      // Parse error.
2935                      $this->run_adoption_agency_algorithm();
2936                      $this->reconstruct_active_formatting_elements();
2937                  }
2938  
2939                  $this->insert_html_element( $this->state->current_token );
2940                  $this->state->active_formatting_elements->push( $this->state->current_token );
2941                  return true;
2942  
2943              /*
2944               * > An end tag whose tag name is one of: "a", "b", "big", "code", "em", "font", "i",
2945               * > "nobr", "s", "small", "strike", "strong", "tt", "u"
2946               */
2947              case '-A':
2948              case '-B':
2949              case '-BIG':
2950              case '-CODE':
2951              case '-EM':
2952              case '-FONT':
2953              case '-I':
2954              case '-NOBR':
2955              case '-S':
2956              case '-SMALL':
2957              case '-STRIKE':
2958              case '-STRONG':
2959              case '-TT':
2960              case '-U':
2961                  $this->run_adoption_agency_algorithm();
2962                  return true;
2963  
2964              /*
2965               * > A start tag whose tag name is one of: "applet", "marquee", "object"
2966               */
2967              case '+APPLET':
2968              case '+MARQUEE':
2969              case '+OBJECT':
2970                  $this->reconstruct_active_formatting_elements();
2971                  $this->insert_html_element( $this->state->current_token );
2972                  $this->state->active_formatting_elements->insert_marker();
2973                  $this->state->frameset_ok = false;
2974                  return true;
2975  
2976              /*
2977               * > A end tag token whose tag name is one of: "applet", "marquee", "object"
2978               */
2979              case '-APPLET':
2980              case '-MARQUEE':
2981              case '-OBJECT':
2982                  if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $token_name ) ) {
2983                      // Parse error: ignore the token.
2984                      return $this->step();
2985                  }
2986  
2987                  $this->generate_implied_end_tags();
2988                  if ( ! $this->state->stack_of_open_elements->current_node_is( $token_name ) ) {
2989                      // This is a parse error.
2990                  }
2991  
2992                  $this->state->stack_of_open_elements->pop_until( $token_name );
2993                  $this->state->active_formatting_elements->clear_up_to_last_marker();
2994                  return true;
2995  
2996              /*
2997               * > A start tag whose tag name is "table"
2998               */
2999              case '+TABLE':
3000                  /*
3001                   * > If the Document is not set to quirks mode, and the stack of open elements
3002                   * > has a p element in button scope, then close a p element.
3003                   */
3004                  if (
3005                      WP_HTML_Tag_Processor::QUIRKS_MODE !== $this->compat_mode &&
3006                      $this->state->stack_of_open_elements->has_p_in_button_scope()
3007                  ) {
3008                      $this->close_a_p_element();
3009                  }
3010  
3011                  $this->insert_html_element( $this->state->current_token );
3012                  $this->state->frameset_ok    = false;
3013                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
3014                  return true;
3015  
3016              /*
3017               * > An end tag whose tag name is "br"
3018               *
3019               * This is prevented from happening because the Tag Processor
3020               * reports all closing BR tags as if they were opening tags.
3021               */
3022  
3023              /*
3024               * > A start tag whose tag name is one of: "area", "br", "embed", "img", "keygen", "wbr"
3025               */
3026              case '+AREA':
3027              case '+BR':
3028              case '+EMBED':
3029              case '+IMG':
3030              case '+KEYGEN':
3031              case '+WBR':
3032                  $this->reconstruct_active_formatting_elements();
3033                  $this->insert_html_element( $this->state->current_token );
3034                  $this->state->frameset_ok = false;
3035                  return true;
3036  
3037              /*
3038               * > A start tag whose tag name is "input"
3039               */
3040              case '+INPUT':
3041                  /*
3042                   * > If the parser was created as part of the HTML fragment parsing algorithm
3043                   * > (fragment case) and the context element passed to that algorithm is a
3044                   * > select element:
3045                   * >   1. Parse error.
3046                   * >   2. Ignore the token.
3047                   * >   3. Return.
3048                   */
3049                  if ( isset( $this->context_node ) && 'SELECT' === $this->context_node->node_name ) {
3050                      return $this->step();
3051                  }
3052  
3053                  /*
3054                   * > If the stack of open elements has a select element in scope:
3055                   * >   1. Parse error.
3056                   * >   2. Pop elements from the stack of open elements until a select element
3057                   * >      has been popped from the stack.
3058                   */
3059                  if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) {
3060                      $this->state->stack_of_open_elements->pop_until( 'SELECT' );
3061                  }
3062  
3063                  $this->reconstruct_active_formatting_elements();
3064                  $this->insert_html_element( $this->state->current_token );
3065  
3066                  /*
3067                   * > If the token does not have an attribute with the name "type", or if it does,
3068                   * > but that attribute's value is not an ASCII case-insensitive match for the
3069                   * > string "hidden", then: set the frameset-ok flag to "not ok".
3070                   */
3071                  $type_attribute = $this->get_attribute( 'type' );
3072                  if ( ! is_string( $type_attribute ) || 'hidden' !== strtolower( $type_attribute ) ) {
3073                      $this->state->frameset_ok = false;
3074                  }
3075  
3076                  return true;
3077  
3078              /*
3079               * > A start tag whose tag name is one of: "param", "source", "track"
3080               */
3081              case '+PARAM':
3082              case '+SOURCE':
3083              case '+TRACK':
3084                  $this->insert_html_element( $this->state->current_token );
3085                  return true;
3086  
3087              /*
3088               * > A start tag whose tag name is "hr"
3089               */
3090              case '+HR':
3091                  if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
3092                      $this->close_a_p_element();
3093                  }
3094  
3095                  if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) {
3096                      $this->generate_implied_end_tags();
3097                      /*
3098                       * > If the stack of open elements has an option element in scope or has
3099                       * > an optgroup element in scope, then this is a parse error.
3100                       *
3101                       * @todo Indicate a parse error once it's possible.
3102                       */
3103                  }
3104  
3105                  $this->insert_html_element( $this->state->current_token );
3106                  $this->state->frameset_ok = false;
3107                  return true;
3108  
3109              /*
3110               * > A start tag whose tag name is "image"
3111               */
3112              case '+IMAGE':
3113                  /*
3114                   * > Parse error. Change the token's tag name to "img" and reprocess it. (Don't ask.)
3115                   *
3116                   * Note that this is handled elsewhere, so it should not be possible to reach this code.
3117                   */
3118                  $this->bail( "Cannot process an IMAGE tag. (Don't ask.)" );
3119                  break;
3120  
3121              /*
3122               * > A start tag whose tag name is "textarea"
3123               */
3124              case '+TEXTAREA':
3125                  $this->insert_html_element( $this->state->current_token );
3126  
3127                  /*
3128                   * > If the next token is a U+000A LINE FEED (LF) character token, then ignore
3129                   * > that token and move on to the next one. (Newlines at the start of
3130                   * > textarea elements are ignored as an authoring convenience.)
3131                   *
3132                   * This is handled in `get_modifiable_text()`.
3133                   */
3134  
3135                  $this->state->frameset_ok = false;
3136  
3137                  /*
3138                   * > Switch the insertion mode to "text".
3139                   *
3140                   * As a self-contained node, this behavior is handled in the Tag Processor.
3141                   */
3142                  return true;
3143  
3144              /*
3145               * > A start tag whose tag name is "xmp"
3146               */
3147              case '+XMP':
3148                  if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) {
3149                      $this->close_a_p_element();
3150                  }
3151  
3152                  $this->reconstruct_active_formatting_elements();
3153                  $this->state->frameset_ok = false;
3154  
3155                  /*
3156                   * > Follow the generic raw text element parsing algorithm.
3157                   *
3158                   * As a self-contained node, this behavior is handled in the Tag Processor.
3159                   */
3160                  $this->insert_html_element( $this->state->current_token );
3161                  return true;
3162  
3163              /*
3164               * A start tag whose tag name is "iframe"
3165               */
3166              case '+IFRAME':
3167                  $this->state->frameset_ok = false;
3168  
3169                  /*
3170                   * > Follow the generic raw text element parsing algorithm.
3171                   *
3172                   * As a self-contained node, this behavior is handled in the Tag Processor.
3173                   */
3174                  $this->insert_html_element( $this->state->current_token );
3175                  return true;
3176  
3177              /*
3178               * > A start tag whose tag name is "noembed"
3179               * > A start tag whose tag name is "noscript", if the scripting flag is enabled
3180               *
3181               * The scripting flag is never enabled in this parser.
3182               */
3183              case '+NOEMBED':
3184                  $this->insert_html_element( $this->state->current_token );
3185                  return true;
3186  
3187              /*
3188               * > A start tag whose tag name is "select"
3189               */
3190              case '+SELECT':
3191                  /*
3192                   * > If the parser was created as part of the HTML fragment parsing algorithm
3193                   * > (fragment case) and the context element passed to that algorithm is a
3194                   * > select element:
3195                   * >   1. Parse error.
3196                   * >   2. Ignore the token.
3197                   */
3198                  if ( isset( $this->context_node ) && 'SELECT' === $this->context_node->node_name ) {
3199                      // @todo Indicate a parse error once it's possible.
3200                      return $this->step();
3201                  }
3202                  /*
3203                   * > Otherwise, if the stack of open elements has a select element in scope:
3204                   * >   1. Parse error.
3205                   * >   2. Ignore the token.
3206                   * >   3. Pop elements from the stack of open elements until a select element
3207                   * >      has been popped from the stack.
3208                   */
3209                  if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) {
3210                      // @todo Indicate a parse error once it's possible.
3211                      $this->state->stack_of_open_elements->pop_until( 'SELECT' );
3212                      return $this->step();
3213                  }
3214  
3215                  $this->reconstruct_active_formatting_elements();
3216                  $this->insert_html_element( $this->state->current_token );
3217                  $this->state->frameset_ok = false;
3218                  return true;
3219  
3220              /*
3221               * > A start tag whose tag name is "option"
3222               */
3223              case '+OPTION':
3224                  if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) {
3225                      $this->generate_implied_end_tags( 'OPTGROUP' );
3226                      /*
3227                       * > If the stack of open elements has an option element in scope, then this
3228                       * > is a parse error.
3229                       * @todo Indicate a parse error once it's possible.
3230                       */
3231                  } elseif ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) {
3232                      $this->state->stack_of_open_elements->pop();
3233                  }
3234  
3235                  $this->reconstruct_active_formatting_elements();
3236                  $this->insert_html_element( $this->state->current_token );
3237                  return true;
3238  
3239              /*
3240               * > A start tag whose tag name is "optgroup"
3241               */
3242              case '+OPTGROUP':
3243                  if ( $this->state->stack_of_open_elements->has_element_in_scope( 'SELECT' ) ) {
3244                      $this->generate_implied_end_tags();
3245                      /*
3246                       * > If the stack of open elements has an option element in scope or has an
3247                       * > optgroup element in scope, then this is a parse error.
3248                       * @todo Indicate a parse error once it's possible.
3249                       */
3250                  } elseif ( $this->state->stack_of_open_elements->current_node_is( 'OPTION' ) ) {
3251                      $this->state->stack_of_open_elements->pop();
3252                  }
3253  
3254                  $this->reconstruct_active_formatting_elements();
3255                  $this->insert_html_element( $this->state->current_token );
3256                  return true;
3257  
3258              /*
3259               * > A start tag whose tag name is one of: "rb", "rtc"
3260               */
3261              case '+RB':
3262              case '+RTC':
3263                  if ( $this->state->stack_of_open_elements->has_element_in_scope( 'RUBY' ) ) {
3264                      $this->generate_implied_end_tags();
3265  
3266                      if ( $this->state->stack_of_open_elements->current_node_is( 'RUBY' ) ) {
3267                          // @todo Indicate a parse error once it's possible.
3268                      }
3269                  }
3270  
3271                  $this->insert_html_element( $this->state->current_token );
3272                  return true;
3273  
3274              /*
3275               * > A start tag whose tag name is one of: "rp", "rt"
3276               */
3277              case '+RP':
3278              case '+RT':
3279                  if ( $this->state->stack_of_open_elements->has_element_in_scope( 'RUBY' ) ) {
3280                      $this->generate_implied_end_tags( 'RTC' );
3281  
3282                      $current_node_name = $this->state->stack_of_open_elements->current_node()->node_name;
3283                      if ( 'RTC' === $current_node_name || 'RUBY' === $current_node_name ) {
3284                          // @todo Indicate a parse error once it's possible.
3285                      }
3286                  }
3287  
3288                  $this->insert_html_element( $this->state->current_token );
3289                  return true;
3290  
3291              /*
3292               * > A start tag whose tag name is "math"
3293               */
3294              case '+MATH':
3295                  $this->reconstruct_active_formatting_elements();
3296  
3297                  /*
3298                   * @todo Adjust MathML attributes for the token. (This fixes the case of MathML attributes that are not all lowercase.)
3299                   * @todo Adjust foreign attributes for the token. (This fixes the use of namespaced attributes, in particular XLink.)
3300                   *
3301                   * These ought to be handled in the attribute methods.
3302                   */
3303                  $this->state->current_token->namespace = 'math';
3304                  $this->insert_html_element( $this->state->current_token );
3305                  if ( $this->state->current_token->has_self_closing_flag ) {
3306                      $this->state->stack_of_open_elements->pop();
3307                  }
3308                  return true;
3309  
3310              /*
3311               * > A start tag whose tag name is "svg"
3312               */
3313              case '+SVG':
3314                  $this->reconstruct_active_formatting_elements();
3315  
3316                  /*
3317                   * @todo Adjust SVG attributes for the token. (This fixes the case of SVG attributes that are not all lowercase.)
3318                   * @todo Adjust foreign attributes for the token. (This fixes the use of namespaced attributes, in particular XLink in SVG.)
3319                   *
3320                   * These ought to be handled in the attribute methods.
3321                   */
3322                  $this->state->current_token->namespace = 'svg';
3323                  $this->insert_html_element( $this->state->current_token );
3324                  if ( $this->state->current_token->has_self_closing_flag ) {
3325                      $this->state->stack_of_open_elements->pop();
3326                  }
3327                  return true;
3328  
3329              /*
3330               * > A start tag whose tag name is one of: "caption", "col", "colgroup",
3331               * > "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr"
3332               */
3333              case '+CAPTION':
3334              case '+COL':
3335              case '+COLGROUP':
3336              case '+FRAME':
3337              case '+HEAD':
3338              case '+TBODY':
3339              case '+TD':
3340              case '+TFOOT':
3341              case '+TH':
3342              case '+THEAD':
3343              case '+TR':
3344                  // Parse error. Ignore the token.
3345                  return $this->step();
3346          }
3347  
3348          if ( ! parent::is_tag_closer() ) {
3349              /*
3350               * > Any other start tag
3351               */
3352              $this->reconstruct_active_formatting_elements();
3353              $this->insert_html_element( $this->state->current_token );
3354              return true;
3355          } else {
3356              /*
3357               * > Any other end tag
3358               */
3359              return $this->in_body_any_other_end_tag();
3360          }
3361  
3362          $this->bail( 'Should not have been able to reach end of IN BODY processing. Check HTML API code.' );
3363      }
3364  
3365      /**
3366       * Applies the "any other end tag" parsing instructions for the IN BODY insertion mode.
3367       *
3368       * @since 7.1.0
3369       * @ignore
3370       *
3371       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
3372       *
3373       * @see https://html.spec.whatwg.org/#parsing-main-inbody
3374       * @see WP_HTML_Processor::step_in_body
3375       *
3376       * @return bool Whether an element was found.
3377       */
3378  	private function in_body_any_other_end_tag(): bool {
3379          $node       = null;
3380          $token_name = $this->get_token_name();
3381  
3382          /*
3383           * Find the corresponding tag opener in the stack of open elements, if
3384           * it exists before reaching a special element, which provides a kind
3385           * of boundary in the stack. For example, a `</custom-tag>` should not
3386           * close anything beyond its containing `P` or `DIV` element.
3387           */
3388          foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) {
3389              if ( 'html' === $node->namespace && $token_name === $node->node_name ) {
3390                  break;
3391              }
3392  
3393              if ( self::is_special( $node ) ) {
3394                  // This is a parse error, ignore the token.
3395                  return $this->step();
3396              }
3397          }
3398  
3399          $this->generate_implied_end_tags( $token_name );
3400          if ( $node !== $this->state->stack_of_open_elements->current_node() ) {
3401              // @todo Record parse error: this error doesn't impact parsing.
3402          }
3403  
3404          foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
3405              $this->state->stack_of_open_elements->pop();
3406              if ( $node === $item ) {
3407                  return true;
3408              }
3409          }
3410  
3411          $this->bail( 'Should not have been able to reach end of "any other end tag" IN BODY processing. Check HTML API code.' );
3412      }
3413  
3414      /**
3415       * Parses next element in the 'in table' insertion mode.
3416       *
3417       * This internal function performs the 'in table' insertion mode
3418       * logic for the generalized WP_HTML_Processor::step() function.
3419       *
3420       * @since 6.7.0
3421       * @ignore
3422       *
3423       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
3424       *
3425       * @see https://html.spec.whatwg.org/#parsing-main-intable
3426       * @see WP_HTML_Processor::step
3427       *
3428       * @return bool Whether an element was found.
3429       */
3430  	private function step_in_table(): bool {
3431          $token_name = $this->get_token_name();
3432          $token_type = $this->get_token_type();
3433          $op_sigil   = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : '';
3434          $op         = "{$op_sigil}{$token_name}";
3435  
3436          switch ( $op ) {
3437              /*
3438               * > A character token, if the current node is table,
3439               * > tbody, template, tfoot, thead, or tr element
3440               */
3441              case '#text':
3442                  $current_node      = $this->state->stack_of_open_elements->current_node();
3443                  $current_node_name = $current_node ? $current_node->node_name : null;
3444                  if (
3445                      $current_node_name && (
3446                          'TABLE' === $current_node_name ||
3447                          'TBODY' === $current_node_name ||
3448                          'TEMPLATE' === $current_node_name ||
3449                          'TFOOT' === $current_node_name ||
3450                          'THEAD' === $current_node_name ||
3451                          'TR' === $current_node_name
3452                      )
3453                  ) {
3454                      /*
3455                       * If the text is empty after processing HTML entities and stripping
3456                       * U+0000 NULL bytes then ignore the token.
3457                       */
3458                      if ( parent::TEXT_IS_NULL_SEQUENCE === $this->text_node_classification ) {
3459                          return $this->step();
3460                      }
3461  
3462                      /*
3463                       * This follows the rules for "in table text" insertion mode.
3464                       *
3465                       * Whitespace-only text nodes are inserted in-place. Otherwise
3466                       * foster parenting is enabled and the nodes would be
3467                       * inserted out-of-place.
3468                       *
3469                       * > If any of the tokens in the pending table character tokens
3470                       * > list are character tokens that are not ASCII whitespace,
3471                       * > then this is a parse error: reprocess the character tokens
3472                       * > in the pending table character tokens list using the rules
3473                       * > given in the "anything else" entry in the "in table"
3474                       * > insertion mode.
3475                       * >
3476                       * > Otherwise, insert the characters given by the pending table
3477                       * > character tokens list.
3478                       *
3479                       * @see https://html.spec.whatwg.org/#parsing-main-intabletext
3480                       */
3481                      if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
3482                          $this->insert_html_element( $this->state->current_token );
3483                          return true;
3484                      }
3485  
3486                      // Non-whitespace would trigger fostering, unsupported at this time.
3487                      $this->bail( 'Foster parenting is not supported.' );
3488                  }
3489                  break;
3490  
3491              /*
3492               * > A comment token
3493               * > A processing instruction token
3494               */
3495              case '#comment':
3496              case '#funky-comment':
3497              case '#presumptuous-tag':
3498              case '#processing-instruction':
3499                  $this->insert_html_element( $this->state->current_token );
3500                  return true;
3501  
3502              /*
3503               * > A DOCTYPE token
3504               */
3505              case 'html':
3506                  // Parse error: ignore the token.
3507                  return $this->step();
3508  
3509              /*
3510               * > A start tag whose tag name is "caption"
3511               */
3512              case '+CAPTION':
3513                  $this->state->stack_of_open_elements->clear_to_table_context();
3514                  $this->state->active_formatting_elements->insert_marker();
3515                  $this->insert_html_element( $this->state->current_token );
3516                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION;
3517                  return true;
3518  
3519              /*
3520               * > A start tag whose tag name is "colgroup"
3521               */
3522              case '+COLGROUP':
3523                  $this->state->stack_of_open_elements->clear_to_table_context();
3524                  $this->insert_html_element( $this->state->current_token );
3525                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP;
3526                  return true;
3527  
3528              /*
3529               * > A start tag whose tag name is "col"
3530               */
3531              case '+COL':
3532                  $this->state->stack_of_open_elements->clear_to_table_context();
3533  
3534                  /*
3535                   * > Insert an HTML element for a "colgroup" start tag token with no attributes,
3536                   * > then switch the insertion mode to "in column group".
3537                   */
3538                  $this->insert_virtual_node( 'COLGROUP' );
3539                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP;
3540                  return $this->step( self::REPROCESS_CURRENT_NODE );
3541  
3542              /*
3543               * > A start tag whose tag name is one of: "tbody", "tfoot", "thead"
3544               */
3545              case '+TBODY':
3546              case '+TFOOT':
3547              case '+THEAD':
3548                  $this->state->stack_of_open_elements->clear_to_table_context();
3549                  $this->insert_html_element( $this->state->current_token );
3550                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
3551                  return true;
3552  
3553              /*
3554               * > A start tag whose tag name is one of: "td", "th", "tr"
3555               */
3556              case '+TD':
3557              case '+TH':
3558              case '+TR':
3559                  $this->state->stack_of_open_elements->clear_to_table_context();
3560                  /*
3561                   * > Insert an HTML element for a "tbody" start tag token with no attributes,
3562                   * > then switch the insertion mode to "in table body".
3563                   */
3564                  $this->insert_virtual_node( 'TBODY' );
3565                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
3566                  return $this->step( self::REPROCESS_CURRENT_NODE );
3567  
3568              /*
3569               * > A start tag whose tag name is "table"
3570               *
3571               * This tag in the IN TABLE insertion mode is a parse error.
3572               */
3573              case '+TABLE':
3574                  if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TABLE' ) ) {
3575                      return $this->step();
3576                  }
3577  
3578                  $this->state->stack_of_open_elements->pop_until( 'TABLE' );
3579                  $this->reset_insertion_mode_appropriately();
3580                  return $this->step( self::REPROCESS_CURRENT_NODE );
3581  
3582              /*
3583               * > An end tag whose tag name is "table"
3584               */
3585              case '-TABLE':
3586                  if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TABLE' ) ) {
3587                      // @todo Indicate a parse error once it's possible.
3588                      return $this->step();
3589                  }
3590  
3591                  $this->state->stack_of_open_elements->pop_until( 'TABLE' );
3592                  $this->reset_insertion_mode_appropriately();
3593                  return true;
3594  
3595              /*
3596               * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr"
3597               */
3598              case '-BODY':
3599              case '-CAPTION':
3600              case '-COL':
3601              case '-COLGROUP':
3602              case '-HTML':
3603              case '-TBODY':
3604              case '-TD':
3605              case '-TFOOT':
3606              case '-TH':
3607              case '-THEAD':
3608              case '-TR':
3609                  // Parse error: ignore the token.
3610                  return $this->step();
3611  
3612              /*
3613               * > A start tag whose tag name is one of: "style", "script", "template"
3614               * > An end tag whose tag name is "template"
3615               */
3616              case '+STYLE':
3617              case '+SCRIPT':
3618              case '+TEMPLATE':
3619              case '-TEMPLATE':
3620                  /*
3621                   * > Process the token using the rules for the "in head" insertion mode.
3622                   */
3623                  return $this->step_in_head();
3624  
3625              /*
3626               * > A start tag whose tag name is "input"
3627               *
3628               * > If the token does not have an attribute with the name "type", or if it does, but
3629               * > that attribute's value is not an ASCII case-insensitive match for the string
3630               * > "hidden", then: act as described in the "anything else" entry below.
3631               */
3632              case '+INPUT':
3633                  $type_attribute = $this->get_attribute( 'type' );
3634                  if ( ! is_string( $type_attribute ) || 'hidden' !== strtolower( $type_attribute ) ) {
3635                      goto anything_else;
3636                  }
3637                  // @todo Indicate a parse error once it's possible.
3638                  $this->insert_html_element( $this->state->current_token );
3639                  return true;
3640  
3641              /*
3642               * > A start tag whose tag name is "form"
3643               *
3644               * This tag in the IN TABLE insertion mode is a parse error.
3645               */
3646              case '+FORM':
3647                  if (
3648                      $this->state->stack_of_open_elements->has_element_in_scope( 'TEMPLATE' ) ||
3649                      isset( $this->state->form_element )
3650                  ) {
3651                      return $this->step();
3652                  }
3653  
3654                  // This FORM is special because it immediately closes and cannot have other children.
3655                  $this->insert_html_element( $this->state->current_token );
3656                  $this->state->form_element = $this->state->current_token;
3657                  $this->state->stack_of_open_elements->pop();
3658                  return true;
3659          }
3660  
3661          /*
3662           * > Anything else
3663           * > Parse error. Enable foster parenting, process the token using the rules for the
3664           * > "in body" insertion mode, and then disable foster parenting.
3665           *
3666           * @todo Indicate a parse error once it's possible.
3667           */
3668          anything_else:
3669          $this->bail( 'Foster parenting is not supported.' );
3670      }
3671  
3672      /**
3673       * Parses next element in the 'in table text' insertion mode.
3674       *
3675       * This internal function performs the 'in table text' insertion mode
3676       * logic for the generalized WP_HTML_Processor::step() function.
3677       *
3678       * @since 6.7.0 Stub implementation.
3679       * @ignore
3680       *
3681       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
3682       *
3683       * @see https://html.spec.whatwg.org/#parsing-main-intabletext
3684       * @see WP_HTML_Processor::step
3685       *
3686       * @return bool Whether an element was found.
3687       */
3688  	private function step_in_table_text(): bool {
3689          $this->bail( 'No support for parsing in the ' . WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_TEXT . ' state.' );
3690      }
3691  
3692      /**
3693       * Parses next element in the 'in caption' insertion mode.
3694       *
3695       * This internal function performs the 'in caption' insertion mode
3696       * logic for the generalized WP_HTML_Processor::step() function.
3697       *
3698       * @since 6.7.0
3699       * @ignore
3700       *
3701       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
3702       *
3703       * @see https://html.spec.whatwg.org/#parsing-main-incaption
3704       * @see WP_HTML_Processor::step
3705       *
3706       * @return bool Whether an element was found.
3707       */
3708  	private function step_in_caption(): bool {
3709          $tag_name = $this->get_tag();
3710          $op_sigil = $this->is_tag_closer() ? '-' : '+';
3711          $op       = "{$op_sigil}{$tag_name}";
3712  
3713          switch ( $op ) {
3714              /*
3715               * > An end tag whose tag name is "caption"
3716               * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"
3717               * > An end tag whose tag name is "table"
3718               *
3719               * These tag handling rules are identical except for the final instruction.
3720               * Handle them in a single block.
3721               */
3722              case '-CAPTION':
3723              case '+CAPTION':
3724              case '+COL':
3725              case '+COLGROUP':
3726              case '+TBODY':
3727              case '+TD':
3728              case '+TFOOT':
3729              case '+TH':
3730              case '+THEAD':
3731              case '+TR':
3732              case '-TABLE':
3733                  if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'CAPTION' ) ) {
3734                      // Parse error: ignore the token.
3735                      return $this->step();
3736                  }
3737  
3738                  $this->generate_implied_end_tags();
3739                  if ( ! $this->state->stack_of_open_elements->current_node_is( 'CAPTION' ) ) {
3740                      // @todo Indicate a parse error once it's possible.
3741                  }
3742  
3743                  $this->state->stack_of_open_elements->pop_until( 'CAPTION' );
3744                  $this->state->active_formatting_elements->clear_up_to_last_marker();
3745                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
3746  
3747                  // If this is not a CAPTION end tag, the token should be reprocessed.
3748                  if ( '-CAPTION' === $op ) {
3749                      return true;
3750                  }
3751                  return $this->step( self::REPROCESS_CURRENT_NODE );
3752  
3753              /**
3754               * > An end tag whose tag name is one of: "body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr"
3755               */
3756              case '-BODY':
3757              case '-COL':
3758              case '-COLGROUP':
3759              case '-HTML':
3760              case '-TBODY':
3761              case '-TD':
3762              case '-TFOOT':
3763              case '-TH':
3764              case '-THEAD':
3765              case '-TR':
3766                  // Parse error: ignore the token.
3767                  return $this->step();
3768          }
3769  
3770          /**
3771           * > Anything else
3772           * >   Process the token using the rules for the "in body" insertion mode.
3773           */
3774          return $this->step_in_body();
3775      }
3776  
3777      /**
3778       * Parses next element in the 'in column group' insertion mode.
3779       *
3780       * This internal function performs the 'in column group' insertion mode
3781       * logic for the generalized WP_HTML_Processor::step() function.
3782       *
3783       * @since 6.7.0
3784       * @ignore
3785       *
3786       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
3787       *
3788       * @see https://html.spec.whatwg.org/#parsing-main-incolgroup
3789       * @see WP_HTML_Processor::step
3790       *
3791       * @return bool Whether an element was found.
3792       */
3793  	private function step_in_column_group(): bool {
3794          $token_name = $this->get_token_name();
3795          $token_type = $this->get_token_type();
3796          $op_sigil   = '#tag' === $token_type ? ( parent::is_tag_closer() ? '-' : '+' ) : '';
3797          $op         = "{$op_sigil}{$token_name}";
3798  
3799          switch ( $op ) {
3800              /*
3801               * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF),
3802               * > U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
3803               */
3804              case '#text':
3805                  if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
3806                      // Insert the character.
3807                      $this->insert_html_element( $this->state->current_token );
3808                      return true;
3809                  }
3810  
3811                  goto in_column_group_anything_else;
3812  
3813              /*
3814               * > A comment token
3815               * > A processing instruction token
3816               */
3817              case '#comment':
3818              case '#funky-comment':
3819              case '#presumptuous-tag':
3820              case '#processing-instruction':
3821                  $this->insert_html_element( $this->state->current_token );
3822                  return true;
3823  
3824              /*
3825               * > A DOCTYPE token
3826               */
3827              case 'html':
3828                  // @todo Indicate a parse error once it's possible.
3829                  return $this->step();
3830  
3831              /*
3832               * > A start tag whose tag name is "html"
3833               */
3834              case '+HTML':
3835                  return $this->step_in_body();
3836  
3837              /*
3838               * > A start tag whose tag name is "col"
3839               */
3840              case '+COL':
3841                  $this->insert_html_element( $this->state->current_token );
3842                  $this->state->stack_of_open_elements->pop();
3843                  return true;
3844  
3845              /*
3846               * > An end tag whose tag name is "colgroup"
3847               */
3848              case '-COLGROUP':
3849                  if ( ! $this->state->stack_of_open_elements->current_node_is( 'COLGROUP' ) ) {
3850                      // @todo Indicate a parse error once it's possible.
3851                      return $this->step();
3852                  }
3853                  $this->state->stack_of_open_elements->pop();
3854                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
3855                  return true;
3856  
3857              /*
3858               * > An end tag whose tag name is "col"
3859               */
3860              case '-COL':
3861                  // Parse error: ignore the token.
3862                  return $this->step();
3863  
3864              /*
3865               * > A start tag whose tag name is "template"
3866               * > An end tag whose tag name is "template"
3867               */
3868              case '+TEMPLATE':
3869              case '-TEMPLATE':
3870                  return $this->step_in_head();
3871          }
3872  
3873          in_column_group_anything_else:
3874          /*
3875           * > Anything else
3876           */
3877          if ( ! $this->state->stack_of_open_elements->current_node_is( 'COLGROUP' ) ) {
3878              // @todo Indicate a parse error once it's possible.
3879              return $this->step();
3880          }
3881          $this->state->stack_of_open_elements->pop();
3882          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
3883          return $this->step( self::REPROCESS_CURRENT_NODE );
3884      }
3885  
3886      /**
3887       * Parses next element in the 'in table body' insertion mode.
3888       *
3889       * This internal function performs the 'in table body' insertion mode
3890       * logic for the generalized WP_HTML_Processor::step() function.
3891       *
3892       * @since 6.7.0
3893       * @ignore
3894       *
3895       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
3896       *
3897       * @see https://html.spec.whatwg.org/#parsing-main-intbody
3898       * @see WP_HTML_Processor::step
3899       *
3900       * @return bool Whether an element was found.
3901       */
3902  	private function step_in_table_body(): bool {
3903          $tag_name = $this->get_tag();
3904          $op_sigil = $this->is_tag_closer() ? '-' : '+';
3905          $op       = "{$op_sigil}{$tag_name}";
3906  
3907          switch ( $op ) {
3908              /*
3909               * > A start tag whose tag name is "tr"
3910               */
3911              case '+TR':
3912                  $this->state->stack_of_open_elements->clear_to_table_body_context();
3913                  $this->insert_html_element( $this->state->current_token );
3914                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
3915                  return true;
3916  
3917              /*
3918               * > A start tag whose tag name is one of: "th", "td"
3919               */
3920              case '+TH':
3921              case '+TD':
3922                  // @todo Indicate a parse error once it's possible.
3923                  $this->state->stack_of_open_elements->clear_to_table_body_context();
3924                  $this->insert_virtual_node( 'TR' );
3925                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
3926                  return $this->step( self::REPROCESS_CURRENT_NODE );
3927  
3928              /*
3929               * > An end tag whose tag name is one of: "tbody", "tfoot", "thead"
3930               */
3931              case '-TBODY':
3932              case '-TFOOT':
3933              case '-THEAD':
3934                  if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) {
3935                      // Parse error: ignore the token.
3936                      return $this->step();
3937                  }
3938  
3939                  $this->state->stack_of_open_elements->clear_to_table_body_context();
3940                  $this->state->stack_of_open_elements->pop();
3941                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
3942                  return true;
3943  
3944              /*
3945               * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "tfoot", "thead"
3946               * > An end tag whose tag name is "table"
3947               */
3948              case '+CAPTION':
3949              case '+COL':
3950              case '+COLGROUP':
3951              case '+TBODY':
3952              case '+TFOOT':
3953              case '+THEAD':
3954              case '-TABLE':
3955                  if (
3956                      ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TBODY' ) &&
3957                      ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'THEAD' ) &&
3958                      ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TFOOT' )
3959                  ) {
3960                      // Parse error: ignore the token.
3961                      return $this->step();
3962                  }
3963                  $this->state->stack_of_open_elements->clear_to_table_body_context();
3964                  $this->state->stack_of_open_elements->pop();
3965                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
3966                  return $this->step( self::REPROCESS_CURRENT_NODE );
3967  
3968              /*
3969               * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "td", "th", "tr"
3970               */
3971              case '-BODY':
3972              case '-CAPTION':
3973              case '-COL':
3974              case '-COLGROUP':
3975              case '-HTML':
3976              case '-TD':
3977              case '-TH':
3978              case '-TR':
3979                  // Parse error: ignore the token.
3980                  return $this->step();
3981          }
3982  
3983          /*
3984           * > Anything else
3985           * > Process the token using the rules for the "in table" insertion mode.
3986           */
3987          return $this->step_in_table();
3988      }
3989  
3990      /**
3991       * Parses next element in the 'in row' insertion mode.
3992       *
3993       * This internal function performs the 'in row' insertion mode
3994       * logic for the generalized WP_HTML_Processor::step() function.
3995       *
3996       * @since 6.7.0
3997       * @ignore
3998       *
3999       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
4000       *
4001       * @see https://html.spec.whatwg.org/#parsing-main-intr
4002       * @see WP_HTML_Processor::step
4003       *
4004       * @return bool Whether an element was found.
4005       */
4006  	private function step_in_row(): bool {
4007          $tag_name = $this->get_tag();
4008          $op_sigil = $this->is_tag_closer() ? '-' : '+';
4009          $op       = "{$op_sigil}{$tag_name}";
4010  
4011          switch ( $op ) {
4012              /*
4013               * > A start tag whose tag name is one of: "th", "td"
4014               */
4015              case '+TH':
4016              case '+TD':
4017                  $this->state->stack_of_open_elements->clear_to_table_row_context();
4018                  $this->insert_html_element( $this->state->current_token );
4019                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CELL;
4020                  $this->state->active_formatting_elements->insert_marker();
4021                  return true;
4022  
4023              /*
4024               * > An end tag whose tag name is "tr"
4025               */
4026              case '-TR':
4027                  if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TR' ) ) {
4028                      // Parse error: ignore the token.
4029                      return $this->step();
4030                  }
4031  
4032                  $this->state->stack_of_open_elements->clear_to_table_row_context();
4033                  $this->state->stack_of_open_elements->pop();
4034                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
4035                  return true;
4036  
4037              /*
4038               * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "tfoot", "thead", "tr"
4039               * > An end tag whose tag name is "table"
4040               */
4041              case '+CAPTION':
4042              case '+COL':
4043              case '+COLGROUP':
4044              case '+TBODY':
4045              case '+TFOOT':
4046              case '+THEAD':
4047              case '+TR':
4048              case '-TABLE':
4049                  if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TR' ) ) {
4050                      // Parse error: ignore the token.
4051                      return $this->step();
4052                  }
4053  
4054                  $this->state->stack_of_open_elements->clear_to_table_row_context();
4055                  $this->state->stack_of_open_elements->pop();
4056                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
4057                  return $this->step( self::REPROCESS_CURRENT_NODE );
4058  
4059              /*
4060               * > An end tag whose tag name is one of: "tbody", "tfoot", "thead"
4061               */
4062              case '-TBODY':
4063              case '-TFOOT':
4064              case '-THEAD':
4065                  if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) {
4066                      // Parse error: ignore the token.
4067                      return $this->step();
4068                  }
4069  
4070                  if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( 'TR' ) ) {
4071                      // Ignore the token.
4072                      return $this->step();
4073                  }
4074  
4075                  $this->state->stack_of_open_elements->clear_to_table_row_context();
4076                  $this->state->stack_of_open_elements->pop();
4077                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
4078                  return $this->step( self::REPROCESS_CURRENT_NODE );
4079  
4080              /*
4081               * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "td", "th"
4082               */
4083              case '-BODY':
4084              case '-CAPTION':
4085              case '-COL':
4086              case '-COLGROUP':
4087              case '-HTML':
4088              case '-TD':
4089              case '-TH':
4090                  // Parse error: ignore the token.
4091                  return $this->step();
4092          }
4093  
4094          /*
4095           * > Anything else
4096           * >   Process the token using the rules for the "in table" insertion mode.
4097           */
4098          return $this->step_in_table();
4099      }
4100  
4101      /**
4102       * Parses next element in the 'in cell' insertion mode.
4103       *
4104       * This internal function performs the 'in cell' insertion mode
4105       * logic for the generalized WP_HTML_Processor::step() function.
4106       *
4107       * @since 6.7.0
4108       * @ignore
4109       *
4110       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
4111       *
4112       * @see https://html.spec.whatwg.org/#parsing-main-intd
4113       * @see WP_HTML_Processor::step
4114       *
4115       * @return bool Whether an element was found.
4116       */
4117  	private function step_in_cell(): bool {
4118          $tag_name = $this->get_tag();
4119          $op_sigil = $this->is_tag_closer() ? '-' : '+';
4120          $op       = "{$op_sigil}{$tag_name}";
4121  
4122          switch ( $op ) {
4123              /*
4124               * > An end tag whose tag name is one of: "td", "th"
4125               */
4126              case '-TD':
4127              case '-TH':
4128                  if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) {
4129                      // Parse error: ignore the token.
4130                      return $this->step();
4131                  }
4132  
4133                  $this->generate_implied_end_tags();
4134  
4135                  /*
4136                   * @todo This needs to check if the current node is an HTML element, meaning that
4137                   *       when SVG and MathML support is added, this needs to differentiate between an
4138                   *       HTML element of the given name, such as `<center>`, and a foreign element of
4139                   *       the same given name.
4140                   */
4141                  if ( ! $this->state->stack_of_open_elements->current_node_is( $tag_name ) ) {
4142                      // @todo Indicate a parse error once it's possible.
4143                  }
4144  
4145                  $this->state->stack_of_open_elements->pop_until( $tag_name );
4146                  $this->state->active_formatting_elements->clear_up_to_last_marker();
4147                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
4148                  return true;
4149  
4150              /*
4151               * > A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td",
4152               * > "tfoot", "th", "thead", "tr"
4153               */
4154              case '+CAPTION':
4155              case '+COL':
4156              case '+COLGROUP':
4157              case '+TBODY':
4158              case '+TD':
4159              case '+TFOOT':
4160              case '+TH':
4161              case '+THEAD':
4162              case '+TR':
4163                  /*
4164                   * > Assert: The stack of open elements has a td or th element in table scope.
4165                   *
4166                   * Nothing to do here, except to verify in tests that this never appears.
4167                   */
4168  
4169                  $this->close_cell();
4170                  return $this->step( self::REPROCESS_CURRENT_NODE );
4171  
4172              /*
4173               * > An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html"
4174               */
4175              case '-BODY':
4176              case '-CAPTION':
4177              case '-COL':
4178              case '-COLGROUP':
4179              case '-HTML':
4180                  // Parse error: ignore the token.
4181                  return $this->step();
4182  
4183              /*
4184               * > An end tag whose tag name is one of: "table", "tbody", "tfoot", "thead", "tr"
4185               */
4186              case '-TABLE':
4187              case '-TBODY':
4188              case '-TFOOT':
4189              case '-THEAD':
4190              case '-TR':
4191                  if ( ! $this->state->stack_of_open_elements->has_element_in_table_scope( $tag_name ) ) {
4192                      // Parse error: ignore the token.
4193                      return $this->step();
4194                  }
4195                  $this->close_cell();
4196                  return $this->step( self::REPROCESS_CURRENT_NODE );
4197          }
4198  
4199          /*
4200           * > Anything else
4201           * >   Process the token using the rules for the "in body" insertion mode.
4202           */
4203          return $this->step_in_body();
4204      }
4205  
4206      /**
4207       * Parses next element in the 'in template' insertion mode.
4208       *
4209       * This internal function performs the 'in template' insertion mode
4210       * logic for the generalized WP_HTML_Processor::step() function.
4211       *
4212       * @since 6.7.0
4213       * @ignore
4214       *
4215       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
4216       *
4217       * @see https://html.spec.whatwg.org/#parsing-main-intemplate
4218       * @see WP_HTML_Processor::step
4219       *
4220       * @return bool Whether an element was found.
4221       */
4222  	private function step_in_template(): bool {
4223          $token_name = $this->get_token_name();
4224          $token_type = $this->get_token_type();
4225          $is_closer  = $this->is_tag_closer();
4226          $op_sigil   = '#tag' === $token_type ? ( $is_closer ? '-' : '+' ) : '';
4227          $op         = "{$op_sigil}{$token_name}";
4228  
4229          switch ( $op ) {
4230              /*
4231               * > A character token
4232               * > A comment token
4233               * > A processing instruction token
4234               * > A DOCTYPE token
4235               */
4236              case '#text':
4237              case '#comment':
4238              case '#funky-comment':
4239              case '#presumptuous-tag':
4240              case '#processing-instruction':
4241              case 'html':
4242                  return $this->step_in_body();
4243  
4244              /*
4245               * > A start tag whose tag name is one of: "base", "basefont", "bgsound", "link",
4246               * > "meta", "noframes", "script", "style", "template", "title"
4247               * > An end tag whose tag name is "template"
4248               */
4249              case '+BASE':
4250              case '+BASEFONT':
4251              case '+BGSOUND':
4252              case '+LINK':
4253              case '+META':
4254              case '+NOFRAMES':
4255              case '+SCRIPT':
4256              case '+STYLE':
4257              case '+TEMPLATE':
4258              case '+TITLE':
4259              case '-TEMPLATE':
4260                  return $this->step_in_head();
4261  
4262              /*
4263               * > A start tag whose tag name is one of: "caption", "colgroup", "tbody", "tfoot", "thead"
4264               */
4265              case '+CAPTION':
4266              case '+COLGROUP':
4267              case '+TBODY':
4268              case '+TFOOT':
4269              case '+THEAD':
4270                  array_pop( $this->state->stack_of_template_insertion_modes );
4271                  $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
4272                  $this->state->insertion_mode                      = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
4273                  return $this->step( self::REPROCESS_CURRENT_NODE );
4274  
4275              /*
4276               * > A start tag whose tag name is "col"
4277               */
4278              case '+COL':
4279                  array_pop( $this->state->stack_of_template_insertion_modes );
4280                  $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP;
4281                  $this->state->insertion_mode                      = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP;
4282                  return $this->step( self::REPROCESS_CURRENT_NODE );
4283  
4284              /*
4285               * > A start tag whose tag name is "tr"
4286               */
4287              case '+TR':
4288                  array_pop( $this->state->stack_of_template_insertion_modes );
4289                  $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
4290                  $this->state->insertion_mode                      = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
4291                  return $this->step( self::REPROCESS_CURRENT_NODE );
4292  
4293              /*
4294               * > A start tag whose tag name is one of: "td", "th"
4295               */
4296              case '+TD':
4297              case '+TH':
4298                  array_pop( $this->state->stack_of_template_insertion_modes );
4299                  $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
4300                  $this->state->insertion_mode                      = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
4301                  return $this->step( self::REPROCESS_CURRENT_NODE );
4302          }
4303  
4304          /*
4305           * > Any other start tag
4306           */
4307          if ( ! $is_closer ) {
4308              array_pop( $this->state->stack_of_template_insertion_modes );
4309              $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
4310              $this->state->insertion_mode                      = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
4311              return $this->step( self::REPROCESS_CURRENT_NODE );
4312          }
4313  
4314          /*
4315           * > Any other end tag
4316           */
4317          if ( $is_closer ) {
4318              // Parse error: ignore the token.
4319              return $this->step();
4320          }
4321  
4322          /*
4323           * > An end-of-file token
4324           */
4325          if ( ! $this->state->stack_of_open_elements->contains( 'TEMPLATE' ) ) {
4326              // Stop parsing.
4327              return false;
4328          }
4329  
4330          // @todo Indicate a parse error once it's possible.
4331          $this->state->stack_of_open_elements->pop_until( 'TEMPLATE' );
4332          $this->state->active_formatting_elements->clear_up_to_last_marker();
4333          array_pop( $this->state->stack_of_template_insertion_modes );
4334          $this->reset_insertion_mode_appropriately();
4335          return $this->step( self::REPROCESS_CURRENT_NODE );
4336      }
4337  
4338      /**
4339       * Parses next element in the 'after body' insertion mode.
4340       *
4341       * This internal function performs the 'after body' insertion mode
4342       * logic for the generalized WP_HTML_Processor::step() function.
4343       *
4344       * @since 6.7.0
4345       * @ignore
4346       *
4347       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
4348       *
4349       * @see https://html.spec.whatwg.org/#parsing-main-afterbody
4350       * @see WP_HTML_Processor::step
4351       *
4352       * @return bool Whether an element was found.
4353       */
4354  	private function step_after_body(): bool {
4355          $tag_name   = $this->get_token_name();
4356          $token_type = $this->get_token_type();
4357          $op_sigil   = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : '';
4358          $op         = "{$op_sigil}{$tag_name}";
4359  
4360          switch ( $op ) {
4361              /*
4362               * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF),
4363               * >   U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
4364               *
4365               * > Process the token using the rules for the "in body" insertion mode.
4366               */
4367              case '#text':
4368                  if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
4369                      return $this->step_in_body();
4370                  }
4371                  goto after_body_anything_else;
4372  
4373              /*
4374               * > A comment token
4375               * > A processing instruction token
4376               */
4377              case '#comment':
4378              case '#funky-comment':
4379              case '#presumptuous-tag':
4380              case '#processing-instruction':
4381                  $this->bail( 'Content outside of BODY is unsupported.' );
4382                  break;
4383  
4384              /*
4385               * > A DOCTYPE token
4386               */
4387              case 'html':
4388                  // Parse error: ignore the token.
4389                  return $this->step();
4390  
4391              /*
4392               * > A start tag whose tag name is "html"
4393               */
4394              case '+HTML':
4395                  return $this->step_in_body();
4396  
4397              /*
4398               * > An end tag whose tag name is "html"
4399               *
4400               * > If the parser was created as part of the HTML fragment parsing algorithm,
4401               * > this is a parse error; ignore the token. (fragment case)
4402               * >
4403               * > Otherwise, switch the insertion mode to "after after body".
4404               */
4405              case '-HTML':
4406                  if ( isset( $this->context_node ) ) {
4407                      return $this->step();
4408                  }
4409  
4410                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_BODY;
4411                  /*
4412                   * The HTML element is not removed from the stack of open elements.
4413                   * Only internal state has changed, this does not qualify as a "step"
4414                   * in terms of advancing through the document to another token.
4415                   * Nothing has been pushed or popped.
4416                   * Proceed to parse the next item.
4417                   */
4418                  return $this->step();
4419          }
4420  
4421          /*
4422           * > Parse error. Switch the insertion mode to "in body" and reprocess the token.
4423           */
4424          after_body_anything_else:
4425          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
4426          return $this->step( self::REPROCESS_CURRENT_NODE );
4427      }
4428  
4429      /**
4430       * Parses next element in the 'in frameset' insertion mode.
4431       *
4432       * This internal function performs the 'in frameset' insertion mode
4433       * logic for the generalized WP_HTML_Processor::step() function.
4434       *
4435       * @since 6.7.0
4436       * @ignore
4437       *
4438       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
4439       *
4440       * @see https://html.spec.whatwg.org/#parsing-main-inframeset
4441       * @see WP_HTML_Processor::step
4442       *
4443       * @return bool Whether an element was found.
4444       */
4445  	private function step_in_frameset(): bool {
4446          $tag_name   = $this->get_token_name();
4447          $token_type = $this->get_token_type();
4448          $op_sigil   = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : '';
4449          $op         = "{$op_sigil}{$tag_name}";
4450  
4451          switch ( $op ) {
4452              /*
4453               * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF),
4454               * >   U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
4455               * >
4456               * > Insert the character.
4457               *
4458               * This algorithm effectively strips non-whitespace characters from text and inserts
4459               * them under HTML. This is not supported at this time.
4460               */
4461              case '#text':
4462                  if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
4463                      return $this->step_in_body();
4464                  }
4465                  $this->bail( 'Non-whitespace characters cannot be handled in frameset.' );
4466                  break;
4467  
4468              /*
4469               * > A comment token
4470               * > A processing instruction token
4471               */
4472              case '#comment':
4473              case '#funky-comment':
4474              case '#presumptuous-tag':
4475              case '#processing-instruction':
4476                  $this->insert_html_element( $this->state->current_token );
4477                  return true;
4478  
4479              /*
4480               * > A DOCTYPE token
4481               */
4482              case 'html':
4483                  // Parse error: ignore the token.
4484                  return $this->step();
4485  
4486              /*
4487               * > A start tag whose tag name is "html"
4488               */
4489              case '+HTML':
4490                  return $this->step_in_body();
4491  
4492              /*
4493               * > A start tag whose tag name is "frameset"
4494               */
4495              case '+FRAMESET':
4496                  $this->insert_html_element( $this->state->current_token );
4497                  return true;
4498  
4499              /*
4500               * > An end tag whose tag name is "frameset"
4501               */
4502              case '-FRAMESET':
4503                  /*
4504                   * > If the current node is the root html element, then this is a parse error;
4505                   * > ignore the token. (fragment case)
4506                   */
4507                  if ( $this->state->stack_of_open_elements->current_node_is( 'HTML' ) ) {
4508                      return $this->step();
4509                  }
4510  
4511                  /*
4512                   * > Otherwise, pop the current node from the stack of open elements.
4513                   */
4514                  $this->state->stack_of_open_elements->pop();
4515  
4516                  /*
4517                   * > If the parser was not created as part of the HTML fragment parsing algorithm
4518                   * > (fragment case), and the current node is no longer a frameset element, then
4519                   * > switch the insertion mode to "after frameset".
4520                   */
4521                  if ( ! isset( $this->context_node ) && ! $this->state->stack_of_open_elements->current_node_is( 'FRAMESET' ) ) {
4522                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_FRAMESET;
4523                  }
4524  
4525                  return true;
4526  
4527              /*
4528               * > A start tag whose tag name is "frame"
4529               *
4530               * > Insert an HTML element for the token. Immediately pop the
4531               * > current node off the stack of open elements.
4532               * >
4533               * > Acknowledge the token's self-closing flag, if it is set.
4534               */
4535              case '+FRAME':
4536                  $this->insert_html_element( $this->state->current_token );
4537                  $this->state->stack_of_open_elements->pop();
4538                  return true;
4539  
4540              /*
4541               * > A start tag whose tag name is "noframes"
4542               */
4543              case '+NOFRAMES':
4544                  return $this->step_in_head();
4545          }
4546  
4547          // Parse error: ignore the token.
4548          return $this->step();
4549      }
4550  
4551      /**
4552       * Parses next element in the 'after frameset' insertion mode.
4553       *
4554       * This internal function performs the 'after frameset' insertion mode
4555       * logic for the generalized WP_HTML_Processor::step() function.
4556       *
4557       * @since 6.7.0
4558       * @ignore
4559       *
4560       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
4561       *
4562       * @see https://html.spec.whatwg.org/#parsing-main-afterframeset
4563       * @see WP_HTML_Processor::step
4564       *
4565       * @return bool Whether an element was found.
4566       */
4567  	private function step_after_frameset(): bool {
4568          $tag_name   = $this->get_token_name();
4569          $token_type = $this->get_token_type();
4570          $op_sigil   = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : '';
4571          $op         = "{$op_sigil}{$tag_name}";
4572  
4573          switch ( $op ) {
4574              /*
4575               * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF),
4576               * >   U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
4577               * >
4578               * > Insert the character.
4579               *
4580               * This algorithm effectively strips non-whitespace characters from text and inserts
4581               * them under HTML. This is not supported at this time.
4582               */
4583              case '#text':
4584                  if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
4585                      return $this->step_in_body();
4586                  }
4587                  $this->bail( 'Non-whitespace characters cannot be handled in after frameset' );
4588                  break;
4589  
4590              /*
4591               * > A comment token
4592               * > A processing instruction token
4593               */
4594              case '#comment':
4595              case '#funky-comment':
4596              case '#presumptuous-tag':
4597              case '#processing-instruction':
4598                  $this->insert_html_element( $this->state->current_token );
4599                  return true;
4600  
4601              /*
4602               * > A DOCTYPE token
4603               */
4604              case 'html':
4605                  // Parse error: ignore the token.
4606                  return $this->step();
4607  
4608              /*
4609               * > A start tag whose tag name is "html"
4610               */
4611              case '+HTML':
4612                  return $this->step_in_body();
4613  
4614              /*
4615               * > An end tag whose tag name is "html"
4616               */
4617              case '-HTML':
4618                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET;
4619                  /*
4620                   * The HTML element is not removed from the stack of open elements.
4621                   * Only internal state has changed, this does not qualify as a "step"
4622                   * in terms of advancing through the document to another token.
4623                   * Nothing has been pushed or popped.
4624                   * Proceed to parse the next item.
4625                   */
4626                  return $this->step();
4627  
4628              /*
4629               * > A start tag whose tag name is "noframes"
4630               */
4631              case '+NOFRAMES':
4632                  return $this->step_in_head();
4633          }
4634  
4635          // Parse error: ignore the token.
4636          return $this->step();
4637      }
4638  
4639      /**
4640       * Parses next element in the 'after after body' insertion mode.
4641       *
4642       * This internal function performs the 'after after body' insertion mode
4643       * logic for the generalized WP_HTML_Processor::step() function.
4644       *
4645       * @since 6.7.0
4646       * @ignore
4647       *
4648       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
4649       *
4650       * @see https://html.spec.whatwg.org/#the-after-after-body-insertion-mode
4651       * @see WP_HTML_Processor::step
4652       *
4653       * @return bool Whether an element was found.
4654       */
4655  	private function step_after_after_body(): bool {
4656          $tag_name   = $this->get_token_name();
4657          $token_type = $this->get_token_type();
4658          $op_sigil   = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : '';
4659          $op         = "{$op_sigil}{$tag_name}";
4660  
4661          switch ( $op ) {
4662              /*
4663               * > A comment token
4664               * > A processing instruction token
4665               */
4666              case '#comment':
4667              case '#funky-comment':
4668              case '#presumptuous-tag':
4669              case '#processing-instruction':
4670                  $this->bail( 'Content outside of HTML is unsupported.' );
4671                  break;
4672  
4673              /*
4674               * > A DOCTYPE token
4675               * > A start tag whose tag name is "html"
4676               *
4677               * > Process the token using the rules for the "in body" insertion mode.
4678               */
4679              case 'html':
4680              case '+HTML':
4681                  return $this->step_in_body();
4682  
4683              /*
4684               * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF),
4685               * >   U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
4686               * >
4687               * > Process the token using the rules for the "in body" insertion mode.
4688               */
4689              case '#text':
4690                  if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
4691                      return $this->step_in_body();
4692                  }
4693                  goto after_after_body_anything_else;
4694          }
4695  
4696          /*
4697           * > Parse error. Switch the insertion mode to "in body" and reprocess the token.
4698           */
4699          after_after_body_anything_else:
4700          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
4701          return $this->step( self::REPROCESS_CURRENT_NODE );
4702      }
4703  
4704      /**
4705       * Parses next element in the 'after after frameset' insertion mode.
4706       *
4707       * This internal function performs the 'after after frameset' insertion mode
4708       * logic for the generalized WP_HTML_Processor::step() function.
4709       *
4710       * @since 6.7.0
4711       * @ignore
4712       *
4713       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
4714       *
4715       * @see https://html.spec.whatwg.org/#the-after-after-frameset-insertion-mode
4716       * @see WP_HTML_Processor::step
4717       *
4718       * @return bool Whether an element was found.
4719       */
4720  	private function step_after_after_frameset(): bool {
4721          $tag_name   = $this->get_token_name();
4722          $token_type = $this->get_token_type();
4723          $op_sigil   = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : '';
4724          $op         = "{$op_sigil}{$tag_name}";
4725  
4726          switch ( $op ) {
4727              /*
4728               * > A comment token
4729               * > A processing instruction token
4730               */
4731              case '#comment':
4732              case '#funky-comment':
4733              case '#presumptuous-tag':
4734              case '#processing-instruction':
4735                  $this->bail( 'Content outside of HTML is unsupported.' );
4736                  break;
4737  
4738              /*
4739               * > A DOCTYPE token
4740               * > A start tag whose tag name is "html"
4741               *
4742               * > Process the token using the rules for the "in body" insertion mode.
4743               */
4744              case 'html':
4745              case '+HTML':
4746                  return $this->step_in_body();
4747  
4748              /*
4749               * > A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF),
4750               * >   U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE
4751               * >
4752               * > Process the token using the rules for the "in body" insertion mode.
4753               *
4754               * This algorithm effectively strips non-whitespace characters from text and inserts
4755               * them under HTML. This is not supported at this time.
4756               */
4757              case '#text':
4758                  if ( parent::TEXT_IS_WHITESPACE === $this->text_node_classification ) {
4759                      return $this->step_in_body();
4760                  }
4761                  $this->bail( 'Non-whitespace characters cannot be handled in after after frameset.' );
4762                  break;
4763  
4764              /*
4765               * > A start tag whose tag name is "noframes"
4766               */
4767              case '+NOFRAMES':
4768                  return $this->step_in_head();
4769          }
4770  
4771          // Parse error: ignore the token.
4772          return $this->step();
4773      }
4774  
4775      /**
4776       * Parses next element in the 'in foreign content' insertion mode.
4777       *
4778       * This internal function performs the 'in foreign content' insertion mode
4779       * logic for the generalized WP_HTML_Processor::step() function.
4780       *
4781       * @since 6.7.0
4782       * @ignore
4783       *
4784       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
4785       *
4786       * @see https://html.spec.whatwg.org/#parsing-main-inforeign
4787       * @see WP_HTML_Processor::step
4788       *
4789       * @return bool Whether an element was found.
4790       */
4791  	private function step_in_foreign_content(): bool {
4792          $tag_name   = $this->get_token_name();
4793          $token_type = $this->get_token_type();
4794          $op_sigil   = '#tag' === $token_type ? ( $this->is_tag_closer() ? '-' : '+' ) : '';
4795          $op         = "{$op_sigil}{$tag_name}";
4796  
4797          /*
4798           * > A start tag whose name is "font", if the token has any attributes named "color", "face", or "size"
4799           *
4800           * This section drawn out above the switch to more easily incorporate
4801           * the additional rules based on the presence of the attributes.
4802           */
4803          if (
4804              '+FONT' === $op &&
4805              (
4806                  null !== $this->get_attribute( 'color' ) ||
4807                  null !== $this->get_attribute( 'face' ) ||
4808                  null !== $this->get_attribute( 'size' )
4809              )
4810          ) {
4811              $op = '+FONT with attributes';
4812          }
4813  
4814          switch ( $op ) {
4815              case '#text':
4816                  /*
4817                   * > A character token that is U+0000 NULL
4818                   *
4819                   * This is handled by `get_modifiable_text()`.
4820                   */
4821  
4822                  /*
4823                   * Whitespace-only text does not affect the frameset-ok flag.
4824                   * It is probably inter-element whitespace, but it may also
4825                   * contain character references which decode only to whitespace.
4826                   */
4827                  if ( parent::TEXT_IS_GENERIC === $this->text_node_classification ) {
4828                      $this->state->frameset_ok = false;
4829                  }
4830  
4831                  $this->insert_foreign_element( $this->state->current_token, false );
4832                  return true;
4833  
4834              /*
4835               * CDATA sections are alternate wrappers for text content and therefore
4836               * ought to follow the same rules as text nodes.
4837               */
4838              case '#cdata-section':
4839                  /*
4840                   * NULL bytes and whitespace do not change the frameset-ok flag.
4841                   */
4842                  $current_token        = $this->bookmarks[ $this->state->current_token->bookmark_name ];
4843                  $cdata_content_start  = $current_token->start + 9;
4844                  $cdata_content_length = $current_token->length - 12;
4845                  if ( strspn( $this->html, "\0 \t\n\f\r", $cdata_content_start, $cdata_content_length ) !== $cdata_content_length ) {
4846                      $this->state->frameset_ok = false;
4847                  }
4848  
4849                  $this->insert_foreign_element( $this->state->current_token, false );
4850                  return true;
4851  
4852              /*
4853               * > A comment token
4854               * > A processing instruction token
4855               */
4856              case '#comment':
4857              case '#funky-comment':
4858              case '#presumptuous-tag':
4859              case '#processing-instruction':
4860                  $this->insert_foreign_element( $this->state->current_token, false );
4861                  return true;
4862  
4863              /*
4864               * > A DOCTYPE token
4865               */
4866              case 'html':
4867                  // Parse error: ignore the token.
4868                  return $this->step();
4869  
4870              /*
4871               * > A start tag whose tag name is "b", "big", "blockquote", "body", "br", "center",
4872               * > "code", "dd", "div", "dl", "dt", "em", "embed", "h1", "h2", "h3", "h4", "h5",
4873               * > "h6", "head", "hr", "i", "img", "li", "listing", "menu", "meta", "nobr", "ol",
4874               * > "p", "pre", "ruby", "s", "small", "span", "strong", "strike", "sub", "sup",
4875               * > "table", "tt", "u", "ul", "var"
4876               *
4877               * > A start tag whose name is "font", if the token has any attributes named "color", "face", or "size"
4878               *
4879               * > An end tag whose tag name is "br", "p"
4880               *
4881               * Closing BR tags are always reported by the Tag Processor as opening tags.
4882               */
4883              case '+B':
4884              case '+BIG':
4885              case '+BLOCKQUOTE':
4886              case '+BODY':
4887              case '+BR':
4888              case '+CENTER':
4889              case '+CODE':
4890              case '+DD':
4891              case '+DIV':
4892              case '+DL':
4893              case '+DT':
4894              case '+EM':
4895              case '+EMBED':
4896              case '+H1':
4897              case '+H2':
4898              case '+H3':
4899              case '+H4':
4900              case '+H5':
4901              case '+H6':
4902              case '+HEAD':
4903              case '+HR':
4904              case '+I':
4905              case '+IMG':
4906              case '+LI':
4907              case '+LISTING':
4908              case '+MENU':
4909              case '+META':
4910              case '+NOBR':
4911              case '+OL':
4912              case '+P':
4913              case '+PRE':
4914              case '+RUBY':
4915              case '+S':
4916              case '+SMALL':
4917              case '+SPAN':
4918              case '+STRONG':
4919              case '+STRIKE':
4920              case '+SUB':
4921              case '+SUP':
4922              case '+TABLE':
4923              case '+TT':
4924              case '+U':
4925              case '+UL':
4926              case '+VAR':
4927              case '+FONT with attributes':
4928              case '-BR':
4929              case '-P':
4930                  // @todo Indicate a parse error once it's possible.
4931                  foreach ( $this->state->stack_of_open_elements->walk_up() as $current_node ) {
4932                      if (
4933                          'math' === $current_node->integration_node_type ||
4934                          'html' === $current_node->integration_node_type ||
4935                          'html' === $current_node->namespace
4936                      ) {
4937                          break;
4938                      }
4939  
4940                      $this->state->stack_of_open_elements->pop();
4941                  }
4942                  goto in_foreign_content_process_in_current_insertion_mode;
4943          }
4944  
4945          /*
4946           * > Any other start tag
4947           */
4948          if ( ! $this->is_tag_closer() ) {
4949              $this->insert_foreign_element( $this->state->current_token, false );
4950  
4951              /*
4952               * > If the token has its self-closing flag set, then run
4953               * > the appropriate steps from the following list:
4954               * >
4955               * >   ↪ the token's tag name is "script", and the new current node is in the SVG namespace
4956               * >         Acknowledge the token's self-closing flag, and then act as
4957               * >         described in the steps for a "script" end tag below.
4958               * >
4959               * >   ↪ Otherwise
4960               * >         Pop the current node off the stack of open elements and
4961               * >         acknowledge the token's self-closing flag.
4962               *
4963               * Since the rules for SCRIPT below indicate to pop the element off of the stack of
4964               * open elements, which is the same for the Otherwise condition, there's no need to
4965               * separate these checks. The difference comes when a parser operates with the scripting
4966               * flag enabled, and executes the script, which this parser does not support.
4967               */
4968              if ( $this->state->current_token->has_self_closing_flag ) {
4969                  $this->state->stack_of_open_elements->pop();
4970              }
4971              return true;
4972          }
4973  
4974          /*
4975           * > An end tag whose name is "script", if the current node is an SVG script element.
4976           */
4977          if ( $this->is_tag_closer() && 'SCRIPT' === $this->state->current_token->node_name && 'svg' === $this->state->current_token->namespace ) {
4978              $this->state->stack_of_open_elements->pop();
4979              return true;
4980          }
4981  
4982          /*
4983           * > Any other end tag
4984           */
4985          if ( $this->is_tag_closer() ) {
4986              $node = $this->state->stack_of_open_elements->current_node();
4987              if ( $tag_name !== $node->node_name ) {
4988                  // @todo Indicate a parse error once it's possible.
4989              }
4990              in_foreign_content_end_tag_loop:
4991              if ( $node === $this->state->stack_of_open_elements->at( 1 ) ) {
4992                  return true;
4993              }
4994  
4995              /*
4996               * > If node's tag name, converted to ASCII lowercase, is the same as the tag name
4997               * > of the token, pop elements from the stack of open elements until node has
4998               * > been popped from the stack, and then return.
4999               */
5000              if ( 0 === strcasecmp( $node->node_name, $tag_name ) ) {
5001                  foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
5002                      $this->state->stack_of_open_elements->pop();
5003                      if ( $node === $item ) {
5004                          return true;
5005                      }
5006                  }
5007              }
5008  
5009              foreach ( $this->state->stack_of_open_elements->walk_up( $node ) as $item ) {
5010                  $node = $item;
5011                  break;
5012              }
5013  
5014              if ( 'html' !== $node->namespace ) {
5015                  goto in_foreign_content_end_tag_loop;
5016              }
5017  
5018              in_foreign_content_process_in_current_insertion_mode:
5019              switch ( $this->state->insertion_mode ) {
5020                  case WP_HTML_Processor_State::INSERTION_MODE_INITIAL:
5021                      return $this->step_initial();
5022  
5023                  case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML:
5024                      return $this->step_before_html();
5025  
5026                  case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD:
5027                      return $this->step_before_head();
5028  
5029                  case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD:
5030                      return $this->step_in_head();
5031  
5032                  case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD_NOSCRIPT:
5033                      return $this->step_in_head_noscript();
5034  
5035                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD:
5036                      return $this->step_after_head();
5037  
5038                  case WP_HTML_Processor_State::INSERTION_MODE_IN_BODY:
5039                      return $this->step_in_body();
5040  
5041                  case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE:
5042                      return $this->step_in_table();
5043  
5044                  case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_TEXT:
5045                      return $this->step_in_table_text();
5046  
5047                  case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION:
5048                      return $this->step_in_caption();
5049  
5050                  case WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP:
5051                      return $this->step_in_column_group();
5052  
5053                  case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY:
5054                      return $this->step_in_table_body();
5055  
5056                  case WP_HTML_Processor_State::INSERTION_MODE_IN_ROW:
5057                      return $this->step_in_row();
5058  
5059                  case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL:
5060                      return $this->step_in_cell();
5061  
5062                  case WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE:
5063                      return $this->step_in_template();
5064  
5065                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_BODY:
5066                      return $this->step_after_body();
5067  
5068                  case WP_HTML_Processor_State::INSERTION_MODE_IN_FRAMESET:
5069                      return $this->step_in_frameset();
5070  
5071                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_FRAMESET:
5072                      return $this->step_after_frameset();
5073  
5074                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_BODY:
5075                      return $this->step_after_after_body();
5076  
5077                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET:
5078                      return $this->step_after_after_frameset();
5079  
5080                  // This should be unreachable but PHP doesn't have total type checking on switch.
5081                  default:
5082                      $this->bail( "Unaware of the requested parsing mode: '{$this->state->insertion_mode}'." );
5083              }
5084          }
5085  
5086          $this->bail( 'Should not have been able to reach end of IN FOREIGN CONTENT processing. Check HTML API code.' );
5087      }
5088  
5089      /*
5090       * Internal helpers
5091       */
5092  
5093      /**
5094       * Creates a new bookmark for the currently-matched token and returns the generated name.
5095       *
5096       * @since 6.4.0
5097       * @since 6.5.0 Renamed from bookmark_tag() to bookmark_token().
5098       * @ignore
5099       *
5100       * @throws Exception When unable to allocate requested bookmark.
5101       *
5102       * @return string Name of created bookmark.
5103       */
5104  	private function bookmark_token() {
5105          if ( ! parent::set_bookmark( (string) ++$this->bookmark_counter ) ) {
5106              $this->last_error = self::ERROR_EXCEEDED_MAX_BOOKMARKS;
5107              throw new Exception( 'could not allocate bookmark' );
5108          }
5109  
5110          return "{$this->bookmark_counter}";
5111      }
5112  
5113      /*
5114       * HTML semantic overrides for Tag Processor
5115       */
5116  
5117      /**
5118       * Indicates the namespace of the current token, or "html" if there is none.
5119       *
5120       * @since 6.7.0
5121       *
5122       * @return string One of "html", "math", or "svg".
5123       */
5124  	public function get_namespace(): string {
5125          if ( ! isset( $this->current_element ) ) {
5126              return parent::get_namespace();
5127          }
5128  
5129          return $this->current_element->token->namespace;
5130      }
5131  
5132      /**
5133       * Returns the uppercase name of the matched tag.
5134       *
5135       * The semantic rules for HTML specify that certain tags be reprocessed
5136       * with a different tag name. Because of this, the tag name presented
5137       * by the HTML Processor may differ from the one reported by the HTML
5138       * Tag Processor, which doesn't apply these semantic rules.
5139       *
5140       * Example:
5141       *
5142       *     $processor = new WP_HTML_Tag_Processor( '<div class="test">Test</div>' );
5143       *     $processor->next_tag() === true;
5144       *     $processor->get_tag() === 'DIV';
5145       *
5146       *     $processor->next_tag() === false;
5147       *     $processor->get_tag() === null;
5148       *
5149       * @since 6.4.0
5150       *
5151       * @return string|null Name of currently matched tag in input HTML, or `null` if none found.
5152       */
5153  	public function get_tag(): ?string {
5154          if ( null !== $this->last_error ) {
5155              return null;
5156          }
5157  
5158          if ( $this->is_virtual() ) {
5159              return $this->current_element->token->node_name;
5160          }
5161  
5162          $tag_name = parent::get_tag();
5163  
5164          /*
5165           * > A start tag whose tag name is "image"
5166           * > Change the token's tag name to "img" and reprocess it. (Don't ask.)
5167           *
5168           * This only applies to tags; a processing instruction target or a
5169           * comment which looks like a processing instruction may also report
5170           * a tag name and must not be rewritten.
5171           */
5172          return ( 'IMAGE' === $tag_name && 'html' === $this->get_namespace() && '#tag' === $this->get_token_type() )
5173              ? 'IMG'
5174              : $tag_name;
5175      }
5176  
5177      /**
5178       * Indicates if the currently matched tag contains the self-closing flag.
5179       *
5180       * No HTML elements ought to have the self-closing flag and for those, the self-closing
5181       * flag will be ignored. For void elements this is benign because they "self close"
5182       * automatically. For non-void HTML elements though problems will appear if someone
5183       * intends to use a self-closing element in place of that element with an empty body.
5184       * For HTML foreign elements and custom elements the self-closing flag determines if
5185       * they self-close or not.
5186       *
5187       * This function does not determine if a tag is self-closing,
5188       * but only if the self-closing flag is present in the syntax.
5189       *
5190       * @since 6.6.0 Subclassed for the HTML Processor.
5191       *
5192       * @return bool Whether the currently matched tag contains the self-closing flag.
5193       */
5194  	public function has_self_closing_flag(): bool {
5195          return $this->is_virtual() ? false : parent::has_self_closing_flag();
5196      }
5197  
5198      /**
5199       * Returns the node name represented by the token.
5200       *
5201       * This matches the DOM API value `nodeName`. Some values
5202       * are static, such as `#text` for a text node, while others
5203       * are dynamically generated from the token itself.
5204       *
5205       * Dynamic names:
5206       *  - Uppercase tag name for tag matches.
5207       *  - `html` for DOCTYPE declarations.
5208       *
5209       * Note that if the Tag Processor is not matched on a token
5210       * then this function will return `null`, either because it
5211       * hasn't yet found a token or because it reached the end
5212       * of the document without matching a token.
5213       *
5214       * @since 6.6.0 Subclassed for the HTML Processor.
5215       *
5216       * @return string|null Name of the matched token.
5217       */
5218  	public function get_token_name(): ?string {
5219          return $this->is_virtual()
5220              ? $this->current_element->token->node_name
5221              : parent::get_token_name();
5222      }
5223  
5224      /**
5225       * Indicates the kind of matched token, if any.
5226       *
5227       * This differs from `get_token_name()` in that it always
5228       * returns a static string indicating the type, whereas
5229       * `get_token_name()` may return values derived from the
5230       * token itself, such as a tag name or processing
5231       * instruction tag.
5232       *
5233       * Possible values:
5234       *  - `#tag` when matched on a tag.
5235       *  - `#text` when matched on a text node.
5236       *  - `#cdata-section` when matched on a CDATA node.
5237       *  - `#comment` when matched on a comment.
5238       *  - `#doctype` when matched on a DOCTYPE declaration.
5239       *  - `#presumptuous-tag` when matched on an empty tag closer.
5240       *  - `#funky-comment` when matched on a funky comment.
5241       *  - `#processing-instruction` when matched on a processing instruction.
5242       *
5243       * @since 6.6.0 Subclassed for the HTML Processor.
5244       * @since 7.1.0 Recognize processing instructions according to an HTML
5245       *              specification update.
5246       *
5247       * @return string|null What kind of token is matched, or null.
5248       */
5249  	public function get_token_type(): ?string {
5250          if ( $this->is_virtual() ) {
5251              /*
5252               * This logic comes from the Tag Processor.
5253               *
5254               * @todo It would be ideal not to repeat this here, but it's not clearly
5255               *       better to allow passing a token name to `get_token_type()`.
5256               */
5257              $node_name     = $this->current_element->token->node_name;
5258              $starting_char = $node_name[0];
5259              if ( 'A' <= $starting_char && 'Z' >= $starting_char ) {
5260                  return '#tag';
5261              }
5262  
5263              if ( 'html' === $node_name ) {
5264                  return '#doctype';
5265              }
5266  
5267              return $node_name;
5268          }
5269  
5270          return parent::get_token_type();
5271      }
5272  
5273      /**
5274       * Returns the value of a requested attribute from a matched tag opener if that attribute exists.
5275       *
5276       * Example:
5277       *
5278       *     $p = WP_HTML_Processor::create_fragment( '<div enabled class="test" data-test-id="14">Test</div>' );
5279       *     $p->next_token() === true;
5280       *     $p->get_attribute( 'data-test-id' ) === '14';
5281       *     $p->get_attribute( 'enabled' ) === true;
5282       *     $p->get_attribute( 'aria-label' ) === null;
5283       *
5284       *     $p->next_tag() === false;
5285       *     $p->get_attribute( 'class' ) === null;
5286       *
5287       * @since 6.6.0 Subclassed for HTML Processor.
5288       *
5289       * @param string $name Name of attribute whose value is requested.
5290       * @return string|true|null Value of attribute or `null` if not available. Boolean attributes return `true`.
5291       */
5292  	public function get_attribute( $name ) {
5293          return $this->is_virtual() ? null : parent::get_attribute( $name );
5294      }
5295  
5296      /**
5297       * Updates or creates a new attribute on the currently matched tag with the passed value.
5298       *
5299       * This function handles all necessary HTML encoding. Provide normal, unescaped string values.
5300       * The HTML API will encode the strings appropriately so that the browser will interpret them
5301       * as the intended value.
5302       *
5303       * Example:
5304       *
5305       *     // Renders “Eggs & Milk” in a browser, encoded as `<abbr title="Eggs &amp; Milk">`.
5306       *     $processor->set_attribute( 'title', 'Eggs & Milk' );
5307       *
5308       *     // Renders “Eggs &amp; Milk” in a browser, encoded as `<abbr title="Eggs &amp;amp; Milk">`.
5309       *     $processor->set_attribute( 'title', 'Eggs &amp; Milk' );
5310       *
5311       *     // Renders `true` as `<abbr title>`.
5312       *     $processor->set_attribute( 'title', true );
5313       *
5314       *     // Renders without the attribute for `false` as `<abbr>`.
5315       *     $processor->set_attribute( 'title', false );
5316       *
5317       * Special handling is provided for boolean attribute values:
5318       *  - When `true` is passed as the value, then only the attribute name is added to the tag.
5319       *  - When `false` is passed, the attribute gets removed if it existed before.
5320       *
5321       * @since 6.6.0 Subclassed for the HTML Processor.
5322       * @since 6.9.0 Escapes all character references instead of trying to avoid double-escaping.
5323       *
5324       * @param string      $name  The attribute name to target.
5325       * @param string|bool $value The new attribute value.
5326       * @return bool Whether an attribute value was set.
5327       */
5328  	public function set_attribute( $name, $value ): bool {
5329          return $this->is_virtual() ? false : parent::set_attribute( $name, $value );
5330      }
5331  
5332      /**
5333       * Remove an attribute from the currently-matched tag.
5334       *
5335       * @since 6.6.0 Subclassed for HTML Processor.
5336       *
5337       * @param string $name The attribute name to remove.
5338       * @return bool Whether an attribute was removed.
5339       */
5340  	public function remove_attribute( $name ): bool {
5341          return $this->is_virtual() ? false : parent::remove_attribute( $name );
5342      }
5343  
5344      /**
5345       * Gets lowercase names of all attributes matching a given prefix in the current tag.
5346       *
5347       * Note that matching is case-insensitive. This is in accordance with the spec:
5348       *
5349       * > There must never be two or more attributes on
5350       * > the same start tag whose names are an ASCII
5351       * > case-insensitive match for each other.
5352       *     - HTML 5 spec
5353       *
5354       * Example:
5355       *
5356       *     $p = new WP_HTML_Tag_Processor( '<div data-ENABLED class="test" DATA-test-id="14">Test</div>' );
5357       *     $p->next_tag( array( 'class_name' => 'test' ) ) === true;
5358       *     $p->get_attribute_names_with_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' );
5359       *
5360       *     $p->next_tag() === false;
5361       *     $p->get_attribute_names_with_prefix( 'data-' ) === null;
5362       *
5363       * @since 6.6.0 Subclassed for the HTML Processor.
5364       *
5365       * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive
5366       *
5367       * @param string $prefix Prefix of requested attribute names.
5368       * @return array|null List of attribute names, or `null` when no tag opener is matched.
5369       */
5370  	public function get_attribute_names_with_prefix( $prefix ): ?array {
5371          return $this->is_virtual() ? null : parent::get_attribute_names_with_prefix( $prefix );
5372      }
5373  
5374      /**
5375       * Adds a new class name to the currently matched tag.
5376       *
5377       * @since 6.6.0 Subclassed for the HTML Processor.
5378       *
5379       * @param string $class_name The class name to add.
5380       * @return bool Whether the class was set to be added.
5381       */
5382  	public function add_class( $class_name ): bool {
5383          return $this->is_virtual() ? false : parent::add_class( $class_name );
5384      }
5385  
5386      /**
5387       * Removes a class name from the currently matched tag.
5388       *
5389       * @since 6.6.0 Subclassed for the HTML Processor.
5390       *
5391       * @param string $class_name The class name to remove.
5392       * @return bool Whether the class was set to be removed.
5393       */
5394  	public function remove_class( $class_name ): bool {
5395          return $this->is_virtual() ? false : parent::remove_class( $class_name );
5396      }
5397  
5398      /**
5399       * Returns if a matched tag contains the given ASCII case-insensitive class name.
5400       *
5401       * @since 6.6.0 Subclassed for the HTML Processor.
5402       *
5403       * @todo When reconstructing active formatting elements with attributes, find a way
5404       *       to indicate if the virtually-reconstructed formatting elements contain the
5405       *       wanted class name.
5406       *
5407       * @param string $wanted_class Look for this CSS class name, ASCII case-insensitive.
5408       * @return bool|null Whether the matched tag contains the given class name, or null if not matched.
5409       */
5410  	public function has_class( $wanted_class ): ?bool {
5411          return $this->is_virtual() ? null : parent::has_class( $wanted_class );
5412      }
5413  
5414      /**
5415       * Generator for a foreach loop to step through each class name for the matched tag.
5416       *
5417       * This generator function is designed to be used inside a "foreach" loop.
5418       *
5419       * ```php interactive
5420       * $p = WP_HTML_Processor::create_fragment( "<div class='free &lt;egg&gt;\tlang-en'>" );
5421       * $p->next_tag();
5422       * foreach ( $p->class_list() as $class_name ) {
5423       *   echo "{$class_name} ";
5424       * }
5425       * // Outputs: "free <egg> lang-en "
5426       * ```
5427       *
5428       * @since 6.6.0 Subclassed for the HTML Processor.
5429       */
5430  	public function class_list() {
5431          return $this->is_virtual() ? null : parent::class_list();
5432      }
5433  
5434      /**
5435       * Returns the modifiable text for a matched token, or an empty string.
5436       *
5437       * Modifiable text is text content that may be read and changed without
5438       * changing the HTML structure of the document around it. This includes
5439       * the contents of `#text` nodes in the HTML as well as the inner
5440       * contents of HTML comments, Processing Instructions, and others, even
5441       * though these nodes aren't part of a parsed DOM tree. They also contain
5442       * the contents of SCRIPT and STYLE tags, of TEXTAREA tags, and of any
5443       * other section in an HTML document which cannot contain HTML markup (DATA).
5444       *
5445       * If a token has no modifiable text then an empty string is returned to
5446       * avoid needless crashing or type errors. An empty string does not mean
5447       * that a token has modifiable text, and a token with modifiable text may
5448       * have an empty string (e.g. a comment with no contents).
5449       *
5450       * @since 6.6.0 Subclassed for the HTML Processor.
5451       *
5452       * @return string
5453       */
5454  	public function get_modifiable_text(): string {
5455          return $this->is_virtual() ? '' : parent::get_modifiable_text();
5456      }
5457  
5458      /**
5459       * Indicates what kind of comment produced the comment node.
5460       *
5461       * Because there are different kinds of HTML syntax which produce
5462       * comments, the Tag Processor tracks and exposes this as a type
5463       * for the comment. Nominally only regular HTML comments exist as
5464       * they are commonly known, but a number of unrelated syntax errors
5465       * also produce comments.
5466       *
5467       * @see self::COMMENT_AS_ABRUPTLY_CLOSED_COMMENT
5468       * @see self::COMMENT_AS_CDATA_LOOKALIKE
5469       * @see self::COMMENT_AS_INVALID_HTML
5470       * @see self::COMMENT_AS_HTML_COMMENT
5471       * @see self::COMMENT_AS_PI_NODE_LOOKALIKE
5472       *
5473       * @since 6.6.0 Subclassed for the HTML Processor.
5474       *
5475       * @return string|null
5476       */
5477  	public function get_comment_type(): ?string {
5478          return $this->is_virtual() ? null : parent::get_comment_type();
5479      }
5480  
5481      /**
5482       * Removes a bookmark that is no longer needed.
5483       *
5484       * Releasing a bookmark frees up the small
5485       * performance overhead it requires.
5486       *
5487       * @since 6.4.0
5488       *
5489       * @param string $bookmark_name Name of the bookmark to remove.
5490       * @return bool Whether the bookmark already existed before removal.
5491       */
5492  	public function release_bookmark( $bookmark_name ): bool {
5493          return parent::release_bookmark( "_{$bookmark_name}" );
5494      }
5495  
5496      /**
5497       * Moves the internal cursor in the HTML Processor to a given bookmark's location.
5498       *
5499       * Be careful! Seeking backwards to a previous location resets the parser to the
5500       * start of the document and reparses the entire contents up until it finds the
5501       * sought-after bookmarked location.
5502       *
5503       * In order to prevent accidental infinite loops, there's a
5504       * maximum limit on the number of times seek() can be called.
5505       *
5506       * @throws Exception When unable to allocate a bookmark for the next token in the input HTML document.
5507       *
5508       * @since 6.4.0
5509       *
5510       * @param string $bookmark_name Jump to the place in the document identified by this bookmark name.
5511       * @return bool Whether the internal cursor was successfully moved to the bookmark's location.
5512       */
5513  	public function seek( $bookmark_name ): bool {
5514          // Flush any pending updates to the document before beginning.
5515          $this->get_updated_html();
5516  
5517          $actual_bookmark_name = "_{$bookmark_name}";
5518          $processor_started_at = $this->state->current_token
5519              ? $this->bookmarks[ $this->state->current_token->bookmark_name ]->start
5520              : 0;
5521          $bookmark_starts_at   = $this->bookmarks[ $actual_bookmark_name ]->start;
5522          $direction            = $bookmark_starts_at > $processor_started_at ? 'forward' : 'backward';
5523  
5524          /*
5525           * If seeking backwards, it's possible that the sought-after bookmark exists within an element
5526           * which has been closed before the current cursor; in other words, it has already been removed
5527           * from the stack of open elements. This means that it's insufficient to simply pop off elements
5528           * from the stack of open elements which appear after the bookmarked location and then jump to
5529           * that location, as the elements which were open before won't be re-opened.
5530           *
5531           * In order to maintain consistency, the HTML Processor rewinds to the start of the document
5532           * and reparses everything until it finds the sought-after bookmark.
5533           *
5534           * There are potentially better ways to do this: cache the parser state for each bookmark and
5535           * restore it when seeking; store an immutable and idempotent register of where elements open
5536           * and close.
5537           *
5538           * If caching the parser state it will be essential to properly maintain the cached stack of
5539           * open elements and active formatting elements when modifying the document. This could be a
5540           * tedious and time-consuming process as well, and so for now will not be performed.
5541           *
5542           * It may be possible to track bookmarks for where elements open and close, and in doing so
5543           * be able to quickly recalculate breadcrumbs for any element in the document. It may even
5544           * be possible to remove the stack of open elements and compute it on the fly this way.
5545           * If doing this, the parser would need to track the opening and closing locations for all
5546           * tokens in the breadcrumb path for any and all bookmarks. By utilizing bookmarks themselves
5547           * this list could be automatically maintained while modifying the document. Finding the
5548           * breadcrumbs would then amount to traversing that list from the start until the token
5549           * being inspected. Once an element closes, if there are no bookmarks pointing to locations
5550           * within that element, then all of these locations may be forgotten to save on memory use
5551           * and computation time.
5552           */
5553          if ( 'backward' === $direction ) {
5554  
5555              /*
5556               * When moving backward, stateful stacks should be cleared.
5557               */
5558              foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
5559                  $this->state->stack_of_open_elements->remove_node( $item );
5560              }
5561  
5562              foreach ( $this->state->active_formatting_elements->walk_up() as $item ) {
5563                  $this->state->active_formatting_elements->remove_node( $item );
5564              }
5565  
5566              /*
5567               * **After** clearing stacks, more processor state can be reset.
5568               * This must be done after clearing the stack because those stacks generate events that
5569               * would appear on a subsequent call to `next_token()`.
5570               */
5571              $this->state->frameset_ok                       = true;
5572              $this->state->stack_of_template_insertion_modes = array();
5573              $this->state->head_element                      = null;
5574              $this->state->form_element                      = null;
5575              $this->state->current_token                     = null;
5576              $this->current_element                          = null;
5577              $this->element_queue                            = array();
5578  
5579              /*
5580               * The absence of a context node indicates a full parse.
5581               * The presence of a context node indicates a fragment parser.
5582               */
5583              if ( null === $this->context_node ) {
5584                  $this->change_parsing_namespace( 'html' );
5585                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_INITIAL;
5586                  $this->breadcrumbs           = array();
5587  
5588                  $this->bookmarks['initial'] = new WP_HTML_Span( 0, 0 );
5589                  parent::seek( 'initial' );
5590                  unset( $this->bookmarks['initial'] );
5591              } else {
5592  
5593                  /*
5594                   * Push the root-node (HTML) back onto the stack of open elements.
5595                   *
5596                   * Fragment parsers require this extra bit of setup.
5597                   * It's handled in full parsers by advancing the processor state.
5598                   */
5599                  $this->state->stack_of_open_elements->push(
5600                      new WP_HTML_Token(
5601                          'root-node',
5602                          'HTML',
5603                          false
5604                      )
5605                  );
5606  
5607                  $this->change_parsing_namespace(
5608                      $this->context_node->integration_node_type
5609                          ? 'html'
5610                          : $this->context_node->namespace
5611                  );
5612  
5613                  if ( 'TEMPLATE' === $this->context_node->node_name ) {
5614                      $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE;
5615                  }
5616  
5617                  $this->reset_insertion_mode_appropriately();
5618                  $this->breadcrumbs = array_slice( $this->breadcrumbs, 0, 2 );
5619                  parent::seek( $this->context_node->bookmark_name );
5620              }
5621          }
5622  
5623          /*
5624           * Here, the processor moves forward through the document until it matches the bookmark.
5625           * do-while is used here because the processor is expected to already be stopped on
5626           * a token than may match the bookmarked location.
5627           */
5628          do {
5629              /*
5630               * The processor will stop on virtual tokens, but bookmarks may not be set on them.
5631               * They should not be matched when seeking a bookmark, skip them.
5632               */
5633              if ( $this->is_virtual() ) {
5634                  continue;
5635              }
5636              if ( $bookmark_starts_at === $this->bookmarks[ $this->state->current_token->bookmark_name ]->start ) {
5637                  return true;
5638              }
5639          } while ( $this->next_token() );
5640  
5641          return false;
5642      }
5643  
5644      /**
5645       * Sets a bookmark in the HTML document.
5646       *
5647       * Bookmarks represent specific places or tokens in the HTML
5648       * document, such as a tag opener or closer. When applying
5649       * edits to a document, such as setting an attribute, the
5650       * text offsets of that token may shift; the bookmark is
5651       * kept updated with those shifts and remains stable unless
5652       * the entire span of text in which the token sits is removed.
5653       *
5654       * Release bookmarks when they are no longer needed.
5655       *
5656       * Example:
5657       *
5658       *     <main><h2>Surprising fact you may not know!</h2></main>
5659       *           ^  ^
5660       *            \-|-- this `H2` opener bookmark tracks the token
5661       *
5662       *     <main class="clickbait"><h2>Surprising fact you may no…
5663       *                             ^  ^
5664       *                              \-|-- it shifts with edits
5665       *
5666       * Bookmarks provide the ability to seek to a previously-scanned
5667       * place in the HTML document. This avoids the need to re-scan
5668       * the entire document.
5669       *
5670       * Example:
5671       *
5672       *     <ul><li>One</li><li>Two</li><li>Three</li></ul>
5673       *                                 ^^^^
5674       *                                 want to note this last item
5675       *
5676       *     $p = new WP_HTML_Tag_Processor( $html );
5677       *     $in_list = false;
5678       *     while ( $p->next_tag( array( 'tag_closers' => $in_list ? 'visit' : 'skip' ) ) ) {
5679       *         if ( 'UL' === $p->get_tag() ) {
5680       *             if ( $p->is_tag_closer() ) {
5681       *                 $in_list = false;
5682       *                 $p->set_bookmark( 'resume' );
5683       *                 if ( $p->seek( 'last-li' ) ) {
5684       *                     $p->add_class( 'last-li' );
5685       *                 }
5686       *                 $p->seek( 'resume' );
5687       *                 $p->release_bookmark( 'last-li' );
5688       *                 $p->release_bookmark( 'resume' );
5689       *             } else {
5690       *                 $in_list = true;
5691       *             }
5692       *         }
5693       *
5694       *         if ( 'LI' === $p->get_tag() ) {
5695       *             $p->set_bookmark( 'last-li' );
5696       *         }
5697       *     }
5698       *
5699       * Bookmarks intentionally hide the internal string offsets
5700       * to which they refer. They are maintained internally as
5701       * updates are applied to the HTML document and therefore
5702       * retain their "position" - the location to which they
5703       * originally pointed. The inability to use bookmarks with
5704       * functions like `substr` is therefore intentional to guard
5705       * against accidentally breaking the HTML.
5706       *
5707       * Because bookmarks allocate memory and require processing
5708       * for every applied update, they are limited and require
5709       * a name. They should not be created with programmatically-made
5710       * names, such as "li_{$index}" with some loop. As a general
5711       * rule they should only be created with string-literal names
5712       * like "start-of-section" or "last-paragraph".
5713       *
5714       * Bookmarks are a powerful tool to enable complicated behavior.
5715       * Consider double-checking that you need this tool if you are
5716       * reaching for it, as inappropriate use could lead to broken
5717       * HTML structure or unwanted processing overhead.
5718       *
5719       * Bookmarks cannot be set on tokens that do no appear in the original
5720       * HTML text. For example, the HTML `<table><td>` stops at tags `TABLE`,
5721       * `TBODY`, `TR`, and `TD`. The `TBODY` and `TR` tags do not appear in
5722       * the original HTML and cannot be used as bookmarks.
5723       *
5724       * @since 6.4.0
5725       *
5726       * @param string $bookmark_name Identifies this particular bookmark.
5727       * @return bool Whether the bookmark was successfully created.
5728       */
5729  	public function set_bookmark( $bookmark_name ): bool {
5730          if ( $this->is_virtual() ) {
5731              _doing_it_wrong(
5732                  __METHOD__,
5733                  __( 'Cannot set bookmarks on tokens that do no appear in the original HTML text.' ),
5734                  '6.8.0'
5735              );
5736              return false;
5737          }
5738          return parent::set_bookmark( "_{$bookmark_name}" );
5739      }
5740  
5741      /**
5742       * Checks whether a bookmark with the given name exists.
5743       *
5744       * @since 6.5.0
5745       *
5746       * @param string $bookmark_name Name to identify a bookmark that potentially exists.
5747       * @return bool Whether that bookmark exists.
5748       */
5749  	public function has_bookmark( $bookmark_name ): bool {
5750          return parent::has_bookmark( "_{$bookmark_name}" );
5751      }
5752  
5753      /*
5754       * HTML Parsing Algorithms
5755       */
5756  
5757      /**
5758       * Closes a P element.
5759       *
5760       * @since 6.4.0
5761       * @ignore
5762       *
5763       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
5764       *
5765       * @see https://html.spec.whatwg.org/#close-a-p-element
5766       */
5767  	private function close_a_p_element(): void {
5768          $this->generate_implied_end_tags( 'P' );
5769          $this->state->stack_of_open_elements->pop_until( 'P' );
5770      }
5771  
5772      /**
5773       * Closes elements that have implied end tags.
5774       *
5775       * @since 6.4.0
5776       * @since 6.7.0 Full spec support.
5777       * @ignore
5778       *
5779       * @see https://html.spec.whatwg.org/#generate-implied-end-tags
5780       *
5781       * @param string|null $except_for_this_element Perform as if this element doesn't exist in the stack of open elements.
5782       */
5783  	private function generate_implied_end_tags( ?string $except_for_this_element = null ): void {
5784          $elements_with_implied_end_tags = array(
5785              'DD',
5786              'DT',
5787              'LI',
5788              'OPTGROUP',
5789              'OPTION',
5790              'P',
5791              'RB',
5792              'RP',
5793              'RT',
5794              'RTC',
5795          );
5796  
5797          $no_exclusions = ! isset( $except_for_this_element );
5798  
5799          while (
5800              ( $no_exclusions || ! $this->state->stack_of_open_elements->current_node_is( $except_for_this_element ) ) &&
5801              in_array( $this->state->stack_of_open_elements->current_node()->node_name, $elements_with_implied_end_tags, true )
5802          ) {
5803              $this->state->stack_of_open_elements->pop();
5804          }
5805      }
5806  
5807      /**
5808       * Closes elements that have implied end tags, thoroughly.
5809       *
5810       * See the HTML specification for an explanation why this is
5811       * different from generating end tags in the normal sense.
5812       *
5813       * @since 6.4.0
5814       * @since 6.7.0 Full spec support.
5815       * @ignore
5816       *
5817       * @see WP_HTML_Processor::generate_implied_end_tags
5818       * @see https://html.spec.whatwg.org/#generate-implied-end-tags
5819       */
5820  	private function generate_implied_end_tags_thoroughly(): void {
5821          $elements_with_implied_end_tags = array(
5822              'CAPTION',
5823              'COLGROUP',
5824              'DD',
5825              'DT',
5826              'LI',
5827              'OPTGROUP',
5828              'OPTION',
5829              'P',
5830              'RB',
5831              'RP',
5832              'RT',
5833              'RTC',
5834              'TBODY',
5835              'TD',
5836              'TFOOT',
5837              'TH',
5838              'THEAD',
5839              'TR',
5840          );
5841  
5842          while ( in_array( $this->state->stack_of_open_elements->current_node()->node_name, $elements_with_implied_end_tags, true ) ) {
5843              $this->state->stack_of_open_elements->pop();
5844          }
5845      }
5846  
5847      /**
5848       * Returns the adjusted current node.
5849       *
5850       * > The adjusted current node is the context element if the parser was created as
5851       * > part of the HTML fragment parsing algorithm and the stack of open elements
5852       * > has only one element in it (fragment case); otherwise, the adjusted current
5853       * > node is the current node.
5854       *
5855       * @see https://html.spec.whatwg.org/#adjusted-current-node
5856       *
5857       * @since 6.7.0
5858       * @ignore
5859       *
5860       * @return WP_HTML_Token|null The adjusted current node.
5861       */
5862  	private function get_adjusted_current_node(): ?WP_HTML_Token {
5863          if ( isset( $this->context_node ) && 1 === $this->state->stack_of_open_elements->count() ) {
5864              return $this->context_node;
5865          }
5866  
5867          return $this->state->stack_of_open_elements->current_node();
5868      }
5869  
5870      /**
5871       * Reconstructs the active formatting elements.
5872       *
5873       * > This has the effect of reopening all the formatting elements that were opened
5874       * > in the current body, cell, or caption (whichever is youngest) that haven't
5875       * > been explicitly closed.
5876       *
5877       * @since 6.4.0
5878       * @ignore
5879       *
5880       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
5881       *
5882       * @see https://html.spec.whatwg.org/#reconstruct-the-active-formatting-elements
5883       *
5884       * @return bool Whether any formatting elements needed to be reconstructed.
5885       */
5886  	private function reconstruct_active_formatting_elements(): bool {
5887          /*
5888           * > If there are no entries in the list of active formatting elements, then there is nothing
5889           * > to reconstruct; stop this algorithm.
5890           */
5891          if ( 0 === $this->state->active_formatting_elements->count() ) {
5892              return false;
5893          }
5894  
5895          $last_entry = $this->state->active_formatting_elements->current_node();
5896          if (
5897  
5898              /*
5899               * > If the last (most recently added) entry in the list of active formatting elements is a marker;
5900               * > stop this algorithm.
5901               */
5902              'marker' === $last_entry->node_name ||
5903  
5904              /*
5905               * > If the last (most recently added) entry in the list of active formatting elements is an
5906               * > element that is in the stack of open elements, then there is nothing to reconstruct;
5907               * > stop this algorithm.
5908               */
5909              $this->state->stack_of_open_elements->contains_node( $last_entry )
5910          ) {
5911              return false;
5912          }
5913  
5914          $this->bail( 'Cannot reconstruct active formatting elements when advancing and rewinding is required.' );
5915      }
5916  
5917      /**
5918       * Runs the reset the insertion mode appropriately algorithm.
5919       *
5920       * @since 6.7.0
5921       * @ignore
5922       *
5923       * @see https://html.spec.whatwg.org/multipage/parsing.html#reset-the-insertion-mode-appropriately
5924       */
5925  	private function reset_insertion_mode_appropriately(): void {
5926          // Set the first node.
5927          $first_node = null;
5928          foreach ( $this->state->stack_of_open_elements->walk_down() as $first_node ) {
5929              break;
5930          }
5931  
5932          /*
5933           * > 1. Let _last_ be false.
5934           */
5935          $last = false;
5936          foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) {
5937              /*
5938               * > 2. Let _node_ be the last node in the stack of open elements.
5939               * > 3. _Loop_: If _node_ is the first node in the stack of open elements, then set _last_
5940               * >            to true, and, if the parser was created as part of the HTML fragment parsing
5941               * >            algorithm (fragment case), set node to the context element passed to
5942               * >            that algorithm.
5943               * > …
5944               */
5945              if ( $node === $first_node ) {
5946                  $last = true;
5947                  if ( isset( $this->context_node ) ) {
5948                      $node = $this->context_node;
5949                  }
5950              }
5951  
5952              // All of the following rules are for matching HTML elements.
5953              if ( 'html' !== $node->namespace ) {
5954                  continue;
5955              }
5956  
5957              switch ( $node->node_name ) {
5958                  /*
5959                   * > 4. If _node_ is a `td` or `th` element and _last_ is false, then switch the
5960                   * >    insertion mode to "in cell" and return.
5961                   */
5962                  case 'TD':
5963                  case 'TH':
5964                      if ( ! $last ) {
5965                          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CELL;
5966                          return;
5967                      }
5968                      break;
5969  
5970                  /*
5971                  * > 5. If _node_ is a `tr` element, then switch the insertion mode to "in row"
5972                  * >    and return.
5973                  */
5974                  case 'TR':
5975                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
5976                      return;
5977  
5978                  /*
5979                   * > 6. If _node_ is a `tbody`, `thead`, or `tfoot` element, then switch the
5980                   * >    insertion mode to "in table body" and return.
5981                   */
5982                  case 'TBODY':
5983                  case 'THEAD':
5984                  case 'TFOOT':
5985                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
5986                      return;
5987  
5988                  /*
5989                   * > 7. If _node_ is a `caption` element, then switch the insertion mode to
5990                   * >    "in caption" and return.
5991                   */
5992                  case 'CAPTION':
5993                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION;
5994                      return;
5995  
5996                  /*
5997                   * > 8. If _node_ is a `colgroup` element, then switch the insertion mode to
5998                   * >    "in column group" and return.
5999                   */
6000                  case 'COLGROUP':
6001                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP;
6002                      return;
6003  
6004                  /*
6005                   * > 9. If _node_ is a `table` element, then switch the insertion mode to
6006                   * >    "in table" and return.
6007                   */
6008                  case 'TABLE':
6009                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
6010                      return;
6011  
6012                  /*
6013                   * > 10. If _node_ is a `template` element, then switch the insertion mode to the
6014                   * >     current template insertion mode and return.
6015                   */
6016                  case 'TEMPLATE':
6017                      $this->state->insertion_mode = end( $this->state->stack_of_template_insertion_modes );
6018                      return;
6019  
6020                  /*
6021                   * > 11. If _node_ is a `head` element and _last_ is false, then switch the
6022                   * >     insertion mode to "in head" and return.
6023                   */
6024                  case 'HEAD':
6025                      if ( ! $last ) {
6026                          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD;
6027                          return;
6028                      }
6029                      break;
6030  
6031                  /*
6032                   * > 12. If _node_ is a `body` element, then switch the insertion mode to "in body"
6033                   * >     and return.
6034                   */
6035                  case 'BODY':
6036                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
6037                      return;
6038  
6039                  /*
6040                   * > 13. If _node_ is a `frameset` element, then switch the insertion mode to
6041                   * >     "in frameset" and return. (fragment case)
6042                   */
6043                  case 'FRAMESET':
6044                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_FRAMESET;
6045                      return;
6046  
6047                  /*
6048                   * > 14. If _node_ is an `html` element, run these substeps:
6049                   * >     1. If the head element pointer is null, switch the insertion mode to
6050                   * >        "before head" and return. (fragment case)
6051                   * >     2. Otherwise, the head element pointer is not null, switch the insertion
6052                   * >        mode to "after head" and return.
6053                   */
6054                  case 'HTML':
6055                      $this->state->insertion_mode = isset( $this->state->head_element )
6056                          ? WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD
6057                          : WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD;
6058                      return;
6059              }
6060          }
6061  
6062          /*
6063           * > 15. If _last_ is true, then switch the insertion mode to "in body"
6064           * >     and return. (fragment case)
6065           *
6066           * This is only reachable if `$last` is true, as per the fragment parsing case.
6067           */
6068          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
6069      }
6070  
6071      /**
6072       * Runs the adoption agency algorithm.
6073       *
6074       * @since 6.4.0
6075       * @ignore
6076       *
6077       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
6078       *
6079       * @see https://html.spec.whatwg.org/#adoption-agency-algorithm
6080       */
6081  	private function run_adoption_agency_algorithm(): void {
6082          $budget       = 1000;
6083          $subject      = $this->get_tag();
6084          $current_node = $this->state->stack_of_open_elements->current_node();
6085  
6086          if (
6087              // > If the current node is an HTML element whose tag name is subject
6088              $current_node && $subject === $current_node->node_name &&
6089              // > the current node is not in the list of active formatting elements
6090              ! $this->state->active_formatting_elements->contains_node( $current_node )
6091          ) {
6092              $this->state->stack_of_open_elements->pop();
6093              return;
6094          }
6095  
6096          $outer_loop_counter = 0;
6097          while ( $budget-- > 0 ) {
6098              if ( $outer_loop_counter++ >= 8 ) {
6099                  return;
6100              }
6101  
6102              /*
6103               * > Let formatting element be the last element in the list of active formatting elements that:
6104               * >   - is between the end of the list and the last marker in the list,
6105               * >     if any, or the start of the list otherwise,
6106               * >   - and has the tag name subject.
6107               */
6108              $formatting_element = null;
6109              foreach ( $this->state->active_formatting_elements->walk_up() as $item ) {
6110                  if ( 'marker' === $item->node_name ) {
6111                      break;
6112                  }
6113  
6114                  if ( $subject === $item->node_name ) {
6115                      $formatting_element = $item;
6116                      break;
6117                  }
6118              }
6119  
6120              // > If there is no such element, then return and instead act as described in the "any other end tag" entry above.
6121              if ( null === $formatting_element ) {
6122                  $this->bail( 'Cannot run adoption agency when "any other end tag" is required.' );
6123              }
6124  
6125              // > If formatting element is not in the stack of open elements, then this is a parse error; remove the element from the list, and return.
6126              if ( ! $this->state->stack_of_open_elements->contains_node( $formatting_element ) ) {
6127                  $this->state->active_formatting_elements->remove_node( $formatting_element );
6128                  return;
6129              }
6130  
6131              // > If formatting element is in the stack of open elements, but the element is not in scope, then this is a parse error; return.
6132              if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $formatting_element->node_name ) ) {
6133                  return;
6134              }
6135  
6136              /*
6137               * > Let furthest block be the topmost node in the stack of open elements that is lower in the stack
6138               * > than formatting element, and is an element in the special category. There might not be one.
6139               */
6140              $is_above_formatting_element = true;
6141              $furthest_block              = null;
6142              foreach ( $this->state->stack_of_open_elements->walk_down() as $item ) {
6143                  if ( $is_above_formatting_element && $formatting_element->bookmark_name !== $item->bookmark_name ) {
6144                      continue;
6145                  }
6146  
6147                  if ( $is_above_formatting_element ) {
6148                      $is_above_formatting_element = false;
6149                      continue;
6150                  }
6151  
6152                  if ( self::is_special( $item ) ) {
6153                      $furthest_block = $item;
6154                      break;
6155                  }
6156              }
6157  
6158              /*
6159               * > If there is no furthest block, then the UA must first pop all the nodes from the bottom of the
6160               * > stack of open elements, from the current node up to and including formatting element, then
6161               * > remove formatting element from the list of active formatting elements, and finally return.
6162               */
6163              if ( null === $furthest_block ) {
6164                  foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
6165                      $this->state->stack_of_open_elements->pop();
6166  
6167                      if ( $formatting_element->bookmark_name === $item->bookmark_name ) {
6168                          $this->state->active_formatting_elements->remove_node( $formatting_element );
6169                          return;
6170                      }
6171                  }
6172              }
6173  
6174              $this->bail( 'Cannot extract common ancestor in adoption agency algorithm.' );
6175          }
6176  
6177          $this->bail( 'Cannot run adoption agency when looping required.' );
6178      }
6179  
6180      /**
6181       * Runs the "close the cell" algorithm.
6182       *
6183       * > Where the steps above say to close the cell, they mean to run the following algorithm:
6184       * >   1. Generate implied end tags.
6185       * >   2. If the current node is not now a td element or a th element, then this is a parse error.
6186       * >   3. Pop elements from the stack of open elements stack until a td element or a th element has been popped from the stack.
6187       * >   4. Clear the list of active formatting elements up to the last marker.
6188       * >   5. Switch the insertion mode to "in row".
6189       *
6190       * @see https://html.spec.whatwg.org/multipage/parsing.html#close-the-cell
6191       *
6192       * @since 6.7.0
6193       * @ignore
6194       */
6195  	private function close_cell(): void {
6196          $this->generate_implied_end_tags();
6197          // @todo Parse error if the current node is a "td" or "th" element.
6198          foreach ( $this->state->stack_of_open_elements->walk_up() as $element ) {
6199              $this->state->stack_of_open_elements->pop();
6200              if ( 'TD' === $element->node_name || 'TH' === $element->node_name ) {
6201                  break;
6202              }
6203          }
6204          $this->state->active_formatting_elements->clear_up_to_last_marker();
6205          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
6206      }
6207  
6208      /**
6209       * Inserts an HTML element on the stack of open elements.
6210       *
6211       * @since 6.4.0
6212       * @ignore
6213       *
6214       * @see https://html.spec.whatwg.org/#insert-a-foreign-element
6215       *
6216       * @param WP_HTML_Token $token Name of bookmark pointing to element in original input HTML.
6217       */
6218  	private function insert_html_element( WP_HTML_Token $token ): void {
6219          $this->state->stack_of_open_elements->push( $token );
6220      }
6221  
6222      /**
6223       * Inserts a foreign element on to the stack of open elements.
6224       *
6225       * @since 6.7.0
6226       * @ignore
6227       *
6228       * @see https://html.spec.whatwg.org/#insert-a-foreign-element
6229       *
6230       * @param WP_HTML_Token $token                     Insert this token. The token's namespace and
6231       *                                                 insertion point will be updated correctly.
6232       * @param bool          $only_add_to_element_stack Whether to skip the "insert an element at the adjusted
6233       *                                                 insertion location" algorithm when adding this element.
6234       */
6235  	private function insert_foreign_element( WP_HTML_Token $token, bool $only_add_to_element_stack ): void {
6236          $adjusted_current_node = $this->get_adjusted_current_node();
6237  
6238          $token->namespace = $adjusted_current_node ? $adjusted_current_node->namespace : 'html';
6239  
6240          if ( $this->is_mathml_integration_point() ) {
6241              $token->integration_node_type = 'math';
6242          } elseif ( $this->is_html_integration_point() ) {
6243              $token->integration_node_type = 'html';
6244          }
6245  
6246          if ( false === $only_add_to_element_stack ) {
6247              /*
6248               * @todo Implement the "appropriate place for inserting a node" and the
6249               *       "insert an element at the adjusted insertion location" algorithms.
6250               *
6251               * These algorithms mostly impacts DOM tree construction and not the HTML API.
6252               * Here, there's no DOM node onto which the element will be appended, so the
6253               * parser will skip this step.
6254               *
6255               * @see https://html.spec.whatwg.org/#insert-an-element-at-the-adjusted-insertion-location
6256               */
6257          }
6258  
6259          $this->insert_html_element( $token );
6260      }
6261  
6262      /**
6263       * Inserts a virtual element on the stack of open elements.
6264       *
6265       * @since 6.7.0
6266       * @ignore
6267       *
6268       * @throws Exception When unable to allocate a bookmark for the next token in the input HTML document.
6269       *
6270       * @param string      $token_name    Name of token to create and insert into the stack of open elements.
6271       * @param string|null $bookmark_name Optional. Name to give bookmark for created virtual node.
6272       *                                   Defaults to auto-creating a bookmark name.
6273       * @return WP_HTML_Token Newly-created virtual token.
6274       */
6275  	private function insert_virtual_node( $token_name, $bookmark_name = null ): WP_HTML_Token {
6276          $here = $this->bookmarks[ $this->state->current_token->bookmark_name ];
6277          $name = $bookmark_name ?? $this->bookmark_token();
6278  
6279          $this->bookmarks[ $name ] = new WP_HTML_Span( $here->start, 0 );
6280  
6281          $token = new WP_HTML_Token( $name, $token_name, false );
6282          $this->insert_html_element( $token );
6283          return $token;
6284      }
6285  
6286      /*
6287       * HTML Specification Helpers
6288       */
6289  
6290      /**
6291       * Indicates if the current token is a MathML integration point.
6292       *
6293       * @since 6.7.0
6294       * @ignore
6295       *
6296       * @see https://html.spec.whatwg.org/#mathml-text-integration-point
6297       *
6298       * @return bool Whether the current token is a MathML integration point.
6299       */
6300  	private function is_mathml_integration_point(): bool {
6301          $current_token = $this->state->current_token;
6302          if ( ! isset( $current_token ) ) {
6303              return false;
6304          }
6305  
6306          if ( 'math' !== $current_token->namespace || 'M' !== $current_token->node_name[0] ) {
6307              return false;
6308          }
6309  
6310          $tag_name = $current_token->node_name;
6311  
6312          return (
6313              'MI' === $tag_name ||
6314              'MO' === $tag_name ||
6315              'MN' === $tag_name ||
6316              'MS' === $tag_name ||
6317              'MTEXT' === $tag_name
6318          );
6319      }
6320  
6321      /**
6322       * Indicates if the current token is an HTML integration point.
6323       *
6324       * Note that this method must be an instance method with access
6325       * to the current token, since it needs to examine the attributes
6326       * of the currently-matched tag, if it's in the MathML namespace.
6327       * Otherwise it would be required to scan the HTML and ensure that
6328       * no other accounting is overlooked.
6329       *
6330       * @since 6.7.0
6331       * @ignore
6332       *
6333       * @see https://html.spec.whatwg.org/#html-integration-point
6334       *
6335       * @return bool Whether the current token is an HTML integration point.
6336       */
6337  	private function is_html_integration_point(): bool {
6338          $current_token = $this->state->current_token;
6339          if ( ! isset( $current_token ) ) {
6340              return false;
6341          }
6342  
6343          if ( 'html' === $current_token->namespace ) {
6344              return false;
6345          }
6346  
6347          $tag_name = $current_token->node_name;
6348  
6349          if ( 'svg' === $current_token->namespace ) {
6350              return (
6351                  'DESC' === $tag_name ||
6352                  'FOREIGNOBJECT' === $tag_name ||
6353                  'TITLE' === $tag_name
6354              );
6355          }
6356  
6357          if ( 'math' === $current_token->namespace ) {
6358              if ( 'ANNOTATION-XML' !== $tag_name ) {
6359                  return false;
6360              }
6361  
6362              $encoding = $this->get_attribute( 'encoding' );
6363  
6364              return (
6365                  is_string( $encoding ) &&
6366                  (
6367                      0 === strcasecmp( $encoding, 'application/xhtml+xml' ) ||
6368                      0 === strcasecmp( $encoding, 'text/html' )
6369                  )
6370              );
6371          }
6372  
6373          $this->bail( 'Should not have reached end of HTML Integration Point detection: check HTML API code.' );
6374      }
6375  
6376      /**
6377       * Returns whether an element of a given name is in the HTML special category.
6378       *
6379       * @since 6.4.0
6380       *
6381       * @see https://html.spec.whatwg.org/#special
6382       *
6383       * @param WP_HTML_Token|string $tag_name Node to check, or only its name if in the HTML namespace.
6384       * @return bool Whether the element of the given name is in the special category.
6385       */
6386  	public static function is_special( $tag_name ): bool {
6387          if ( is_string( $tag_name ) ) {
6388              $tag_name = strtoupper( $tag_name );
6389          } else {
6390              $tag_name = 'html' === $tag_name->namespace
6391                  ? strtoupper( $tag_name->node_name )
6392                  : "{$tag_name->namespace} {$tag_name->node_name}";
6393          }
6394  
6395          return (
6396              'ADDRESS' === $tag_name ||
6397              'APPLET' === $tag_name ||
6398              'AREA' === $tag_name ||
6399              'ARTICLE' === $tag_name ||
6400              'ASIDE' === $tag_name ||
6401              'BASE' === $tag_name ||
6402              'BASEFONT' === $tag_name ||
6403              'BGSOUND' === $tag_name ||
6404              'BLOCKQUOTE' === $tag_name ||
6405              'BODY' === $tag_name ||
6406              'BR' === $tag_name ||
6407              'BUTTON' === $tag_name ||
6408              'CAPTION' === $tag_name ||
6409              'CENTER' === $tag_name ||
6410              'COL' === $tag_name ||
6411              'COLGROUP' === $tag_name ||
6412              'DD' === $tag_name ||
6413              'DETAILS' === $tag_name ||
6414              'DIR' === $tag_name ||
6415              'DIV' === $tag_name ||
6416              'DL' === $tag_name ||
6417              'DT' === $tag_name ||
6418              'EMBED' === $tag_name ||
6419              'FIELDSET' === $tag_name ||
6420              'FIGCAPTION' === $tag_name ||
6421              'FIGURE' === $tag_name ||
6422              'FOOTER' === $tag_name ||
6423              'FORM' === $tag_name ||
6424              'FRAME' === $tag_name ||
6425              'FRAMESET' === $tag_name ||
6426              'H1' === $tag_name ||
6427              'H2' === $tag_name ||
6428              'H3' === $tag_name ||
6429              'H4' === $tag_name ||
6430              'H5' === $tag_name ||
6431              'H6' === $tag_name ||
6432              'HEAD' === $tag_name ||
6433              'HEADER' === $tag_name ||
6434              'HGROUP' === $tag_name ||
6435              'HR' === $tag_name ||
6436              'HTML' === $tag_name ||
6437              'IFRAME' === $tag_name ||
6438              'IMG' === $tag_name ||
6439              'INPUT' === $tag_name ||
6440              'KEYGEN' === $tag_name ||
6441              'LI' === $tag_name ||
6442              'LINK' === $tag_name ||
6443              'LISTING' === $tag_name ||
6444              'MAIN' === $tag_name ||
6445              'MARQUEE' === $tag_name ||
6446              'MENU' === $tag_name ||
6447              'META' === $tag_name ||
6448              'NAV' === $tag_name ||
6449              'NOEMBED' === $tag_name ||
6450              'NOFRAMES' === $tag_name ||
6451              'NOSCRIPT' === $tag_name ||
6452              'OBJECT' === $tag_name ||
6453              'OL' === $tag_name ||
6454              'P' === $tag_name ||
6455              'PARAM' === $tag_name ||
6456              'PLAINTEXT' === $tag_name ||
6457              'PRE' === $tag_name ||
6458              'SCRIPT' === $tag_name ||
6459              'SEARCH' === $tag_name ||
6460              'SECTION' === $tag_name ||
6461              'SELECT' === $tag_name ||
6462              'SOURCE' === $tag_name ||
6463              'STYLE' === $tag_name ||
6464              'SUMMARY' === $tag_name ||
6465              'TABLE' === $tag_name ||
6466              'TBODY' === $tag_name ||
6467              'TD' === $tag_name ||
6468              'TEMPLATE' === $tag_name ||
6469              'TEXTAREA' === $tag_name ||
6470              'TFOOT' === $tag_name ||
6471              'TH' === $tag_name ||
6472              'THEAD' === $tag_name ||
6473              'TITLE' === $tag_name ||
6474              'TR' === $tag_name ||
6475              'TRACK' === $tag_name ||
6476              'UL' === $tag_name ||
6477              'WBR' === $tag_name ||
6478              'XMP' === $tag_name ||
6479  
6480              // MathML.
6481              'math MI' === $tag_name ||
6482              'math MO' === $tag_name ||
6483              'math MN' === $tag_name ||
6484              'math MS' === $tag_name ||
6485              'math MTEXT' === $tag_name ||
6486              'math ANNOTATION-XML' === $tag_name ||
6487  
6488              // SVG.
6489              'svg DESC' === $tag_name ||
6490              'svg FOREIGNOBJECT' === $tag_name ||
6491              'svg TITLE' === $tag_name
6492          );
6493      }
6494  
6495      /**
6496       * Returns whether a given element is an HTML Void Element
6497       *
6498       * > area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
6499       *
6500       * @since 6.4.0
6501       *
6502       * @see https://html.spec.whatwg.org/#void-elements
6503       *
6504       * @param string $tag_name Name of HTML tag to check.
6505       * @return bool Whether the given tag is an HTML Void Element.
6506       */
6507  	public static function is_void( $tag_name ): bool {
6508          $tag_name = strtoupper( $tag_name );
6509  
6510          return (
6511              'AREA' === $tag_name ||
6512              'BASE' === $tag_name ||
6513              'BASEFONT' === $tag_name || // Obsolete but still treated as void.
6514              'BGSOUND' === $tag_name || // Obsolete but still treated as void.
6515              'BR' === $tag_name ||
6516              'COL' === $tag_name ||
6517              'EMBED' === $tag_name ||
6518              'FRAME' === $tag_name ||
6519              'HR' === $tag_name ||
6520              'IMG' === $tag_name ||
6521              'INPUT' === $tag_name ||
6522              'KEYGEN' === $tag_name || // Obsolete but still treated as void.
6523              'LINK' === $tag_name ||
6524              'META' === $tag_name ||
6525              'PARAM' === $tag_name || // Obsolete but still treated as void.
6526              'SOURCE' === $tag_name ||
6527              'TRACK' === $tag_name ||
6528              'WBR' === $tag_name
6529          );
6530      }
6531  
6532      /**
6533       * Gets an encoding from a given string.
6534       *
6535       * This is an algorithm defined in the WHAT-WG specification.
6536       *
6537       * Example:
6538       *
6539       *     'UTF-8' === self::get_encoding( 'utf8' );
6540       *     'UTF-8' === self::get_encoding( "  \tUTF-8 " );
6541       *     null    === self::get_encoding( 'UTF-7' );
6542       *     null    === self::get_encoding( 'utf8; charset=' );
6543       *
6544       * @see https://encoding.spec.whatwg.org/#concept-encoding-get
6545       *
6546       * @todo As this parser only supports UTF-8, only the UTF-8
6547       *       encodings are detected. Add more as desired, but the
6548       *       parser will bail on non-UTF-8 encodings.
6549       *
6550       * @since 6.7.0
6551       *
6552       * @param string $label A string which may specify a known encoding.
6553       * @return string|null Known encoding if matched, otherwise null.
6554       */
6555  	protected static function get_encoding( string $label ): ?string {
6556          /*
6557           * > Remove any leading and trailing ASCII whitespace from label.
6558           */
6559          $label = trim( $label, " \t\f\r\n" );
6560  
6561          /*
6562           * > If label is an ASCII case-insensitive match for any of the labels listed in the
6563           * > table below, then return the corresponding encoding; otherwise return failure.
6564           */
6565          switch ( strtolower( $label ) ) {
6566              case 'unicode-1-1-utf-8':
6567              case 'unicode11utf8':
6568              case 'unicode20utf8':
6569              case 'utf-8':
6570              case 'utf8':
6571              case 'x-unicode20utf8':
6572                  return 'UTF-8';
6573  
6574              default:
6575                  return null;
6576          }
6577      }
6578  
6579      /*
6580       * Constants that would pollute the top of the class if they were found there.
6581       */
6582  
6583      /**
6584       * Indicates that the next HTML token should be parsed and processed.
6585       *
6586       * @since 6.4.0
6587       *
6588       * @var string
6589       */
6590      const PROCESS_NEXT_NODE = 'process-next-node';
6591  
6592      /**
6593       * Indicates that the current HTML token should be reprocessed in the newly-selected insertion mode.
6594       *
6595       * @since 6.4.0
6596       *
6597       * @var string
6598       */
6599      const REPROCESS_CURRENT_NODE = 'reprocess-current-node';
6600  
6601      /**
6602       * Indicates that the current HTML token should be processed without advancing the parser.
6603       *
6604       * @since 6.5.0
6605       *
6606       * @var string
6607       */
6608      const PROCESS_CURRENT_NODE = 'process-current-node';
6609  
6610      /**
6611       * Indicates that the parser encountered unsupported markup and has bailed.
6612       *
6613       * @since 6.4.0
6614       *
6615       * @var string
6616       */
6617      const ERROR_UNSUPPORTED = 'unsupported';
6618  
6619      /**
6620       * Indicates that the parser encountered more HTML tokens than it
6621       * was able to process and has bailed.
6622       *
6623       * @since 6.4.0
6624       *
6625       * @var string
6626       */
6627      const ERROR_EXCEEDED_MAX_BOOKMARKS = 'exceeded-max-bookmarks';
6628  
6629      /**
6630       * Unlock code that must be passed into the constructor to create this class.
6631       *
6632       * This class extends the WP_HTML_Tag_Processor, which has a public class
6633       * constructor. Therefore, it's not possible to have a private constructor here.
6634       *
6635       * This unlock code is used to ensure that anyone calling the constructor is
6636       * doing so with a full understanding that it's intended to be a private API.
6637       *
6638       * @since 6.4.0
6639       * @access private
6640       */
6641      const CONSTRUCTOR_UNLOCK_CODE = 'Use WP_HTML_Processor::create_fragment() instead of calling the class constructor directly.';
6642  }


Generated : Thu Oct 15 08:20:32 2026 Cross-referenced by PHPXref