[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Fri Sep 4 08:20:24 2026 Cross-referenced by PHPXref