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


Generated : Tue Nov 3 08:20:36 2026 Cross-referenced by PHPXref