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