[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> class-wp-block-processor.php (source)

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


Generated : Wed Sep 23 08:20:35 2026 Cross-referenced by PHPXref