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


Generated : Wed Oct 14 08:20:33 2026 Cross-referenced by PHPXref