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