| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Efficiently scan through block structure in document without parsing 4 * the entire block tree and all of its JSON attributes into memory. 5 * 6 * @package WordPress 7 * @subpackage Blocks 8 * @since 6.9.0 9 */ 10 11 /** 12 * Class for efficiently scanning through block structure in a document 13 * without parsing the entire block tree and JSON attributes into memory. 14 * 15 * ## Overview 16 * 17 * This class is designed to help analyze and modify block structure in a 18 * streaming fashion and to bridge the gap between parsed block trees and 19 * the text representing them. 20 * 21 * Use-cases for this class include but are not limited to: 22 * 23 * - Counting block types in a document. 24 * - Queuing stylesheets based on the presence of various block types. 25 * - Modifying blocks of a given type, i.e. migrations, updates, and styling. 26 * - Searching for content of specific kinds, e.g. checking for blocks 27 * with certain theme support attributes, or block bindings. 28 * - Adding CSS class names to the element wrapping a block’s inner blocks. 29 * 30 * > *Note!* If a fully-parsed block tree of a document is necessary, including 31 * > all the parsed JSON attributes, nested blocks, and HTML, consider 32 * > using {@see \parse_blocks()} instead which will parse the document 33 * > in one swift pass. 34 * 35 * For typical usage, jump first to the methods {@see self::next_block()}, 36 * {@see self::next_delimiter()}, or {@see self::next_token()}. 37 * 38 * ### Values 39 * 40 * As a lower-level interface than {@see parse_blocks()} this class follows 41 * different performance-focused values: 42 * 43 * - Minimize allocations so that documents of any size may be processed 44 * on a fixed or marginal amount of memory. 45 * - Make hidden costs explicit so that calling code only has to pay the 46 * performance penalty for features it needs. 47 * - Operate with a streaming and re-entrant design to make it possible 48 * to operate on chunks of a document and to resume after pausing. 49 * 50 * This means that some operations might appear more cumbersome than one 51 * might expect. This design tradeoff opens up opportunity to wrap this in 52 * a convenience class to add higher-level functionality. 53 * 54 * ## Concepts 55 * 56 * All text documents can be considered a block document containing a combination 57 * of “freeform HTML” and explicit block structure. Block structure forms through 58 * special HTML comments called _delimiters_ which include a block type and, 59 * optionally, block attributes encoded as a JSON object payload. 60 * 61 * This processor is designed to scan through a block document from delimiter to 62 * delimiter, tracking how the delimiters impact the structure of the document. 63 * Spans of HTML appear between delimiters. If these spans exist at the top level 64 * of the document, meaning there is no containing block around them, they are 65 * considered freeform HTML content. If, however, they appear _inside_ block 66 * structure they are interpreted as `innerHTML` for the containing block. 67 * 68 * ### Tokens and scanning 69 * 70 * As the processor scans through a document is reports information about the token 71 * on which is pauses. Tokens represent spans of text in the input comprising block 72 * delimiters and spans of HTML. 73 * 74 * - {@see self::next_token()} visits every contiguous subspan of text in the 75 * input document. This includes all explicit block comment delimiters and spans 76 * of HTML content (whether freeform or inner HTML). 77 * - {@see self::next_delimiter()} visits every explicit block comment delimiter 78 * unless passed a block type which covers freeform HTML content. In these cases 79 * it will stop at top-level spans of HTML and report a `null` block type. 80 * - {@see self::next_block()} visits every block delimiter which _opens_ a block. 81 * This includes opening block delimiters as well as void block delimiters. With 82 * the same exception as above for freeform HTML block types, this will visit 83 * top-level spans of HTML content. 84 * 85 * When matched on a particular token, the following methods provide structural 86 * and textual information about it: 87 * 88 * - {@see self::get_delimiter_type()} reports whether the delimiter is an opener, 89 * a closer, or if it represents a whole void block. 90 * - {@see self::get_block_type()} reports the fully-qualified block type which 91 * the delimiter represents. 92 * - {@see self::get_printable_block_type()} reports the fully-qualified block type, 93 * but returns `core/freeform` instead of `null` for top-level freeform HTML content. 94 * - {@see self::is_block_type()} indicates if the delimiter represents a block of 95 * the given block type, or wildcard or pseudo-block type described below. 96 * - {@see self::opens_block()} indicates if the delimiter opens a block of one 97 * of the provided block types. Opening, void, and top-level freeform HTML content 98 * all open blocks. 99 * - {@see static::get_attributes()} is currently reserved for a future streaming 100 * JSON parser class. 101 * - {@see self::allocate_and_return_parsed_attributes()} extracts the JSON attributes 102 * for delimiters which open blocks and return the fully-parsed attributes as an 103 * associative array. {@see static::get_last_json_error()} for when this fails. 104 * - {@see self::is_html()} indicates if the token is a span of HTML which might 105 * be top-level freeform content or a block’s inner HTML. 106 * - {@see self::get_html_content()} returns the span of HTML. 107 * - {@see self::get_span()} for the byte offset and length into the input document 108 * representing the token. 109 * 110 * It’s possible for the processor to fail to scan forward if the input document ends 111 * in a proper prefix of an explicit block comment delimiter. For example, if the input 112 * ends in `<!-- wp:` then it _might_ be the start of another delimiter. The parser 113 * cannot know, however, and therefore refuses to proceed. {@see static::get_last_error()} 114 * to distinguish between a failure to find the next token and an incomplete input. 115 * 116 * ### Block types 117 * 118 * A block’s “type” comprises an optional _namespace_ and _name_. If the namespace 119 * isn’t provided it will be interpreted as the implicit `core` namespace. For example, 120 * the type `gallery` is the name of the block in the `core` namespace, but the type 121 * `abc/gallery` is the _fully-qualified_ block type for the block whose name is still 122 * `gallery`, but in the `abc` namespace. 123 * 124 * Methods on this class are aware of this block naming semantic and anywhere a block 125 * type is an argument to a method it will be normalized to account for implicit namespaces. 126 * Passing `paragraph` is the same as passing `core/paragraph`. On the contrary, anywhere 127 * this class returns a block type, it will return the fully-qualified and normalized form. 128 * For example, for the `<!-- wp:group -->` delimiter it will return `core/group` as the 129 * block type. 130 * 131 * There are two special block types that change the behavior of the processor: 132 * 133 * - The wildcard `*` represents _any block_. In addition to matching all block types, 134 * it also represents top-level freeform HTML whose block type is reported as `null`. 135 * 136 * - The `core/freeform` block type is a pseudo-block type which explicitly matches 137 * top-level freeform HTML. 138 * 139 * These special block types can be passed into any method which searches for blocks. 140 * 141 * There is one additional special block type which may be returned from 142 * {@see self::get_printable_block_type()}. This is the `#innerHTML` type, which 143 * indicates that the HTML span on which the processor is paused is inner HTML for 144 * a containing block. 145 * 146 * ### Spans of HTML 147 * 148 * Non-block content plays a complicated role in processing block documents. This 149 * processor exposes tools to help work with these spans of HTML. 150 * 151 * - {@see self::is_html()} indicates if the processor is paused at a span of 152 * HTML but does not differentiate between top-level freeform content and inner HTML. 153 * - {@see self::is_non_whitespace_html()} indicates not only if the processor 154 * is paused at a span of HTML, but also whether that span incorporates more than 155 * whitespace characters. Because block serialization often inserts newlines between 156 * block comment delimiters, this is useful for distinguishing “real” freeform 157 * content from purely aesthetic syntax. 158 * - {@see self::is_block_type()} matches top-level freeform HTML content when 159 * provided one of the special block types described above. 160 * 161 * ### Block structure 162 * 163 * As the processor traverses block delimiters it maintains a stack of which blocks are 164 * open at the given place in the document where it’s paused. This stack represents the 165 * block structure of a document and is used to determine where blocks end, which blocks 166 * represent inner blocks, whether a span of HTML is top-level freeform content, and 167 * more. Investigate the stack with {@see self::get_breadcrumbs()}, which returns an 168 * array of block types starting at the outermost-open block and descending to the 169 * currently-visited block. 170 * 171 * Unlike {@parse_blocks()}, spans of HTML appear in this structure as the special 172 * reported block type `#html`. Such a span represents inner HTML for a block if the 173 * depth reported by {@see self::get_depth()} is greater than one. 174 * 175 * It will generally not be necessary to inspect the stack of open blocks, though 176 * depth may be important for finding where blocks end. When visiting a block opener, 177 * the depth will have been increased before pausing; in contrast the depth is 178 * decremented before visiting a closer. This makes the following an easy way to 179 * determine if a block is still open. 180 * 181 * Example: 182 * 183 * $depth = $processor->get_depth(); 184 * while ( $processor->next_token() && $processor->get_depth() > $depth ) { 185 * continue 186 * } 187 * // Processor is now paused at the token immediately following the closed block. 188 * 189 * #### Extracting blocks 190 * 191 * A unique feature of this processor is the ability to return the same output as 192 * {@see \parse_blocks()} would produce, but for a subset of the input document. 193 * For example, it’s possible to extract an image block, manipulate that parsed 194 * block, and re-serialize it into the original document. It’s possible to do so 195 * while skipping over the parse of the rest of the document. 196 * 197 * {@see self::extract_full_block_and_advance()} will scan forward from the current block opener 198 * and build the parsed block structure until the current block is closed. It will 199 * include all inner HTML and inner blocks, and parse all of the inner blocks. It 200 * can be used to extract a block at any depth in the document, helpful for operating 201 * on blocks within nested structure. 202 * 203 * Example: 204 * 205 * if ( ! $processor->next_block( 'gallery' ) ) { 206 * return $post_content; 207 * } 208 * 209 * $gallery_at = $processor->get_span()->start; 210 * $gallery_block = $processor->extract_full_block_and_advance(); 211 * $after_gallery = $processor->get_span()->start; 212 * return ( 213 * substr( $post_content, 0, $gallery_at ) . 214 * serialize_block( modify_gallery( $gallery_block ) . 215 * substr( $post_content, $after_gallery ) 216 * ); 217 * 218 * #### Handling of malformed structure 219 * 220 * There are situations where closing block delimiters appear for which no open block 221 * exists, or where a document ends before a block is closed, or where a closing block 222 * delimiter appears but references a different block type than the most-recently 223 * opened block does. In all of these cases, the stack of open blocks should mirror 224 * the behavior in {@see \parse_blocks()}. 225 * 226 * Unlike {@see \parse_blocks()}, however, this processor can still operate on the 227 * invalid block delimiters. It provides a few functions which can be used for building 228 * custom and non-spec-compliant error handling. 229 * 230 * - {@see self::has_closing_flag()} indicates if the block delimiter contains the 231 * closing flag at the end. Some invalid block delimiters might contain both the 232 * void and closing flag, in which case {@see self::get_delimiter_type()} will 233 * report that it’s a void block. 234 * - {@see static::get_last_error()} indicates if the processor reached an invalid 235 * block closing. Depending on the context, {@see \parse_blocks()} might instead 236 * ignore the token or treat it as freeform HTML content. 237 * 238 * ## Static helpers 239 * 240 * This class provides helpers for performing semantic block-related operations. 241 * 242 * - {@see self::normalize_block_type()} takes a block type with or without the 243 * implicit `core` namespace and returns a fully-qualified block type. 244 * - {@see self::are_equal_block_types()} indicates if two spans across one or 245 * more input texts represent the same fully-qualified block type. 246 * 247 * ## Subclassing 248 * 249 * This processor is designed to accurately parse a block document. Therefore, many 250 * of its methods are not meant for subclassing. However, overall this class supports 251 * building higher-level convenience classes which may choose to subclass it. For those 252 * classes, avoid re-implementing methods except for the list below. Instead, create 253 * new names representing the higher-level concepts being introduced. For example, instead 254 * of creating a new method named `next_block()` which only advances to blocks of a given 255 * kind, consider creating a new method named something like `next_layout_block()` which 256 * won’t interfere with the base class method. 257 * 258 * - {@see static::get_last_error()} may be reimplemented to report new errors in the subclass 259 * which aren’t intrinsic to block parsing. 260 * - {@see static::get_attributes()} may be reimplemented to provide a streaming interface 261 * to reading and modifying a block’s JSON attributes. It should be fast and memory efficient. 262 * - {@see static::get_last_json_error()} may be reimplemented to report new errors introduced 263 * with a reimplementation of {@see static::get_attributes()}. 264 * 265 * @since 6.9.0 266 */ 267 class WP_Block_Processor { 268 /** 269 * Indicates if the last operation failed, otherwise 270 * will be `null` for success. 271 * 272 * @since 6.9.0 273 * 274 * @var string|null 275 */ 276 private $last_error = null; 277 278 /** 279 * Indicates failures from decoding JSON attributes. 280 * 281 * @since 6.9.0 282 * 283 * @see \json_last_error() 284 * 285 * @var int 286 */ 287 private $last_json_error = JSON_ERROR_NONE; 288 289 /** 290 * Source text provided to processor. 291 * 292 * @since 6.9.0 293 * 294 * @var string 295 */ 296 protected $source_text; 297 298 /** 299 * Byte offset into source text where a matched delimiter starts. 300 * 301 * Example: 302 * 303 * 5 10 15 20 25 30 35 40 45 50 304 * <!-- wp:group --><!-- wp:void /--><!-- /wp:group --> 305 * ╰─ Starts at byte offset 17. 306 * 307 * @since 6.9.0 308 * 309 * @var int 310 */ 311 private $matched_delimiter_at = 0; 312 313 /** 314 * Byte length of full span of a matched delimiter. 315 * 316 * Example: 317 * 318 * 5 10 15 20 25 30 35 40 45 50 319 * <!-- wp:group --><!-- wp:void /--><!-- /wp:group --> 320 * ╰───────────────╯ 321 * 17 bytes long. 322 * 323 * @since 6.9.0 324 * 325 * @var int 326 */ 327 private $matched_delimiter_length = 0; 328 329 /** 330 * First byte offset into source text following any previously-matched delimiter. 331 * Used to indicate where an HTML span starts. 332 * 333 * Example: 334 * 335 * 5 10 15 20 25 30 35 40 45 50 55 336 * <!-- wp:paragraph --><p>Content</p><⃨!⃨-⃨-⃨ ⃨/⃨w⃨p⃨:⃨p⃨a⃨r⃨a⃨g⃨r⃨a⃨p⃨h⃨ ⃨-⃨-⃨>⃨ 337 * │ ╰─ This delimiter was matched, and after matching, 338 * │ revealed the preceding HTML span. 339 * │ 340 * ╰─ The first byte offset after the previous matched delimiter 341 * is 21. Because the matched delimiter starts at 55, which is after 342 * this, a span of HTML must exist between these boundaries. 343 * 344 * @since 6.9.0 345 * 346 * @var int 347 */ 348 private $after_previous_delimiter = 0; 349 350 /** 351 * Byte offset where namespace span begins. 352 * 353 * When no namespace is present, this will be the same as the starting 354 * byte offset for the block name. 355 * 356 * Example: 357 * 358 * <!-- wp:core/gallery --> 359 * │ ╰─ Name starts here. 360 * ╰─ Namespace starts here. 361 * 362 * <!-- wp:gallery --> 363 * ├─ The namespace would start here but is implied as “core.” 364 * ╰─ The name starts here. 365 * 366 * @since 6.9.0 367 * 368 * @var int 369 */ 370 private $namespace_at = 0; 371 372 /** 373 * Byte offset where block name span begins. 374 * 375 * When no namespace is present, this will be the same as the starting 376 * byte offset for the block namespace. 377 * 378 * Example: 379 * 380 * <!-- wp:core/gallery --> 381 * │ ╰─ Name starts here. 382 * ╰─ Namespace starts here. 383 * 384 * <!-- wp:gallery --> 385 * ├─ The namespace would start here but is implied as “core.” 386 * ╰─ The name starts here. 387 * 388 * @since 6.9.0 389 * 390 * @var int 391 */ 392 private $name_at = 0; 393 394 /** 395 * Byte length of block name span. 396 * 397 * Example: 398 * 399 * 5 10 15 20 25 400 * <!-- wp:core/gallery --> 401 * ╰─────╯ 402 * 7 bytes long. 403 * 404 * @since 6.9.0 405 * 406 * @var int 407 */ 408 private $name_length = 0; 409 410 /** 411 * Whether the delimiter contains the block-closing flag. 412 * 413 * This may be erroneous if present within a void block, 414 * therefore the {@see self::has_closing_flag()} can be used by 415 * calling code to perform custom error-handling. 416 * 417 * @since 6.9.0 418 * 419 * @var bool 420 */ 421 private $has_closing_flag = false; 422 423 /** 424 * Byte offset where JSON attributes span begins. 425 * 426 * Example: 427 * 428 * 5 10 15 20 25 30 35 40 429 * <!-- wp:paragraph {"dropCaps":true} --> 430 * ╰─ Starts at byte offset 18. 431 * 432 * @since 6.9.0 433 * 434 * @var int 435 */ 436 private $json_at; 437 438 /** 439 * Byte length of JSON attributes span, or 0 if none are present. 440 * 441 * Example: 442 * 443 * 5 10 15 20 25 30 35 40 444 * <!-- wp:paragraph {"dropCaps":true} --> 445 * ╰───────────────╯ 446 * 17 bytes long. 447 * 448 * @since 6.9.0 449 * 450 * @var int 451 */ 452 private $json_length = 0; 453 454 /** 455 * Internal parser state, differentiating whether the instance is currently matched, 456 * on an implicit freeform node, in error, or ready to begin parsing. 457 * 458 * @see self::READY 459 * @see self::MATCHED 460 * @see self::HTML_SPAN 461 * @see self::INCOMPLETE_INPUT 462 * @see self::COMPLETE 463 * 464 * @since 6.9.0 465 * 466 * @var string 467 */ 468 protected $state = self::READY; 469 470 /** 471 * Indicates what kind of block comment delimiter was matched. 472 * 473 * One of: 474 * 475 * - {@see self::OPENER} If the delimiter is opening a block. 476 * - {@see self::CLOSER} If the delimiter is closing an open block. 477 * - {@see self::VOID} If the delimiter represents a void block with no inner content. 478 * 479 * If a parsed comment delimiter contains both the closing and the void 480 * flags then it will be interpreted as a void block to match the behavior 481 * of the official block parser, however, this is a syntax error and probably 482 * the block ought to close an open block of the same name, if one is open. 483 * 484 * @since 6.9.0 485 * 486 * @var string 487 */ 488 private $type; 489 490 /** 491 * Whether the last-matched delimiter acts like a void block and should be 492 * popped from the stack of open blocks as soon as the parser advances. 493 * 494 * This applies to void block delimiters and to HTML spans. 495 * 496 * @since 6.9.0 497 * 498 * @var bool 499 */ 500 private $was_void = false; 501 502 /** 503 * For every open block, in hierarchical order, this stores the byte offset 504 * into the source text where the block type starts, including for HTML spans. 505 * 506 * To avoid allocating and normalizing block names when they aren’t requested, 507 * the stack of open blocks is stored as the byte offsets and byte lengths of 508 * each open block’s block type. This allows for minimal tracking and quick 509 * reading or comparison of block types when requested. 510 * 511 * @since 6.9.0 512 * 513 * @see self::$open_blocks_length 514 * 515 * @var int[] 516 */ 517 private $open_blocks_at = array(); 518 519 /** 520 * For every open block, in hierarchical order, this stores the byte length 521 * of the block’s block type in the source text. For HTML spans this is 0. 522 * 523 * @since 6.9.0 524 * 525 * @see self::$open_blocks_at 526 * 527 * @var int[] 528 */ 529 private $open_blocks_length = array(); 530 531 /** 532 * Indicates which operation should apply to the stack of open blocks after 533 * processing any pending spans of HTML. 534 * 535 * Since HTML spans are discovered after matching block delimiters, those 536 * delimiters need to defer modifying the stack of open blocks. This value, 537 * if set, indicates what operation should be applied. The properties 538 * associated with token boundaries still point to the delimiters even 539 * when processing HTML spans, so there’s no need to track them independently. 540 * 541 * @since 6.9.0 542 * @var 'push'|'void'|'pop'|null 543 */ 544 private $next_stack_op = null; 545 546 /** 547 * Creates a new block processor. 548 * 549 * Example: 550 * 551 * $processor = new WP_Block_Processor( $post_content ); 552 * if ( $processor->next_block( 'core/image' ) ) { 553 * echo "Found an image!\n"; 554 * } 555 * 556 * @see self::next_block() to advance to the start of the next block (skips closers). 557 * @see self::next_delimiter() to advance to the next explicit block delimiter. 558 * @see self::next_token() to advance to the next block delimiter or HTML span. 559 * 560 * @since 6.9.0 561 * 562 * @param string $source_text Input document potentially containing block content. 563 */ 564 public function __construct( string $source_text ) { 565 $this->source_text = $source_text; 566 } 567 568 /** 569 * Advance to the next block delimiter which opens a block, indicating if one was found. 570 * 571 * Delimiters which open blocks include opening and void block delimiters. To visit 572 * freeform HTML content, pass the wildcard “*” as the block type. 573 * 574 * Use this function to walk through the blocks in a document, pausing where they open. 575 * 576 * Example blocks: 577 * 578 * // The first delimiter opens the paragraph block. 579 * <⃨!⃨-⃨-⃨ ⃨w⃨p⃨:⃨p⃨a⃨r⃨a⃨g⃨r⃨a⃨p⃨h⃨ ⃨-⃨-⃨>⃨<p>Content</p><!-- /wp:paragraph--> 580 * 581 * // The void block is the first opener in this sequence of closers. 582 * <!-- /wp:group --><⃨!⃨-⃨-⃨ ⃨w⃨p⃨:⃨s⃨p⃨a⃨c⃨e⃨r⃨ ⃨{⃨"⃨h⃨e⃨i⃨g⃨h⃨t⃨"⃨:⃨"⃨2⃨0⃨0⃨p⃨x⃨"⃨}⃨ ⃨/⃨-⃨-⃨>⃨<!-- /wp:group --> 583 * 584 * // If, however, `*` is provided as the block type, freeform content is matched. 585 * <⃨h⃨2⃨>⃨M⃨y⃨ ⃨s⃨y⃨n⃨o⃨p⃨s⃨i⃨s⃨<⃨/⃨h⃨2⃨>⃨\⃨n⃨<!-- wp:my/table-of-contents /--> 586 * 587 * // Inner HTML is never freeform content, and will not be matched even with the wildcard. 588 * <!-- /wp:list-item --></ul><!-- /wp:list --><⃨!⃨-⃨-⃨ ⃨w⃨p⃨:⃨p⃨a⃨r⃨a⃨g⃨r⃨a⃨p⃨h⃨ ⃨-⃨>⃨<p> 589 * 590 * Example: 591 * 592 * // Find all textual ranges of image block opening delimiters. 593 * $images = array(); 594 * $processor = new WP_Block_Processor( $html ); 595 * while ( $processor->next_block( 'core/image' ) ) { 596 * $images[] = $processor->get_span(); 597 * } 598 * 599 * In some cases it may be useful to conditionally visit the implicit freeform 600 * blocks, such as when determining if a post contains freeform content that 601 * isn’t purely whitespace. 602 * 603 * Example: 604 * 605 * $seen_block_types = []; 606 * $block_type = '*'; 607 * $processor = new WP_Block_Processor( $html ); 608 * while ( $processor->next_block( $block_type ) { 609 * // Stop wasting time visiting freeform blocks after one has been found. 610 * if ( 611 * '*' === $block_type && 612 * null === $processor->get_block_type() && 613 * $processor->is_non_whitespace_html() 614 * ) { 615 * $block_type = null; 616 * $seen_block_types['core/freeform'] = true; 617 * continue; 618 * } 619 * 620 * $seen_block_types[ $processor->get_block_type() ] = true; 621 * } 622 * 623 * @since 6.9.0 624 * 625 * @see self::next_delimiter() to advance to the next explicit block delimiter. 626 * @see self::next_token() to advance to the next block delimiter or HTML span. 627 * 628 * @param string|null $block_type Optional. If provided, advance until a block of this type is found. 629 * Default is to stop at any block regardless of its type. 630 * @return bool Whether an opening delimiter for a block was found. 631 */ 632 public function next_block( ?string $block_type = null ): bool { 633 while ( $this->next_delimiter( $block_type ) ) { 634 if ( self::CLOSER !== $this->get_delimiter_type() ) { 635 return true; 636 } 637 } 638 639 return false; 640 } 641 642 /** 643 * Advance to the next block delimiter in a document, indicating if one was found. 644 * 645 * Delimiters may include invalid JSON. This parser does not attempt to parse the 646 * JSON attributes until requested; when invalid, the attributes will be null. This 647 * matches the behavior of {@see \parse_blocks()}. To visit freeform HTML content, 648 * pass the wildcard “*” as the block type. 649 * 650 * Use this function to walk through the block delimiters in a document. 651 * 652 * Example delimiters: 653 * 654 * <!-- wp:paragraph {"dropCap": true} --> 655 * <!-- wp:separator /--> 656 * <!-- /wp:paragraph --> 657 * 658 * // If the wildcard `*` is provided as the block type, freeform content is matched. 659 * <⃨h⃨2⃨>⃨M⃨y⃨ ⃨s⃨y⃨n⃨o⃨p⃨s⃨i⃨s⃨<⃨/⃨h⃨2⃨>⃨\⃨n⃨<!-- wp:my/table-of-contents /--> 660 * 661 * // Inner HTML is never freeform content, and will not be matched even with the wildcard. 662 * ...</ul><⃨!⃨-⃨-⃨ ⃨/⃨w⃨p⃨:⃨l⃨i⃨s⃨t⃨ ⃨-⃨-⃨>⃨<!-- wp:paragraph --><p> 663 * 664 * Example: 665 * 666 * $html = '<!-- wp:void /-->\n<!-- wp:void /-->'; 667 * $processor = new WP_Block_Processor( $html ); 668 * while ( $processor->next_delimiter() { 669 * // Runs twice, seeing both void blocks of type “core/void.” 670 * } 671 * 672 * $processor = new WP_Block_Processor( $html ); 673 * while ( $processor->next_delimiter( '*' ) ) { 674 * // Runs thrice, seeing the void block, the newline span, and the void block. 675 * } 676 * 677 * @since 6.9.0 678 * 679 * @param string|null $block_name Optional. Keep searching until a block of this name is found. 680 * Defaults to visit every block regardless of type. 681 * @return bool Whether a block delimiter was matched. 682 */ 683 public function next_delimiter( ?string $block_name = null ): bool { 684 if ( ! isset( $block_name ) ) { 685 while ( $this->next_token() ) { 686 if ( ! $this->is_html() ) { 687 return true; 688 } 689 } 690 691 return false; 692 } 693 694 while ( $this->next_token() ) { 695 if ( $this->is_block_type( $block_name ) ) { 696 return true; 697 } 698 } 699 700 return false; 701 } 702 703 /** 704 * Advance to the next block delimiter or HTML span in a document, indicating if one was found. 705 * 706 * This function steps through every syntactic chunk in a document. This includes explicit 707 * block comment delimiters, freeform non-block content, and inner HTML segments. 708 * 709 * Example tokens: 710 * 711 * <!-- wp:paragraph {"dropCap": true} --> 712 * <!-- wp:separator /--> 713 * <!-- /wp:paragraph --> 714 * <p>Normal HTML content</p> 715 * Plaintext content too! 716 * 717 * Example: 718 * 719 * // Find span containing wrapping HTML element surrounding inner blocks. 720 * $processor = new WP_Block_Processor( $html ); 721 * if ( ! $processor->next_block( 'gallery' ) ) { 722 * return null; 723 * } 724 * 725 * $containing_span = null; 726 * while ( $processor->next_token() && $processor->is_html() ) { 727 * $containing_span = $processor->get_span(); 728 * } 729 * 730 * This method will visit all HTML spans including those forming freeform non-block 731 * content as well as those which are part of a block’s inner HTML. 732 * 733 * @since 6.9.0 734 * 735 * @return bool Whether a token was matched or the end of the document was reached without finding any. 736 */ 737 public function next_token(): bool { 738 if ( $this->last_error || self::COMPLETE === $this->state || self::INCOMPLETE_INPUT === $this->state ) { 739 return false; 740 } 741 742 // Void tokens automatically pop off the stack of open blocks. 743 if ( $this->was_void ) { 744 array_pop( $this->open_blocks_at ); 745 array_pop( $this->open_blocks_length ); 746 $this->was_void = false; 747 } 748 749 $text = $this->source_text; 750 $end = strlen( $text ); 751 752 /* 753 * Because HTML spans are inferred after finding the next delimiter, it means that 754 * the parser must transition out of that HTML state and reuse the token boundaries 755 * it found after the HTML span. If those boundaries are before the end of the 756 * document it implies that a real delimiter was found; otherwise this must be the 757 * terminating HTML span and the parsing is complete. 758 */ 759 if ( self::HTML_SPAN === $this->state ) { 760 if ( $this->matched_delimiter_at >= $end ) { 761 $this->state = self::COMPLETE; 762 return false; 763 } 764 765 switch ( $this->next_stack_op ) { 766 case 'void': 767 $this->was_void = true; 768 $this->open_blocks_at[] = $this->namespace_at; 769 $this->open_blocks_length[] = $this->name_at + $this->name_length - $this->namespace_at; 770 break; 771 772 case 'push': 773 $this->open_blocks_at[] = $this->namespace_at; 774 $this->open_blocks_length[] = $this->name_at + $this->name_length - $this->namespace_at; 775 break; 776 777 case 'pop': 778 array_pop( $this->open_blocks_at ); 779 array_pop( $this->open_blocks_length ); 780 break; 781 } 782 783 $this->next_stack_op = null; 784 $this->state = self::MATCHED; 785 return true; 786 } 787 788 $this->state = self::READY; 789 $after_prev_delimiter = $this->matched_delimiter_at + $this->matched_delimiter_length; 790 $at = $after_prev_delimiter; 791 792 while ( $at < $end ) { 793 /* 794 * Find the next possible start of a delimiter. 795 * 796 * This follows the behavior in the official block parser, which segments a post 797 * by the block comment delimiters. It is possible for an HTML attribute to contain 798 * what looks like a block comment delimiter but which is actually an HTML attribute 799 * value. In such a case, the parser here will break apart the HTML and create the 800 * block boundary inside the HTML attribute. In other words, the block parser 801 * isolates sections of HTML from each other, even if that leads to malformed markup. 802 * 803 * For a more robust parse, scan through the document with the HTML API and parse 804 * comments once they are matched to see if they are also block delimiters. In 805 * practice, this nuance has not caused any known problems since developing blocks. 806 * 807 * <⃨!⃨-⃨-⃨ /wp:core/paragraph {"dropCap":true} /--> 808 */ 809 $comment_opening_at = strpos( $text, '<!--', $at ); 810 811 /* 812 * Even if the start of a potential block delimiter is not found, the document 813 * might end in a prefix of such, and in that case there is incomplete input. 814 */ 815 if ( false === $comment_opening_at ) { 816 if ( str_ends_with( $text, '<!-' ) ) { 817 $backup = 3; 818 } elseif ( str_ends_with( $text, '<!' ) ) { 819 $backup = 2; 820 } elseif ( str_ends_with( $text, '<' ) ) { 821 $backup = 1; 822 } else { 823 $backup = 0; 824 } 825 826 // Whether or not there is a potential delimiter, there might be an HTML span. 827 if ( $after_prev_delimiter < ( $end - $backup ) ) { 828 $this->state = self::HTML_SPAN; 829 $this->after_previous_delimiter = $after_prev_delimiter; 830 $this->matched_delimiter_at = $end - $backup; 831 $this->matched_delimiter_length = $backup; 832 $this->open_blocks_at[] = $after_prev_delimiter; 833 $this->open_blocks_length[] = 0; 834 $this->was_void = true; 835 return true; 836 } 837 838 /* 839 * In the case that there is the start of an HTML comment, it means that there 840 * might be a block delimiter, but it’s not possible know, therefore it’s incomplete. 841 */ 842 if ( $backup > 0 ) { 843 goto incomplete; 844 } 845 846 // Otherwise this is the end. 847 $this->state = self::COMPLETE; 848 return false; 849 } 850 851 // <!-- ⃨/wp:core/paragraph {"dropCap":true} /--> 852 $opening_whitespace_at = $comment_opening_at + 4; 853 if ( $opening_whitespace_at >= $end ) { 854 goto incomplete; 855 } 856 857 $opening_whitespace_length = strspn( $text, " \t\f\r\n", $opening_whitespace_at ); 858 859 /* 860 * The `wp` prefix cannot come before this point, but it may come after it 861 * depending on the presence of the closer. This is detected next. 862 */ 863 $wp_prefix_at = $opening_whitespace_at + $opening_whitespace_length; 864 if ( $wp_prefix_at >= $end ) { 865 goto incomplete; 866 } 867 868 if ( 0 === $opening_whitespace_length ) { 869 $at = $this->find_html_comment_end( $comment_opening_at, $end ); 870 continue; 871 } 872 873 // <!-- /⃨wp:core/paragraph {"dropCap":true} /--> 874 $has_closer = false; 875 if ( '/' === $text[ $wp_prefix_at ] ) { 876 $has_closer = true; 877 ++$wp_prefix_at; 878 } 879 880 // <!-- /w⃨p⃨:⃨core/paragraph {"dropCap":true} /--> 881 if ( $wp_prefix_at < $end && 0 !== substr_compare( $text, 'wp:', $wp_prefix_at, 3 ) ) { 882 if ( 883 ( $wp_prefix_at + 2 >= $end && str_ends_with( $text, 'wp' ) ) || 884 ( $wp_prefix_at + 1 >= $end && str_ends_with( $text, 'w' ) ) 885 ) { 886 goto incomplete; 887 } 888 889 $at = $this->find_html_comment_end( $comment_opening_at, $end ); 890 continue; 891 } 892 893 /* 894 * If the block contains no namespace, this will end up masquerading with 895 * the block name. It’s easier to first detect the span and then determine 896 * if it’s a namespace of a name. 897 * 898 * <!-- /wp:c⃨o⃨r⃨e⃨/paragraph {"dropCap":true} /--> 899 */ 900 $namespace_at = $wp_prefix_at + 3; 901 if ( $namespace_at >= $end ) { 902 goto incomplete; 903 } 904 905 $start_of_namespace = $text[ $namespace_at ]; 906 907 // The namespace must start with a-z. 908 if ( 'a' > $start_of_namespace || 'z' < $start_of_namespace ) { 909 $at = $this->find_html_comment_end( $comment_opening_at, $end ); 910 continue; 911 } 912 913 $namespace_length = 1 + strspn( $text, 'abcdefghijklmnopqrstuvwxyz0123456789-_', $namespace_at + 1 ); 914 $separator_at = $namespace_at + $namespace_length; 915 if ( $separator_at >= $end ) { 916 goto incomplete; 917 } 918 919 // <!-- /wp:core/⃨paragraph {"dropCap":true} /--> 920 $has_separator = '/' === $text[ $separator_at ]; 921 if ( $has_separator ) { 922 $name_at = $separator_at + 1; 923 924 if ( $name_at >= $end ) { 925 goto incomplete; 926 } 927 928 // <!-- /wp:core/p⃨a⃨r⃨a⃨g⃨r⃨a⃨p⃨h⃨ {"dropCap":true} /--> 929 $start_of_name = $text[ $name_at ]; 930 if ( 'a' > $start_of_name || 'z' < $start_of_name ) { 931 $at = $this->find_html_comment_end( $comment_opening_at, $end ); 932 continue; 933 } 934 935 $name_length = 1 + strspn( $text, 'abcdefghijklmnopqrstuvwxyz0123456789-_', $name_at + 1 ); 936 } else { 937 $name_at = $namespace_at; 938 $name_length = $namespace_length; 939 } 940 941 if ( $name_at + $name_length >= $end ) { 942 goto incomplete; 943 } 944 945 /* 946 * For this next section of the delimiter, it could be the JSON attributes 947 * or it could be the end of the comment. Assume that the JSON is there and 948 * update if it’s not. 949 */ 950 951 // <!-- /wp:core/paragraph ⃨{"dropCap":true} /--> 952 $after_name_whitespace_at = $name_at + $name_length; 953 $after_name_whitespace_length = strspn( $text, " \t\f\r\n", $after_name_whitespace_at ); 954 $json_at = $after_name_whitespace_at + $after_name_whitespace_length; 955 956 if ( $json_at >= $end ) { 957 goto incomplete; 958 } 959 960 if ( 0 === $after_name_whitespace_length ) { 961 $at = $this->find_html_comment_end( $comment_opening_at, $end ); 962 continue; 963 } 964 965 // <!-- /wp:core/paragraph {⃨"dropCap":true} /--> 966 $has_json = '{' === $text[ $json_at ]; 967 $json_length = 0; 968 969 /* 970 * For the final span of the delimiter it's most efficient to find the end of the 971 * HTML comment and work backwards. This prevents complicated parsing inside the 972 * JSON span, which is not allowed to contain the HTML comment terminator. 973 * 974 * This also matches the behavior in the official block parser, 975 * even though it allows for matching invalid JSON content. 976 * 977 * <!-- /wp:core/paragraph {"dropCap":true} /-⃨-⃨>⃨ 978 */ 979 $comment_closing_at = strpos( $text, '-->', $json_at ); 980 if ( false === $comment_closing_at ) { 981 goto incomplete; 982 } 983 984 // <!-- /wp:core/paragraph {"dropCap":true} /⃨--> 985 if ( '/' === $text[ $comment_closing_at - 1 ] ) { 986 $has_void_flag = true; 987 $void_flag_length = 1; 988 } else { 989 $has_void_flag = false; 990 $void_flag_length = 0; 991 } 992 993 /* 994 * If there's no JSON, then the span of text after the name 995 * until the comment closing must be completely whitespace. 996 * Otherwise it’s a normal HTML comment. 997 */ 998 if ( ! $has_json ) { 999 if ( $after_name_whitespace_at + $after_name_whitespace_length === $comment_closing_at - $void_flag_length ) { 1000 // This must be a block delimiter! 1001 $this->state = self::MATCHED; 1002 break; 1003 } 1004 1005 $at = $this->find_html_comment_end( $comment_opening_at, $end ); 1006 continue; 1007 } 1008 1009 /* 1010 * There's JSON, so attempt to find its boundary. 1011 * 1012 * @todo It’s likely faster to scan forward instead of in reverse. 1013 * 1014 * <!-- /wp:core/paragraph {"dropCap":true}⃨ ⃨/--> 1015 */ 1016 $after_json_whitespace_length = 0; 1017 for ( $char_at = $comment_closing_at - $void_flag_length - 1; $char_at > $json_at; $char_at-- ) { 1018 $char = $text[ $char_at ]; 1019 1020 switch ( $char ) { 1021 case ' ': 1022 case "\t": 1023 case "\f": 1024 case "\r": 1025 case "\n": 1026 ++$after_json_whitespace_length; 1027 continue 2; 1028 1029 case '}': 1030 $json_length = $char_at - $json_at + 1; 1031 break 2; 1032 1033 default: 1034 ++$at; 1035 continue 3; 1036 } 1037 } 1038 1039 /* 1040 * This covers cases where there is no terminating “}” or where 1041 * mandatory whitespace is missing. 1042 */ 1043 if ( 0 === $json_length || 0 === $after_json_whitespace_length ) { 1044 $at = $this->find_html_comment_end( $comment_opening_at, $end ); 1045 continue; 1046 } 1047 1048 // This must be a block delimiter! 1049 $this->state = self::MATCHED; 1050 break; 1051 } 1052 1053 // The end of the document was reached without a match. 1054 if ( self::MATCHED !== $this->state ) { 1055 $this->state = self::COMPLETE; 1056 return false; 1057 } 1058 1059 /* 1060 * From this point forward, a delimiter has been matched. There 1061 * might also be an HTML span that appears before the delimiter. 1062 */ 1063 1064 $this->after_previous_delimiter = $after_prev_delimiter; 1065 1066 $this->matched_delimiter_at = $comment_opening_at; 1067 $this->matched_delimiter_length = $comment_closing_at + 3 - $comment_opening_at; 1068 1069 $this->namespace_at = $namespace_at; 1070 $this->name_at = $name_at; 1071 $this->name_length = $name_length; 1072 1073 $this->json_at = $json_at; 1074 $this->json_length = $json_length; 1075 1076 /* 1077 * When delimiters contain both the void flag and the closing flag 1078 * they shall be interpreted as void blocks, per the spec parser. 1079 */ 1080 if ( $has_void_flag ) { 1081 $this->type = self::VOID; 1082 $this->next_stack_op = 'void'; 1083 } elseif ( $has_closer ) { 1084 $this->type = self::CLOSER; 1085 $this->next_stack_op = 'pop'; 1086 1087 /* 1088 * @todo Check if the name matches and bail according to the spec parser. 1089 * The default parser doesn’t examine the names. 1090 */ 1091 } else { 1092 $this->type = self::OPENER; 1093 $this->next_stack_op = 'push'; 1094 } 1095 1096 $this->has_closing_flag = $has_closer; 1097 1098 // HTML spans are visited before the delimiter that follows them. 1099 if ( $comment_opening_at > $after_prev_delimiter ) { 1100 $this->state = self::HTML_SPAN; 1101 $this->open_blocks_at[] = $after_prev_delimiter; 1102 $this->open_blocks_length[] = 0; 1103 $this->was_void = true; 1104 1105 return true; 1106 } 1107 1108 // If there were no HTML spans then flush the enqueued stack operations immediately. 1109 switch ( $this->next_stack_op ) { 1110 case 'void': 1111 $this->was_void = true; 1112 $this->open_blocks_at[] = $namespace_at; 1113 $this->open_blocks_length[] = $name_at + $name_length - $namespace_at; 1114 break; 1115 1116 case 'push': 1117 $this->open_blocks_at[] = $namespace_at; 1118 $this->open_blocks_length[] = $name_at + $name_length - $namespace_at; 1119 break; 1120 1121 case 'pop': 1122 array_pop( $this->open_blocks_at ); 1123 array_pop( $this->open_blocks_length ); 1124 break; 1125 } 1126 1127 $this->next_stack_op = null; 1128 1129 return true; 1130 1131 incomplete: 1132 $this->state = self::COMPLETE; 1133 $this->last_error = self::INCOMPLETE_INPUT; 1134 return false; 1135 } 1136 1137 /** 1138 * Returns an array containing the names of the currently-open blocks, in order 1139 * from outermost to innermost, with HTML spans indicated as “#html”. 1140 * 1141 * Example: 1142 * 1143 * // Freeform HTML content is an HTML span. 1144 * $processor = new WP_Block_Processor( 'Just text' ); 1145 * $processor->next_token(); 1146 * array( '#text' ) === $processor->get_breadcrumbs(); 1147 * 1148 * $processor = new WP_Block_Processor( '<!-- wp:a --><!-- wp:b --><!-- wp:c /--><!-- /wp:b --><!-- /wp:a -->' ); 1149 * $processor->next_token(); 1150 * array( 'core/a' ) === $processor->get_breadcrumbs(); 1151 * $processor->next_token(); 1152 * array( 'core/a', 'core/b' ) === $processor->get_breadcrumbs(); 1153 * $processor->next_token(); 1154 * // Void blocks are only open while visiting them. 1155 * array( 'core/a', 'core/b', 'core/c' ) === $processor->get_breadcrumbs(); 1156 * $processor->next_token(); 1157 * // Blocks are closed before visiting their closing delimiter. 1158 * array( 'core/a' ) === $processor->get_breadcrumbs(); 1159 * $processor->next_token(); 1160 * array() === $processor->get_breadcrumbs(); 1161 * 1162 * // Inner HTML is also an HTML span. 1163 * $processor = new WP_Block_Processor( '<!-- wp:a -->Inner HTML<!-- /wp:a -->' ); 1164 * $processor->next_token(); 1165 * $processor->next_token(); 1166 * array( 'core/a', '#html' ) === $processor->get_breadcrumbs(); 1167 * 1168 * @since 6.9.0 1169 * 1170 * @return string[] 1171 */ 1172 public function get_breadcrumbs(): array { 1173 $breadcrumbs = array_fill( 0, count( $this->open_blocks_at ), null ); 1174 1175 /* 1176 * Since HTML spans can only be at the very end, set the normalized block name for 1177 * each open element and then work backwards after creating the array. This allows 1178 * for the elimination of a conditional on each iteration of the loop. 1179 */ 1180 foreach ( $this->open_blocks_at as $i => $at ) { 1181 $block_type = substr( $this->source_text, $at, $this->open_blocks_length[ $i ] ); 1182 $breadcrumbs[ $i ] = self::normalize_block_type( $block_type ); 1183 } 1184 1185 if ( isset( $i ) && 0 === $this->open_blocks_length[ $i ] ) { 1186 $breadcrumbs[ $i ] = '#html'; 1187 } 1188 1189 return $breadcrumbs; 1190 } 1191 1192 /** 1193 * Returns the depth of the open blocks where the processor is currently matched. 1194 * 1195 * Depth increases before visiting openers and void blocks and decreases before 1196 * visiting closers. HTML spans behave like void blocks. 1197 * 1198 * @since 6.9.0 1199 * 1200 * @return int 1201 */ 1202 public function get_depth(): int { 1203 return count( $this->open_blocks_at ); 1204 } 1205 1206 /** 1207 * Extracts a block object, and all inner content, starting at a matched opening 1208 * block delimiter, or at a matched top-level HTML span as freeform HTML content. 1209 * 1210 * Use this function to extract some blocks within a document, but not all. For example, 1211 * one might want to find image galleries, parse them, modify them, and then reserialize 1212 * them in place. 1213 * 1214 * Once this function returns, the parser will be matched on token following the close 1215 * of the given block. 1216 * 1217 * The return type of this method is compatible with the return of {@see \parse_blocks()}. 1218 * 1219 * Example: 1220 * 1221 * $processor = new WP_Block_Processor( $post_content ); 1222 * if ( ! $processor->next_block( 'gallery' ) ) { 1223 * return $post_content; 1224 * } 1225 * 1226 * $gallery_at = $processor->get_span()->start; 1227 * $gallery = $processor->extract_full_block_and_advance(); 1228 * $ends_before = $processor->get_span(); 1229 * $ends_before = $ends_before->start ?? strlen( $post_content ); 1230 * 1231 * $new_gallery = update_gallery( $gallery ); 1232 * $new_gallery = serialize_block( $new_gallery ); 1233 * 1234 * return ( 1235 * substr( $post_content, 0, $gallery_at ) . 1236 * $new_gallery . 1237 * substr( $post_content, $ends_before ) 1238 * ); 1239 * 1240 * @since 6.9.0 1241 * 1242 * @return array[]|null { 1243 * Array of block structures. 1244 * 1245 * @type array ...$0 { 1246 * An associative array of a single parsed block object. See WP_Block_Parser_Block. 1247 * 1248 * @type string|null $blockName Name of block. 1249 * @type array $attrs Attributes from block comment delimiters. 1250 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1251 * have the same structure as this one. 1252 * @type string $innerHTML HTML from inside block comment delimiters. 1253 * @type array $innerContent List of string fragments and null markers where 1254 * inner blocks were found. 1255 * } 1256 * } 1257 */ 1258 public function extract_full_block_and_advance(): ?array { 1259 if ( $this->is_html() ) { 1260 $chunk = $this->get_html_content(); 1261 1262 return array( 1263 'blockName' => null, 1264 'attrs' => array(), 1265 'innerBlocks' => array(), 1266 'innerHTML' => $chunk, 1267 'innerContent' => array( $chunk ), 1268 ); 1269 } 1270 1271 $block = array( 1272 'blockName' => $this->get_block_type(), 1273 'attrs' => $this->allocate_and_return_parsed_attributes() ?? array(), 1274 'innerBlocks' => array(), 1275 'innerHTML' => '', 1276 'innerContent' => array(), 1277 ); 1278 1279 $depth = $this->get_depth(); 1280 while ( $this->next_token() && $this->get_depth() > $depth ) { 1281 if ( $this->is_html() ) { 1282 $chunk = $this->get_html_content(); 1283 $block['innerHTML'] .= $chunk; 1284 $block['innerContent'][] = $chunk; 1285 continue; 1286 } 1287 1288 /** 1289 * Inner blocks. 1290 * 1291 * @todo This is a decent place to call {@link \render_block()} 1292 * @todo Use iteration instead of recursion, or at least refactor to tail-call form. 1293 */ 1294 if ( $this->opens_block() ) { 1295 $inner_block = $this->extract_full_block_and_advance(); 1296 $block['innerBlocks'][] = $inner_block; 1297 $block['innerContent'][] = null; 1298 } 1299 1300 /* 1301 * Because the parser has advanced past the closing block token, it 1302 * may be matched on an HTML span. This needs to be processed before 1303 * moving on to the next token at the start of the next loop iteration. 1304 */ 1305 if ( $this->is_html() ) { 1306 $chunk = $this->get_html_content(); 1307 $block['innerHTML'] .= $chunk; 1308 $block['innerContent'][] = $chunk; 1309 } 1310 } 1311 1312 return $block; 1313 } 1314 1315 /** 1316 * Returns the byte-offset after the ending character of an HTML comment, 1317 * assuming the proper starting byte offset. 1318 * 1319 * @since 6.9.0 1320 * 1321 * @param int $comment_starting_at Where the HTML comment started, the leading `<`. 1322 * @param int $search_end Last offset in which to search, for limiting search span. 1323 * @return int Offset after the current HTML comment ends, or `$search_end` if no end was found. 1324 */ 1325 private function find_html_comment_end( int $comment_starting_at, int $search_end ): int { 1326 $text = $this->source_text; 1327 1328 // Find span-of-dashes comments which look like `<!----->`. 1329 $span_of_dashes = strspn( $text, '-', $comment_starting_at + 2 ); 1330 if ( 1331 $comment_starting_at + 2 + $span_of_dashes < $search_end && 1332 '>' === $text[ $comment_starting_at + 2 + $span_of_dashes ] 1333 ) { 1334 return $comment_starting_at + $span_of_dashes + 1; 1335 } 1336 1337 // Otherwise, there are other characters inside the comment, find the first `-->` or `--!>`. 1338 $now_at = $comment_starting_at + 4; 1339 while ( $now_at < $search_end ) { 1340 $dashes_at = strpos( $text, '--', $now_at ); 1341 if ( false === $dashes_at ) { 1342 return $search_end; 1343 } 1344 1345 $closer_must_be_at = $dashes_at + 2 + strspn( $text, '-', $dashes_at + 2 ); 1346 if ( $closer_must_be_at < $search_end && '!' === $text[ $closer_must_be_at ] ) { 1347 ++$closer_must_be_at; 1348 } 1349 1350 if ( $closer_must_be_at < $search_end && '>' === $text[ $closer_must_be_at ] ) { 1351 return $closer_must_be_at + 1; 1352 } 1353 1354 ++$now_at; 1355 } 1356 1357 return $search_end; 1358 } 1359 1360 /** 1361 * Indicates if the last attempt to parse a block comment delimiter 1362 * failed, if set, otherwise `null` if the last attempt succeeded. 1363 * 1364 * @since 6.9.0 1365 * 1366 * @return string|null Error from last attempt at parsing next block delimiter, 1367 * or `null` if last attempt succeeded. 1368 */ 1369 public function get_last_error(): ?string { 1370 return $this->last_error; 1371 } 1372 1373 /** 1374 * Indicates if the last attempt to parse a block’s JSON attributes failed. 1375 * 1376 * @see \json_last_error() 1377 * 1378 * @since 6.9.0 1379 * 1380 * @return int JSON_ERROR_ code from last attempt to parse block JSON attributes. 1381 */ 1382 public function get_last_json_error(): int { 1383 return $this->last_json_error; 1384 } 1385 1386 /** 1387 * Returns the type of the block comment delimiter. 1388 * 1389 * One of: 1390 * 1391 * - {@see self::OPENER} 1392 * - {@see self::CLOSER} 1393 * - {@see self::VOID} 1394 * - `null` 1395 * 1396 * @since 6.9.0 1397 * 1398 * @return string|null type of the block comment delimiter, if currently matched. 1399 */ 1400 public function get_delimiter_type(): ?string { 1401 switch ( $this->state ) { 1402 case self::HTML_SPAN: 1403 return self::VOID; 1404 1405 case self::MATCHED: 1406 return $this->type; 1407 1408 default: 1409 return null; 1410 } 1411 } 1412 1413 /** 1414 * Returns whether the delimiter contains the closing flag. 1415 * 1416 * This should be avoided except in cases of custom error-handling 1417 * with block closers containing the void flag. For normative use, 1418 * {@see self::get_delimiter_type()}. 1419 * 1420 * @since 6.9.0 1421 * 1422 * @return bool Whether the currently-matched block delimiter contains the closing flag. 1423 */ 1424 public function has_closing_flag(): bool { 1425 return $this->has_closing_flag; 1426 } 1427 1428 /** 1429 * Indicates if the block delimiter represents a block of the given type. 1430 * 1431 * Since the “core” namespace may be implicit, it’s allowable to pass 1432 * either the fully-qualified block type with namespace and block name 1433 * as well as the shorthand version only containing the block name, if 1434 * the desired block is in the “core” namespace. 1435 * 1436 * Since freeform HTML content is non-block content, it has no block type. 1437 * Passing the wildcard “*” will, however, return true for all block types, 1438 * even the implicit freeform content, though not for spans of inner HTML. 1439 * 1440 * Example: 1441 * 1442 * $is_core_paragraph = $processor->is_block_type( 'paragraph' ); 1443 * $is_core_paragraph = $processor->is_block_type( 'core/paragraph' ); 1444 * $is_formula = $processor->is_block_type( 'math-block/formula' ); 1445 * 1446 * @since 6.9.0 1447 * 1448 * @param string $block_type Block type name for the desired block. 1449 * E.g. "paragraph", "core/paragraph", "math-blocks/formula". 1450 * @return bool Whether this delimiter represents a block of the given type. 1451 */ 1452 public function is_block_type( string $block_type ): bool { 1453 if ( '*' === $block_type ) { 1454 return true; 1455 } 1456 1457 if ( $this->is_html() ) { 1458 // This is a core/freeform text block, it’s special. 1459 if ( 0 === ( $this->open_blocks_length[0] ?? null ) ) { 1460 return ( 1461 'core/freeform' === $block_type || 1462 'freeform' === $block_type 1463 ); 1464 } 1465 1466 // Otherwise this is innerHTML and not a block. 1467 return false; 1468 } 1469 1470 return $this->are_equal_block_types( $this->source_text, $this->namespace_at, $this->name_at - $this->namespace_at + $this->name_length, $block_type, 0, strlen( $block_type ) ); 1471 } 1472 1473 /** 1474 * Given two spans of text, indicate if they represent identical block types. 1475 * 1476 * This function normalizes block types to account for implicit core namespacing. 1477 * 1478 * Note! This function only returns valid results when the complete block types are 1479 * represented in the span offsets and lengths. This means that the full optional 1480 * namespace and block name must be represented in the input arguments. 1481 * 1482 * Example: 1483 * 1484 * 0 5 10 15 20 25 30 35 40 1485 * $text = '<!-- wp:block --><!-- /wp:core/block -->'; 1486 * 1487 * true === WP_Block_Processor::are_equal_block_types( $text, 9, 5, $text, 27, 10 ); 1488 * false === WP_Block_Processor::are_equal_block_types( $text, 9, 5, 'my/block', 0, 8 ); 1489 * 1490 * @since 6.9.0 1491 * 1492 * @param string $a_text Text in which first block type appears. 1493 * @param int $a_at Byte offset into text in which first block type starts. 1494 * @param int $a_length Byte length of first block type. 1495 * @param string $b_text Text in which second block type appears (may be the same as the first text). 1496 * @param int $b_at Byte offset into text in which second block type starts. 1497 * @param int $b_length Byte length of second block type. 1498 * @return bool Whether the spans of text represent identical block types, normalized for namespacing. 1499 */ 1500 public static function are_equal_block_types( string $a_text, int $a_at, int $a_length, string $b_text, int $b_at, int $b_length ): bool { 1501 $a_ns_length = strcspn( $a_text, '/', $a_at, $a_length ); 1502 $b_ns_length = strcspn( $b_text, '/', $b_at, $b_length ); 1503 1504 $a_has_ns = $a_ns_length !== $a_length; 1505 $b_has_ns = $b_ns_length !== $b_length; 1506 1507 // Both contain namespaces. 1508 if ( $a_has_ns && $b_has_ns ) { 1509 if ( $a_length !== $b_length ) { 1510 return false; 1511 } 1512 1513 $a_block_type = substr( $a_text, $a_at, $a_length ); 1514 1515 return 0 === substr_compare( $b_text, $a_block_type, $b_at, $b_length ); 1516 } 1517 1518 if ( $a_has_ns ) { 1519 $b_block_type = 'core/' . substr( $b_text, $b_at, $b_length ); 1520 1521 return ( 1522 strlen( $b_block_type ) === $a_length && 1523 0 === substr_compare( $a_text, $b_block_type, $a_at, $a_length ) 1524 ); 1525 } 1526 1527 if ( $b_has_ns ) { 1528 $a_block_type = 'core/' . substr( $a_text, $a_at, $a_length ); 1529 1530 return ( 1531 strlen( $a_block_type ) === $b_length && 1532 0 === substr_compare( $b_text, $a_block_type, $b_at, $b_length ) 1533 ); 1534 } 1535 1536 // Neither contains a namespace. 1537 if ( $a_length !== $b_length ) { 1538 return false; 1539 } 1540 1541 $a_name = substr( $a_text, $a_at, $a_length ); 1542 1543 return 0 === substr_compare( $b_text, $a_name, $b_at, $b_length ); 1544 } 1545 1546 /** 1547 * Indicates if the matched delimiter is an opening or void delimiter of the given type, 1548 * if a type is provided, otherwise if it opens any block or implicit freeform HTML content. 1549 * 1550 * This is a helper method to ease handling of code inspecting where blocks start, and for 1551 * checking if the blocks are of a given type. The function is variadic to allow for 1552 * checking if the delimiter opens one of many possible block types. 1553 * 1554 * To advance to the start of a block {@see self::next_block()}. 1555 * 1556 * Example: 1557 * 1558 * $processor = new WP_Block_Processor( $html ); 1559 * while ( $processor->next_delimiter() ) { 1560 * if ( $processor->opens_block( 'core/code', 'syntaxhighlighter/code' ) ) { 1561 * echo "Found code!"; 1562 * continue; 1563 * } 1564 * 1565 * if ( $processor->opens_block( 'core/image' ) ) { 1566 * echo "Found an image!"; 1567 * continue; 1568 * } 1569 * 1570 * if ( $processor->opens_block() ) { 1571 * echo "Found a new block!"; 1572 * } 1573 * } 1574 * 1575 * @since 6.9.0 1576 * 1577 * @see self::is_block_type() 1578 * 1579 * @param string ...$block_type Optional. Is the matched block type one of these? 1580 * If none are provided, will not test block type. 1581 * @return bool Whether the matched block delimiter opens a block, and whether it 1582 * opens a block of one of the given block types, if provided. 1583 */ 1584 public function opens_block( string ...$block_type ): bool { 1585 // HTML spans only open implicit freeform content at the top level. 1586 if ( self::HTML_SPAN === $this->state && 1 !== count( $this->open_blocks_at ) ) { 1587 return false; 1588 } 1589 1590 /* 1591 * Because HTML spans are discovered after the next delimiter is found, 1592 * the delimiter type when visiting HTML spans refers to the type of the 1593 * following delimiter. Therefore the HTML case is handled by checking 1594 * the state and depth of the stack of open block. 1595 */ 1596 if ( self::CLOSER === $this->type && ! $this->is_html() ) { 1597 return false; 1598 } 1599 1600 if ( count( $block_type ) === 0 ) { 1601 return true; 1602 } 1603 1604 return array_any( $block_type, fn( $block ) => $this->is_block_type( $block ) ); 1605 } 1606 1607 /** 1608 * Indicates if the matched delimiter is an HTML span. 1609 * 1610 * @since 6.9.0 1611 * 1612 * @see self::is_non_whitespace_html() 1613 * 1614 * @return bool Whether the processor is matched on an HTML span. 1615 */ 1616 public function is_html(): bool { 1617 return self::HTML_SPAN === $this->state; 1618 } 1619 1620 /** 1621 * Indicates if the matched delimiter is an HTML span and comprises more 1622 * than whitespace characters, i.e. contains real content. 1623 * 1624 * Many block serializers introduce newlines between block delimiters, 1625 * so the presence of top-level non-block content does not imply that 1626 * there are “real” freeform HTML blocks. Checking if there is content 1627 * beyond whitespace is a more certain check, such as for determining 1628 * whether to load CSS for the freeform or fallback block type. 1629 * 1630 * @since 6.9.0 1631 * 1632 * @see self::is_html() 1633 * 1634 * @return bool Whether the currently-matched delimiter is an HTML 1635 * span containing non-whitespace text. 1636 */ 1637 public function is_non_whitespace_html(): bool { 1638 if ( ! $this->is_html() ) { 1639 return false; 1640 } 1641 1642 $length = $this->matched_delimiter_at - $this->after_previous_delimiter; 1643 1644 $whitespace_length = strspn( 1645 $this->source_text, 1646 " \t\f\r\n", 1647 $this->after_previous_delimiter, 1648 $length 1649 ); 1650 1651 return $whitespace_length !== $length; 1652 } 1653 1654 /** 1655 * Returns the string content of a matched HTML span, or `null` otherwise. 1656 * 1657 * @since 6.9.0 1658 * 1659 * @return string|null Raw HTML content, or `null` if not currently matched on HTML. 1660 */ 1661 public function get_html_content(): ?string { 1662 if ( ! $this->is_html() ) { 1663 return null; 1664 } 1665 1666 return substr( 1667 $this->source_text, 1668 $this->after_previous_delimiter, 1669 $this->matched_delimiter_at - $this->after_previous_delimiter 1670 ); 1671 } 1672 1673 /** 1674 * Allocates a substring for the block type and returns the fully-qualified 1675 * name, including the namespace, if matched on a delimiter, otherwise `null`. 1676 * 1677 * This function is like {@see self::get_printable_block_type()} but when 1678 * paused on a freeform HTML block, will return `null` instead of “core/freeform”. 1679 * The `null` behavior matches what {@see \parse_blocks()} returns but may not 1680 * be as useful as having a string value. 1681 * 1682 * This function allocates a substring for the given block type. This 1683 * allocation will be small and likely fine in most cases, but it's 1684 * preferable to call {@see self::is_block_type()} if only needing 1685 * to know whether the delimiter is for a given block type, as that 1686 * function is more efficient for this purpose and avoids the allocation. 1687 * 1688 * Example: 1689 * 1690 * // Avoid. 1691 * 'core/paragraph' = $processor->get_block_type(); 1692 * 1693 * // Prefer. 1694 * $processor->is_block_type( 'core/paragraph' ); 1695 * $processor->is_block_type( 'paragraph' ); 1696 * $processor->is_block_type( 'core/freeform' ); 1697 * 1698 * // Freeform HTML content has no block type. 1699 * $processor = new WP_Block_Processor( 'non-block content' ); 1700 * $processor->next_token(); 1701 * null === $processor->get_block_type(); 1702 * 1703 * @since 6.9.0 1704 * 1705 * @see self::are_equal_block_types() 1706 * 1707 * @return string|null Fully-qualified block namespace and type, e.g. "core/paragraph", 1708 * if matched on an explicit delimiter, otherwise `null`. 1709 */ 1710 public function get_block_type(): ?string { 1711 if ( 1712 self::READY === $this->state || 1713 self::COMPLETE === $this->state || 1714 self::INCOMPLETE_INPUT === $this->state 1715 ) { 1716 return null; 1717 } 1718 1719 // This is a core/freeform text block, it’s special. 1720 if ( $this->is_html() ) { 1721 return null; 1722 } 1723 1724 $block_type = substr( $this->source_text, $this->namespace_at, $this->name_at - $this->namespace_at + $this->name_length ); 1725 return self::normalize_block_type( $block_type ); 1726 } 1727 1728 /** 1729 * Allocates a printable substring for the block type and returns the fully-qualified 1730 * name, including the namespace, if matched on a delimiter or freeform block, otherwise `null`. 1731 * 1732 * This function is like {@see self::get_block_type()} but when paused on a freeform 1733 * HTML block, will return “core/freeform” instead of `null`. The `null` behavior matches 1734 * what {@see \parse_blocks()} returns but may not be as useful as having a string value. 1735 * 1736 * This function allocates a substring for the given block type. This 1737 * allocation will be small and likely fine in most cases, but it's 1738 * preferable to call {@see self::is_block_type()} if only needing 1739 * to know whether the delimiter is for a given block type, as that 1740 * function is more efficient for this purpose and avoids the allocation. 1741 * 1742 * Example: 1743 * 1744 * // Avoid. 1745 * 'core/paragraph' = $processor->get_printable_block_type(); 1746 * 1747 * // Prefer. 1748 * $processor->is_block_type( 'core/paragraph' ); 1749 * $processor->is_block_type( 'paragraph' ); 1750 * $processor->is_block_type( 'core/freeform' ); 1751 * 1752 * // Freeform HTML content is given an implicit type. 1753 * $processor = new WP_Block_Processor( 'non-block content' ); 1754 * $processor->next_token(); 1755 * 'core/freeform' === $processor->get_printable_block_type(); 1756 * 1757 * @since 6.9.0 1758 * 1759 * @see self::are_equal_block_types() 1760 * 1761 * @return string|null Fully-qualified block namespace and type, e.g. "core/paragraph", 1762 * if matched on an explicit delimiter or freeform block, otherwise `null`. 1763 */ 1764 public function get_printable_block_type(): ?string { 1765 if ( 1766 self::READY === $this->state || 1767 self::COMPLETE === $this->state || 1768 self::INCOMPLETE_INPUT === $this->state 1769 ) { 1770 return null; 1771 } 1772 1773 // This is a core/freeform text block, it’s special. 1774 if ( $this->is_html() ) { 1775 return 1 === count( $this->open_blocks_at ) 1776 ? 'core/freeform' 1777 : '#innerHTML'; 1778 } 1779 1780 $block_type = substr( $this->source_text, $this->namespace_at, $this->name_at - $this->namespace_at + $this->name_length ); 1781 return self::normalize_block_type( $block_type ); 1782 } 1783 1784 /** 1785 * Normalizes a block name to ensure that missing implicit “core” namespaces are present. 1786 * 1787 * Example: 1788 * 1789 * 'core/paragraph' === WP_Block_Processor::normalize_block_byte( 'paragraph' ); 1790 * 'core/paragraph' === WP_Block_Processor::normalize_block_byte( 'core/paragraph' ); 1791 * 'my/paragraph' === WP_Block_Processor::normalize_block_byte( 'my/paragraph' ); 1792 * 1793 * @since 6.9.0 1794 * 1795 * @param string $block_type Valid block name, potentially without a namespace. 1796 * @return string Fully-qualified block type including namespace. 1797 */ 1798 public static function normalize_block_type( string $block_type ): string { 1799 return str_contains( $block_type, '/' ) 1800 ? $block_type 1801 : "core/{$block_type}"; 1802 } 1803 1804 /** 1805 * Returns a lazy wrapper around the block attributes, which can be used 1806 * for efficiently interacting with the JSON attributes. 1807 * 1808 * This stub hints that there should be a lazy interface for parsing 1809 * block attributes but doesn’t define it. It serves both as a placeholder 1810 * for one to come as well as a guard against implementing an eager 1811 * function in its place. 1812 * 1813 * @throws Exception This function is a stub for subclasses to implement 1814 * when providing streaming attribute parsing. 1815 * 1816 * @since 6.9.0 1817 * 1818 * @see self::allocate_and_return_parsed_attributes() 1819 * 1820 * @return never 1821 */ 1822 public function get_attributes() { 1823 throw new Exception( 'Lazy attribute parsing not yet supported' ); 1824 } 1825 1826 /** 1827 * Attempts to parse and return the entire JSON attributes from the delimiter, 1828 * allocating memory and processing the JSON span in the process. 1829 * 1830 * This does not return any parsed attributes for a closing block delimiter 1831 * even if there is a span of JSON content; this JSON is a parsing error. 1832 * 1833 * Consider calling {@see static::get_attributes()} instead if it's not 1834 * necessary to read all the attributes at the same time, as that provides 1835 * a more efficient mechanism for typical use cases. 1836 * 1837 * Since the JSON span inside the comment delimiter may not be valid JSON, 1838 * this function will return `null` if it cannot parse the span and set the 1839 * {@see static::get_last_json_error()} to the appropriate JSON_ERROR_ constant. 1840 * 1841 * If the delimiter contains no JSON span, it will also return `null`, 1842 * but the last error will be set to {@see \JSON_ERROR_NONE}. 1843 * 1844 * Example: 1845 * 1846 * $processor = new WP_Block_Processor( '<!-- wp:image {"url": "https://wordpress.org/favicon.ico"} -->' ); 1847 * $processor->next_delimiter(); 1848 * $memory_hungry_and_slow_attributes = $processor->allocate_and_return_parsed_attributes(); 1849 * $memory_hungry_and_slow_attributes === array( 'url' => 'https://wordpress.org/favicon.ico' ); 1850 * 1851 * $processor = new WP_Block_Processor( '<!-- /wp:image {"url": "https://wordpress.org/favicon.ico"} -->' ); 1852 * $processor->next_delimiter(); 1853 * null = $processor->allocate_and_return_parsed_attributes(); 1854 * JSON_ERROR_NONE = $processor->get_last_json_error(); 1855 * 1856 * $processor = new WP_Block_Processor( '<!-- wp:separator {} /-->' ); 1857 * $processor->next_delimiter(); 1858 * array() === $processor->allocate_and_return_parsed_attributes(); 1859 * 1860 * $processor = new WP_Block_Processor( '<!-- wp:separator /-->' ); 1861 * $processor->next_delimiter(); 1862 * null = $processor->allocate_and_return_parsed_attributes(); 1863 * 1864 * $processor = new WP_Block_Processor( '<!-- wp:image {"url} -->' ); 1865 * $processor->next_delimiter(); 1866 * null = $processor->allocate_and_return_parsed_attributes(); 1867 * JSON_ERROR_CTRL_CHAR = $processor->get_last_json_error(); 1868 * 1869 * @since 6.9.0 1870 * 1871 * @return array|null Parsed JSON attributes, if present and valid, otherwise `null`. 1872 */ 1873 public function allocate_and_return_parsed_attributes(): ?array { 1874 $this->last_json_error = JSON_ERROR_NONE; 1875 1876 if ( self::CLOSER === $this->type || $this->is_html() || 0 === $this->json_length ) { 1877 return null; 1878 } 1879 1880 $json_span = substr( $this->source_text, $this->json_at, $this->json_length ); 1881 $parsed = json_decode( $json_span, null, 512, JSON_OBJECT_AS_ARRAY | JSON_INVALID_UTF8_SUBSTITUTE ); 1882 1883 $last_error = json_last_error(); 1884 $this->last_json_error = $last_error; 1885 1886 return ( JSON_ERROR_NONE === $last_error && is_array( $parsed ) ) 1887 ? $parsed 1888 : null; 1889 } 1890 1891 /** 1892 * Returns the span representing the currently-matched delimiter, if matched, otherwise `null`. 1893 * 1894 * Example: 1895 * 1896 * $processor = new WP_Block_Processor( '<!-- wp:void /-->' ); 1897 * null === $processor->get_span(); 1898 * 1899 * $processor->next_delimiter(); 1900 * WP_HTML_Span( 0, 17 ) === $processor->get_span(); 1901 * 1902 * @since 6.9.0 1903 * 1904 * @return WP_HTML_Span|null Span of text in source text spanning matched delimiter. 1905 */ 1906 public function get_span(): ?WP_HTML_Span { 1907 switch ( $this->state ) { 1908 case self::HTML_SPAN: 1909 return new WP_HTML_Span( $this->after_previous_delimiter, $this->matched_delimiter_at - $this->after_previous_delimiter ); 1910 1911 case self::MATCHED: 1912 return new WP_HTML_Span( $this->matched_delimiter_at, $this->matched_delimiter_length ); 1913 1914 default: 1915 return null; 1916 } 1917 } 1918 1919 // 1920 // Constant declarations that would otherwise pollute the top of the class. 1921 // 1922 1923 /** 1924 * Indicates that the block comment delimiter closes an open block. 1925 * 1926 * @see self::$type 1927 * 1928 * @since 6.9.0 1929 */ 1930 const CLOSER = 'closer'; 1931 1932 /** 1933 * Indicates that the block comment delimiter opens a block. 1934 * 1935 * @see self::$type 1936 * 1937 * @since 6.9.0 1938 */ 1939 const OPENER = 'opener'; 1940 1941 /** 1942 * Indicates that the block comment delimiter represents a void block 1943 * with no inner content of any kind. 1944 * 1945 * @see self::$type 1946 * 1947 * @since 6.9.0 1948 */ 1949 const VOID = 'void'; 1950 1951 /** 1952 * Indicates that the processor is ready to start parsing but hasn’t yet begun. 1953 * 1954 * @see self::$state 1955 * 1956 * @since 6.9.0 1957 */ 1958 const READY = 'processor-ready'; 1959 1960 /** 1961 * Indicates that the processor is matched on an explicit block delimiter. 1962 * 1963 * @see self::$state 1964 * 1965 * @since 6.9.0 1966 */ 1967 const MATCHED = 'processor-matched'; 1968 1969 /** 1970 * Indicates that the processor is matched on the opening of an implicit freeform delimiter. 1971 * 1972 * @see self::$state 1973 * 1974 * @since 6.9.0 1975 */ 1976 const HTML_SPAN = 'processor-html-span'; 1977 1978 /** 1979 * Indicates that the parser started parsing a block comment delimiter, but 1980 * the input document ended before it could finish. The document was likely truncated. 1981 * 1982 * @see self::$state 1983 * 1984 * @since 6.9.0 1985 */ 1986 const INCOMPLETE_INPUT = 'incomplete-input'; 1987 1988 /** 1989 * Indicates that the processor has finished parsing and has nothing left to scan. 1990 * 1991 * @see self::$state 1992 * 1993 * @since 6.9.0 1994 */ 1995 const COMPLETE = 'processor-complete'; 1996 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Wed Sep 23 08:20:35 2026 | Cross-referenced by PHPXref |