| [ 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 * Array of block structures. 1282 * 1283 * @type array ...$0 { 1284 * An associative array of a single parsed block object. See WP_Block_Parser_Block. 1285 * 1286 * @type string|null $blockName Name of block. 1287 * @type array $attrs Attributes from block comment delimiters. 1288 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1289 * have the same structure as this one. 1290 * @type string $innerHTML HTML from inside block comment delimiters. 1291 * @type array $innerContent List of string fragments and null markers where 1292 * inner blocks were found. 1293 * } 1294 * } 1295 */ 1296 public function extract_full_block_and_advance(): ?array { 1297 if ( $this->is_html() ) { 1298 $chunk = $this->get_html_content(); 1299 1300 return array( 1301 'blockName' => null, 1302 'attrs' => array(), 1303 'innerBlocks' => array(), 1304 'innerHTML' => $chunk, 1305 'innerContent' => array( $chunk ), 1306 ); 1307 } 1308 1309 $block = array( 1310 'blockName' => $this->get_block_type(), 1311 'attrs' => $this->allocate_and_return_parsed_attributes() ?? array(), 1312 'innerBlocks' => array(), 1313 'innerHTML' => '', 1314 'innerContent' => array(), 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 * Because the parser has advanced past the closing block token, it 1340 * may be matched on an HTML span. This needs to be processed before 1341 * moving on to the next token at the start of the next loop iteration. 1342 */ 1343 if ( $this->is_html() ) { 1344 $chunk = $this->get_html_content(); 1345 $block['innerHTML'] .= $chunk; 1346 $block['innerContent'][] = $chunk; 1347 } 1348 } 1349 1350 return $block; 1351 } 1352 1353 /** 1354 * Returns the byte-offset after the ending character of an HTML comment, 1355 * assuming the proper starting byte offset. 1356 * 1357 * @since 6.9.0 1358 * 1359 * @param int $comment_starting_at Where the HTML comment started, the leading `<`. 1360 * @param int $search_end Last offset in which to search, for limiting search span. 1361 * @return int Offset after the current HTML comment ends, or `$search_end` if no end was found. 1362 */ 1363 private function find_html_comment_end( int $comment_starting_at, int $search_end ): int { 1364 $text = $this->source_text; 1365 1366 // Find span-of-dashes comments which look like `<!----->`. 1367 $span_of_dashes = strspn( $text, '-', $comment_starting_at + 2 ); 1368 if ( 1369 $comment_starting_at + 2 + $span_of_dashes < $search_end && 1370 '>' === $text[ $comment_starting_at + 2 + $span_of_dashes ] 1371 ) { 1372 return $comment_starting_at + 2 + $span_of_dashes + 1; 1373 } 1374 1375 // Otherwise, there are other characters inside the comment, find the first `-->` or `--!>`. 1376 $now_at = $comment_starting_at + 4; 1377 while ( $now_at < $search_end ) { 1378 $dashes_at = strpos( $text, '--', $now_at ); 1379 if ( false === $dashes_at ) { 1380 return $search_end; 1381 } 1382 1383 $closer_must_be_at = $dashes_at + 2 + strspn( $text, '-', $dashes_at + 2 ); 1384 if ( $closer_must_be_at < $search_end && '!' === $text[ $closer_must_be_at ] ) { 1385 ++$closer_must_be_at; 1386 } 1387 1388 if ( $closer_must_be_at < $search_end && '>' === $text[ $closer_must_be_at ] ) { 1389 return $closer_must_be_at + 1; 1390 } 1391 1392 $now_at = $closer_must_be_at; 1393 } 1394 1395 return $search_end; 1396 } 1397 1398 /** 1399 * Indicates if the last attempt to parse a block comment delimiter 1400 * failed, if set, otherwise `null` if the last attempt succeeded. 1401 * 1402 * @since 6.9.0 1403 * 1404 * @return string|null Error from last attempt at parsing next block delimiter, 1405 * or `null` if last attempt succeeded. 1406 */ 1407 public function get_last_error(): ?string { 1408 return $this->last_error; 1409 } 1410 1411 /** 1412 * Indicates if the last attempt to parse a block’s JSON attributes failed. 1413 * 1414 * @see \json_last_error() 1415 * 1416 * @since 6.9.0 1417 * 1418 * @return int JSON_ERROR_ code from last attempt to parse block JSON attributes. 1419 */ 1420 public function get_last_json_error(): int { 1421 return $this->last_json_error; 1422 } 1423 1424 /** 1425 * Returns the type of the block comment delimiter. 1426 * 1427 * One of: 1428 * 1429 * - {@see self::OPENER} 1430 * - {@see self::CLOSER} 1431 * - {@see self::VOID} 1432 * - `null` 1433 * 1434 * @since 6.9.0 1435 * 1436 * @return string|null type of the block comment delimiter, if currently matched. 1437 */ 1438 public function get_delimiter_type(): ?string { 1439 switch ( $this->state ) { 1440 case self::HTML_SPAN: 1441 return self::VOID; 1442 1443 case self::MATCHED: 1444 return $this->type; 1445 1446 default: 1447 return null; 1448 } 1449 } 1450 1451 /** 1452 * Returns whether the delimiter contains the closing flag. 1453 * 1454 * This should be avoided except in cases of custom error-handling 1455 * with block closers containing the void flag. For normative use, 1456 * {@see self::get_delimiter_type()}. 1457 * 1458 * @since 6.9.0 1459 * 1460 * @return bool Whether the currently-matched block delimiter contains the closing flag. 1461 */ 1462 public function has_closing_flag(): bool { 1463 return $this->has_closing_flag; 1464 } 1465 1466 /** 1467 * Indicates if the block delimiter represents a block of the given type. 1468 * 1469 * Since the “core” namespace may be implicit, it’s allowable to pass 1470 * either the fully-qualified block type with namespace and block name 1471 * as well as the shorthand version only containing the block name, if 1472 * the desired block is in the “core” namespace. 1473 * 1474 * Since freeform HTML content is non-block content, it has no block type. 1475 * Passing the wildcard “*” will, however, return true for all block types, 1476 * even the implicit freeform content, though not for spans of inner HTML. 1477 * 1478 * Example: 1479 * 1480 * $is_core_paragraph = $processor->is_block_type( 'paragraph' ); 1481 * $is_core_paragraph = $processor->is_block_type( 'core/paragraph' ); 1482 * $is_formula = $processor->is_block_type( 'math-block/formula' ); 1483 * 1484 * @since 6.9.0 1485 * 1486 * @param string $block_type Block type name for the desired block. 1487 * E.g. "paragraph", "core/paragraph", "math-blocks/formula". 1488 * @return bool Whether this delimiter represents a block of the given type. 1489 */ 1490 public function is_block_type( string $block_type ): bool { 1491 if ( '*' === $block_type ) { 1492 return true; 1493 } 1494 1495 if ( $this->is_html() ) { 1496 // This is a core/freeform text block, it’s special. 1497 if ( 0 === ( $this->open_blocks_length[0] ?? null ) ) { 1498 return ( 1499 'core/freeform' === $block_type || 1500 'freeform' === $block_type 1501 ); 1502 } 1503 1504 // Otherwise this is innerHTML and not a block. 1505 return false; 1506 } 1507 1508 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 ) ); 1509 } 1510 1511 /** 1512 * Given two spans of text, indicate if they represent identical block types. 1513 * 1514 * This function normalizes block types to account for implicit core namespacing. 1515 * 1516 * Note! This function only returns valid results when the complete block types are 1517 * represented in the span offsets and lengths. This means that the full optional 1518 * namespace and block name must be represented in the input arguments. 1519 * 1520 * Example: 1521 * 1522 * 0 5 10 15 20 25 30 35 40 1523 * $text = '<!-- wp:block --><!-- /wp:core/block -->'; 1524 * 1525 * true === WP_Block_Processor::are_equal_block_types( $text, 9, 5, $text, 27, 10 ); 1526 * false === WP_Block_Processor::are_equal_block_types( $text, 9, 5, 'my/block', 0, 8 ); 1527 * 1528 * @since 6.9.0 1529 * 1530 * @param string $a_text Text in which first block type appears. 1531 * @param int $a_at Byte offset into text in which first block type starts. 1532 * @param int $a_length Byte length of first block type. 1533 * @param string $b_text Text in which second block type appears (may be the same as the first text). 1534 * @param int $b_at Byte offset into text in which second block type starts. 1535 * @param int $b_length Byte length of second block type. 1536 * @return bool Whether the spans of text represent identical block types, normalized for namespacing. 1537 */ 1538 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 { 1539 $a_ns_length = strcspn( $a_text, '/', $a_at, $a_length ); 1540 $b_ns_length = strcspn( $b_text, '/', $b_at, $b_length ); 1541 1542 $a_has_ns = $a_ns_length !== $a_length; 1543 $b_has_ns = $b_ns_length !== $b_length; 1544 1545 // Both contain namespaces. 1546 if ( $a_has_ns && $b_has_ns ) { 1547 if ( $a_length !== $b_length ) { 1548 return false; 1549 } 1550 1551 $a_block_type = substr( $a_text, $a_at, $a_length ); 1552 1553 return 0 === substr_compare( $b_text, $a_block_type, $b_at, $b_length ); 1554 } 1555 1556 if ( $a_has_ns ) { 1557 $b_block_type = 'core/' . substr( $b_text, $b_at, $b_length ); 1558 1559 return ( 1560 strlen( $b_block_type ) === $a_length && 1561 0 === substr_compare( $a_text, $b_block_type, $a_at, $a_length ) 1562 ); 1563 } 1564 1565 if ( $b_has_ns ) { 1566 $a_block_type = 'core/' . substr( $a_text, $a_at, $a_length ); 1567 1568 return ( 1569 strlen( $a_block_type ) === $b_length && 1570 0 === substr_compare( $b_text, $a_block_type, $b_at, $b_length ) 1571 ); 1572 } 1573 1574 // Neither contains a namespace. 1575 if ( $a_length !== $b_length ) { 1576 return false; 1577 } 1578 1579 $a_name = substr( $a_text, $a_at, $a_length ); 1580 1581 return 0 === substr_compare( $b_text, $a_name, $b_at, $b_length ); 1582 } 1583 1584 /** 1585 * Indicates if the matched delimiter is an opening or void delimiter of the given type, 1586 * if a type is provided, otherwise if it opens any block or implicit freeform HTML content. 1587 * 1588 * This is a helper method to ease handling of code inspecting where blocks start, and for 1589 * checking if the blocks are of a given type. The function is variadic to allow for 1590 * checking if the delimiter opens one of many possible block types. 1591 * 1592 * To advance to the start of a block {@see self::next_block()}. 1593 * 1594 * Example: 1595 * 1596 * $processor = new WP_Block_Processor( $html ); 1597 * while ( $processor->next_delimiter() ) { 1598 * if ( $processor->opens_block( 'core/code', 'syntaxhighlighter/code' ) ) { 1599 * echo "Found code!"; 1600 * continue; 1601 * } 1602 * 1603 * if ( $processor->opens_block( 'core/image' ) ) { 1604 * echo "Found an image!"; 1605 * continue; 1606 * } 1607 * 1608 * if ( $processor->opens_block() ) { 1609 * echo "Found a new block!"; 1610 * } 1611 * } 1612 * 1613 * @since 6.9.0 1614 * 1615 * @see self::is_block_type() 1616 * 1617 * @param string ...$block_type Optional. Is the matched block type one of these? 1618 * If none are provided, will not test block type. 1619 * @return bool Whether the matched block delimiter opens a block, and whether it 1620 * opens a block of one of the given block types, if provided. 1621 */ 1622 public function opens_block( string ...$block_type ): bool { 1623 // HTML spans only open implicit freeform content at the top level. 1624 if ( self::HTML_SPAN === $this->state && 1 !== count( $this->open_blocks_at ) ) { 1625 return false; 1626 } 1627 1628 /* 1629 * Because HTML spans are discovered after the next delimiter is found, 1630 * the delimiter type when visiting HTML spans refers to the type of the 1631 * following delimiter. Therefore the HTML case is handled by checking 1632 * the state and depth of the stack of open block. 1633 */ 1634 if ( self::CLOSER === $this->type && ! $this->is_html() ) { 1635 return false; 1636 } 1637 1638 if ( count( $block_type ) === 0 ) { 1639 return true; 1640 } 1641 1642 return array_any( $block_type, fn( $block ) => $this->is_block_type( $block ) ); 1643 } 1644 1645 /** 1646 * Indicates if the matched delimiter is an HTML span. 1647 * 1648 * @since 6.9.0 1649 * 1650 * @see self::is_non_whitespace_html() 1651 * 1652 * @return bool Whether the processor is matched on an HTML span. 1653 */ 1654 public function is_html(): bool { 1655 return self::HTML_SPAN === $this->state; 1656 } 1657 1658 /** 1659 * Indicates if the matched delimiter is an HTML span and comprises more 1660 * than whitespace characters, i.e. contains real content. 1661 * 1662 * Many block serializers introduce newlines between block delimiters, 1663 * so the presence of top-level non-block content does not imply that 1664 * there are “real” freeform HTML blocks. Checking if there is content 1665 * beyond whitespace is a more certain check, such as for determining 1666 * whether to load CSS for the freeform or fallback block type. 1667 * 1668 * @since 6.9.0 1669 * 1670 * @see self::is_html() 1671 * 1672 * @return bool Whether the currently-matched delimiter is an HTML 1673 * span containing non-whitespace text. 1674 */ 1675 public function is_non_whitespace_html(): bool { 1676 if ( ! $this->is_html() ) { 1677 return false; 1678 } 1679 1680 $length = $this->matched_delimiter_at - $this->after_previous_delimiter; 1681 1682 $whitespace_length = strspn( 1683 $this->source_text, 1684 " \t\f\r\n", 1685 $this->after_previous_delimiter, 1686 $length 1687 ); 1688 1689 return $whitespace_length !== $length; 1690 } 1691 1692 /** 1693 * Returns the string content of a matched HTML span, or `null` otherwise. 1694 * 1695 * @since 6.9.0 1696 * 1697 * @return string|null Raw HTML content, or `null` if not currently matched on HTML. 1698 */ 1699 public function get_html_content(): ?string { 1700 if ( ! $this->is_html() ) { 1701 return null; 1702 } 1703 1704 return substr( 1705 $this->source_text, 1706 $this->after_previous_delimiter, 1707 $this->matched_delimiter_at - $this->after_previous_delimiter 1708 ); 1709 } 1710 1711 /** 1712 * Allocates a substring for the block type and returns the fully-qualified 1713 * name, including the namespace, if matched on a delimiter, otherwise `null`. 1714 * 1715 * This function is like {@see self::get_printable_block_type()} but when 1716 * paused on a freeform HTML block, will return `null` instead of “core/freeform”. 1717 * The `null` behavior matches what {@see \parse_blocks()} returns but may not 1718 * be as useful as having a string value. 1719 * 1720 * This function allocates a substring for the given block type. This 1721 * allocation will be small and likely fine in most cases, but it's 1722 * preferable to call {@see self::is_block_type()} if only needing 1723 * to know whether the delimiter is for a given block type, as that 1724 * function is more efficient for this purpose and avoids the allocation. 1725 * 1726 * Example: 1727 * 1728 * // Avoid. 1729 * 'core/paragraph' = $processor->get_block_type(); 1730 * 1731 * // Prefer. 1732 * $processor->is_block_type( 'core/paragraph' ); 1733 * $processor->is_block_type( 'paragraph' ); 1734 * $processor->is_block_type( 'core/freeform' ); 1735 * 1736 * // Freeform HTML content has no block type. 1737 * $processor = new WP_Block_Processor( 'non-block content' ); 1738 * $processor->next_token(); 1739 * null === $processor->get_block_type(); 1740 * 1741 * @since 6.9.0 1742 * 1743 * @see self::are_equal_block_types() 1744 * 1745 * @return string|null Fully-qualified block namespace and type, e.g. "core/paragraph", 1746 * if matched on an explicit delimiter, otherwise `null`. 1747 */ 1748 public function get_block_type(): ?string { 1749 if ( 1750 self::READY === $this->state || 1751 self::COMPLETE === $this->state || 1752 self::INCOMPLETE_INPUT === $this->state 1753 ) { 1754 return null; 1755 } 1756 1757 // This is a core/freeform text block, it’s special. 1758 if ( $this->is_html() ) { 1759 return null; 1760 } 1761 1762 $block_type = substr( $this->source_text, $this->namespace_at, $this->name_at - $this->namespace_at + $this->name_length ); 1763 return self::normalize_block_type( $block_type ); 1764 } 1765 1766 /** 1767 * Allocates a printable substring for the block type and returns the fully-qualified 1768 * name, including the namespace, if matched on a delimiter or freeform block, otherwise `null`. 1769 * 1770 * This function is like {@see self::get_block_type()} but when paused on a freeform 1771 * HTML block, will return “core/freeform” instead of `null`. The `null` behavior matches 1772 * what {@see \parse_blocks()} returns but may not be as useful as having a string value. 1773 * 1774 * This function allocates a substring for the given block type. This 1775 * allocation will be small and likely fine in most cases, but it's 1776 * preferable to call {@see self::is_block_type()} if only needing 1777 * to know whether the delimiter is for a given block type, as that 1778 * function is more efficient for this purpose and avoids the allocation. 1779 * 1780 * Example: 1781 * 1782 * // Avoid. 1783 * 'core/paragraph' = $processor->get_printable_block_type(); 1784 * 1785 * // Prefer. 1786 * $processor->is_block_type( 'core/paragraph' ); 1787 * $processor->is_block_type( 'paragraph' ); 1788 * $processor->is_block_type( 'core/freeform' ); 1789 * 1790 * // Freeform HTML content is given an implicit type. 1791 * $processor = new WP_Block_Processor( 'non-block content' ); 1792 * $processor->next_token(); 1793 * 'core/freeform' === $processor->get_printable_block_type(); 1794 * 1795 * @since 6.9.0 1796 * 1797 * @see self::are_equal_block_types() 1798 * 1799 * @return string|null Fully-qualified block namespace and type, e.g. "core/paragraph", 1800 * if matched on an explicit delimiter or freeform block, otherwise `null`. 1801 */ 1802 public function get_printable_block_type(): ?string { 1803 if ( 1804 self::READY === $this->state || 1805 self::COMPLETE === $this->state || 1806 self::INCOMPLETE_INPUT === $this->state 1807 ) { 1808 return null; 1809 } 1810 1811 // This is a core/freeform text block, it’s special. 1812 if ( $this->is_html() ) { 1813 return 1 === count( $this->open_blocks_at ) 1814 ? 'core/freeform' 1815 : '#innerHTML'; 1816 } 1817 1818 $block_type = substr( $this->source_text, $this->namespace_at, $this->name_at - $this->namespace_at + $this->name_length ); 1819 return self::normalize_block_type( $block_type ); 1820 } 1821 1822 /** 1823 * Normalizes a block name to ensure that missing implicit “core” namespaces are present. 1824 * 1825 * Example: 1826 * 1827 * 'core/paragraph' === WP_Block_Processor::normalize_block_byte( 'paragraph' ); 1828 * 'core/paragraph' === WP_Block_Processor::normalize_block_byte( 'core/paragraph' ); 1829 * 'my/paragraph' === WP_Block_Processor::normalize_block_byte( 'my/paragraph' ); 1830 * 1831 * @since 6.9.0 1832 * 1833 * @param string $block_type Valid block name, potentially without a namespace. 1834 * @return string Fully-qualified block type including namespace. 1835 */ 1836 public static function normalize_block_type( string $block_type ): string { 1837 return str_contains( $block_type, '/' ) 1838 ? $block_type 1839 : "core/{$block_type}"; 1840 } 1841 1842 /** 1843 * Returns a lazy wrapper around the block attributes, which can be used 1844 * for efficiently interacting with the JSON attributes. 1845 * 1846 * This stub hints that there should be a lazy interface for parsing 1847 * block attributes but doesn’t define it. It serves both as a placeholder 1848 * for one to come as well as a guard against implementing an eager 1849 * function in its place. 1850 * 1851 * @throws Exception This function is a stub for subclasses to implement 1852 * when providing streaming attribute parsing. 1853 * 1854 * @since 6.9.0 1855 * 1856 * @see self::allocate_and_return_parsed_attributes() 1857 * 1858 * @return never 1859 */ 1860 public function get_attributes() { 1861 throw new Exception( 'Lazy attribute parsing not yet supported' ); 1862 } 1863 1864 /** 1865 * Attempts to parse and return the entire JSON attributes from the delimiter, 1866 * allocating memory and processing the JSON span in the process. 1867 * 1868 * This does not return any parsed attributes for a closing block delimiter 1869 * even if there is a span of JSON content; this JSON is a parsing error. 1870 * 1871 * Consider calling {@see static::get_attributes()} instead if it's not 1872 * necessary to read all the attributes at the same time, as that provides 1873 * a more efficient mechanism for typical use cases. 1874 * 1875 * Since the JSON span inside the comment delimiter may not be valid JSON, 1876 * this function will return `null` if it cannot parse the span and set the 1877 * {@see static::get_last_json_error()} to the appropriate JSON_ERROR_ constant. 1878 * 1879 * If the delimiter contains no JSON span, it will also return `null`, 1880 * but the last error will be set to {@see \JSON_ERROR_NONE}. 1881 * 1882 * Example: 1883 * 1884 * $processor = new WP_Block_Processor( '<!-- wp:image {"url": "https://wordpress.org/favicon.ico"} -->' ); 1885 * $processor->next_delimiter(); 1886 * $memory_hungry_and_slow_attributes = $processor->allocate_and_return_parsed_attributes(); 1887 * $memory_hungry_and_slow_attributes === array( 'url' => 'https://wordpress.org/favicon.ico' ); 1888 * 1889 * $processor = new WP_Block_Processor( '<!-- /wp:image {"url": "https://wordpress.org/favicon.ico"} -->' ); 1890 * $processor->next_delimiter(); 1891 * null = $processor->allocate_and_return_parsed_attributes(); 1892 * JSON_ERROR_NONE = $processor->get_last_json_error(); 1893 * 1894 * $processor = new WP_Block_Processor( '<!-- wp:separator {} /-->' ); 1895 * $processor->next_delimiter(); 1896 * array() === $processor->allocate_and_return_parsed_attributes(); 1897 * 1898 * $processor = new WP_Block_Processor( '<!-- wp:separator /-->' ); 1899 * $processor->next_delimiter(); 1900 * null = $processor->allocate_and_return_parsed_attributes(); 1901 * 1902 * $processor = new WP_Block_Processor( '<!-- wp:image {"url} -->' ); 1903 * $processor->next_delimiter(); 1904 * null = $processor->allocate_and_return_parsed_attributes(); 1905 * JSON_ERROR_CTRL_CHAR = $processor->get_last_json_error(); 1906 * 1907 * @since 6.9.0 1908 * 1909 * @return array|null Parsed JSON attributes, if present and valid, otherwise `null`. 1910 */ 1911 public function allocate_and_return_parsed_attributes(): ?array { 1912 $this->last_json_error = JSON_ERROR_NONE; 1913 1914 if ( self::CLOSER === $this->type || $this->is_html() || 0 === $this->json_length ) { 1915 return null; 1916 } 1917 1918 $json_span = substr( $this->source_text, $this->json_at, $this->json_length ); 1919 $parsed = json_decode( $json_span, null, 512, JSON_OBJECT_AS_ARRAY | JSON_INVALID_UTF8_SUBSTITUTE ); 1920 1921 $last_error = json_last_error(); 1922 $this->last_json_error = $last_error; 1923 1924 return ( JSON_ERROR_NONE === $last_error && is_array( $parsed ) ) 1925 ? $parsed 1926 : null; 1927 } 1928 1929 /** 1930 * Returns the span representing the currently-matched delimiter, if matched, otherwise `null`. 1931 * 1932 * Example: 1933 * 1934 * $processor = new WP_Block_Processor( '<!-- wp:void /-->' ); 1935 * null === $processor->get_span(); 1936 * 1937 * $processor->next_delimiter(); 1938 * WP_HTML_Span( 0, 17 ) === $processor->get_span(); 1939 * 1940 * @since 6.9.0 1941 * 1942 * @return WP_HTML_Span|null Span of text in source text spanning matched delimiter. 1943 */ 1944 public function get_span(): ?WP_HTML_Span { 1945 switch ( $this->state ) { 1946 case self::HTML_SPAN: 1947 return new WP_HTML_Span( $this->after_previous_delimiter, $this->matched_delimiter_at - $this->after_previous_delimiter ); 1948 1949 case self::MATCHED: 1950 return new WP_HTML_Span( $this->matched_delimiter_at, $this->matched_delimiter_length ); 1951 1952 default: 1953 return null; 1954 } 1955 } 1956 1957 // 1958 // Constant declarations that would otherwise pollute the top of the class. 1959 // 1960 1961 /** 1962 * Indicates that the block comment delimiter closes an open block. 1963 * 1964 * @see self::$type 1965 * 1966 * @since 6.9.0 1967 */ 1968 const CLOSER = 'closer'; 1969 1970 /** 1971 * Indicates that the block comment delimiter opens a block. 1972 * 1973 * @see self::$type 1974 * 1975 * @since 6.9.0 1976 */ 1977 const OPENER = 'opener'; 1978 1979 /** 1980 * Indicates that the block comment delimiter represents a void block 1981 * with no inner content of any kind. 1982 * 1983 * @see self::$type 1984 * 1985 * @since 6.9.0 1986 */ 1987 const VOID = 'void'; 1988 1989 /** 1990 * Indicates that the processor is ready to start parsing but hasn’t yet begun. 1991 * 1992 * @see self::$state 1993 * 1994 * @since 6.9.0 1995 */ 1996 const READY = 'processor-ready'; 1997 1998 /** 1999 * Indicates that the processor is matched on an explicit block delimiter. 2000 * 2001 * @see self::$state 2002 * 2003 * @since 6.9.0 2004 */ 2005 const MATCHED = 'processor-matched'; 2006 2007 /** 2008 * Indicates that the processor is matched on the opening of an implicit freeform delimiter. 2009 * 2010 * @see self::$state 2011 * 2012 * @since 6.9.0 2013 */ 2014 const HTML_SPAN = 'processor-html-span'; 2015 2016 /** 2017 * Indicates that the parser started parsing a block comment delimiter, but 2018 * the input document ended before it could finish. The document was likely truncated. 2019 * 2020 * @see self::$state 2021 * 2022 * @since 6.9.0 2023 */ 2024 const INCOMPLETE_INPUT = 'incomplete-input'; 2025 2026 /** 2027 * Indicates that the processor has finished parsing and has nothing left to scan. 2028 * 2029 * @see self::$state 2030 * 2031 * @since 6.9.0 2032 */ 2033 const COMPLETE = 'processor-complete'; 2034 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Wed Oct 14 08:20:33 2026 | Cross-referenced by PHPXref |