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


Generated : Wed Nov 4 08:20:38 2026 Cross-referenced by PHPXref