[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

   1  <?php
   2  /**
   3   * HTML API: WP_HTML_Processor class
   4   *
   5   * @package WordPress
   6   * @subpackage HTML-API
   7   * @since 6.4.0
   8   */
   9  
  10  /**
  11   * Core class used to safely parse and modify an HTML document.
  12   *
  13   * The HTML Processor class properly parses and modifies HTML5 documents.
  14   *
  15   * It supports a subset of the HTML5 specification, and when it encounters
  16   * unsupported markup, it aborts early to avoid unintentionally breaking
  17   * the document. The HTML Processor should never break an HTML document.
  18   *
  19   * While the `WP_HTML_Tag_Processor` is a valuable tool for modifying
  20   * attributes on individual HTML tags, the HTML Processor is more capable
  21   * and useful for the following operations:
  22   *
  23   *  - Querying based on nested HTML structure.
  24   *
  25   * Eventually the HTML Processor will also support:
  26   *  - Wrapping a tag in surrounding HTML.
  27   *  - Unwrapping a tag by removing its parent.
  28   *  - Inserting and removing nodes.
  29   *  - Reading and changing inner content.
  30   *  - Navigating up or around HTML structure.
  31   *
  32   * ## Usage
  33   *
  34   * Use of this class requires three steps:
  35   *
  36   *   1. Call a static creator method with your input HTML document.
  37   *   2. Find the location in the document you are looking for.
  38   *   3. Request changes to the document at that location.
  39   *
  40   * Example:
  41   *
  42   *     $processor = WP_HTML_Processor::create_fragment( $html );
  43   *     if ( $processor->next_tag( array( 'breadcrumbs' => array( 'DIV', 'FIGURE', 'IMG' ) ) ) ) {
  44   *         $processor->add_class( 'responsive-image' );
  45   *     }
  46   *
  47   * #### Breadcrumbs
  48   *
  49   * Breadcrumbs represent the stack of open elements from the root
  50   * of the document or fragment down to the currently-matched node,
  51   * if one is currently selected. Call WP_HTML_Processor::get_breadcrumbs()
  52   * to inspect the breadcrumbs for a matched tag.
  53   *
  54   * Breadcrumbs can specify nested HTML structure and are equivalent
  55   * to a CSS selector comprising tag names separated by the child
  56   * combinator, such as "DIV > FIGURE > IMG".
  57   *
  58   * Since all elements find themselves inside a full HTML document
  59   * when parsed, the return value from `get_breadcrumbs()` will always
  60   * contain any implicit outermost elements. For example, when parsing
  61   * with `create_fragment()` in the `BODY` context (the default), any
  62   * tag in the given HTML document will contain `array( 'HTML', 'BODY', … )`
  63   * in its breadcrumbs.
  64   *
  65   * Despite containing the implied outermost elements in their breadcrumbs,
  66   * tags may be found with the shortest-matching breadcrumb query. That is,
  67   * `array( 'IMG' )` matches all IMG elements and `array( 'P', 'IMG' )`
  68   * matches all IMG elements directly inside a P element. To ensure that no
  69   * partial matches erroneously match it's possible to specify in a query
  70   * the full breadcrumb match all the way down from the root HTML element.
  71   *
  72   * Example:
  73   *
  74   *     $html = '<figure><img><figcaption>A <em>lovely</em> day outside</figcaption></figure>';
  75   *     //               ----- Matches here.
  76   *     $processor->next_tag( array( 'breadcrumbs' => array( 'FIGURE', 'IMG' ) ) );
  77   *
  78   *     $html = '<figure><img><figcaption>A <em>lovely</em> day outside</figcaption></figure>';
  79   *     //                                  ---- Matches here.
  80   *     $processor->next_tag( array( 'breadcrumbs' => array( 'FIGURE', 'FIGCAPTION', 'EM' ) ) );
  81   *
  82   *     $html = '<div><img></div><img>';
  83   *     //                       ----- Matches here, because IMG must be a direct child of the implicit BODY.
  84   *     $processor->next_tag( array( 'breadcrumbs' => array( 'BODY', 'IMG' ) ) );
  85   *
  86   * ## HTML Support
  87   *
  88   * This class implements a small part of the HTML5 specification.
  89   * It's designed to operate within its support and abort early whenever
  90   * encountering circumstances it can't properly handle. This is
  91   * the principle way in which this class remains as simple as possible
  92   * without cutting corners and breaking compliance.
  93   *
  94   * ### Supported elements
  95   *
  96   * If any unsupported element appears in the HTML input the HTML Processor
  97   * will abort early and stop all processing. This draconian measure ensures
  98   * that the HTML Processor won't break any HTML it doesn't fully understand.
  99   *
 100   * The HTML Processor supports all elements other than a specific set:
 101   *
 102   *  - Any element inside a TABLE.
 103   *  - Any element inside foreign content, including SVG and MATH.
 104   *  - Any element outside the IN BODY insertion mode, e.g. doctype declarations, meta, links.
 105   *
 106   * ### Supported markup
 107   *
 108   * Some kinds of non-normative HTML involve reconstruction of formatting elements and
 109   * re-parenting of mis-nested elements. For example, a DIV tag found inside a TABLE
 110   * may in fact belong _before_ the table in the DOM. If the HTML Processor encounters
 111   * such a case it will stop processing.
 112   *
 113   * The following list illustrates some common examples of unexpected HTML inputs that
 114   * the HTML Processor properly parses and represents:
 115   *
 116   *  - HTML with optional tags omitted, e.g. `<p>one<p>two`.
 117   *  - HTML with unexpected tag closers, e.g. `<p>one </span> more</p>`.
 118   *  - Non-void tags with self-closing flag, e.g. `<div/>the DIV is still open.</div>`.
 119   *  - Heading elements which close open heading elements of another level, e.g. `<h1>Closed by </h2>`.
 120   *  - Elements containing text that looks like other tags but isn't, e.g. `<title>The <img> is plaintext</title>`.
 121   *  - SCRIPT and STYLE tags containing text that looks like HTML but isn't, e.g. `<script>document.write('<p>Hi</p>');</script>`.
 122   *  - SCRIPT content which has been escaped, e.g. `<script><!-- document.write('<script>console.log("hi")</script>') --></script>`.
 123   *
 124   * ### Unsupported Features
 125   *
 126   * This parser does not report parse errors.
 127   *
 128   * Normally, when additional HTML or BODY tags are encountered in a document, if there
 129   * are any additional attributes on them that aren't found on the previous elements,
 130   * the existing HTML and BODY elements adopt those missing attribute values. This
 131   * parser does not add those additional attributes.
 132   *
 133   * In certain situations, elements are moved to a different part of the document in
 134   * a process called "adoption" and "fostering." Because the nodes move to a location
 135   * in the document that the parser had already processed, this parser does not support
 136   * these situations and will bail.
 137   *
 138   * The parser does not implement the "maybe clone an option into selectedcontent" algorithm.
 139   * SELECTEDCONTENT elements may not reflect the actual selected content.
 140   *
 141   * @since 6.4.0
 142   *
 143   * @see WP_HTML_Tag_Processor
 144   * @link https://html.spec.whatwg.org/
 145   * @phpstan-consistent-constructor
 146   */
 147  class WP_HTML_Processor extends WP_HTML_Tag_Processor {
 148      /**
 149       * The maximum number of bookmarks allowed to exist at any given time.
 150       *
 151       * HTML processing requires more bookmarks than basic tag processing,
 152       * so this class constant from the Tag Processor is overwritten.
 153       *
 154       * @since 6.4.0
 155       * @since 7.0.0 Increased from 100 to 10,000
 156       *
 157       * @var int
 158       */
 159      const MAX_BOOKMARKS = 10_000;
 160  
 161      /**
 162       * Holds the working state of the parser, including the stack of
 163       * open elements and the stack of active formatting elements.
 164       *
 165       * Initialized in the constructor.
 166       *
 167       * @since 6.4.0
 168       *
 169       * @var WP_HTML_Processor_State
 170       */
 171      private $state;
 172  
 173      /**
 174       * Used to create unique bookmark names.
 175       *
 176       * This class sets a bookmark for every tag in the HTML document that it encounters.
 177       * The bookmark name is auto-generated and increments, starting with `1`. These are
 178       * internal bookmarks and are automatically released when the referring WP_HTML_Token
 179       * goes out of scope and is garbage-collected.
 180       *
 181       * @since 6.4.0
 182       *
 183       * @see WP_HTML_Processor::$release_internal_bookmark_on_destruct
 184       *
 185       * @var int
 186       */
 187      private $bookmark_counter = 0;
 188  
 189      /**
 190       * Stores an explanation for why something failed, if it did.
 191       *
 192       * @see self::get_last_error
 193       *
 194       * @since 6.4.0
 195       *
 196       * @var string|null
 197       */
 198      private $last_error = null;
 199  
 200      /**
 201       * Stores context for why the parser bailed on unsupported HTML, if it did.
 202       *
 203       * @see self::get_unsupported_exception
 204       *
 205       * @since 6.7.0
 206       *
 207       * @var WP_HTML_Unsupported_Exception|null
 208       */
 209      private $unsupported_exception = null;
 210  
 211      /**
 212       * Releases a bookmark when PHP garbage-collects its wrapping WP_HTML_Token instance.
 213       *
 214       * This function is created inside the class constructor so that it can be passed to
 215       * the stack of open elements and the stack of active formatting elements without
 216       * exposing it as a public method on the class.
 217       *
 218       * @since 6.4.0
 219       *
 220       * @var Closure|null
 221       */
 222      private $release_internal_bookmark_on_destruct = null;
 223  
 224      /**
 225       * Stores stack events which arise during parsing of the
 226       * HTML document, which will then supply the "match" events.
 227       *
 228       * @since 6.6.0
 229       *
 230       * @var WP_HTML_Stack_Event[]
 231       */
 232      private $element_queue = array();
 233  
 234      /**
 235       * Stores the current breadcrumbs.
 236       *
 237       * @since 6.7.0
 238       *
 239       * @var string[]
 240       */
 241      private $breadcrumbs = array();
 242  
 243      /**
 244       * Current stack event, if set, representing a matched token.
 245       *
 246       * Because the parser may internally point to a place further along in a document
 247       * than the nodes which have already been processed (some "virtual" nodes may have
 248       * appeared while scanning the HTML document), this will point at the "current" node
 249       * being processed. It comes from the front of the element queue.
 250       *
 251       * @since 6.6.0
 252       *
 253       * @var WP_HTML_Stack_Event|null
 254       */
 255      private $current_element = null;
 256  
 257      /**
 258       * Context node if created as a fragment parser.
 259       *
 260       * @since 6.6.0
 261       * @var WP_HTML_Token|null
 262       */
 263      private $context_node = null;
 264  
 265      /*
 266       * Public Interface Functions
 267       */
 268  
 269      /**
 270       * Creates an HTML processor in the fragment parsing mode.
 271       *
 272       * Use this for cases where you are processing chunks of HTML that
 273       * will be found within a bigger HTML document, such as rendered
 274       * block output that exists within a post, `the_content` inside a
 275       * rendered site layout.
 276       *
 277       * Fragment parsing occurs within a context, which is an HTML element
 278       * that the document will eventually be placed in. It becomes important
 279       * when special elements have different rules than others, such as inside
 280       * a TEXTAREA or a TITLE tag where things that look like tags are text,
 281       * or inside a SCRIPT tag where things that look like HTML syntax are JS.
 282       *
 283       * The context value should be a representation of the tag into which the
 284       * HTML is found. For most cases this will be the body element. The HTML
 285       * form is provided because a context element may have attributes that
 286       * impact the parse, such as with a SCRIPT tag and its `type` attribute.
 287       *
 288       * ## Current HTML Support
 289       *
 290       *  - The only supported context is `<body>`, which is the default value.
 291       *  - The only supported document encoding is `UTF-8`, which is the default value.
 292       *
 293       * @since 6.4.0
 294       * @since 6.6.0 Returns `static` instead of `self` so it can create subclass instances.
 295       *
 296       * @param string $html     Input HTML fragment to process.
 297       * @param string $context  Context element for the fragment, must be default of `<body>`.
 298       * @param string $encoding Text encoding of the document; must be default of 'UTF-8'.
 299       * @return static|null The created processor if successful, otherwise null.
 300       */
 301  	public static function create_fragment( $html, $context = '<body>', $encoding = 'UTF-8' ) {
 302          if ( '<body>' !== $context || 'UTF-8' !== $encoding ) {
 303              return null;
 304          }
 305  
 306          if ( ! is_string( $html ) ) {
 307              _doing_it_wrong(
 308                  __METHOD__,
 309                  __( 'The HTML parameter must be a string.' ),
 310                  '6.9.0'
 311              );
 312              return null;
 313          }
 314  
 315          $context_processor = static::create_full_parser( "<!DOCTYPE html>{$context}", $encoding );
 316          if ( null === $context_processor ) {
 317              return null;
 318          }
 319  
 320          while ( $context_processor->next_tag() ) {
 321              if ( ! $context_processor->is_virtual() ) {
 322                  $context_processor->set_bookmark( 'final_node' );
 323              }
 324          }
 325  
 326          if (
 327              ! $context_processor->has_bookmark( 'final_node' ) ||
 328              ! $context_processor->seek( 'final_node' )
 329          ) {
 330              _doing_it_wrong( __METHOD__, __( 'No valid context element was detected.' ), '6.8.0' );
 331              return null;
 332          }
 333  
 334          return $context_processor->create_fragment_at_current_node( $html );
 335      }
 336  
 337      /**
 338       * Creates an HTML processor in the full parsing mode.
 339       *
 340       * It's likely that a fragment parser is more appropriate, unless sending an
 341       * entire HTML document from start to finish. Consider a fragment parser with
 342       * a context node of `<body>`.
 343       *
 344       * UTF-8 is the only allowed encoding. If working with a document that
 345       * isn't UTF-8, first convert the document to UTF-8, then pass in the
 346       * converted HTML.
 347       *
 348       * @since 6.7.0
 349       *
 350       * @param string      $html                    Input HTML document to process.
 351       * @param string|null $known_definite_encoding Optional. If provided, specifies the charset used
 352       *                                             in the input byte stream. Currently must be UTF-8.
 353       * @return static|null The created processor if successful, otherwise null.
 354       */
 355  	public static function create_full_parser( $html, $known_definite_encoding = 'UTF-8' ) {
 356          if ( 'UTF-8' !== $known_definite_encoding ) {
 357              return null;
 358          }
 359          if ( ! is_string( $html ) ) {
 360              _doing_it_wrong(
 361                  __METHOD__,
 362                  __( 'The HTML parameter must be a string.' ),
 363                  '6.9.0'
 364              );
 365              return null;
 366          }
 367  
 368          $processor                             = new static( $html, self::CONSTRUCTOR_UNLOCK_CODE );
 369          $processor->state->encoding            = $known_definite_encoding;
 370          $processor->state->encoding_confidence = 'certain';
 371  
 372          return $processor;
 373      }
 374  
 375      /**
 376       * Constructor.
 377       *
 378       * Do not use this method. Use the static creator methods instead.
 379       *
 380       * @access private
 381       *
 382       * @since 6.4.0
 383       *
 384       * @see WP_HTML_Processor::create_fragment()
 385       *
 386       * @param string      $html                                  HTML to process.
 387       * @param string|null $use_the_static_create_methods_instead This constructor should not be called manually.
 388       */
 389  	public function __construct( $html, $use_the_static_create_methods_instead = null ) {
 390          parent::__construct( $html );
 391  
 392          if ( self::CONSTRUCTOR_UNLOCK_CODE !== $use_the_static_create_methods_instead ) {
 393              _doing_it_wrong(
 394                  __METHOD__,
 395                  sprintf(
 396                      /* translators: %s: WP_HTML_Processor::create_fragment(). */
 397                      __( 'Call %s to create an HTML Processor instead of calling the constructor directly.' ),
 398                      '<code>WP_HTML_Processor::create_fragment()</code>'
 399                  ),
 400                  '6.4.0'
 401              );
 402          }
 403  
 404          $this->state = new WP_HTML_Processor_State();
 405  
 406          $this->state->stack_of_open_elements->set_push_handler(
 407              function ( WP_HTML_Token $token ): void {
 408                  $is_virtual            = ! isset( $this->state->current_token ) || $this->is_tag_closer();
 409                  $same_node             = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name;
 410                  $provenance            = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real';
 411                  $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::PUSH, $provenance );
 412  
 413                  $this->change_parsing_namespace( $token->integration_node_type ? 'html' : $token->namespace );
 414              }
 415          );
 416  
 417          $this->state->stack_of_open_elements->set_pop_handler(
 418              function ( WP_HTML_Token $token ): void {
 419                  $is_virtual            = ! isset( $this->state->current_token ) || ! $this->is_tag_closer();
 420                  $same_node             = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name;
 421                  $provenance            = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real';
 422                  $this->element_queue[] = new WP_HTML_Stack_Event( $token, WP_HTML_Stack_Event::POP, $provenance );
 423  
 424                  $adjusted_current_node = $this->get_adjusted_current_node();
 425  
 426                  if ( $adjusted_current_node ) {
 427                      $this->change_parsing_namespace( $adjusted_current_node->integration_node_type ? 'html' : $adjusted_current_node->namespace );
 428                  } else {
 429                      $this->change_parsing_namespace( 'html' );
 430                  }
 431              }
 432          );
 433  
 434          /*
 435           * Create this wrapper so that it's possible to pass
 436           * a private method into WP_HTML_Token classes without
 437           * exposing it to any public API.
 438           */
 439          $this->release_internal_bookmark_on_destruct = function ( string $name ): void {
 440              parent::release_bookmark( $name );
 441          };
 442      }
 443  
 444      /**
 445       * Creates a fragment processor at the current node.
 446       *
 447       * HTML Fragment parsing always happens with a context node. HTML Fragment Processors can be
 448       * instantiated with a `BODY` context node via `WP_HTML_Processor::create_fragment( $html )`.
 449       *
 450       * The context node may impact how a fragment of HTML is parsed. For example, consider the HTML
 451       * fragment `<td />Inside TD?</td>`.
 452       *
 453       * A BODY context node will produce the following tree:
 454       *
 455       *     └─#text Inside TD?
 456       *
 457       * Notice that the `<td>` tags are completely ignored.
 458       *
 459       * Compare that with an SVG context node that produces the following tree:
 460       *
 461       *     ├─svg:td
 462       *     └─#text Inside TD?
 463       *
 464       * Here, a `td` node in the `svg` namespace is created, and its self-closing flag is respected.
 465       * This is a peculiarity of parsing HTML in foreign content like SVG.
 466       *
 467       * Finally, consider the tree produced with a TABLE context node:
 468       *
 469       *     └─TBODY
 470       *       └─TR
 471       *         └─TD
 472       *           └─#text Inside TD?
 473       *
 474       * These examples demonstrate how important the context node may be when processing an HTML
 475       * fragment. Special care must be taken when processing fragments that are expected to appear
 476       * in specific contexts. SVG and TABLE are good examples, but there are others.
 477       *
 478       * @see https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-parsing-algorithm
 479       *
 480       * @since 6.8.0
 481       *
 482       * @param string $html Input HTML fragment to process.
 483       * @return static|null The created processor if successful, otherwise null.
 484       */
 485  	private function create_fragment_at_current_node( string $html ) {
 486          if ( $this->get_token_type() !== '#tag' || $this->is_tag_closer() ) {
 487              _doing_it_wrong(
 488                  __METHOD__,
 489                  __( 'The context element must be a start tag.' ),
 490                  '6.8.0'
 491              );
 492              return null;
 493          }
 494  
 495          $tag_name  = $this->current_element->token->node_name;
 496          $namespace = $this->current_element->token->namespace;
 497  
 498          if ( 'html' === $namespace && self::is_void( $tag_name ) ) {
 499              _doing_it_wrong(
 500                  __METHOD__,
 501                  sprintf(
 502                      // translators: %s: A tag name like INPUT or BR.
 503                      __( 'The context element cannot be a void element, found "%s".' ),
 504                      $tag_name
 505                  ),
 506                  '6.8.0'
 507              );
 508              return null;
 509          }
 510  
 511          /*
 512           * Prevent creating fragments at nodes that require a special tokenizer state.
 513           * This is unsupported by the HTML Processor.
 514           */
 515          if (
 516              'html' === $namespace &&
 517              in_array( $tag_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP', 'PLAINTEXT' ), true )
 518          ) {
 519              _doing_it_wrong(
 520                  __METHOD__,
 521                  sprintf(
 522                      // translators: %s: A tag name like IFRAME or TEXTAREA.
 523                      __( 'The context element "%s" is not supported.' ),
 524                      $tag_name
 525                  ),
 526                  '6.8.0'
 527              );
 528              return null;
 529          }
 530  
 531          $fragment_processor = new static( $html, self::CONSTRUCTOR_UNLOCK_CODE );
 532  
 533          $fragment_processor->compat_mode = $this->compat_mode;
 534  
 535          // @todo Create "fake" bookmarks for non-existent but implied nodes.
 536          $fragment_processor->bookmarks['root-node'] = new WP_HTML_Span( 0, 0 );
 537          $root_node                                  = new WP_HTML_Token(
 538              'root-node',
 539              'HTML',
 540              false
 541          );
 542          $fragment_processor->state->stack_of_open_elements->push( $root_node );
 543  
 544          $fragment_processor->bookmarks['context-node']   = new WP_HTML_Span( 0, 0 );
 545          $fragment_processor->context_node                = clone $this->current_element->token;
 546          $fragment_processor->context_node->bookmark_name = 'context-node';
 547          $fragment_processor->context_node->on_destroy    = null;
 548  
 549          $fragment_processor->breadcrumbs = array( 'HTML', $fragment_processor->context_node->node_name );
 550  
 551          if ( 'TEMPLATE' === $fragment_processor->context_node->node_name ) {
 552              $fragment_processor->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE;
 553          }
 554  
 555          $fragment_processor->reset_insertion_mode_appropriately();
 556  
 557          /*
 558           * > Set the parser's form element pointer to the nearest node to the context element that
 559           * > is a form element (going straight up the ancestor chain, and including the element
 560           * > itself, if it is a form element), if any. (If there is no such form element, the
 561           * > form element pointer keeps its initial value, null.)
 562           */
 563          foreach ( $this->state->stack_of_open_elements->walk_up() as $element ) {
 564              if ( 'FORM' === $element->node_name && 'html' === $element->namespace ) {
 565                  $fragment_processor->state->form_element                = clone $element;
 566                  $fragment_processor->state->form_element->bookmark_name = null;
 567                  $fragment_processor->state->form_element->on_destroy    = null;
 568                  break;
 569              }
 570          }
 571  
 572          $fragment_processor->state->encoding_confidence = 'irrelevant';
 573  
 574          /*
 575           * Update the parsing namespace near the end of the process.
 576           * This is important so that any push/pop from the stack of open
 577           * elements does not change the parsing namespace.
 578           */
 579          $fragment_processor->change_parsing_namespace(
 580              $this->current_element->token->integration_node_type ? 'html' : $namespace
 581          );
 582  
 583          return $fragment_processor;
 584      }
 585  
 586      /**
 587       * Stops the parser and terminates its execution when encountering unsupported markup.
 588       *
 589       * @throws WP_HTML_Unsupported_Exception Halts execution of the parser.
 590       *
 591       * @since 6.7.0
 592       * @ignore
 593       *
 594       * @param string $message Explains support is missing in order to parse the current node.
 595       * @return never
 596       */
 597  	private function bail( string $message ) {
 598          $here  = $this->bookmarks[ $this->state->current_token->bookmark_name ];
 599          $token = substr( $this->html, $here->start, $here->length );
 600  
 601          $open_elements = array();
 602          foreach ( $this->state->stack_of_open_elements->stack as $item ) {
 603              $open_elements[] = $item->node_name;
 604          }
 605  
 606          $active_formats = array();
 607          foreach ( $this->state->active_formatting_elements->walk_down() as $item ) {
 608              $active_formats[] = $item->node_name;
 609          }
 610  
 611          $this->last_error = self::ERROR_UNSUPPORTED;
 612  
 613          $this->unsupported_exception = new WP_HTML_Unsupported_Exception(
 614              $message,
 615              $this->state->current_token->node_name,
 616              $here->start,
 617              $token,
 618              $open_elements,
 619              $active_formats
 620          );
 621  
 622          throw $this->unsupported_exception;
 623      }
 624  
 625      /**
 626       * Returns the last error, if any.
 627       *
 628       * Various situations lead to parsing failure but this class will
 629       * return `false` in all those cases. To determine why something
 630       * failed it's possible to request the last error. This can be
 631       * helpful to know to distinguish whether a given tag couldn't
 632       * be found or if content in the document caused the processor
 633       * to give up and abort processing.
 634       *
 635       * Example
 636       *
 637       *     $processor = WP_HTML_Processor::create_fragment( '<template><strong><button><em><p><em>' );
 638       *     false === $processor->next_tag();
 639       *     WP_HTML_Processor::ERROR_UNSUPPORTED === $processor->get_last_error();
 640       *
 641       * @since 6.4.0
 642       *
 643       * @see self::ERROR_UNSUPPORTED
 644       * @see self::ERROR_EXCEEDED_MAX_BOOKMARKS
 645       *
 646       * @return string|null The last error, if one exists, otherwise null.
 647       */
 648  	public function get_last_error(): ?string {
 649          return $this->last_error;
 650      }
 651  
 652      /**
 653       * Returns context for why the parser aborted due to unsupported HTML, if it did.
 654       *
 655       * This is meant for debugging purposes, not for production use.
 656       *
 657       * @since 6.7.0
 658       *
 659       * @see self::$unsupported_exception
 660       *
 661       * @return WP_HTML_Unsupported_Exception|null
 662       */
 663  	public function get_unsupported_exception() {
 664          return $this->unsupported_exception;
 665      }
 666  
 667      /**
 668       * Finds the next tag matching the $query.
 669       *
 670       * @todo Support matching the class name and tag name.
 671       *
 672       * @since 6.4.0
 673       * @since 6.6.0 Visits all tokens, including virtual ones.
 674       *
 675       * @throws Exception When unable to allocate a bookmark for the next token in the input HTML document.
 676       *
 677       * @param array|string|null $query {
 678       *     Optional. Which tag name to find, having which class, etc. Default is to find any tag.
 679       *
 680       *     @type string|null $tag_name     Which tag to find, or `null` for "any tag."
 681       *     @type string      $tag_closers  'visit' to pause at tag closers, 'skip' or unset to only visit openers.
 682       *     @type int|null    $match_offset Find the Nth tag matching all search criteria.
 683       *                                     1 for "first" tag, 3 for "third," etc.
 684       *                                     Defaults to first tag.
 685       *     @type string|null $class_name   Tag must contain this whole class name to match.
 686       *     @type string[]    $breadcrumbs  DOM sub-path at which element is found, e.g. `array( 'FIGURE', 'IMG' )`.
 687       *                                     May also contain the wildcard `*` which matches a single element, e.g. `array( 'SECTION', '*' )`.
 688       * }
 689       * @return bool Whether a tag was matched.
 690       */
 691  	public function next_tag( $query = null ): bool {
 692          $visit_closers = isset( $query['tag_closers'] ) && 'visit' === $query['tag_closers'];
 693  
 694          if ( null === $query ) {
 695              while ( $this->next_token() ) {
 696                  if ( '#tag' !== $this->get_token_type() ) {
 697                      continue;
 698                  }
 699  
 700                  if ( ! $this->is_tag_closer() || $visit_closers ) {
 701                      return true;
 702                  }
 703              }
 704  
 705              return false;
 706          }
 707  
 708          if ( is_string( $query ) ) {
 709              $query = array( 'breadcrumbs' => array( $query ) );
 710          }
 711  
 712          if ( ! is_array( $query ) ) {
 713              _doing_it_wrong(
 714                  __METHOD__,
 715                  __( 'Please pass a query array to this function.' ),
 716                  '6.4.0'
 717              );
 718              return false;
 719          }
 720  
 721          if ( isset( $query['tag_name'] ) ) {
 722              $query['tag_name'] = strtoupper( $query['tag_name'] );
 723          }
 724  
 725          $needs_class = ( isset( $query['class_name'] ) && is_string( $query['class_name'] ) )
 726              ? $query['class_name']
 727              : null;
 728  
 729          if ( ! ( array_key_exists( 'breadcrumbs', $query ) && is_array( $query['breadcrumbs'] ) ) ) {
 730              while ( $this->next_token() ) {
 731                  if ( '#tag' !== $this->get_token_type() ) {
 732                      continue;
 733                  }
 734  
 735                  if ( isset( $query['tag_name'] ) && $query['tag_name'] !== $this->get_token_name() ) {
 736                      continue;
 737                  }
 738  
 739                  if ( isset( $needs_class ) && ! $this->has_class( $needs_class ) ) {
 740                      continue;
 741                  }
 742  
 743                  if ( ! $this->is_tag_closer() || $visit_closers ) {
 744                      return true;
 745                  }
 746              }
 747  
 748              return false;
 749          }
 750  
 751          $breadcrumbs  = $query['breadcrumbs'];
 752          $match_offset = isset( $query['match_offset'] ) ? (int) $query['match_offset'] : 1;
 753  
 754          while ( $match_offset > 0 && $this->next_token() ) {
 755              if ( '#tag' !== $this->get_token_type() || $this->is_tag_closer() ) {
 756                  continue;
 757              }
 758  
 759              if ( isset( $needs_class ) && ! $this->has_class( $needs_class ) ) {
 760                  continue;
 761              }
 762  
 763              if ( $this->matches_breadcrumbs( $breadcrumbs ) && 0 === --$match_offset ) {
 764                  return true;
 765              }
 766          }
 767  
 768          return false;
 769      }
 770  
 771      /**
 772       * Finds the next token in the HTML document.
 773       *
 774       * This doesn't currently have a way to represent non-tags and doesn't process
 775       * semantic rules for text nodes. For access to the raw tokens consider using
 776       * WP_HTML_Tag_Processor instead.
 777       *
 778       * @since 6.5.0 Added for internal support; do not use.
 779       * @since 6.7.2 Refactored so subclasses may extend.
 780       *
 781       * @return bool Whether a token was parsed.
 782       */
 783  	public function next_token(): bool {
 784          return $this->next_visitable_token();
 785      }
 786  
 787      /**
 788       * Ensures internal accounting is maintained for HTML semantic rules while
 789       * the underlying Tag Processor class is seeking to a bookmark.
 790       *
 791       * This doesn't currently have a way to represent non-tags and doesn't process
 792       * semantic rules for text nodes. For access to the raw tokens consider using
 793       * WP_HTML_Tag_Processor instead.
 794       *
 795       * Note that this method may call itself recursively. This is why it is not
 796       * implemented as {@see WP_HTML_Processor::next_token()}, which instead calls
 797       * this method similarly to how {@see WP_HTML_Tag_Processor::next_token()}
 798       * calls the {@see WP_HTML_Tag_Processor::base_class_next_token()} method.
 799       *
 800       * @since 6.7.2 Added for internal support.
 801       * @ignore
 802       *
 803       * @return bool
 804       */
 805  	private function next_visitable_token(): bool {
 806          $this->current_element = null;
 807  
 808          if ( isset( $this->last_error ) ) {
 809              return false;
 810          }
 811  
 812          /*
 813           * Prime the events if there are none.
 814           *
 815           * @todo In some cases, probably related to the adoption agency
 816           *       algorithm, this call to step() doesn't create any new
 817           *       events. Calling it again creates them. Figure out why
 818           *       this is and if it's inherent or if it's a bug. Looping
 819           *       until there are events or until there are no more
 820           *       tokens works in the meantime and isn't obviously wrong.
 821           */
 822          if ( empty( $this->element_queue ) ) {
 823              if ( $this->step() ) {
 824                  return $this->next_visitable_token();
 825              }
 826  
 827              if ( isset( $this->last_error ) ) {
 828                  return false;
 829              }
 830          }
 831  
 832          // Process the next event on the queue.
 833          $this->current_element = array_shift( $this->element_queue );
 834          if ( ! isset( $this->current_element ) ) {
 835              // There are no tokens left, so close all remaining open elements.
 836              while ( $this->state->stack_of_open_elements->pop() ) {
 837                  continue;
 838              }
 839  
 840              return empty( $this->element_queue ) ? false : $this->next_visitable_token();
 841          }
 842  
 843          $is_pop = WP_HTML_Stack_Event::POP === $this->current_element->operation;
 844  
 845          /*
 846           * The root node only exists in the fragment parser, and closing it
 847           * indicates that the parse is complete. Stop before popping it from
 848           * the breadcrumbs.
 849           */
 850          if ( 'root-node' === $this->current_element->token->bookmark_name ) {
 851              return $this->next_visitable_token();
 852          }
 853  
 854          // Adjust the breadcrumbs for this event.
 855          if ( $is_pop ) {
 856              array_pop( $this->breadcrumbs );
 857          } else {
 858              $this->breadcrumbs[] = $this->current_element->token->node_name;
 859          }
 860  
 861          // Avoid sending close events for elements which don't expect a closing.
 862          if ( $is_pop && ! $this->expects_closer( $this->current_element->token ) ) {
 863              return $this->next_visitable_token();
 864          }
 865  
 866          return true;
 867      }
 868  
 869      /**
 870       * Indicates if the current tag token is a tag closer.
 871       *
 872       * Example:
 873       *
 874       *     $p = WP_HTML_Processor::create_fragment( '<div></div>' );
 875       *     $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) );
 876       *     $p->is_tag_closer() === false;
 877       *
 878       *     $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) );
 879       *     $p->is_tag_closer() === true;
 880       *
 881       * @since 6.6.0 Subclassed for HTML Processor.
 882       *
 883       * @return bool Whether the current tag is a tag closer.
 884       */
 885  	public function is_tag_closer(): bool {
 886          return $this->is_virtual()
 887              ? ( WP_HTML_Stack_Event::POP === $this->current_element->operation && '#tag' === $this->get_token_type() )
 888              : parent::is_tag_closer();
 889      }
 890  
 891      /**
 892       * Indicates if the currently-matched token is virtual, created by a stack operation
 893       * while processing HTML, rather than a token found in the HTML text itself.
 894       *
 895       * @since 6.6.0
 896       * @ignore
 897       *
 898       * @return bool Whether the current token is virtual.
 899       */
 900  	private function is_virtual(): bool {
 901          return (
 902              isset( $this->current_element->provenance ) &&
 903              'virtual' === $this->current_element->provenance
 904          );
 905      }
 906  
 907      /**
 908       * Indicates if the currently-matched tag matches the given breadcrumbs.
 909       *
 910       * A "*" represents a single tag wildcard, where any tag matches, but not no tags.
 911       *
 912       * At some point this function _may_ support a `**` syntax for matching any number
 913       * of unspecified tags in the breadcrumb stack. This has been intentionally left
 914       * out, however, to keep this function simple and to avoid introducing backtracking,
 915       * which could open up surprising performance breakdowns.
 916       *
 917       * Example:
 918       *
 919       *     $processor = WP_HTML_Processor::create_fragment( '<div><span><figure><img></figure></span></div>' );
 920       *     $processor->next_tag( 'img' );
 921       *     true  === $processor->matches_breadcrumbs( array( 'figure', 'img' ) );
 922       *     true  === $processor->matches_breadcrumbs( array( 'span', 'figure', 'img' ) );
 923       *     false === $processor->matches_breadcrumbs( array( 'span', 'img' ) );
 924       *     true  === $processor->matches_breadcrumbs( array( 'span', '*', 'img' ) );
 925       *
 926       * @since 6.4.0
 927       *
 928       * @param string[] $breadcrumbs DOM sub-path at which element is found, e.g. `array( 'FIGURE', 'IMG' )`.
 929       *                              May also contain the wildcard `*` which matches a single element, e.g. `array( 'SECTION', '*' )`.
 930       * @return bool Whether the currently-matched tag is found at the given nested structure.
 931       */
 932  	public function matches_breadcrumbs( $breadcrumbs ): bool {
 933          // Everything matches when there are zero constraints.
 934          if ( 0 === count( $breadcrumbs ) ) {
 935              return true;
 936          }
 937  
 938          // Start at the last crumb.
 939          $crumb = end( $breadcrumbs );
 940  
 941          if ( '*' !== $crumb && $this->get_tag() !== strtoupper( $crumb ) ) {
 942              return false;
 943          }
 944  
 945          for ( $i = count( $this->breadcrumbs ) - 1; $i >= 0; $i-- ) {
 946              $node  = $this->breadcrumbs[ $i ];
 947              $crumb = strtoupper( current( $breadcrumbs ) );
 948  
 949              if ( '*' !== $crumb && $node !== $crumb ) {
 950                  return false;
 951              }
 952  
 953              if ( false === prev( $breadcrumbs ) ) {
 954                  return true;
 955              }
 956          }
 957  
 958          return false;
 959      }
 960  
 961      /**
 962       * Indicates if the currently-matched node expects a closing
 963       * token, or if it will self-close on the next step.
 964       *
 965       * Most HTML elements expect a closer, such as a P element or
 966       * a DIV element. Others, like an IMG element, are void and don't
 967       * have a closing tag. Special elements, such as SCRIPT and STYLE,
 968       * are treated just like void tags. Text nodes and self-closing
 969       * foreign content will also act just like a void tag, immediately
 970       * closing as soon as the processor advances to the next token.
 971       *
 972       * @since 6.6.0
 973       *
 974       * @param WP_HTML_Token|null $node Optional. Node to examine, if provided.
 975       *                                 Default is to examine current node.
 976       * @return bool|null Whether to expect a closer for the currently-matched node,
 977       *                   or `null` if not matched on any token.
 978       */
 979  	public function expects_closer( ?WP_HTML_Token $node = null ): ?bool {
 980          $token_name = $node->node_name ?? $this->get_token_name();
 981  
 982          if ( ! isset( $token_name ) ) {
 983              return null;
 984          }
 985  
 986          $token_namespace        = $node->namespace ?? $this->get_namespace();
 987          $token_has_self_closing = $node->has_self_closing_flag ?? $this->has_self_closing_flag();
 988  
 989          return ! (
 990              // Comments, text nodes, and other atomic tokens.
 991              '#' === $token_name[0] ||
 992              // Doctype declarations.
 993              'html' === $token_name ||
 994              // Void elements.
 995              ( 'html' === $token_namespace && self::is_void( $token_name ) ) ||
 996              // Special atomic elements.
 997              ( 'html' === $token_namespace && in_array( $token_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true ) ) ||
 998              // Self-closing elements in foreign content.
 999              ( 'html' !== $token_namespace && $token_has_self_closing )
1000          );
1001      }
1002  
1003      /**
1004       * Steps through the HTML document and stop at the next tag, if any.
1005       *
1006       * This is an internal method. The relevant public methods are
1007       * {@see WP_HTML_Processor::next_tag()} and {@see WP_HTML_Processor::next_token()}.
1008       *
1009       * @since 6.4.0
1010       *
1011       * @access private
1012       *
1013       * @throws Exception When unable to allocate a bookmark for the next token in the input HTML document.
1014       *
1015       * @see self::PROCESS_NEXT_NODE
1016       * @see self::REPROCESS_CURRENT_NODE
1017       *
1018       * @param string $node_to_process Whether to parse the next node or reprocess the current node.
1019       * @return bool Whether a tag was matched.
1020       */
1021  	public function step( $node_to_process = self::PROCESS_NEXT_NODE ): bool {
1022          // Refuse to proceed if there was a previous error.
1023          if ( null !== $this->last_error ) {
1024              return false;
1025          }
1026  
1027          if ( self::REPROCESS_CURRENT_NODE !== $node_to_process ) {
1028              /*
1029               * Void elements still hop onto the stack of open elements even though
1030               * there's no corresponding closing tag. This is important for managing
1031               * stack-based operations such as "navigate to parent node" or checking
1032               * on an element's breadcrumbs.
1033               *
1034               * When moving on to the next node, therefore, if the bottom-most element
1035               * on the stack is a void element, it must be closed.
1036               */
1037              $top_node = $this->state->stack_of_open_elements->current_node();
1038              if ( isset( $top_node ) && ! $this->expects_closer( $top_node ) ) {
1039                  $this->state->stack_of_open_elements->pop();
1040              }
1041          }
1042  
1043          if ( self::PROCESS_NEXT_NODE === $node_to_process ) {
1044              parent::next_token();
1045              if ( WP_HTML_Tag_Processor::STATE_TEXT_NODE === $this->parser_state ) {
1046                  parent::subdivide_text_appropriately();
1047              }
1048          }
1049  
1050          // Finish stepping when there are no more tokens in the document.
1051          if (
1052              WP_HTML_Tag_Processor::STATE_INCOMPLETE_INPUT === $this->parser_state ||
1053              WP_HTML_Tag_Processor::STATE_COMPLETE === $this->parser_state
1054          ) {
1055              return false;
1056          }
1057  
1058          $adjusted_current_node = $this->get_adjusted_current_node();
1059          $is_closer             = $this->is_tag_closer();
1060          $is_start_tag          = WP_HTML_Tag_Processor::STATE_MATCHED_TAG === $this->parser_state && ! $is_closer;
1061          $token_name            = $this->get_token_name();
1062  
1063          if ( self::REPROCESS_CURRENT_NODE !== $node_to_process ) {
1064              try {
1065                  $bookmark_name = $this->bookmark_token();
1066              } catch ( Exception $e ) {
1067                  if ( self::ERROR_EXCEEDED_MAX_BOOKMARKS === $this->last_error ) {
1068                      return false;
1069                  }
1070                  throw $e;
1071              }
1072  
1073              $this->state->current_token = new WP_HTML_Token(
1074                  $bookmark_name,
1075                  $token_name,
1076                  $this->has_self_closing_flag(),
1077                  $this->release_internal_bookmark_on_destruct
1078              );
1079          }
1080  
1081          $parse_in_current_insertion_mode = (
1082              0 === $this->state->stack_of_open_elements->count() ||
1083              'html' === $adjusted_current_node->namespace ||
1084              (
1085                  'math' === $adjusted_current_node->integration_node_type &&
1086                  (
1087                      ( $is_start_tag && ! in_array( $token_name, array( 'MGLYPH', 'MALIGNMARK' ), true ) ) ||
1088                      '#text' === $token_name
1089                  )
1090              ) ||
1091              (
1092                  'math' === $adjusted_current_node->namespace &&
1093                  'ANNOTATION-XML' === $adjusted_current_node->node_name &&
1094                  $is_start_tag && 'SVG' === $token_name
1095              ) ||
1096              (
1097                  'html' === $adjusted_current_node->integration_node_type &&
1098                  ( $is_start_tag || '#text' === $token_name )
1099              )
1100          );
1101  
1102          try {
1103              if ( ! $parse_in_current_insertion_mode ) {
1104                  return $this->step_in_foreign_content();
1105              }
1106  
1107              switch ( $this->state->insertion_mode ) {
1108                  case WP_HTML_Processor_State::INSERTION_MODE_INITIAL:
1109                      return $this->step_initial();
1110  
1111                  case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HTML:
1112                      return $this->step_before_html();
1113  
1114                  case WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD:
1115                      return $this->step_before_head();
1116  
1117                  case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD:
1118                      return $this->step_in_head();
1119  
1120                  case WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD_NOSCRIPT:
1121                      return $this->step_in_head_noscript();
1122  
1123                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD:
1124                      return $this->step_after_head();
1125  
1126                  case WP_HTML_Processor_State::INSERTION_MODE_IN_BODY:
1127                      return $this->step_in_body();
1128  
1129                  case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE:
1130                      return $this->step_in_table();
1131  
1132                  case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_TEXT:
1133                      return $this->step_in_table_text();
1134  
1135                  case WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION:
1136                      return $this->step_in_caption();
1137  
1138                  case WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP:
1139                      return $this->step_in_column_group();
1140  
1141                  case WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY:
1142                      return $this->step_in_table_body();
1143  
1144                  case WP_HTML_Processor_State::INSERTION_MODE_IN_ROW:
1145                      return $this->step_in_row();
1146  
1147                  case WP_HTML_Processor_State::INSERTION_MODE_IN_CELL:
1148                      return $this->step_in_cell();
1149  
1150                  case WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE:
1151                      return $this->step_in_template();
1152  
1153                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_BODY:
1154                      return $this->step_after_body();
1155  
1156                  case WP_HTML_Processor_State::INSERTION_MODE_IN_FRAMESET:
1157                      return $this->step_in_frameset();
1158  
1159                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_FRAMESET:
1160                      return $this->step_after_frameset();
1161  
1162                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_BODY:
1163                      return $this->step_after_after_body();
1164  
1165                  case WP_HTML_Processor_State::INSERTION_MODE_AFTER_AFTER_FRAMESET:
1166                      return $this->step_after_after_frameset();
1167  
1168                  // This should be unreachable but PHP doesn't have total type checking on switch.
1169                  default:
1170                      $this->bail( "Unaware of the requested parsing mode: '{$this->state->insertion_mode}'." );
1171              }
1172          } catch ( WP_HTML_Unsupported_Exception $e ) {
1173              /*
1174               * Exceptions are used in this class to escape deep call stacks that
1175               * otherwise might involve messier calling and return conventions.
1176               */
1177              return false;
1178          } catch ( Exception $e ) {
1179              if ( self::ERROR_EXCEEDED_MAX_BOOKMARKS === $this->last_error ) {
1180                  return false;
1181              }
1182              // Rethrow any other exceptions for higher-level handling.
1183              throw $e;
1184          }
1185      }
1186  
1187      /**
1188       * Computes the HTML breadcrumbs for the currently-matched node, if matched.
1189       *
1190       * Breadcrumbs start at the outermost parent and descend toward the matched element.
1191       * They always include the entire path from the root HTML node to the matched element.
1192       *
1193       * Example:
1194       *
1195       *     $processor = WP_HTML_Processor::create_fragment( '<p><strong><em><img></em></strong></p>' );
1196       *     $processor->next_tag( 'IMG' );
1197       *     $processor->get_breadcrumbs() === array( 'HTML', 'BODY', 'P', 'STRONG', 'EM', 'IMG' );
1198       *
1199       * @since 6.4.0
1200       *
1201       * @return string[] Array of tag names representing path to matched node.
1202       */
1203  	public function get_breadcrumbs(): array {
1204          return $this->breadcrumbs;
1205      }
1206  
1207      /**
1208       * Returns the nesting depth of the current location in the document.
1209       *
1210       * Example:
1211       *
1212       *     $processor = WP_HTML_Processor::create_fragment( '<div><p></p></div>' );
1213       *     // The processor starts in the BODY context, meaning it has depth from the start: HTML > BODY.
1214       *     2 === $processor->get_current_depth();
1215       *
1216       *     // Opening the DIV element increases the depth.
1217       *     $processor->next_token();
1218       *     3 === $processor->get_current_depth();
1219       *
1220       *     // Opening the P element increases the depth.
1221       *     $processor->next_token();
1222       *     4 === $processor->get_current_depth();
1223       *
1224       *     // The P element is closed during `next_token()` so the depth is decreased to reflect that.
1225       *     $processor->next_token();
1226       *     3 === $processor->get_current_depth();
1227       *
1228       * @since 6.6.0
1229       *
1230       * @return int Nesting-depth of current location in the document.
1231       */
1232  	public function get_current_depth(): int {
1233          return count( $this->breadcrumbs );
1234      }
1235  
1236      /**
1237       * Normalizes an HTML fragment by serializing it.
1238       *
1239       * This method assumes that the given HTML snippet is found in BODY context.
1240       * For normalizing full documents or fragments found in other contexts, create
1241       * a new processor using {@see WP_HTML_Processor::create_fragment} or
1242       * {@see WP_HTML_Processor::create_full_parser} and call {@see WP_HTML_Processor::serialize}
1243       * on the created instances.
1244       *
1245       * Many aspects of an input HTML fragment may be changed during normalization.
1246       *
1247       *  - Attribute values will be double-quoted.
1248       *  - Duplicate attributes will be removed.
1249       *  - Omitted tags will be added.
1250       *  - Tag and attribute name casing will be lower-cased,
1251       *    except for specific SVG and MathML tags or attributes.
1252       *  - Text will be re-encoded, null bytes handled,
1253       *    and invalid UTF-8 replaced with U+FFFD.
1254       *  - Any incomplete syntax trailing at the end will be omitted,
1255       *    for example, an unclosed comment opener will be removed.
1256       *
1257       * Example:
1258       *
1259       *     echo WP_HTML_Processor::normalize( '<a href=#anchor v=5 href="/" enabled>One</a another v=5><!--' );
1260       *     // <a href="#anchor" v="5" enabled>One</a>
1261       *
1262       *     echo WP_HTML_Processor::normalize( '<div></p>fun<table><td>cell</div>' );
1263       *     // <div><p></p>fun<table><tbody><tr><td>cell</td></tr></tbody></table></div>
1264       *
1265       *     echo WP_HTML_Processor::normalize( '<![CDATA[invalid comment]]> syntax < <> "oddities"' );
1266       *     // <!--[CDATA[invalid comment]]--> syntax &lt; &lt;&gt; &quot;oddities&quot;
1267       *
1268       * @since 6.7.0
1269       *
1270       * @param string $html Input HTML to normalize.
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       * @since 6.7.0
5138       *
5139       * @return string One of "html", "math", or "svg".
5140       */
5141  	public function get_namespace(): string {
5142          if ( ! isset( $this->current_element ) ) {
5143              return parent::get_namespace();
5144          }
5145  
5146          return $this->current_element->token->namespace;
5147      }
5148  
5149      /**
5150       * Returns the uppercase name of the matched tag.
5151       *
5152       * The semantic rules for HTML specify that certain tags be reprocessed
5153       * with a different tag name. Because of this, the tag name presented
5154       * by the HTML Processor may differ from the one reported by the HTML
5155       * Tag Processor, which doesn't apply these semantic rules.
5156       *
5157       * Example:
5158       *
5159       *     $processor = new WP_HTML_Tag_Processor( '<div class="test">Test</div>' );
5160       *     $processor->next_tag() === true;
5161       *     $processor->get_tag() === 'DIV';
5162       *
5163       *     $processor->next_tag() === false;
5164       *     $processor->get_tag() === null;
5165       *
5166       * @since 6.4.0
5167       *
5168       * @return string|null Name of currently matched tag in input HTML, or `null` if none found.
5169       */
5170  	public function get_tag(): ?string {
5171          if ( null !== $this->last_error ) {
5172              return null;
5173          }
5174  
5175          if ( $this->is_virtual() ) {
5176              return $this->current_element->token->node_name;
5177          }
5178  
5179          $tag_name = parent::get_tag();
5180  
5181          /*
5182           * > A start tag whose tag name is "image"
5183           * > Change the token's tag name to "img" and reprocess it. (Don't ask.)
5184           *
5185           * This only applies to tags; a processing instruction target or a
5186           * comment which looks like a processing instruction may also report
5187           * a tag name and must not be rewritten.
5188           */
5189          return ( 'IMAGE' === $tag_name && 'html' === $this->get_namespace() && '#tag' === $this->get_token_type() )
5190              ? 'IMG'
5191              : $tag_name;
5192      }
5193  
5194      /**
5195       * Indicates if the currently matched tag contains the self-closing flag.
5196       *
5197       * No HTML elements ought to have the self-closing flag and for those, the self-closing
5198       * flag will be ignored. For void elements this is benign because they "self close"
5199       * automatically. For non-void HTML elements though problems will appear if someone
5200       * intends to use a self-closing element in place of that element with an empty body.
5201       * For HTML foreign elements and custom elements the self-closing flag determines if
5202       * they self-close or not.
5203       *
5204       * This function does not determine if a tag is self-closing,
5205       * but only if the self-closing flag is present in the syntax.
5206       *
5207       * @since 6.6.0 Subclassed for the HTML Processor.
5208       *
5209       * @return bool Whether the currently matched tag contains the self-closing flag.
5210       */
5211  	public function has_self_closing_flag(): bool {
5212          return $this->is_virtual() ? false : parent::has_self_closing_flag();
5213      }
5214  
5215      /**
5216       * Returns the node name represented by the token.
5217       *
5218       * This matches the DOM API value `nodeName`. Some values
5219       * are static, such as `#text` for a text node, while others
5220       * are dynamically generated from the token itself.
5221       *
5222       * Dynamic names:
5223       *  - Uppercase tag name for tag matches.
5224       *  - `html` for DOCTYPE declarations.
5225       *
5226       * Note that if the Tag Processor is not matched on a token
5227       * then this function will return `null`, either because it
5228       * hasn't yet found a token or because it reached the end
5229       * of the document without matching a token.
5230       *
5231       * @since 6.6.0 Subclassed for the HTML Processor.
5232       *
5233       * @return string|null Name of the matched token.
5234       */
5235  	public function get_token_name(): ?string {
5236          return $this->is_virtual()
5237              ? $this->current_element->token->node_name
5238              : parent::get_token_name();
5239      }
5240  
5241      /**
5242       * Indicates the kind of matched token, if any.
5243       *
5244       * This differs from `get_token_name()` in that it always
5245       * returns a static string indicating the type, whereas
5246       * `get_token_name()` may return values derived from the
5247       * token itself, such as a tag name or processing
5248       * instruction tag.
5249       *
5250       * Possible values:
5251       *  - `#tag` when matched on a tag.
5252       *  - `#text` when matched on a text node.
5253       *  - `#cdata-section` when matched on a CDATA node.
5254       *  - `#comment` when matched on a comment.
5255       *  - `#doctype` when matched on a DOCTYPE declaration.
5256       *  - `#presumptuous-tag` when matched on an empty tag closer.
5257       *  - `#funky-comment` when matched on a funky comment.
5258       *  - `#processing-instruction` when matched on a processing instruction.
5259       *
5260       * @since 6.6.0 Subclassed for the HTML Processor.
5261       * @since 7.1.0 Recognize processing instructions according to an HTML
5262       *              specification update.
5263       *
5264       * @return string|null What kind of token is matched, or null.
5265       */
5266  	public function get_token_type(): ?string {
5267          if ( $this->is_virtual() ) {
5268              /*
5269               * This logic comes from the Tag Processor.
5270               *
5271               * @todo It would be ideal not to repeat this here, but it's not clearly
5272               *       better to allow passing a token name to `get_token_type()`.
5273               */
5274              $node_name     = $this->current_element->token->node_name;
5275              $starting_char = $node_name[0];
5276              if ( 'A' <= $starting_char && 'Z' >= $starting_char ) {
5277                  return '#tag';
5278              }
5279  
5280              if ( 'html' === $node_name ) {
5281                  return '#doctype';
5282              }
5283  
5284              return $node_name;
5285          }
5286  
5287          return parent::get_token_type();
5288      }
5289  
5290      /**
5291       * Returns the value of a requested attribute from a matched tag opener if that attribute exists.
5292       *
5293       * Example:
5294       *
5295       *     $p = WP_HTML_Processor::create_fragment( '<div enabled class="test" data-test-id="14">Test</div>' );
5296       *     $p->next_token() === true;
5297       *     $p->get_attribute( 'data-test-id' ) === '14';
5298       *     $p->get_attribute( 'enabled' ) === true;
5299       *     $p->get_attribute( 'aria-label' ) === null;
5300       *
5301       *     $p->next_tag() === false;
5302       *     $p->get_attribute( 'class' ) === null;
5303       *
5304       * @since 6.6.0 Subclassed for HTML Processor.
5305       *
5306       * @param string $name Name of attribute whose value is requested.
5307       * @return string|true|null Value of attribute or `null` if not available. Boolean attributes return `true`.
5308       */
5309  	public function get_attribute( $name ) {
5310          return $this->is_virtual() ? null : parent::get_attribute( $name );
5311      }
5312  
5313      /**
5314       * Updates or creates a new attribute on the currently matched tag with the passed value.
5315       *
5316       * This function handles all necessary HTML encoding. Provide normal, unescaped string values.
5317       * The HTML API will encode the strings appropriately so that the browser will interpret them
5318       * as the intended value.
5319       *
5320       * Example:
5321       *
5322       *     // Renders “Eggs & Milk” in a browser, encoded as `<abbr title="Eggs &amp; Milk">`.
5323       *     $processor->set_attribute( 'title', 'Eggs & Milk' );
5324       *
5325       *     // Renders “Eggs &amp; Milk” in a browser, encoded as `<abbr title="Eggs &amp;amp; Milk">`.
5326       *     $processor->set_attribute( 'title', 'Eggs &amp; Milk' );
5327       *
5328       *     // Renders `true` as `<abbr title>`.
5329       *     $processor->set_attribute( 'title', true );
5330       *
5331       *     // Renders without the attribute for `false` as `<abbr>`.
5332       *     $processor->set_attribute( 'title', false );
5333       *
5334       * Special handling is provided for boolean attribute values:
5335       *  - When `true` is passed as the value, then only the attribute name is added to the tag.
5336       *  - When `false` is passed, the attribute gets removed if it existed before.
5337       *
5338       * @since 6.6.0 Subclassed for the HTML Processor.
5339       * @since 6.9.0 Escapes all character references instead of trying to avoid double-escaping.
5340       *
5341       * @param string      $name  The attribute name to target.
5342       * @param string|bool $value The new attribute value.
5343       * @return bool Whether an attribute value was set.
5344       */
5345  	public function set_attribute( $name, $value ): bool {
5346          return $this->is_virtual() ? false : parent::set_attribute( $name, $value );
5347      }
5348  
5349      /**
5350       * Remove an attribute from the currently-matched tag.
5351       *
5352       * @since 6.6.0 Subclassed for HTML Processor.
5353       *
5354       * @param string $name The attribute name to remove.
5355       * @return bool Whether an attribute was removed.
5356       */
5357  	public function remove_attribute( $name ): bool {
5358          return $this->is_virtual() ? false : parent::remove_attribute( $name );
5359      }
5360  
5361      /**
5362       * Gets lowercase names of all attributes matching a given prefix in the current tag.
5363       *
5364       * Note that matching is case-insensitive. This is in accordance with the spec:
5365       *
5366       * > There must never be two or more attributes on
5367       * > the same start tag whose names are an ASCII
5368       * > case-insensitive match for each other.
5369       *     - HTML 5 spec
5370       *
5371       * Example:
5372       *
5373       *     $p = new WP_HTML_Tag_Processor( '<div data-ENABLED class="test" DATA-test-id="14">Test</div>' );
5374       *     $p->next_tag( array( 'class_name' => 'test' ) ) === true;
5375       *     $p->get_attribute_names_with_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' );
5376       *
5377       *     $p->next_tag() === false;
5378       *     $p->get_attribute_names_with_prefix( 'data-' ) === null;
5379       *
5380       * @since 6.6.0 Subclassed for the HTML Processor.
5381       *
5382       * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive
5383       *
5384       * @param string $prefix Prefix of requested attribute names.
5385       * @return array|null List of attribute names, or `null` when no tag opener is matched.
5386       */
5387  	public function get_attribute_names_with_prefix( $prefix ): ?array {
5388          return $this->is_virtual() ? null : parent::get_attribute_names_with_prefix( $prefix );
5389      }
5390  
5391      /**
5392       * Adds a new class name to the currently matched tag.
5393       *
5394       * @since 6.6.0 Subclassed for the HTML Processor.
5395       *
5396       * @param string $class_name The class name to add.
5397       * @return bool Whether the class was set to be added.
5398       */
5399  	public function add_class( $class_name ): bool {
5400          return $this->is_virtual() ? false : parent::add_class( $class_name );
5401      }
5402  
5403      /**
5404       * Removes a class name from the currently matched tag.
5405       *
5406       * @since 6.6.0 Subclassed for the HTML Processor.
5407       *
5408       * @param string $class_name The class name to remove.
5409       * @return bool Whether the class was set to be removed.
5410       */
5411  	public function remove_class( $class_name ): bool {
5412          return $this->is_virtual() ? false : parent::remove_class( $class_name );
5413      }
5414  
5415      /**
5416       * Returns if a matched tag contains the given ASCII case-insensitive class name.
5417       *
5418       * @since 6.6.0 Subclassed for the HTML Processor.
5419       *
5420       * @todo When reconstructing active formatting elements with attributes, find a way
5421       *       to indicate if the virtually-reconstructed formatting elements contain the
5422       *       wanted class name.
5423       *
5424       * @param string $wanted_class Look for this CSS class name, ASCII case-insensitive.
5425       * @return bool|null Whether the matched tag contains the given class name, or null if not matched.
5426       */
5427  	public function has_class( $wanted_class ): ?bool {
5428          return $this->is_virtual() ? null : parent::has_class( $wanted_class );
5429      }
5430  
5431      /**
5432       * Generator for a foreach loop to step through each class name for the matched tag.
5433       *
5434       * This generator function is designed to be used inside a "foreach" loop.
5435       *
5436       * ```php interactive
5437       * $p = WP_HTML_Processor::create_fragment( "<div class='free &lt;egg&gt;\tlang-en'>" );
5438       * $p->next_tag();
5439       * foreach ( $p->class_list() as $class_name ) {
5440       *   echo "{$class_name} ";
5441       * }
5442       * // Outputs: "free <egg> lang-en "
5443       * ```
5444       *
5445       * @since 6.6.0 Subclassed for the HTML Processor.
5446       */
5447  	public function class_list() {
5448          return $this->is_virtual() ? null : parent::class_list();
5449      }
5450  
5451      /**
5452       * Returns the modifiable text for a matched token, or an empty string.
5453       *
5454       * Modifiable text is text content that may be read and changed without
5455       * changing the HTML structure of the document around it. This includes
5456       * the contents of `#text` nodes in the HTML as well as the inner
5457       * contents of HTML comments, Processing Instructions, and others, even
5458       * though these nodes aren't part of a parsed DOM tree. They also contain
5459       * the contents of SCRIPT and STYLE tags, of TEXTAREA tags, and of any
5460       * other section in an HTML document which cannot contain HTML markup (DATA).
5461       *
5462       * If a token has no modifiable text then an empty string is returned to
5463       * avoid needless crashing or type errors. An empty string does not mean
5464       * that a token has modifiable text, and a token with modifiable text may
5465       * have an empty string (e.g. a comment with no contents).
5466       *
5467       * @since 6.6.0 Subclassed for the HTML Processor.
5468       *
5469       * @return string
5470       */
5471  	public function get_modifiable_text(): string {
5472          return $this->is_virtual() ? '' : parent::get_modifiable_text();
5473      }
5474  
5475      /**
5476       * Indicates what kind of comment produced the comment node.
5477       *
5478       * Because there are different kinds of HTML syntax which produce
5479       * comments, the Tag Processor tracks and exposes this as a type
5480       * for the comment. Nominally only regular HTML comments exist as
5481       * they are commonly known, but a number of unrelated syntax errors
5482       * also produce comments.
5483       *
5484       * @see self::COMMENT_AS_ABRUPTLY_CLOSED_COMMENT
5485       * @see self::COMMENT_AS_CDATA_LOOKALIKE
5486       * @see self::COMMENT_AS_INVALID_HTML
5487       * @see self::COMMENT_AS_HTML_COMMENT
5488       * @see self::COMMENT_AS_PI_NODE_LOOKALIKE
5489       *
5490       * @since 6.6.0 Subclassed for the HTML Processor.
5491       *
5492       * @return string|null
5493       */
5494  	public function get_comment_type(): ?string {
5495          return $this->is_virtual() ? null : parent::get_comment_type();
5496      }
5497  
5498      /**
5499       * Removes a bookmark that is no longer needed.
5500       *
5501       * Releasing a bookmark frees up the small
5502       * performance overhead it requires.
5503       *
5504       * @since 6.4.0
5505       *
5506       * @param string $bookmark_name Name of the bookmark to remove.
5507       * @return bool Whether the bookmark already existed before removal.
5508       */
5509  	public function release_bookmark( $bookmark_name ): bool {
5510          return parent::release_bookmark( "_{$bookmark_name}" );
5511      }
5512  
5513      /**
5514       * Moves the internal cursor in the HTML Processor to a given bookmark's location.
5515       *
5516       * Be careful! Seeking backwards to a previous location resets the parser to the
5517       * start of the document and reparses the entire contents up until it finds the
5518       * sought-after bookmarked location.
5519       *
5520       * In order to prevent accidental infinite loops, there's a
5521       * maximum limit on the number of times seek() can be called.
5522       *
5523       * @throws Exception When unable to allocate a bookmark for the next token in the input HTML document.
5524       *
5525       * @since 6.4.0
5526       *
5527       * @param string $bookmark_name Jump to the place in the document identified by this bookmark name.
5528       * @return bool Whether the internal cursor was successfully moved to the bookmark's location.
5529       */
5530  	public function seek( $bookmark_name ): bool {
5531          // Flush any pending updates to the document before beginning.
5532          $this->get_updated_html();
5533  
5534          $actual_bookmark_name = "_{$bookmark_name}";
5535          $processor_started_at = $this->state->current_token
5536              ? $this->bookmarks[ $this->state->current_token->bookmark_name ]->start
5537              : 0;
5538          $bookmark_starts_at   = $this->bookmarks[ $actual_bookmark_name ]->start;
5539          $direction            = $bookmark_starts_at > $processor_started_at ? 'forward' : 'backward';
5540  
5541          /*
5542           * If seeking backwards, it's possible that the sought-after bookmark exists within an element
5543           * which has been closed before the current cursor; in other words, it has already been removed
5544           * from the stack of open elements. This means that it's insufficient to simply pop off elements
5545           * from the stack of open elements which appear after the bookmarked location and then jump to
5546           * that location, as the elements which were open before won't be re-opened.
5547           *
5548           * In order to maintain consistency, the HTML Processor rewinds to the start of the document
5549           * and reparses everything until it finds the sought-after bookmark.
5550           *
5551           * There are potentially better ways to do this: cache the parser state for each bookmark and
5552           * restore it when seeking; store an immutable and idempotent register of where elements open
5553           * and close.
5554           *
5555           * If caching the parser state it will be essential to properly maintain the cached stack of
5556           * open elements and active formatting elements when modifying the document. This could be a
5557           * tedious and time-consuming process as well, and so for now will not be performed.
5558           *
5559           * It may be possible to track bookmarks for where elements open and close, and in doing so
5560           * be able to quickly recalculate breadcrumbs for any element in the document. It may even
5561           * be possible to remove the stack of open elements and compute it on the fly this way.
5562           * If doing this, the parser would need to track the opening and closing locations for all
5563           * tokens in the breadcrumb path for any and all bookmarks. By utilizing bookmarks themselves
5564           * this list could be automatically maintained while modifying the document. Finding the
5565           * breadcrumbs would then amount to traversing that list from the start until the token
5566           * being inspected. Once an element closes, if there are no bookmarks pointing to locations
5567           * within that element, then all of these locations may be forgotten to save on memory use
5568           * and computation time.
5569           */
5570          if ( 'backward' === $direction ) {
5571  
5572              /*
5573               * When moving backward, stateful stacks should be cleared.
5574               */
5575              foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
5576                  $this->state->stack_of_open_elements->remove_node( $item );
5577              }
5578  
5579              foreach ( $this->state->active_formatting_elements->walk_up() as $item ) {
5580                  $this->state->active_formatting_elements->remove_node( $item );
5581              }
5582  
5583              /*
5584               * **After** clearing stacks, more processor state can be reset.
5585               * This must be done after clearing the stack because those stacks generate events that
5586               * would appear on a subsequent call to `next_token()`.
5587               */
5588              $this->state->frameset_ok                       = true;
5589              $this->state->stack_of_template_insertion_modes = array();
5590              $this->state->head_element                      = null;
5591              $this->state->form_element                      = null;
5592              $this->state->current_token                     = null;
5593              $this->current_element                          = null;
5594              $this->element_queue                            = array();
5595  
5596              /*
5597               * The absence of a context node indicates a full parse.
5598               * The presence of a context node indicates a fragment parser.
5599               */
5600              if ( null === $this->context_node ) {
5601                  $this->change_parsing_namespace( 'html' );
5602                  $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_INITIAL;
5603                  $this->breadcrumbs           = array();
5604  
5605                  $this->bookmarks['initial'] = new WP_HTML_Span( 0, 0 );
5606                  parent::seek( 'initial' );
5607                  unset( $this->bookmarks['initial'] );
5608              } else {
5609  
5610                  /*
5611                   * Push the root-node (HTML) back onto the stack of open elements.
5612                   *
5613                   * Fragment parsers require this extra bit of setup.
5614                   * It's handled in full parsers by advancing the processor state.
5615                   */
5616                  $this->state->stack_of_open_elements->push(
5617                      new WP_HTML_Token(
5618                          'root-node',
5619                          'HTML',
5620                          false
5621                      )
5622                  );
5623  
5624                  $this->change_parsing_namespace(
5625                      $this->context_node->integration_node_type
5626                          ? 'html'
5627                          : $this->context_node->namespace
5628                  );
5629  
5630                  if ( 'TEMPLATE' === $this->context_node->node_name ) {
5631                      $this->state->stack_of_template_insertion_modes[] = WP_HTML_Processor_State::INSERTION_MODE_IN_TEMPLATE;
5632                  }
5633  
5634                  $this->reset_insertion_mode_appropriately();
5635                  $this->breadcrumbs = array_slice( $this->breadcrumbs, 0, 2 );
5636                  parent::seek( $this->context_node->bookmark_name );
5637              }
5638          }
5639  
5640          /*
5641           * Here, the processor moves forward through the document until it matches the bookmark.
5642           * do-while is used here because the processor is expected to already be stopped on
5643           * a token than may match the bookmarked location.
5644           */
5645          do {
5646              /*
5647               * The processor will stop on virtual tokens, but bookmarks may not be set on them.
5648               * They should not be matched when seeking a bookmark, skip them.
5649               */
5650              if ( $this->is_virtual() ) {
5651                  continue;
5652              }
5653              if ( $bookmark_starts_at === $this->bookmarks[ $this->state->current_token->bookmark_name ]->start ) {
5654                  return true;
5655              }
5656          } while ( $this->next_token() );
5657  
5658          return false;
5659      }
5660  
5661      /**
5662       * Sets a bookmark in the HTML document.
5663       *
5664       * Bookmarks represent specific places or tokens in the HTML
5665       * document, such as a tag opener or closer. When applying
5666       * edits to a document, such as setting an attribute, the
5667       * text offsets of that token may shift; the bookmark is
5668       * kept updated with those shifts and remains stable unless
5669       * the entire span of text in which the token sits is removed.
5670       *
5671       * Release bookmarks when they are no longer needed.
5672       *
5673       * Example:
5674       *
5675       *     <main><h2>Surprising fact you may not know!</h2></main>
5676       *           ^  ^
5677       *            \-|-- this `H2` opener bookmark tracks the token
5678       *
5679       *     <main class="clickbait"><h2>Surprising fact you may no…
5680       *                             ^  ^
5681       *                              \-|-- it shifts with edits
5682       *
5683       * Bookmarks provide the ability to seek to a previously-scanned
5684       * place in the HTML document. This avoids the need to re-scan
5685       * the entire document.
5686       *
5687       * Example:
5688       *
5689       *     <ul><li>One</li><li>Two</li><li>Three</li></ul>
5690       *                                 ^^^^
5691       *                                 want to note this last item
5692       *
5693       *     $p = new WP_HTML_Tag_Processor( $html );
5694       *     $in_list = false;
5695       *     while ( $p->next_tag( array( 'tag_closers' => $in_list ? 'visit' : 'skip' ) ) ) {
5696       *         if ( 'UL' === $p->get_tag() ) {
5697       *             if ( $p->is_tag_closer() ) {
5698       *                 $in_list = false;
5699       *                 $p->set_bookmark( 'resume' );
5700       *                 if ( $p->seek( 'last-li' ) ) {
5701       *                     $p->add_class( 'last-li' );
5702       *                 }
5703       *                 $p->seek( 'resume' );
5704       *                 $p->release_bookmark( 'last-li' );
5705       *                 $p->release_bookmark( 'resume' );
5706       *             } else {
5707       *                 $in_list = true;
5708       *             }
5709       *         }
5710       *
5711       *         if ( 'LI' === $p->get_tag() ) {
5712       *             $p->set_bookmark( 'last-li' );
5713       *         }
5714       *     }
5715       *
5716       * Bookmarks intentionally hide the internal string offsets
5717       * to which they refer. They are maintained internally as
5718       * updates are applied to the HTML document and therefore
5719       * retain their "position" - the location to which they
5720       * originally pointed. The inability to use bookmarks with
5721       * functions like `substr` is therefore intentional to guard
5722       * against accidentally breaking the HTML.
5723       *
5724       * Because bookmarks allocate memory and require processing
5725       * for every applied update, they are limited and require
5726       * a name. They should not be created with programmatically-made
5727       * names, such as "li_{$index}" with some loop. As a general
5728       * rule they should only be created with string-literal names
5729       * like "start-of-section" or "last-paragraph".
5730       *
5731       * Bookmarks are a powerful tool to enable complicated behavior.
5732       * Consider double-checking that you need this tool if you are
5733       * reaching for it, as inappropriate use could lead to broken
5734       * HTML structure or unwanted processing overhead.
5735       *
5736       * Bookmarks cannot be set on tokens that do no appear in the original
5737       * HTML text. For example, the HTML `<table><td>` stops at tags `TABLE`,
5738       * `TBODY`, `TR`, and `TD`. The `TBODY` and `TR` tags do not appear in
5739       * the original HTML and cannot be used as bookmarks.
5740       *
5741       * @since 6.4.0
5742       *
5743       * @param string $bookmark_name Identifies this particular bookmark.
5744       * @return bool Whether the bookmark was successfully created.
5745       */
5746  	public function set_bookmark( $bookmark_name ): bool {
5747          if ( $this->is_virtual() ) {
5748              _doing_it_wrong(
5749                  __METHOD__,
5750                  __( 'Cannot set bookmarks on tokens that do no appear in the original HTML text.' ),
5751                  '6.8.0'
5752              );
5753              return false;
5754          }
5755          return parent::set_bookmark( "_{$bookmark_name}" );
5756      }
5757  
5758      /**
5759       * Checks whether a bookmark with the given name exists.
5760       *
5761       * @since 6.5.0
5762       *
5763       * @param string $bookmark_name Name to identify a bookmark that potentially exists.
5764       * @return bool Whether that bookmark exists.
5765       */
5766  	public function has_bookmark( $bookmark_name ): bool {
5767          return parent::has_bookmark( "_{$bookmark_name}" );
5768      }
5769  
5770      /*
5771       * HTML Parsing Algorithms
5772       */
5773  
5774      /**
5775       * Closes a P element.
5776       *
5777       * @since 6.4.0
5778       * @ignore
5779       *
5780       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
5781       *
5782       * @see https://html.spec.whatwg.org/#close-a-p-element
5783       */
5784  	private function close_a_p_element(): void {
5785          $this->generate_implied_end_tags( 'P' );
5786          $this->state->stack_of_open_elements->pop_until( 'P' );
5787      }
5788  
5789      /**
5790       * Closes elements that have implied end tags.
5791       *
5792       * @since 6.4.0
5793       * @since 6.7.0 Full spec support.
5794       * @ignore
5795       *
5796       * @see https://html.spec.whatwg.org/#generate-implied-end-tags
5797       *
5798       * @param string|null $except_for_this_element Perform as if this element doesn't exist in the stack of open elements.
5799       */
5800  	private function generate_implied_end_tags( ?string $except_for_this_element = null ): void {
5801          $elements_with_implied_end_tags = array(
5802              'DD',
5803              'DT',
5804              'LI',
5805              'OPTGROUP',
5806              'OPTION',
5807              'P',
5808              'RB',
5809              'RP',
5810              'RT',
5811              'RTC',
5812          );
5813  
5814          $no_exclusions = ! isset( $except_for_this_element );
5815  
5816          while (
5817              ( $no_exclusions || ! $this->state->stack_of_open_elements->current_node_is( $except_for_this_element ) ) &&
5818              in_array( $this->state->stack_of_open_elements->current_node()->node_name, $elements_with_implied_end_tags, true )
5819          ) {
5820              $this->state->stack_of_open_elements->pop();
5821          }
5822      }
5823  
5824      /**
5825       * Closes elements that have implied end tags, thoroughly.
5826       *
5827       * See the HTML specification for an explanation why this is
5828       * different from generating end tags in the normal sense.
5829       *
5830       * @since 6.4.0
5831       * @since 6.7.0 Full spec support.
5832       * @ignore
5833       *
5834       * @see WP_HTML_Processor::generate_implied_end_tags
5835       * @see https://html.spec.whatwg.org/#generate-implied-end-tags
5836       */
5837  	private function generate_implied_end_tags_thoroughly(): void {
5838          $elements_with_implied_end_tags = array(
5839              'CAPTION',
5840              'COLGROUP',
5841              'DD',
5842              'DT',
5843              'LI',
5844              'OPTGROUP',
5845              'OPTION',
5846              'P',
5847              'RB',
5848              'RP',
5849              'RT',
5850              'RTC',
5851              'TBODY',
5852              'TD',
5853              'TFOOT',
5854              'TH',
5855              'THEAD',
5856              'TR',
5857          );
5858  
5859          while ( in_array( $this->state->stack_of_open_elements->current_node()->node_name, $elements_with_implied_end_tags, true ) ) {
5860              $this->state->stack_of_open_elements->pop();
5861          }
5862      }
5863  
5864      /**
5865       * Returns the adjusted current node.
5866       *
5867       * > The adjusted current node is the context element if the parser was created as
5868       * > part of the HTML fragment parsing algorithm and the stack of open elements
5869       * > has only one element in it (fragment case); otherwise, the adjusted current
5870       * > node is the current node.
5871       *
5872       * @see https://html.spec.whatwg.org/#adjusted-current-node
5873       *
5874       * @since 6.7.0
5875       * @ignore
5876       *
5877       * @return WP_HTML_Token|null The adjusted current node.
5878       */
5879  	private function get_adjusted_current_node(): ?WP_HTML_Token {
5880          if ( isset( $this->context_node ) && 1 === $this->state->stack_of_open_elements->count() ) {
5881              return $this->context_node;
5882          }
5883  
5884          return $this->state->stack_of_open_elements->current_node();
5885      }
5886  
5887      /**
5888       * Reconstructs the active formatting elements.
5889       *
5890       * > This has the effect of reopening all the formatting elements that were opened
5891       * > in the current body, cell, or caption (whichever is youngest) that haven't
5892       * > been explicitly closed.
5893       *
5894       * @since 6.4.0
5895       * @ignore
5896       *
5897       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
5898       *
5899       * @see https://html.spec.whatwg.org/#reconstruct-the-active-formatting-elements
5900       *
5901       * @return bool Whether any formatting elements needed to be reconstructed.
5902       */
5903  	private function reconstruct_active_formatting_elements(): bool {
5904          /*
5905           * > If there are no entries in the list of active formatting elements, then there is nothing
5906           * > to reconstruct; stop this algorithm.
5907           */
5908          if ( 0 === $this->state->active_formatting_elements->count() ) {
5909              return false;
5910          }
5911  
5912          $last_entry = $this->state->active_formatting_elements->current_node();
5913          if (
5914  
5915              /*
5916               * > If the last (most recently added) entry in the list of active formatting elements is a marker;
5917               * > stop this algorithm.
5918               */
5919              'marker' === $last_entry->node_name ||
5920  
5921              /*
5922               * > If the last (most recently added) entry in the list of active formatting elements is an
5923               * > element that is in the stack of open elements, then there is nothing to reconstruct;
5924               * > stop this algorithm.
5925               */
5926              $this->state->stack_of_open_elements->contains_node( $last_entry )
5927          ) {
5928              return false;
5929          }
5930  
5931          $this->bail( 'Cannot reconstruct active formatting elements when advancing and rewinding is required.' );
5932      }
5933  
5934      /**
5935       * Runs the reset the insertion mode appropriately algorithm.
5936       *
5937       * @since 6.7.0
5938       * @ignore
5939       *
5940       * @see https://html.spec.whatwg.org/multipage/parsing.html#reset-the-insertion-mode-appropriately
5941       */
5942  	private function reset_insertion_mode_appropriately(): void {
5943          // Set the first node.
5944          $first_node = null;
5945          foreach ( $this->state->stack_of_open_elements->walk_down() as $first_node ) {
5946              break;
5947          }
5948  
5949          /*
5950           * > 1. Let _last_ be false.
5951           */
5952          $last = false;
5953          foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) {
5954              /*
5955               * > 2. Let _node_ be the last node in the stack of open elements.
5956               * > 3. _Loop_: If _node_ is the first node in the stack of open elements, then set _last_
5957               * >            to true, and, if the parser was created as part of the HTML fragment parsing
5958               * >            algorithm (fragment case), set node to the context element passed to
5959               * >            that algorithm.
5960               * > …
5961               */
5962              if ( $node === $first_node ) {
5963                  $last = true;
5964                  if ( isset( $this->context_node ) ) {
5965                      $node = $this->context_node;
5966                  }
5967              }
5968  
5969              // All of the following rules are for matching HTML elements.
5970              if ( 'html' !== $node->namespace ) {
5971                  continue;
5972              }
5973  
5974              switch ( $node->node_name ) {
5975                  /*
5976                   * > 4. If _node_ is a `td` or `th` element and _last_ is false, then switch the
5977                   * >    insertion mode to "in cell" and return.
5978                   */
5979                  case 'TD':
5980                  case 'TH':
5981                      if ( ! $last ) {
5982                          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CELL;
5983                          return;
5984                      }
5985                      break;
5986  
5987                  /*
5988                  * > 5. If _node_ is a `tr` element, then switch the insertion mode to "in row"
5989                  * >    and return.
5990                  */
5991                  case 'TR':
5992                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
5993                      return;
5994  
5995                  /*
5996                   * > 6. If _node_ is a `tbody`, `thead`, or `tfoot` element, then switch the
5997                   * >    insertion mode to "in table body" and return.
5998                   */
5999                  case 'TBODY':
6000                  case 'THEAD':
6001                  case 'TFOOT':
6002                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
6003                      return;
6004  
6005                  /*
6006                   * > 7. If _node_ is a `caption` element, then switch the insertion mode to
6007                   * >    "in caption" and return.
6008                   */
6009                  case 'CAPTION':
6010                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_CAPTION;
6011                      return;
6012  
6013                  /*
6014                   * > 8. If _node_ is a `colgroup` element, then switch the insertion mode to
6015                   * >    "in column group" and return.
6016                   */
6017                  case 'COLGROUP':
6018                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP;
6019                      return;
6020  
6021                  /*
6022                   * > 9. If _node_ is a `table` element, then switch the insertion mode to
6023                   * >    "in table" and return.
6024                   */
6025                  case 'TABLE':
6026                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE;
6027                      return;
6028  
6029                  /*
6030                   * > 10. If _node_ is a `template` element, then switch the insertion mode to the
6031                   * >     current template insertion mode and return.
6032                   */
6033                  case 'TEMPLATE':
6034                      $this->state->insertion_mode = end( $this->state->stack_of_template_insertion_modes );
6035                      return;
6036  
6037                  /*
6038                   * > 11. If _node_ is a `head` element and _last_ is false, then switch the
6039                   * >     insertion mode to "in head" and return.
6040                   */
6041                  case 'HEAD':
6042                      if ( ! $last ) {
6043                          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD;
6044                          return;
6045                      }
6046                      break;
6047  
6048                  /*
6049                   * > 12. If _node_ is a `body` element, then switch the insertion mode to "in body"
6050                   * >     and return.
6051                   */
6052                  case 'BODY':
6053                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
6054                      return;
6055  
6056                  /*
6057                   * > 13. If _node_ is a `frameset` element, then switch the insertion mode to
6058                   * >     "in frameset" and return. (fragment case)
6059                   */
6060                  case 'FRAMESET':
6061                      $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_FRAMESET;
6062                      return;
6063  
6064                  /*
6065                   * > 14. If _node_ is an `html` element, run these substeps:
6066                   * >     1. If the head element pointer is null, switch the insertion mode to
6067                   * >        "before head" and return. (fragment case)
6068                   * >     2. Otherwise, the head element pointer is not null, switch the insertion
6069                   * >        mode to "after head" and return.
6070                   */
6071                  case 'HTML':
6072                      $this->state->insertion_mode = isset( $this->state->head_element )
6073                          ? WP_HTML_Processor_State::INSERTION_MODE_AFTER_HEAD
6074                          : WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD;
6075                      return;
6076              }
6077          }
6078  
6079          /*
6080           * > 15. If _last_ is true, then switch the insertion mode to "in body"
6081           * >     and return. (fragment case)
6082           *
6083           * This is only reachable if `$last` is true, as per the fragment parsing case.
6084           */
6085          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
6086      }
6087  
6088      /**
6089       * Runs the adoption agency algorithm.
6090       *
6091       * @since 6.4.0
6092       * @ignore
6093       *
6094       * @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
6095       *
6096       * @see https://html.spec.whatwg.org/#adoption-agency-algorithm
6097       */
6098  	private function run_adoption_agency_algorithm(): void {
6099          $budget       = 1000;
6100          $subject      = $this->get_tag();
6101          $current_node = $this->state->stack_of_open_elements->current_node();
6102  
6103          if (
6104              // > If the current node is an HTML element whose tag name is subject
6105              $current_node && $subject === $current_node->node_name &&
6106              // > the current node is not in the list of active formatting elements
6107              ! $this->state->active_formatting_elements->contains_node( $current_node )
6108          ) {
6109              $this->state->stack_of_open_elements->pop();
6110              return;
6111          }
6112  
6113          $outer_loop_counter = 0;
6114          while ( $budget-- > 0 ) {
6115              if ( $outer_loop_counter++ >= 8 ) {
6116                  return;
6117              }
6118  
6119              /*
6120               * > Let formatting element be the last element in the list of active formatting elements that:
6121               * >   - is between the end of the list and the last marker in the list,
6122               * >     if any, or the start of the list otherwise,
6123               * >   - and has the tag name subject.
6124               */
6125              $formatting_element = null;
6126              foreach ( $this->state->active_formatting_elements->walk_up() as $item ) {
6127                  if ( 'marker' === $item->node_name ) {
6128                      break;
6129                  }
6130  
6131                  if ( $subject === $item->node_name ) {
6132                      $formatting_element = $item;
6133                      break;
6134                  }
6135              }
6136  
6137              // > If there is no such element, then return and instead act as described in the "any other end tag" entry above.
6138              if ( null === $formatting_element ) {
6139                  $this->bail( 'Cannot run adoption agency when "any other end tag" is required.' );
6140              }
6141  
6142              // > 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.
6143              if ( ! $this->state->stack_of_open_elements->contains_node( $formatting_element ) ) {
6144                  $this->state->active_formatting_elements->remove_node( $formatting_element );
6145                  return;
6146              }
6147  
6148              // > If formatting element is in the stack of open elements, but the element is not in scope, then this is a parse error; return.
6149              if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $formatting_element->node_name ) ) {
6150                  return;
6151              }
6152  
6153              /*
6154               * > Let furthest block be the topmost node in the stack of open elements that is lower in the stack
6155               * > than formatting element, and is an element in the special category. There might not be one.
6156               */
6157              $is_above_formatting_element = true;
6158              $furthest_block              = null;
6159              foreach ( $this->state->stack_of_open_elements->walk_down() as $item ) {
6160                  if ( $is_above_formatting_element && $formatting_element->bookmark_name !== $item->bookmark_name ) {
6161                      continue;
6162                  }
6163  
6164                  if ( $is_above_formatting_element ) {
6165                      $is_above_formatting_element = false;
6166                      continue;
6167                  }
6168  
6169                  if ( self::is_special( $item ) ) {
6170                      $furthest_block = $item;
6171                      break;
6172                  }
6173              }
6174  
6175              /*
6176               * > If there is no furthest block, then the UA must first pop all the nodes from the bottom of the
6177               * > stack of open elements, from the current node up to and including formatting element, then
6178               * > remove formatting element from the list of active formatting elements, and finally return.
6179               */
6180              if ( null === $furthest_block ) {
6181                  foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
6182                      $this->state->stack_of_open_elements->pop();
6183  
6184                      if ( $formatting_element->bookmark_name === $item->bookmark_name ) {
6185                          $this->state->active_formatting_elements->remove_node( $formatting_element );
6186                          return;
6187                      }
6188                  }
6189              }
6190  
6191              $this->bail( 'Cannot extract common ancestor in adoption agency algorithm.' );
6192          }
6193  
6194          $this->bail( 'Cannot run adoption agency when looping required.' );
6195      }
6196  
6197      /**
6198       * Runs the "close the cell" algorithm.
6199       *
6200       * > Where the steps above say to close the cell, they mean to run the following algorithm:
6201       * >   1. Generate implied end tags.
6202       * >   2. If the current node is not now a td element or a th element, then this is a parse error.
6203       * >   3. Pop elements from the stack of open elements stack until a td element or a th element has been popped from the stack.
6204       * >   4. Clear the list of active formatting elements up to the last marker.
6205       * >   5. Switch the insertion mode to "in row".
6206       *
6207       * @see https://html.spec.whatwg.org/multipage/parsing.html#close-the-cell
6208       *
6209       * @since 6.7.0
6210       * @ignore
6211       */
6212  	private function close_cell(): void {
6213          $this->generate_implied_end_tags();
6214          // @todo Parse error if the current node is a "td" or "th" element.
6215          foreach ( $this->state->stack_of_open_elements->walk_up() as $element ) {
6216              $this->state->stack_of_open_elements->pop();
6217              if ( 'TD' === $element->node_name || 'TH' === $element->node_name ) {
6218                  break;
6219              }
6220          }
6221          $this->state->active_formatting_elements->clear_up_to_last_marker();
6222          $this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
6223      }
6224  
6225      /**
6226       * Inserts an HTML element on the stack of open elements.
6227       *
6228       * @since 6.4.0
6229       * @ignore
6230       *
6231       * @see https://html.spec.whatwg.org/#insert-a-foreign-element
6232       *
6233       * @param WP_HTML_Token $token Name of bookmark pointing to element in original input HTML.
6234       */
6235  	private function insert_html_element( WP_HTML_Token $token ): void {
6236          $this->state->stack_of_open_elements->push( $token );
6237      }
6238  
6239      /**
6240       * Inserts a foreign element on to the stack of open elements.
6241       *
6242       * @since 6.7.0
6243       * @ignore
6244       *
6245       * @see https://html.spec.whatwg.org/#insert-a-foreign-element
6246       *
6247       * @param WP_HTML_Token $token                     Insert this token. The token's namespace and
6248       *                                                 insertion point will be updated correctly.
6249       * @param bool          $only_add_to_element_stack Whether to skip the "insert an element at the adjusted
6250       *                                                 insertion location" algorithm when adding this element.
6251       */
6252  	private function insert_foreign_element( WP_HTML_Token $token, bool $only_add_to_element_stack ): void {
6253          $adjusted_current_node = $this->get_adjusted_current_node();
6254  
6255          $token->namespace = $adjusted_current_node ? $adjusted_current_node->namespace : 'html';
6256  
6257          if ( $this->is_mathml_integration_point() ) {
6258              $token->integration_node_type = 'math';
6259          } elseif ( $this->is_html_integration_point() ) {
6260              $token->integration_node_type = 'html';
6261          }
6262  
6263          if ( false === $only_add_to_element_stack ) {
6264              /*
6265               * @todo Implement the "appropriate place for inserting a node" and the
6266               *       "insert an element at the adjusted insertion location" algorithms.
6267               *
6268               * These algorithms mostly impacts DOM tree construction and not the HTML API.
6269               * Here, there's no DOM node onto which the element will be appended, so the
6270               * parser will skip this step.
6271               *
6272               * @see https://html.spec.whatwg.org/#insert-an-element-at-the-adjusted-insertion-location
6273               */
6274          }
6275  
6276          $this->insert_html_element( $token );
6277      }
6278  
6279      /**
6280       * Inserts a virtual element on the stack of open elements.
6281       *
6282       * @since 6.7.0
6283       * @ignore
6284       *
6285       * @throws Exception When unable to allocate a bookmark for the next token in the input HTML document.
6286       *
6287       * @param string      $token_name    Name of token to create and insert into the stack of open elements.
6288       * @param string|null $bookmark_name Optional. Name to give bookmark for created virtual node.
6289       *                                   Defaults to auto-creating a bookmark name.
6290       * @return WP_HTML_Token Newly-created virtual token.
6291       */
6292  	private function insert_virtual_node( $token_name, $bookmark_name = null ): WP_HTML_Token {
6293          $here = $this->bookmarks[ $this->state->current_token->bookmark_name ];
6294          $name = $bookmark_name ?? $this->bookmark_token();
6295  
6296          $this->bookmarks[ $name ] = new WP_HTML_Span( $here->start, 0 );
6297  
6298          $token = new WP_HTML_Token( $name, $token_name, false );
6299          $this->insert_html_element( $token );
6300          return $token;
6301      }
6302  
6303      /*
6304       * HTML Specification Helpers
6305       */
6306  
6307      /**
6308       * Indicates if the current token is a MathML integration point.
6309       *
6310       * @since 6.7.0
6311       * @ignore
6312       *
6313       * @see https://html.spec.whatwg.org/#mathml-text-integration-point
6314       *
6315       * @return bool Whether the current token is a MathML integration point.
6316       */
6317  	private function is_mathml_integration_point(): bool {
6318          $current_token = $this->state->current_token;
6319          if ( ! isset( $current_token ) ) {
6320              return false;
6321          }
6322  
6323          if ( 'math' !== $current_token->namespace || 'M' !== $current_token->node_name[0] ) {
6324              return false;
6325          }
6326  
6327          $tag_name = $current_token->node_name;
6328  
6329          return (
6330              'MI' === $tag_name ||
6331              'MO' === $tag_name ||
6332              'MN' === $tag_name ||
6333              'MS' === $tag_name ||
6334              'MTEXT' === $tag_name
6335          );
6336      }
6337  
6338      /**
6339       * Indicates if the current token is an HTML integration point.
6340       *
6341       * Note that this method must be an instance method with access
6342       * to the current token, since it needs to examine the attributes
6343       * of the currently-matched tag, if it's in the MathML namespace.
6344       * Otherwise it would be required to scan the HTML and ensure that
6345       * no other accounting is overlooked.
6346       *
6347       * @since 6.7.0
6348       * @ignore
6349       *
6350       * @see https://html.spec.whatwg.org/#html-integration-point
6351       *
6352       * @return bool Whether the current token is an HTML integration point.
6353       */
6354  	private function is_html_integration_point(): bool {
6355          $current_token = $this->state->current_token;
6356          if ( ! isset( $current_token ) ) {
6357              return false;
6358          }
6359  
6360          if ( 'html' === $current_token->namespace ) {
6361              return false;
6362          }
6363  
6364          $tag_name = $current_token->node_name;
6365  
6366          if ( 'svg' === $current_token->namespace ) {
6367              return (
6368                  'DESC' === $tag_name ||
6369                  'FOREIGNOBJECT' === $tag_name ||
6370                  'TITLE' === $tag_name
6371              );
6372          }
6373  
6374          if ( 'math' === $current_token->namespace ) {
6375              if ( 'ANNOTATION-XML' !== $tag_name ) {
6376                  return false;
6377              }
6378  
6379              $encoding = $this->get_attribute( 'encoding' );
6380  
6381              return (
6382                  is_string( $encoding ) &&
6383                  (
6384                      0 === strcasecmp( $encoding, 'application/xhtml+xml' ) ||
6385                      0 === strcasecmp( $encoding, 'text/html' )
6386                  )
6387              );
6388          }
6389  
6390          $this->bail( 'Should not have reached end of HTML Integration Point detection: check HTML API code.' );
6391          // This unnecessary return prevents tools from inaccurately reporting type errors.
6392          return false;
6393      }
6394  
6395      /**
6396       * Returns whether an element of a given name is in the HTML special category.
6397       *
6398       * @since 6.4.0
6399       *
6400       * @see https://html.spec.whatwg.org/#special
6401       *
6402       * @param WP_HTML_Token|string $tag_name Node to check, or only its name if in the HTML namespace.
6403       * @return bool Whether the element of the given name is in the special category.
6404       */
6405  	public static function is_special( $tag_name ): bool {
6406          if ( is_string( $tag_name ) ) {
6407              $tag_name = strtoupper( $tag_name );
6408          } else {
6409              $tag_name = 'html' === $tag_name->namespace
6410                  ? strtoupper( $tag_name->node_name )
6411                  : "{$tag_name->namespace} {$tag_name->node_name}";
6412          }
6413  
6414          return (
6415              'ADDRESS' === $tag_name ||
6416              'APPLET' === $tag_name ||
6417              'AREA' === $tag_name ||
6418              'ARTICLE' === $tag_name ||
6419              'ASIDE' === $tag_name ||
6420              'BASE' === $tag_name ||
6421              'BASEFONT' === $tag_name ||
6422              'BGSOUND' === $tag_name ||
6423              'BLOCKQUOTE' === $tag_name ||
6424              'BODY' === $tag_name ||
6425              'BR' === $tag_name ||
6426              'BUTTON' === $tag_name ||
6427              'CAPTION' === $tag_name ||
6428              'CENTER' === $tag_name ||
6429              'COL' === $tag_name ||
6430              'COLGROUP' === $tag_name ||
6431              'DD' === $tag_name ||
6432              'DETAILS' === $tag_name ||
6433              'DIR' === $tag_name ||
6434              'DIV' === $tag_name ||
6435              'DL' === $tag_name ||
6436              'DT' === $tag_name ||
6437              'EMBED' === $tag_name ||
6438              'FIELDSET' === $tag_name ||
6439              'FIGCAPTION' === $tag_name ||
6440              'FIGURE' === $tag_name ||
6441              'FOOTER' === $tag_name ||
6442              'FORM' === $tag_name ||
6443              'FRAME' === $tag_name ||
6444              'FRAMESET' === $tag_name ||
6445              'H1' === $tag_name ||
6446              'H2' === $tag_name ||
6447              'H3' === $tag_name ||
6448              'H4' === $tag_name ||
6449              'H5' === $tag_name ||
6450              'H6' === $tag_name ||
6451              'HEAD' === $tag_name ||
6452              'HEADER' === $tag_name ||
6453              'HGROUP' === $tag_name ||
6454              'HR' === $tag_name ||
6455              'HTML' === $tag_name ||
6456              'IFRAME' === $tag_name ||
6457              'IMG' === $tag_name ||
6458              'INPUT' === $tag_name ||
6459              'KEYGEN' === $tag_name ||
6460              'LI' === $tag_name ||
6461              'LINK' === $tag_name ||
6462              'LISTING' === $tag_name ||
6463              'MAIN' === $tag_name ||
6464              'MARQUEE' === $tag_name ||
6465              'MENU' === $tag_name ||
6466              'META' === $tag_name ||
6467              'NAV' === $tag_name ||
6468              'NOEMBED' === $tag_name ||
6469              'NOFRAMES' === $tag_name ||
6470              'NOSCRIPT' === $tag_name ||
6471              'OBJECT' === $tag_name ||
6472              'OL' === $tag_name ||
6473              'P' === $tag_name ||
6474              'PARAM' === $tag_name ||
6475              'PLAINTEXT' === $tag_name ||
6476              'PRE' === $tag_name ||
6477              'SCRIPT' === $tag_name ||
6478              'SEARCH' === $tag_name ||
6479              'SECTION' === $tag_name ||
6480              'SELECT' === $tag_name ||
6481              'SOURCE' === $tag_name ||
6482              'STYLE' === $tag_name ||
6483              'SUMMARY' === $tag_name ||
6484              'TABLE' === $tag_name ||
6485              'TBODY' === $tag_name ||
6486              'TD' === $tag_name ||
6487              'TEMPLATE' === $tag_name ||
6488              'TEXTAREA' === $tag_name ||
6489              'TFOOT' === $tag_name ||
6490              'TH' === $tag_name ||
6491              'THEAD' === $tag_name ||
6492              'TITLE' === $tag_name ||
6493              'TR' === $tag_name ||
6494              'TRACK' === $tag_name ||
6495              'UL' === $tag_name ||
6496              'WBR' === $tag_name ||
6497              'XMP' === $tag_name ||
6498  
6499              // MathML.
6500              'math MI' === $tag_name ||
6501              'math MO' === $tag_name ||
6502              'math MN' === $tag_name ||
6503              'math MS' === $tag_name ||
6504              'math MTEXT' === $tag_name ||
6505              'math ANNOTATION-XML' === $tag_name ||
6506  
6507              // SVG.
6508              'svg DESC' === $tag_name ||
6509              'svg FOREIGNOBJECT' === $tag_name ||
6510              'svg TITLE' === $tag_name
6511          );
6512      }
6513  
6514      /**
6515       * Returns whether a given element is an HTML Void Element
6516       *
6517       * > area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
6518       *
6519       * @since 6.4.0
6520       *
6521       * @see https://html.spec.whatwg.org/#void-elements
6522       *
6523       * @param string $tag_name Name of HTML tag to check.
6524       * @return bool Whether the given tag is an HTML Void Element.
6525       */
6526  	public static function is_void( $tag_name ): bool {
6527          $tag_name = strtoupper( $tag_name );
6528  
6529          return (
6530              'AREA' === $tag_name ||
6531              'BASE' === $tag_name ||
6532              'BASEFONT' === $tag_name || // Obsolete but still treated as void.
6533              'BGSOUND' === $tag_name || // Obsolete but still treated as void.
6534              'BR' === $tag_name ||
6535              'COL' === $tag_name ||
6536              'EMBED' === $tag_name ||
6537              'FRAME' === $tag_name ||
6538              'HR' === $tag_name ||
6539              'IMG' === $tag_name ||
6540              'INPUT' === $tag_name ||
6541              'KEYGEN' === $tag_name || // Obsolete but still treated as void.
6542              'LINK' === $tag_name ||
6543              'META' === $tag_name ||
6544              'PARAM' === $tag_name || // Obsolete but still treated as void.
6545              'SOURCE' === $tag_name ||
6546              'TRACK' === $tag_name ||
6547              'WBR' === $tag_name
6548          );
6549      }
6550  
6551      /**
6552       * Gets an encoding from a given string.
6553       *
6554       * This is an algorithm defined in the WHAT-WG specification.
6555       *
6556       * Example:
6557       *
6558       *     'UTF-8' === self::get_encoding( 'utf8' );
6559       *     'UTF-8' === self::get_encoding( "  \tUTF-8 " );
6560       *     null    === self::get_encoding( 'UTF-7' );
6561       *     null    === self::get_encoding( 'utf8; charset=' );
6562       *
6563       * @see https://encoding.spec.whatwg.org/#concept-encoding-get
6564       *
6565       * @todo As this parser only supports UTF-8, only the UTF-8
6566       *       encodings are detected. Add more as desired, but the
6567       *       parser will bail on non-UTF-8 encodings.
6568       *
6569       * @since 6.7.0
6570       *
6571       * @param string $label A string which may specify a known encoding.
6572       * @return string|null Known encoding if matched, otherwise null.
6573       */
6574  	protected static function get_encoding( string $label ): ?string {
6575          /*
6576           * > Remove any leading and trailing ASCII whitespace from label.
6577           */
6578          $label = trim( $label, " \t\f\r\n" );
6579  
6580          /*
6581           * > If label is an ASCII case-insensitive match for any of the labels listed in the
6582           * > table below, then return the corresponding encoding; otherwise return failure.
6583           */
6584          switch ( strtolower( $label ) ) {
6585              case 'unicode-1-1-utf-8':
6586              case 'unicode11utf8':
6587              case 'unicode20utf8':
6588              case 'utf-8':
6589              case 'utf8':
6590              case 'x-unicode20utf8':
6591                  return 'UTF-8';
6592  
6593              default:
6594                  return null;
6595          }
6596      }
6597  
6598      /*
6599       * Constants that would pollute the top of the class if they were found there.
6600       */
6601  
6602      /**
6603       * Indicates that the next HTML token should be parsed and processed.
6604       *
6605       * @since 6.4.0
6606       *
6607       * @var string
6608       */
6609      const PROCESS_NEXT_NODE = 'process-next-node';
6610  
6611      /**
6612       * Indicates that the current HTML token should be reprocessed in the newly-selected insertion mode.
6613       *
6614       * @since 6.4.0
6615       *
6616       * @var string
6617       */
6618      const REPROCESS_CURRENT_NODE = 'reprocess-current-node';
6619  
6620      /**
6621       * Indicates that the current HTML token should be processed without advancing the parser.
6622       *
6623       * @since 6.5.0
6624       *
6625       * @var string
6626       */
6627      const PROCESS_CURRENT_NODE = 'process-current-node';
6628  
6629      /**
6630       * Indicates that the parser encountered unsupported markup and has bailed.
6631       *
6632       * @since 6.4.0
6633       *
6634       * @var string
6635       */
6636      const ERROR_UNSUPPORTED = 'unsupported';
6637  
6638      /**
6639       * Indicates that the parser encountered more HTML tokens than it
6640       * was able to process and has bailed.
6641       *
6642       * @since 6.4.0
6643       *
6644       * @var string
6645       */
6646      const ERROR_EXCEEDED_MAX_BOOKMARKS = 'exceeded-max-bookmarks';
6647  
6648      /**
6649       * Unlock code that must be passed into the constructor to create this class.
6650       *
6651       * This class extends the WP_HTML_Tag_Processor, which has a public class
6652       * constructor. Therefore, it's not possible to have a private constructor here.
6653       *
6654       * This unlock code is used to ensure that anyone calling the constructor is
6655       * doing so with a full understanding that it's intended to be a private API.
6656       *
6657       * @since 6.4.0
6658       * @access private
6659       */
6660      const CONSTRUCTOR_UNLOCK_CODE = 'Use WP_HTML_Processor::create_fragment() instead of calling the class constructor directly.';
6661  }


Generated : Thu Sep 24 08:20:34 2026 Cross-referenced by PHPXref