| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 < <> "oddities" 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 < <> "oddities" 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", '
', $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 & Milk">`. 5330 * $processor->set_attribute( 'title', 'Eggs & Milk' ); 5331 * 5332 * // Renders “Eggs & Milk” in a browser, encoded as `<abbr title="Eggs &amp; Milk">`. 5333 * $processor->set_attribute( 'title', 'Eggs & 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 <egg>\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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Wed Nov 4 08:20:38 2026 | Cross-referenced by PHPXref |