| [ 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 836 if ( $backup > 0 ) { 837 $this->last_error = self::INCOMPLETE_INPUT; 838 } 839 840 return true; 841 } 842 843 /* 844 * In the case that there is the start of an HTML comment, it means that there 845 * might be a block delimiter, but it’s not possible know, therefore it’s incomplete. 846 */ 847 if ( $backup > 0 ) { 848 goto incomplete; 849 } 850 851 // Otherwise this is the end. 852 $this->state = self::COMPLETE; 853 return false; 854 } 855 856 // <!-- ⃨/wp:core/paragraph {"dropCap":true} /--> 857 $opening_whitespace_at = $comment_opening_at + 4; 858 if ( $opening_whitespace_at >= $end ) { 859 goto incomplete; 860 } 861 862 $opening_whitespace_length = strspn( $text, " \t\f\r\n", $opening_whitespace_at ); 863 864 /* 865 * The `wp` prefix cannot come before this point, but it may come after it 866 * depending on the presence of the closer. This is detected next. 867 */ 868 $wp_prefix_at = $opening_whitespace_at + $opening_whitespace_length; 869 if ( $wp_prefix_at >= $end ) { 870 goto incomplete; 871 } 872 873 if ( 0 === $opening_whitespace_length ) { 874 $at = $this->find_html_comment_end( $comment_opening_at, $end ); 875 continue; 876 } 877 878 // <!-- /⃨wp:core/paragraph {"dropCap":true} /--> 879 $has_closer = false; 880 if ( '/' === $text[ $wp_prefix_at ] ) { 881 $has_closer = true; 882 ++$wp_prefix_at; 883 } 884 885 // <!-- /w⃨p⃨:⃨core/paragraph {"dropCap":true} /--> 886 if ( $wp_prefix_at < $end && 0 !== substr_compare( $text, 'wp:', $wp_prefix_at, 3 ) ) { 887 if ( 888 ( $wp_prefix_at + 2 >= $end && str_ends_with( $text, 'wp' ) ) || 889 ( $wp_prefix_at + 1 >= $end && str_ends_with( $text, 'w' ) ) 890 ) { 891 goto incomplete; 892 } 893 894 $at = $this->find_html_comment_end( $comment_opening_at, $end ); 895 continue; 896 } 897 898 /* 899 * If the block contains no namespace, this will end up masquerading with 900 * the block name. It’s easier to first detect the span and then determine 901 * if it’s a namespace of a name. 902 * 903 * <!-- /wp:c⃨o⃨r⃨e⃨/paragraph {"dropCap":true} /--> 904 */ 905 $namespace_at = $wp_prefix_at + 3; 906 if ( $namespace_at >= $end ) { 907 goto incomplete; 908 } 909 910 $start_of_namespace = $text[ $namespace_at ]; 911 912 // The namespace must start with a-z. 913 if ( 'a' > $start_of_namespace || 'z' < $start_of_namespace ) { 914 $at = $this->find_html_comment_end( $comment_opening_at, $end ); 915 continue; 916 } 917 918 $namespace_length = 1 + strspn( $text, 'abcdefghijklmnopqrstuvwxyz0123456789-_', $namespace_at + 1 ); 919 $separator_at = $namespace_at + $namespace_length; 920 if ( $separator_at >= $end ) { 921 goto incomplete; 922 } 923 924 // <!-- /wp:core/⃨paragraph {"dropCap":true} /--> 925 $has_separator = '/' === $text[ $separator_at ]; 926 if ( $has_separator ) { 927 $name_at = $separator_at + 1; 928 929 if ( $name_at >= $end ) { 930 goto incomplete; 931 } 932 933 // <!-- /wp:core/p⃨a⃨r⃨a⃨g⃨r⃨a⃨p⃨h⃨ {"dropCap":true} /--> 934 $start_of_name = $text[ $name_at ]; 935 if ( 'a' > $start_of_name || 'z' < $start_of_name ) { 936 $at = $this->find_html_comment_end( $comment_opening_at, $end ); 937 continue; 938 } 939 940 $name_length = 1 + strspn( $text, 'abcdefghijklmnopqrstuvwxyz0123456789-_', $name_at + 1 ); 941 } else { 942 $name_at = $namespace_at; 943 $name_length = $namespace_length; 944 } 945 946 if ( $name_at + $name_length >= $end ) { 947 goto incomplete; 948 } 949 950 /* 951 * For this next section of the delimiter, it could be the JSON attributes 952 * or it could be the end of the comment. Assume that the JSON is there and 953 * update if it’s not. 954 */ 955 956 // <!-- /wp:core/paragraph ⃨{"dropCap":true} /--> 957 $after_name_whitespace_at = $name_at + $name_length; 958 $after_name_whitespace_length = strspn( $text, " \t\f\r\n", $after_name_whitespace_at ); 959 $json_at = $after_name_whitespace_at + $after_name_whitespace_length; 960 961 if ( $json_at >= $end ) { 962 goto incomplete; 963 } 964 965 if ( 0 === $after_name_whitespace_length ) { 966 $at = $this->find_html_comment_end( $comment_opening_at, $end ); 967 continue; 968 } 969 970 // <!-- /wp:core/paragraph {⃨"dropCap":true} /--> 971 $has_json = '{' === $text[ $json_at ]; 972 $json_length = 0; 973 974 /* 975 * For the final span of the delimiter it's most efficient to find the end of the 976 * HTML comment and work backwards. This prevents complicated parsing inside the 977 * JSON span, which is not allowed to contain the HTML comment terminator. 978 * 979 * This also matches the behavior in the official block parser, 980 * even though it allows for matching invalid JSON content. 981 * 982 * The delimiter must also be a single complete HTML comment. 983 * 984 * <!-- /wp:core/paragraph {"dropCap":true} /-⃨-⃨>⃨ 985 */ 986 $after_comment_end = $this->find_html_comment_end( $comment_opening_at, $end ); 987 988 /* 989 * The reported end of the comment could be after the end of the document if 990 * no actual end was found, so differentiate a comment ending at the end of 991 * the document from documents with missing comment ends. 992 */ 993 if ( $after_comment_end >= $end && ! str_ends_with( $text, '-->' ) && ! str_ends_with( $text, '--!>' ) ) { 994 goto incomplete; 995 } 996 997 /* 998 * Only normative comment closers are recognized block delimiters, 999 * so skip past any HTML comments ending in `--!>`. 1000 */ 1001 if ( '!' === $text[ $after_comment_end - 2 ] ) { 1002 $at = $after_comment_end; 1003 continue; 1004 } 1005 1006 $comment_closing_at = $after_comment_end - 3; 1007 1008 // <!-- /wp:core/paragraph {"dropCap":true} /⃨--> 1009 if ( '/' === $text[ $comment_closing_at - 1 ] ) { 1010 $has_void_flag = true; 1011 $void_flag_length = 1; 1012 } else { 1013 $has_void_flag = false; 1014 $void_flag_length = 0; 1015 } 1016 1017 /* 1018 * If there's no JSON, then the span of text after the name 1019 * until the comment closing must be completely whitespace. 1020 * Otherwise it’s a normal HTML comment. 1021 */ 1022 if ( ! $has_json ) { 1023 if ( $after_name_whitespace_at + $after_name_whitespace_length === $comment_closing_at - $void_flag_length ) { 1024 // This must be a block delimiter! 1025 $this->state = self::MATCHED; 1026 break; 1027 } 1028 1029 $at = $after_comment_end; 1030 continue; 1031 } 1032 1033 /* 1034 * There's JSON, so attempt to find its boundary. 1035 * 1036 * @todo It’s likely faster to scan forward instead of in reverse. 1037 * @todo Skip ahead with `strcspn()` and decide only on syntax characters. 1038 * 1039 * <!-- /wp:core/paragraph {"dropCap":true}⃨ ⃨/--> 1040 */ 1041 $after_json_whitespace_length = 0; 1042 for ( $char_at = $comment_closing_at - $void_flag_length - 1; $char_at > $json_at; $char_at-- ) { 1043 $char = $text[ $char_at ]; 1044 1045 switch ( $char ) { 1046 case ' ': 1047 case "\t": 1048 case "\f": 1049 case "\r": 1050 case "\n": 1051 ++$after_json_whitespace_length; 1052 continue 2; 1053 1054 case '}': 1055 $json_length = $char_at - $json_at + 1; 1056 break 2; 1057 1058 default: 1059 $at = $after_comment_end; 1060 continue 3; 1061 } 1062 } 1063 1064 /* 1065 * This covers cases where there is no terminating “}” or where 1066 * mandatory whitespace is missing. 1067 */ 1068 if ( 0 === $json_length || 0 === $after_json_whitespace_length ) { 1069 $at = $after_comment_end; 1070 continue; 1071 } 1072 1073 // This must be a block delimiter! 1074 $this->state = self::MATCHED; 1075 break; 1076 } 1077 1078 // The end of the document was reached without a match. 1079 if ( self::MATCHED !== $this->state ) { 1080 // Stop at top-level free-form HTML at the end of the document. 1081 if ( $after_prev_delimiter < $end ) { 1082 $this->state = self::HTML_SPAN; 1083 $this->after_previous_delimiter = $after_prev_delimiter; 1084 $this->matched_delimiter_at = $end; 1085 $this->matched_delimiter_length = 0; 1086 $this->open_blocks_at[] = $after_prev_delimiter; 1087 $this->open_blocks_length[] = 0; 1088 $this->was_void = true; 1089 1090 return true; 1091 } 1092 1093 $this->state = self::COMPLETE; 1094 return false; 1095 } 1096 1097 /* 1098 * From this point forward, a delimiter has been matched. There 1099 * might also be an HTML span that appears before the delimiter. 1100 */ 1101 1102 $this->after_previous_delimiter = $after_prev_delimiter; 1103 1104 $this->matched_delimiter_at = $comment_opening_at; 1105 $this->matched_delimiter_length = $comment_closing_at + 3 - $comment_opening_at; 1106 1107 $this->namespace_at = $namespace_at; 1108 $this->name_at = $name_at; 1109 $this->name_length = $name_length; 1110 1111 $this->json_at = $json_at; 1112 $this->json_length = $json_length; 1113 1114 /* 1115 * When delimiters contain both the void flag and the closing flag 1116 * they shall be interpreted as void blocks, per the spec parser. 1117 */ 1118 if ( $has_void_flag ) { 1119 $this->type = self::VOID; 1120 $this->next_stack_op = 'void'; 1121 } elseif ( $has_closer ) { 1122 $this->type = self::CLOSER; 1123 $this->next_stack_op = 'pop'; 1124 1125 /* 1126 * @todo Check if the name matches and bail according to the spec parser. 1127 * The default parser doesn’t examine the names. 1128 */ 1129 } else { 1130 $this->type = self::OPENER; 1131 $this->next_stack_op = 'push'; 1132 } 1133 1134 $this->has_closing_flag = $has_closer; 1135 1136 // HTML spans are visited before the delimiter that follows them. 1137 if ( $comment_opening_at > $after_prev_delimiter ) { 1138 $this->state = self::HTML_SPAN; 1139 $this->open_blocks_at[] = $after_prev_delimiter; 1140 $this->open_blocks_length[] = 0; 1141 $this->was_void = true; 1142 1143 return true; 1144 } 1145 1146 // If there were no HTML spans then flush the enqueued stack operations immediately. 1147 switch ( $this->next_stack_op ) { 1148 case 'void': 1149 $this->was_void = true; 1150 $this->open_blocks_at[] = $namespace_at; 1151 $this->open_blocks_length[] = $name_at + $name_length - $namespace_at; 1152 break; 1153 1154 case 'push': 1155 $this->open_blocks_at[] = $namespace_at; 1156 $this->open_blocks_length[] = $name_at + $name_length - $namespace_at; 1157 break; 1158 1159 case 'pop': 1160 array_pop( $this->open_blocks_at ); 1161 array_pop( $this->open_blocks_length ); 1162 break; 1163 } 1164 1165 $this->next_stack_op = null; 1166 1167 return true; 1168 1169 incomplete: 1170 $this->state = self::COMPLETE; 1171 $this->last_error = self::INCOMPLETE_INPUT; 1172 return false; 1173 } 1174 1175 /** 1176 * Returns an array containing the names of the currently-open blocks, in order 1177 * from outermost to innermost, with HTML spans indicated as “#html”. 1178 * 1179 * Example: 1180 * 1181 * // Freeform HTML content is an HTML span. 1182 * $processor = new WP_Block_Processor( 'Just text' ); 1183 * $processor->next_token(); 1184 * array( '#text' ) === $processor->get_breadcrumbs(); 1185 * 1186 * $processor = new WP_Block_Processor( '<!-- wp:a --><!-- wp:b --><!-- wp:c /--><!-- /wp:b --><!-- /wp:a -->' ); 1187 * $processor->next_token(); 1188 * array( 'core/a' ) === $processor->get_breadcrumbs(); 1189 * $processor->next_token(); 1190 * array( 'core/a', 'core/b' ) === $processor->get_breadcrumbs(); 1191 * $processor->next_token(); 1192 * // Void blocks are only open while visiting them. 1193 * array( 'core/a', 'core/b', 'core/c' ) === $processor->get_breadcrumbs(); 1194 * $processor->next_token(); 1195 * // Blocks are closed before visiting their closing delimiter. 1196 * array( 'core/a' ) === $processor->get_breadcrumbs(); 1197 * $processor->next_token(); 1198 * array() === $processor->get_breadcrumbs(); 1199 * 1200 * // Inner HTML is also an HTML span. 1201 * $processor = new WP_Block_Processor( '<!-- wp:a -->Inner HTML<!-- /wp:a -->' ); 1202 * $processor->next_token(); 1203 * $processor->next_token(); 1204 * array( 'core/a', '#html' ) === $processor->get_breadcrumbs(); 1205 * 1206 * @since 6.9.0 1207 * 1208 * @return string[] 1209 */ 1210 public function get_breadcrumbs(): array { 1211 $breadcrumbs = array_fill( 0, count( $this->open_blocks_at ), null ); 1212 1213 /* 1214 * Since HTML spans can only be at the very end, set the normalized block name for 1215 * each open element and then work backwards after creating the array. This allows 1216 * for the elimination of a conditional on each iteration of the loop. 1217 */ 1218 foreach ( $this->open_blocks_at as $i => $at ) { 1219 $block_type = substr( $this->source_text, $at, $this->open_blocks_length[ $i ] ); 1220 $breadcrumbs[ $i ] = self::normalize_block_type( $block_type ); 1221 } 1222 1223 if ( isset( $i ) && 0 === $this->open_blocks_length[ $i ] ) { 1224 $breadcrumbs[ $i ] = '#html'; 1225 } 1226 1227 return $breadcrumbs; 1228 } 1229 1230 /** 1231 * Returns the depth of the open blocks where the processor is currently matched. 1232 * 1233 * Depth increases before visiting openers and void blocks and decreases before 1234 * visiting closers. HTML spans behave like void blocks. 1235 * 1236 * @since 6.9.0 1237 * 1238 * @return int 1239 */ 1240 public function get_depth(): int { 1241 return count( $this->open_blocks_at ); 1242 } 1243 1244 /** 1245 * Extracts a block object, and all inner content, starting at a matched opening 1246 * block delimiter, or at a matched top-level HTML span as freeform HTML content. 1247 * 1248 * Use this function to extract some blocks within a document, but not all. For example, 1249 * one might want to find image galleries, parse them, modify them, and then reserialize 1250 * them in place. 1251 * 1252 * Once this function returns, the parser will be matched on token following the close 1253 * of the given block. 1254 * 1255 * The return type of this method is compatible with the return of {@see \parse_blocks()}. 1256 * 1257 * Example: 1258 * 1259 * $processor = new WP_Block_Processor( $post_content ); 1260 * if ( ! $processor->next_block( 'gallery' ) ) { 1261 * return $post_content; 1262 * } 1263 * 1264 * $gallery_at = $processor->get_span()->start; 1265 * $gallery = $processor->extract_full_block_and_advance(); 1266 * $ends_before = $processor->get_span(); 1267 * $ends_before = $ends_before->start ?? strlen( $post_content ); 1268 * 1269 * $new_gallery = update_gallery( $gallery ); 1270 * $new_gallery = serialize_block( $new_gallery ); 1271 * 1272 * return ( 1273 * substr( $post_content, 0, $gallery_at ) . 1274 * $new_gallery . 1275 * substr( $post_content, $ends_before ) 1276 * ); 1277 * 1278 * @since 6.9.0 1279 * 1280 * @return array|null { 1281 * An associative array of a single parsed block object. See WP_Block_Parser_Block. 1282 * 1283 * @type string|null $blockName Name of block. 1284 * @type array $attrs Attributes from block comment delimiters. 1285 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1286 * have the same structure as this one. 1287 * @type string $innerHTML HTML from inside block comment delimiters. 1288 * @type array $innerContent List of string fragments and null markers where 1289 * inner blocks were found. 1290 * } 1291 */ 1292 public function extract_full_block_and_advance(): ?array { 1293 if ( $this->is_html() ) { 1294 $chunk = $this->get_html_content(); 1295 1296 return array( 1297 'blockName' => null, 1298 'attrs' => array(), 1299 'innerBlocks' => array(), 1300 'innerHTML' => $chunk, 1301 'innerContent' => array( $chunk ), 1302 ); 1303 } 1304 1305 $block = array( 1306 'blockName' => $this->get_block_type(), 1307 'attrs' => $this->allocate_and_return_parsed_attributes() ?? array(), 1308 'innerBlocks' => array(), 1309 'innerHTML' => '', 1310 'innerContent' => array(), 1311 ); 1312 1313 if ( self::VOID === $this->get_delimiter_type() ) { 1314 return $block; 1315 } 1316 1317 $depth = $this->get_depth(); 1318 while ( $this->next_token() && $this->get_depth() > $depth ) { 1319 if ( $this->is_html() ) { 1320 $chunk = $this->get_html_content(); 1321 $block['innerHTML'] .= $chunk; 1322 $block['innerContent'][] = $chunk; 1323 continue; 1324 } 1325 1326 /** 1327 * Inner blocks. 1328 * 1329 * @todo This is a decent place to call {@link \render_block()} 1330 * @todo Use iteration instead of recursion, or at least refactor to tail-call form. 1331 */ 1332 if ( $this->opens_block() ) { 1333 $inner_block = $this->extract_full_block_and_advance(); 1334 $block['innerBlocks'][] = $inner_block; 1335 $block['innerContent'][] = null; 1336 } 1337 } 1338 1339 return $block; 1340 } 1341 1342 /** 1343 * Returns the byte-offset after the ending character of an HTML comment, 1344 * assuming the proper starting byte offset. 1345 * 1346 * @since 6.9.0 1347 * 1348 * @param int $comment_starting_at Where the HTML comment started, the leading `<`. 1349 * @param int $search_end Last offset in which to search, for limiting search span. 1350 * @return int Offset after the current HTML comment ends, or `$search_end` if no end was found. 1351 */ 1352 private function find_html_comment_end( int $comment_starting_at, int $search_end ): int { 1353 $text = $this->source_text; 1354 1355 // Find span-of-dashes comments which look like `<!----->`. 1356 $span_of_dashes = strspn( $text, '-', $comment_starting_at + 2 ); 1357 if ( 1358 $comment_starting_at + 2 + $span_of_dashes < $search_end && 1359 '>' === $text[ $comment_starting_at + 2 + $span_of_dashes ] 1360 ) { 1361 return $comment_starting_at + 2 + $span_of_dashes + 1; 1362 } 1363 1364 // Otherwise, there are other characters inside the comment, find the first `-->` or `--!>`. 1365 $now_at = $comment_starting_at + 4; 1366 while ( $now_at < $search_end ) { 1367 $dashes_at = strpos( $text, '--', $now_at ); 1368 if ( false === $dashes_at ) { 1369 return $search_end; 1370 } 1371 1372 $closer_must_be_at = $dashes_at + 2 + strspn( $text, '-', $dashes_at + 2 ); 1373 if ( $closer_must_be_at < $search_end && '!' === $text[ $closer_must_be_at ] ) { 1374 ++$closer_must_be_at; 1375 } 1376 1377 if ( $closer_must_be_at < $search_end && '>' === $text[ $closer_must_be_at ] ) { 1378 return $closer_must_be_at + 1; 1379 } 1380 1381 $now_at = $closer_must_be_at; 1382 } 1383 1384 return $search_end; 1385 } 1386 1387 /** 1388 * Indicates if the last attempt to parse a block comment delimiter 1389 * failed, if set, otherwise `null` if the last attempt succeeded. 1390 * 1391 * @since 6.9.0 1392 * 1393 * @return string|null Error from last attempt at parsing next block delimiter, 1394 * or `null` if last attempt succeeded. 1395 */ 1396 public function get_last_error(): ?string { 1397 return $this->last_error; 1398 } 1399 1400 /** 1401 * Indicates if the last attempt to parse a block’s JSON attributes failed. 1402 * 1403 * @see \json_last_error() 1404 * 1405 * @since 6.9.0 1406 * 1407 * @return int JSON_ERROR_ code from last attempt to parse block JSON attributes. 1408 */ 1409 public function get_last_json_error(): int { 1410 return $this->last_json_error; 1411 } 1412 1413 /** 1414 * Returns the type of the block comment delimiter. 1415 * 1416 * One of: 1417 * 1418 * - {@see self::OPENER} 1419 * - {@see self::CLOSER} 1420 * - {@see self::VOID} 1421 * - `null` 1422 * 1423 * @since 6.9.0 1424 * 1425 * @return string|null type of the block comment delimiter, if currently matched. 1426 */ 1427 public function get_delimiter_type(): ?string { 1428 switch ( $this->state ) { 1429 case self::HTML_SPAN: 1430 return self::VOID; 1431 1432 case self::MATCHED: 1433 return $this->type; 1434 1435 default: 1436 return null; 1437 } 1438 } 1439 1440 /** 1441 * Returns whether the delimiter contains the closing flag. 1442 * 1443 * This should be avoided except in cases of custom error-handling 1444 * with block closers containing the void flag. For normative use, 1445 * {@see self::get_delimiter_type()}. 1446 * 1447 * @since 6.9.0 1448 * 1449 * @return bool Whether the currently-matched block delimiter contains the closing flag. 1450 */ 1451 public function has_closing_flag(): bool { 1452 return $this->has_closing_flag; 1453 } 1454 1455 /** 1456 * Indicates if the block delimiter represents a block of the given type. 1457 * 1458 * Since the “core” namespace may be implicit, it’s allowable to pass 1459 * either the fully-qualified block type with namespace and block name 1460 * as well as the shorthand version only containing the block name, if 1461 * the desired block is in the “core” namespace. 1462 * 1463 * Since freeform HTML content is non-block content, it has no block type. 1464 * Passing the wildcard “*” will, however, return true for all block types, 1465 * even the implicit freeform content, though not for spans of inner HTML. 1466 * 1467 * Example: 1468 * 1469 * $is_core_paragraph = $processor->is_block_type( 'paragraph' ); 1470 * $is_core_paragraph = $processor->is_block_type( 'core/paragraph' ); 1471 * $is_formula = $processor->is_block_type( 'math-block/formula' ); 1472 * 1473 * @since 6.9.0 1474 * 1475 * @param string $block_type Block type name for the desired block. 1476 * E.g. "paragraph", "core/paragraph", "math-blocks/formula". 1477 * @return bool Whether this delimiter represents a block of the given type. 1478 */ 1479 public function is_block_type( string $block_type ): bool { 1480 if ( '*' === $block_type ) { 1481 return true; 1482 } 1483 1484 if ( $this->is_html() ) { 1485 // This is a core/freeform text block, it’s special. 1486 if ( 0 === ( $this->open_blocks_length[0] ?? null ) ) { 1487 return ( 1488 'core/freeform' === $block_type || 1489 'freeform' === $block_type 1490 ); 1491 } 1492 1493 // Otherwise this is innerHTML and not a block. 1494 return false; 1495 } 1496 1497 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 ) ); 1498 } 1499 1500 /** 1501 * Given two spans of text, indicate if they represent identical block types. 1502 * 1503 * This function normalizes block types to account for implicit core namespacing. 1504 * 1505 * Note! This function only returns valid results when the complete block types are 1506 * represented in the span offsets and lengths. This means that the full optional 1507 * namespace and block name must be represented in the input arguments. 1508 * 1509 * Example: 1510 * 1511 * 0 5 10 15 20 25 30 35 40 1512 * $text = '<!-- wp:block --><!-- /wp:core/block -->'; 1513 * 1514 * true === WP_Block_Processor::are_equal_block_types( $text, 9, 5, $text, 27, 10 ); 1515 * false === WP_Block_Processor::are_equal_block_types( $text, 9, 5, 'my/block', 0, 8 ); 1516 * 1517 * @since 6.9.0 1518 * 1519 * @param string $a_text Text in which first block type appears. 1520 * @param int $a_at Byte offset into text in which first block type starts. 1521 * @param int $a_length Byte length of first block type. 1522 * @param string $b_text Text in which second block type appears (may be the same as the first text). 1523 * @param int $b_at Byte offset into text in which second block type starts. 1524 * @param int $b_length Byte length of second block type. 1525 * @return bool Whether the spans of text represent identical block types, normalized for namespacing. 1526 */ 1527 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 { 1528 $a_ns_length = strcspn( $a_text, '/', $a_at, $a_length ); 1529 $b_ns_length = strcspn( $b_text, '/', $b_at, $b_length ); 1530 1531 $a_has_ns = $a_ns_length !== $a_length; 1532 $b_has_ns = $b_ns_length !== $b_length; 1533 1534 // Both contain namespaces. 1535 if ( $a_has_ns && $b_has_ns ) { 1536 if ( $a_length !== $b_length ) { 1537 return false; 1538 } 1539 1540 $a_block_type = substr( $a_text, $a_at, $a_length ); 1541 1542 return 0 === substr_compare( $b_text, $a_block_type, $b_at, $b_length ); 1543 } 1544 1545 if ( $a_has_ns ) { 1546 $b_block_type = 'core/' . substr( $b_text, $b_at, $b_length ); 1547 1548 return ( 1549 strlen( $b_block_type ) === $a_length && 1550 0 === substr_compare( $a_text, $b_block_type, $a_at, $a_length ) 1551 ); 1552 } 1553 1554 if ( $b_has_ns ) { 1555 $a_block_type = 'core/' . substr( $a_text, $a_at, $a_length ); 1556 1557 return ( 1558 strlen( $a_block_type ) === $b_length && 1559 0 === substr_compare( $b_text, $a_block_type, $b_at, $b_length ) 1560 ); 1561 } 1562 1563 // Neither contains a namespace. 1564 if ( $a_length !== $b_length ) { 1565 return false; 1566 } 1567 1568 $a_name = substr( $a_text, $a_at, $a_length ); 1569 1570 return 0 === substr_compare( $b_text, $a_name, $b_at, $b_length ); 1571 } 1572 1573 /** 1574 * Indicates if the matched delimiter is an opening or void delimiter of the given type, 1575 * if a type is provided, otherwise if it opens any block or implicit freeform HTML content. 1576 * 1577 * This is a helper method to ease handling of code inspecting where blocks start, and for 1578 * checking if the blocks are of a given type. The function is variadic to allow for 1579 * checking if the delimiter opens one of many possible block types. 1580 * 1581 * To advance to the start of a block {@see self::next_block()}. 1582 * 1583 * Example: 1584 * 1585 * $processor = new WP_Block_Processor( $html ); 1586 * while ( $processor->next_delimiter() ) { 1587 * if ( $processor->opens_block( 'core/code', 'syntaxhighlighter/code' ) ) { 1588 * echo "Found code!"; 1589 * continue; 1590 * } 1591 * 1592 * if ( $processor->opens_block( 'core/image' ) ) { 1593 * echo "Found an image!"; 1594 * continue; 1595 * } 1596 * 1597 * if ( $processor->opens_block() ) { 1598 * echo "Found a new block!"; 1599 * } 1600 * } 1601 * 1602 * @since 6.9.0 1603 * 1604 * @see self::is_block_type() 1605 * 1606 * @param string ...$block_type Optional. Is the matched block type one of these? 1607 * If none are provided, will not test block type. 1608 * @return bool Whether the matched block delimiter opens a block, and whether it 1609 * opens a block of one of the given block types, if provided. 1610 */ 1611 public function opens_block( string ...$block_type ): bool { 1612 // HTML spans only open implicit freeform content at the top level. 1613 if ( self::HTML_SPAN === $this->state && 1 !== count( $this->open_blocks_at ) ) { 1614 return false; 1615 } 1616 1617 /* 1618 * Because HTML spans are discovered after the next delimiter is found, 1619 * the delimiter type when visiting HTML spans refers to the type of the 1620 * following delimiter. Therefore the HTML case is handled by checking 1621 * the state and depth of the stack of open block. 1622 */ 1623 if ( self::CLOSER === $this->type && ! $this->is_html() ) { 1624 return false; 1625 } 1626 1627 if ( count( $block_type ) === 0 ) { 1628 return true; 1629 } 1630 1631 return array_any( $block_type, fn( $block ) => $this->is_block_type( $block ) ); 1632 } 1633 1634 /** 1635 * Indicates if the matched delimiter is an HTML span. 1636 * 1637 * @since 6.9.0 1638 * 1639 * @see self::is_non_whitespace_html() 1640 * 1641 * @return bool Whether the processor is matched on an HTML span. 1642 * 1643 * @phpstan-impure 1644 */ 1645 public function is_html(): bool { 1646 return self::HTML_SPAN === $this->state; 1647 } 1648 1649 /** 1650 * Indicates if the matched delimiter is an HTML span and comprises more 1651 * than whitespace characters, i.e. contains real content. 1652 * 1653 * Many block serializers introduce newlines between block delimiters, 1654 * so the presence of top-level non-block content does not imply that 1655 * there are “real” freeform HTML blocks. Checking if there is content 1656 * beyond whitespace is a more certain check, such as for determining 1657 * whether to load CSS for the freeform or fallback block type. 1658 * 1659 * @since 6.9.0 1660 * 1661 * @see self::is_html() 1662 * 1663 * @return bool Whether the currently-matched delimiter is an HTML 1664 * span containing non-whitespace text. 1665 */ 1666 public function is_non_whitespace_html(): bool { 1667 if ( ! $this->is_html() ) { 1668 return false; 1669 } 1670 1671 $length = $this->matched_delimiter_at - $this->after_previous_delimiter; 1672 1673 $whitespace_length = strspn( 1674 $this->source_text, 1675 " \t\f\r\n", 1676 $this->after_previous_delimiter, 1677 $length 1678 ); 1679 1680 return $whitespace_length !== $length; 1681 } 1682 1683 /** 1684 * Returns the string content of a matched HTML span, or `null` otherwise. 1685 * 1686 * @since 6.9.0 1687 * 1688 * @return string|null Raw HTML content, or `null` if not currently matched on HTML. 1689 */ 1690 public function get_html_content(): ?string { 1691 if ( ! $this->is_html() ) { 1692 return null; 1693 } 1694 1695 return substr( 1696 $this->source_text, 1697 $this->after_previous_delimiter, 1698 $this->matched_delimiter_at - $this->after_previous_delimiter 1699 ); 1700 } 1701 1702 /** 1703 * Allocates a substring for the block type and returns the fully-qualified 1704 * name, including the namespace, if matched on a delimiter, otherwise `null`. 1705 * 1706 * This function is like {@see self::get_printable_block_type()} but when 1707 * paused on a freeform HTML block, will return `null` instead of “core/freeform”. 1708 * The `null` behavior matches what {@see \parse_blocks()} returns but may not 1709 * be as useful as having a string value. 1710 * 1711 * This function allocates a substring for the given block type. This 1712 * allocation will be small and likely fine in most cases, but it's 1713 * preferable to call {@see self::is_block_type()} if only needing 1714 * to know whether the delimiter is for a given block type, as that 1715 * function is more efficient for this purpose and avoids the allocation. 1716 * 1717 * Example: 1718 * 1719 * // Avoid. 1720 * 'core/paragraph' = $processor->get_block_type(); 1721 * 1722 * // Prefer. 1723 * $processor->is_block_type( 'core/paragraph' ); 1724 * $processor->is_block_type( 'paragraph' ); 1725 * $processor->is_block_type( 'core/freeform' ); 1726 * 1727 * // Freeform HTML content has no block type. 1728 * $processor = new WP_Block_Processor( 'non-block content' ); 1729 * $processor->next_token(); 1730 * null === $processor->get_block_type(); 1731 * 1732 * @since 6.9.0 1733 * 1734 * @see self::are_equal_block_types() 1735 * 1736 * @return string|null Fully-qualified block namespace and type, e.g. "core/paragraph", 1737 * if matched on an explicit delimiter, otherwise `null`. 1738 */ 1739 public function get_block_type(): ?string { 1740 if ( 1741 self::READY === $this->state || 1742 self::COMPLETE === $this->state || 1743 self::INCOMPLETE_INPUT === $this->state 1744 ) { 1745 return null; 1746 } 1747 1748 // This is a core/freeform text block, it’s special. 1749 if ( $this->is_html() ) { 1750 return null; 1751 } 1752 1753 $block_type = substr( $this->source_text, $this->namespace_at, $this->name_at - $this->namespace_at + $this->name_length ); 1754 return self::normalize_block_type( $block_type ); 1755 } 1756 1757 /** 1758 * Allocates a printable substring for the block type and returns the fully-qualified 1759 * name, including the namespace, if matched on a delimiter or freeform block, otherwise `null`. 1760 * 1761 * This function is like {@see self::get_block_type()} but when paused on a freeform 1762 * HTML block, will return “core/freeform” instead of `null`. The `null` behavior matches 1763 * what {@see \parse_blocks()} returns but may not be as useful as having a string value. 1764 * 1765 * This function allocates a substring for the given block type. This 1766 * allocation will be small and likely fine in most cases, but it's 1767 * preferable to call {@see self::is_block_type()} if only needing 1768 * to know whether the delimiter is for a given block type, as that 1769 * function is more efficient for this purpose and avoids the allocation. 1770 * 1771 * Example: 1772 * 1773 * // Avoid. 1774 * 'core/paragraph' = $processor->get_printable_block_type(); 1775 * 1776 * // Prefer. 1777 * $processor->is_block_type( 'core/paragraph' ); 1778 * $processor->is_block_type( 'paragraph' ); 1779 * $processor->is_block_type( 'core/freeform' ); 1780 * 1781 * // Freeform HTML content is given an implicit type. 1782 * $processor = new WP_Block_Processor( 'non-block content' ); 1783 * $processor->next_token(); 1784 * 'core/freeform' === $processor->get_printable_block_type(); 1785 * 1786 * @since 6.9.0 1787 * 1788 * @see self::are_equal_block_types() 1789 * 1790 * @return string|null Fully-qualified block namespace and type, e.g. "core/paragraph", 1791 * if matched on an explicit delimiter or freeform block, otherwise `null`. 1792 */ 1793 public function get_printable_block_type(): ?string { 1794 if ( 1795 self::READY === $this->state || 1796 self::COMPLETE === $this->state || 1797 self::INCOMPLETE_INPUT === $this->state 1798 ) { 1799 return null; 1800 } 1801 1802 // This is a core/freeform text block, it’s special. 1803 if ( $this->is_html() ) { 1804 return 1 === count( $this->open_blocks_at ) 1805 ? 'core/freeform' 1806 : '#innerHTML'; 1807 } 1808 1809 $block_type = substr( $this->source_text, $this->namespace_at, $this->name_at - $this->namespace_at + $this->name_length ); 1810 return self::normalize_block_type( $block_type ); 1811 } 1812 1813 /** 1814 * Normalizes a block name to ensure that missing implicit “core” namespaces are present. 1815 * 1816 * Example: 1817 * 1818 * 'core/paragraph' === WP_Block_Processor::normalize_block_byte( 'paragraph' ); 1819 * 'core/paragraph' === WP_Block_Processor::normalize_block_byte( 'core/paragraph' ); 1820 * 'my/paragraph' === WP_Block_Processor::normalize_block_byte( 'my/paragraph' ); 1821 * 1822 * @since 6.9.0 1823 * 1824 * @param string $block_type Valid block name, potentially without a namespace. 1825 * @return string Fully-qualified block type including namespace. 1826 */ 1827 public static function normalize_block_type( string $block_type ): string { 1828 return str_contains( $block_type, '/' ) 1829 ? $block_type 1830 : "core/{$block_type}"; 1831 } 1832 1833 /** 1834 * Returns a lazy wrapper around the block attributes, which can be used 1835 * for efficiently interacting with the JSON attributes. 1836 * 1837 * This stub hints that there should be a lazy interface for parsing 1838 * block attributes but doesn’t define it. It serves both as a placeholder 1839 * for one to come as well as a guard against implementing an eager 1840 * function in its place. 1841 * 1842 * @throws Exception This function is a stub for subclasses to implement 1843 * when providing streaming attribute parsing. 1844 * 1845 * @since 6.9.0 1846 * 1847 * @see self::allocate_and_return_parsed_attributes() 1848 * 1849 * @return never 1850 */ 1851 public function get_attributes() { 1852 throw new Exception( 'Lazy attribute parsing not yet supported' ); 1853 } 1854 1855 /** 1856 * Attempts to parse and return the entire JSON attributes from the delimiter, 1857 * allocating memory and processing the JSON span in the process. 1858 * 1859 * This does not return any parsed attributes for a closing block delimiter 1860 * even if there is a span of JSON content; this JSON is a parsing error. 1861 * 1862 * Consider calling {@see static::get_attributes()} instead if it's not 1863 * necessary to read all the attributes at the same time, as that provides 1864 * a more efficient mechanism for typical use cases. 1865 * 1866 * Since the JSON span inside the comment delimiter may not be valid JSON, 1867 * this function will return `null` if it cannot parse the span and set the 1868 * {@see static::get_last_json_error()} to the appropriate JSON_ERROR_ constant. 1869 * 1870 * If the delimiter contains no JSON span, it will also return `null`, 1871 * but the last error will be set to {@see \JSON_ERROR_NONE}. 1872 * 1873 * Example: 1874 * 1875 * $processor = new WP_Block_Processor( '<!-- wp:image {"url": "https://wordpress.org/favicon.ico"} -->' ); 1876 * $processor->next_delimiter(); 1877 * $memory_hungry_and_slow_attributes = $processor->allocate_and_return_parsed_attributes(); 1878 * $memory_hungry_and_slow_attributes === array( 'url' => 'https://wordpress.org/favicon.ico' ); 1879 * 1880 * $processor = new WP_Block_Processor( '<!-- /wp:image {"url": "https://wordpress.org/favicon.ico"} -->' ); 1881 * $processor->next_delimiter(); 1882 * null = $processor->allocate_and_return_parsed_attributes(); 1883 * JSON_ERROR_NONE = $processor->get_last_json_error(); 1884 * 1885 * $processor = new WP_Block_Processor( '<!-- wp:separator {} /-->' ); 1886 * $processor->next_delimiter(); 1887 * array() === $processor->allocate_and_return_parsed_attributes(); 1888 * 1889 * $processor = new WP_Block_Processor( '<!-- wp:separator /-->' ); 1890 * $processor->next_delimiter(); 1891 * null = $processor->allocate_and_return_parsed_attributes(); 1892 * 1893 * $processor = new WP_Block_Processor( '<!-- wp:image {"url} -->' ); 1894 * $processor->next_delimiter(); 1895 * null = $processor->allocate_and_return_parsed_attributes(); 1896 * JSON_ERROR_CTRL_CHAR = $processor->get_last_json_error(); 1897 * 1898 * @since 6.9.0 1899 * 1900 * @return array|null Parsed JSON attributes, if present and valid, otherwise `null`. 1901 */ 1902 public function allocate_and_return_parsed_attributes(): ?array { 1903 $this->last_json_error = JSON_ERROR_NONE; 1904 1905 if ( self::CLOSER === $this->type || $this->is_html() || 0 === $this->json_length ) { 1906 return null; 1907 } 1908 1909 $json_span = substr( $this->source_text, $this->json_at, $this->json_length ); 1910 $parsed = json_decode( $json_span, null, 512, JSON_OBJECT_AS_ARRAY | JSON_INVALID_UTF8_SUBSTITUTE ); 1911 1912 $last_error = json_last_error(); 1913 $this->last_json_error = $last_error; 1914 1915 return ( JSON_ERROR_NONE === $last_error && is_array( $parsed ) ) 1916 ? $parsed 1917 : null; 1918 } 1919 1920 /** 1921 * Returns the span representing the currently-matched delimiter, if matched, otherwise `null`. 1922 * 1923 * Example: 1924 * 1925 * $processor = new WP_Block_Processor( '<!-- wp:void /-->' ); 1926 * null === $processor->get_span(); 1927 * 1928 * $processor->next_delimiter(); 1929 * WP_HTML_Span( 0, 17 ) === $processor->get_span(); 1930 * 1931 * @since 6.9.0 1932 * 1933 * @return WP_HTML_Span|null Span of text in source text spanning matched delimiter. 1934 */ 1935 public function get_span(): ?WP_HTML_Span { 1936 switch ( $this->state ) { 1937 case self::HTML_SPAN: 1938 return new WP_HTML_Span( $this->after_previous_delimiter, $this->matched_delimiter_at - $this->after_previous_delimiter ); 1939 1940 case self::MATCHED: 1941 return new WP_HTML_Span( $this->matched_delimiter_at, $this->matched_delimiter_length ); 1942 1943 default: 1944 return null; 1945 } 1946 } 1947 1948 // 1949 // Constant declarations that would otherwise pollute the top of the class. 1950 // 1951 1952 /** 1953 * Indicates that the block comment delimiter closes an open block. 1954 * 1955 * @see self::$type 1956 * 1957 * @since 6.9.0 1958 */ 1959 const CLOSER = 'closer'; 1960 1961 /** 1962 * Indicates that the block comment delimiter opens a block. 1963 * 1964 * @see self::$type 1965 * 1966 * @since 6.9.0 1967 */ 1968 const OPENER = 'opener'; 1969 1970 /** 1971 * Indicates that the block comment delimiter represents a void block 1972 * with no inner content of any kind. 1973 * 1974 * @see self::$type 1975 * 1976 * @since 6.9.0 1977 */ 1978 const VOID = 'void'; 1979 1980 /** 1981 * Indicates that the processor is ready to start parsing but hasn’t yet begun. 1982 * 1983 * @see self::$state 1984 * 1985 * @since 6.9.0 1986 */ 1987 const READY = 'processor-ready'; 1988 1989 /** 1990 * Indicates that the processor is matched on an explicit block delimiter. 1991 * 1992 * @see self::$state 1993 * 1994 * @since 6.9.0 1995 */ 1996 const MATCHED = 'processor-matched'; 1997 1998 /** 1999 * Indicates that the processor is matched on the opening of an implicit freeform delimiter. 2000 * 2001 * @see self::$state 2002 * 2003 * @since 6.9.0 2004 */ 2005 const HTML_SPAN = 'processor-html-span'; 2006 2007 /** 2008 * Indicates that the parser started parsing a block comment delimiter, but 2009 * the input document ended before it could finish. The document was likely truncated. 2010 * 2011 * @see self::$state 2012 * 2013 * @since 6.9.0 2014 */ 2015 const INCOMPLETE_INPUT = 'incomplete-input'; 2016 2017 /** 2018 * Indicates that the processor has finished parsing and has nothing left to scan. 2019 * 2020 * @see self::$state 2021 * 2022 * @since 6.9.0 2023 */ 2024 const COMPLETE = 'processor-complete'; 2025 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Tue Nov 3 08:20:36 2026 | Cross-referenced by PHPXref |