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


Generated : Thu Jul 16 08:20:16 2026 Cross-referenced by PHPXref