[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

   1  <?php
   2  /**
   3   * Block Serialization Parser
   4   *
   5   * @package WordPress
   6   */
   7  
   8  /**
   9   * Class WP_Block_Parser
  10   *
  11   * Parses a document and constructs a list of parsed block objects
  12   *
  13   * @since 5.0.0
  14   * @since 4.0.0 returns arrays not objects, all attributes are arrays
  15   */
  16  class WP_Block_Parser {
  17      /**
  18       * Input document being parsed
  19       *
  20       * @example "Pre-text\n<!-- wp:paragraph -->This is inside a block!<!-- /wp:paragraph -->"
  21       *
  22       * @since 5.0.0
  23       * @var string
  24       */
  25      public $document;
  26  
  27      /**
  28       * Tracks parsing progress through document
  29       *
  30       * @since 5.0.0
  31       * @var int
  32       */
  33      public $offset;
  34  
  35      /**
  36       * List of parsed blocks
  37       *
  38       * See WP_Block_Parser_Block for the structure of each block.
  39       *
  40       * @since 5.0.0
  41       * @var array[]
  42       */
  43      public $output;
  44  
  45      /**
  46       * Stack of partially-parsed structures in memory during parse
  47       *
  48       * @since 5.0.0
  49       * @var WP_Block_Parser_Frame[]
  50       * @phpstan-var list<WP_Block_Parser_Frame>
  51       */
  52      public $stack;
  53  
  54      /**
  55       * Internal block processor for parsing and scanning input document.
  56       *
  57       * @since 7.2.0
  58       *
  59       * @var WP_Block_Processor|null
  60       */
  61      private $block_processor = null;
  62  
  63      /**
  64       * Parses a document and returns a list of block structures
  65       *
  66       * When encountering an invalid parse will return a best-effort
  67       * parse. In contrast to the specification parser this does not
  68       * return an error on invalid inputs.
  69       *
  70       * @since 5.0.0
  71       * @since 7.2.0 Triggers a notice when the document is not a string, and returns
  72       *              an empty array unless the document is a stringable object.
  73       *
  74       * @param string $document Input document being parsed.
  75       * @return array[] {
  76       *     Array of block structures.
  77       *
  78       *     @type array ...$0 {
  79       *         An associative array of a single parsed block object. See WP_Block_Parser_Block.
  80       *
  81       *         @type string|null $blockName    Name of block, or `null` when the block is
  82       *                                         top-level freeform HTML content.
  83       *         @type array       $attrs        Attributes from block comment delimiters.
  84       *         @type array[]     $innerBlocks  List of inner blocks. An array of arrays that
  85       *                                         have the same structure as this one.
  86       *         @type string      $innerHTML    HTML from inside block comment delimiters.
  87       *         @type array       $innerContent List of string fragments and null markers where
  88       *                                         inner blocks were found.
  89       *     }
  90       * }
  91       */
  92  	public function parse( $document ) {
  93          if ( is_object( $document ) && method_exists( $document, '__toString' ) ) {
  94              _doing_it_wrong(
  95                  __METHOD__,
  96                  __( 'Convert the document to a string before calling parse().' ),
  97                  '7.2.0'
  98              );
  99              $document = (string) $document;
 100          }
 101  
 102          if ( ! is_string( $document ) ) {
 103              _doing_it_wrong(
 104                  __METHOD__,
 105                  __( 'The document must be a string.' ),
 106                  '7.2.0'
 107              );
 108              return array();
 109          }
 110  
 111          $this->document = $document;
 112          $this->offset   = 0;
 113          $this->output   = array();
 114          $this->stack    = array();
 115  
 116          $this->block_processor = new WP_Block_Processor( $document );
 117  
 118          while ( $this->proceed() ) {
 119              continue;
 120          }
 121  
 122          return $this->output;
 123      }
 124  
 125      /**
 126       * Processes the next token from the input document
 127       * and returns whether to proceed eating more tokens
 128       *
 129       * This is the "next step" function that essentially
 130       * takes a token as its input and decides what to do
 131       * with that token before descending deeper into a
 132       * nested block tree or continuing along the document
 133       * or breaking out of a level of nesting.
 134       *
 135       * @internal
 136       * @since 5.0.0
 137       *
 138       * @return bool
 139       */
 140  	public function proceed() {
 141          // Ensure this is initialized, because some legacy code calls this method directly.
 142          $this->block_processor ??= new WP_Block_Processor( $this->document );
 143  
 144          $processor   = $this->block_processor;
 145          $stack_depth = count( $this->stack );
 146  
 147          // Handle the cases when no more tokens exist.
 148          if ( ! $processor->next_delimiter() ) {
 149              /*
 150               * Most documents will end with all blocks properly closed.
 151               * In these cases, all that’s necessary is to flush the output.
 152               * There is probably a terminating newline.
 153               */
 154              if ( 0 === $stack_depth ) {
 155                  $this->add_freeform();
 156                  return false;
 157              }
 158  
 159              /*
 160               * However, if blocks remain open it means there were missing
 161               * closing block delimiters. The post may have been truncated,
 162               * for example, but some amount of corruption produced a malformed
 163               * document.
 164               *
 165               * The spec parser considers this a parse error and returns a long
 166               * freeform block containing the full span of text from the input
 167               * document from the start of the un-closed block.
 168               *
 169               * This parser, however, is making a different pragmatic choice: it
 170               * will implicitly close any remaining open blocks, as if the closers
 171               * were provided. This ensures recovery of as much of the provided
 172               * block structure as is possible.
 173               */
 174              $this->implicitly_close_and_add_all_open_blocks();
 175              return false;
 176          }
 177  
 178          $token_type   = $processor->get_delimiter_type();
 179          $block_name   = $processor->get_block_type();
 180          $attrs        = $processor->allocate_and_return_parsed_attributes() ?? (
 181              JSON_ERROR_NONE === $processor->get_last_json_error()
 182                  ? array()
 183                  : null
 184          );
 185          $span         = $processor->get_span();
 186          $start_offset = $span->start;
 187          $token_length = $span->length;
 188  
 189          // we may have some HTML soup before the next block.
 190          $leading_html_start = $start_offset > $this->offset ? $this->offset : null;
 191  
 192          switch ( $token_type ) {
 193              case WP_Block_Processor::VOID:
 194                  /*
 195                   * easy case is if we stumbled upon a void block
 196                   * in the top-level of the document
 197                   */
 198                  if ( 0 === $stack_depth ) {
 199                      if ( isset( $leading_html_start ) ) {
 200                          $this->output[] = (array) $this->freeform(
 201                              substr(
 202                                  $this->document,
 203                                  $leading_html_start,
 204                                  $start_offset - $leading_html_start
 205                              )
 206                          );
 207                      }
 208  
 209                      $this->output[] = (array) new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() );
 210                      $this->offset   = $start_offset + $token_length;
 211                      return true;
 212                  }
 213  
 214                  // otherwise we found an inner block.
 215                  $this->add_inner_block(
 216                      new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ),
 217                      $start_offset,
 218                      $token_length
 219                  );
 220                  $this->offset = $start_offset + $token_length;
 221                  return true;
 222  
 223              case WP_Block_Processor::OPENER:
 224                  // track all newly-opened blocks on the stack.
 225                  $this->stack[] = new WP_Block_Parser_Frame(
 226                      new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ),
 227                      $start_offset,
 228                      $token_length,
 229                      $start_offset + $token_length,
 230                      $leading_html_start
 231                  );
 232                  $this->offset  = $start_offset + $token_length;
 233                  return true;
 234  
 235              case WP_Block_Processor::CLOSER:
 236                  /*
 237                   * if we're missing an opener we're in trouble
 238                   * This is an error
 239                   */
 240                  if ( 0 === $stack_depth ) {
 241                      /*
 242                       * we have options
 243                       * - assume an implicit opener
 244                       * - assume _this_ is the opener
 245                       * - give up and close out the document
 246                       */
 247                      $this->add_freeform();
 248                      return false;
 249                  }
 250  
 251                  // if we're not nesting then this is easy - close the block.
 252                  if ( 1 === $stack_depth ) {
 253                      $this->add_block_from_stack( $start_offset );
 254                      $this->offset = $start_offset + $token_length;
 255                      return true;
 256                  }
 257  
 258                  /*
 259                   * otherwise we're nested and we have to close out the current
 260                   * block and add it as a new innerBlock to the parent
 261                   */
 262                  $stack_top              = array_pop( $this->stack );
 263                  $html                   = substr( $this->document, $stack_top->prev_offset, $start_offset - $stack_top->prev_offset );
 264                  $stack_top->prev_offset = $start_offset + $token_length;
 265  
 266                  if ( '' !== $html ) {
 267                      $stack_top->block->innerHTML     .= $html;
 268                      $stack_top->block->innerContent[] = $html;
 269                  }
 270  
 271                  $this->add_inner_block(
 272                      $stack_top->block,
 273                      $stack_top->token_start,
 274                      $stack_top->token_length,
 275                      $start_offset + $token_length
 276                  );
 277                  $this->offset = $start_offset + $token_length;
 278                  return true;
 279  
 280              default:
 281                  // This is an error.
 282                  $this->add_freeform();
 283                  return false;
 284          }
 285      }
 286  
 287      /**
 288       * Scans the document from where we last left off
 289       * and finds the next valid token to parse if it exists
 290       *
 291       * Returns the type of the find: kind of find, block information, attributes
 292       *
 293       * @internal
 294       * @since 5.0.0
 295       * @since 4.6.1 fixed a bug in attribute parsing which caused catastrophic backtracking on invalid block comments
 296       *
 297       * @deprecated 7.2.0 Use {@see WP_Block_Processor} instead.
 298       *
 299       * @return array
 300       */
 301  	public function next_token() {
 302          // Ensure this is initialized, because some legacy code calls this method directly.
 303          $this->block_processor ??= new WP_Block_Processor( $this->document );
 304  
 305          $processor = $this->block_processor;
 306  
 307          if ( ! $processor->next_delimiter() ) {
 308              return array( 'no-more-tokens', null, null, null, null );
 309          }
 310  
 311          $span       = $processor->get_span();
 312          $started_at = $span->start;
 313          $length     = $span->length;
 314          $name       = $processor->get_block_type();
 315          $attrs      = $processor->allocate_and_return_parsed_attributes() ?? (
 316              JSON_ERROR_NONE === $processor->get_last_json_error()
 317                  ? array()
 318                  : null
 319          );
 320  
 321          switch ( $processor->get_delimiter_type() ) {
 322              case WP_Block_Processor::VOID:
 323                  return array( 'void-block', $name, $attrs, $started_at, $length );
 324  
 325              case WP_Block_Processor::CLOSER:
 326                  return array( 'block-closer', $name, null, $started_at, $length );
 327  
 328              case WP_Block_Processor::OPENER:
 329                  return array( 'block-opener', $name, $attrs, $started_at, $length );
 330          }
 331  
 332          // This should never be reachable.
 333          return $this->next_token();
 334      }
 335  
 336      /**
 337       * Returns a new block object for freeform HTML
 338       *
 339       * @internal
 340       * @since 5.0.0
 341       *
 342       * @param string $inner_html HTML content of block.
 343       * @return WP_Block_Parser_Block freeform block object.
 344       */
 345  	public function freeform( $inner_html ) {
 346          return new WP_Block_Parser_Block( null, array(), array(), $inner_html, array( $inner_html ) );
 347      }
 348  
 349      /**
 350       * Pushes a length of text from the input document
 351       * to the output list as a freeform block.
 352       *
 353       * @internal
 354       * @since 5.0.0
 355       *
 356       * @param null|int $length How many bytes of document text to output.
 357       */
 358  	public function add_freeform( $length = null ) {
 359          $length ??= strlen( $this->document ) - $this->offset;
 360  
 361          if ( 0 === $length ) {
 362              return;
 363          }
 364  
 365          $this->output[] = (array) $this->freeform( substr( $this->document, $this->offset, $length ) );
 366      }
 367  
 368      /**
 369       * Given a block structure from memory pushes
 370       * a new block to the output list.
 371       *
 372       * @internal
 373       * @since 5.0.0
 374       *
 375       * @param WP_Block_Parser_Block $block        The block to add to the output.
 376       * @param int                   $token_start  Byte offset into the document where the first token for the block starts.
 377       * @param int                   $token_length Byte length of entire block from start of opening token to end of closing token.
 378       * @param int|null              $last_offset  Last byte offset into document if continuing from earlier output.
 379       */
 380  	public function add_inner_block( WP_Block_Parser_Block $block, $token_start, $token_length, $last_offset = null ) {
 381          $parent                       = $this->stack[ count( $this->stack ) - 1 ];
 382          $parent->block->innerBlocks[] = (array) $block;
 383          $html                         = substr( $this->document, $parent->prev_offset, $token_start - $parent->prev_offset );
 384  
 385          if ( '' !== $html ) {
 386              $parent->block->innerHTML     .= $html;
 387              $parent->block->innerContent[] = $html;
 388          }
 389  
 390          $parent->block->innerContent[] = null;
 391          $parent->prev_offset           = $last_offset ?? $token_start + $token_length;
 392      }
 393  
 394      /**
 395       * Pushes the top block from the parsing stack to the output list.
 396       *
 397       * @internal
 398       * @since 5.0.0
 399       *
 400       * @param int|null $end_offset byte offset into document for where we should stop sending text output as HTML.
 401       */
 402  	public function add_block_from_stack( $end_offset = null ) {
 403          $stack_top   = array_pop( $this->stack );
 404          $prev_offset = $stack_top->prev_offset;
 405  
 406          $html = isset( $end_offset )
 407              ? substr( $this->document, $prev_offset, $end_offset - $prev_offset )
 408              : substr( $this->document, $prev_offset );
 409  
 410          if ( '' !== $html ) {
 411              $stack_top->block->innerHTML     .= $html;
 412              $stack_top->block->innerContent[] = $html;
 413          }
 414  
 415          if ( isset( $stack_top->leading_html_start ) ) {
 416              $this->output[] = (array) $this->freeform(
 417                  substr(
 418                      $this->document,
 419                      $stack_top->leading_html_start,
 420                      $stack_top->token_start - $stack_top->leading_html_start
 421                  )
 422              );
 423          }
 424  
 425          $this->output[] = (array) $stack_top->block;
 426      }
 427  
 428      /**
 429       * Closes all open blocks and adds remaining blocks and freeform content to the output list.
 430       *
 431       * @internal
 432       * @since 7.2.0
 433       */
 434  	public function implicitly_close_and_add_all_open_blocks(): void {
 435          $last_closed_block = null;
 436          $last_opener_at    = strlen( $this->document );
 437          $last_leading_html = null;
 438  
 439          /**
 440           * There is a stack of open blocks, starting with a top-level block,
 441           * and where each successive item in the stack is a child of that block.
 442           * There are also spans of HTML content between the block delimiters.
 443           *
 444           * As the stack is collapsed, any HTML segments within that block must
 445           * be collected into it, before closing and appending to its parent’s
 446           * inner blocks array.
 447           */
 448          while ( null !== ( $stack_top = array_pop( $this->stack ) ) ) {
 449              /*
 450               * Since there is no open inner block on the top-most block on the stack,
 451               * the first iteration’s remaining HTML span will be from the last `prev_offset`
 452               * until the end of the document. For each containing parent, it will run from
 453               * the `prev_offset` until the opening of the block that was just closed.
 454               */
 455              $html_length = $last_opener_at - $stack_top->prev_offset;
 456              if ( 0 !== $html_length ) {
 457                  $html = substr( $this->document, $stack_top->prev_offset, $html_length );
 458  
 459                  $stack_top->block->innerHTML     .= $html;
 460                  $stack_top->block->innerContent[] = $html;
 461              }
 462  
 463              if ( isset( $last_closed_block ) ) {
 464                  $stack_top->block->innerContent[] = null;
 465                  $stack_top->block->innerBlocks[]  = (array) $last_closed_block;
 466              }
 467  
 468              $last_closed_block = $stack_top->block;
 469              $last_opener_at    = $stack_top->token_start;
 470              $last_leading_html = $stack_top->leading_html_start;
 471          }
 472  
 473          // Collect any HTML preceding the outermost open block.
 474          if ( isset( $last_leading_html ) && $last_opener_at > $last_leading_html ) {
 475              $html           = substr( $this->document, $last_leading_html, $last_opener_at - $last_leading_html );
 476              $this->output[] = (array) $this->freeform( $html );
 477          }
 478  
 479          if ( isset( $last_closed_block ) ) {
 480              $this->output[] = (array) $last_closed_block;
 481          }
 482  
 483          $this->offset = strlen( $this->document );
 484      }
 485  }
 486  
 487  /**
 488   * WP_Block_Parser_Block class.
 489   *
 490   * Required for backward compatibility in WordPress Core.
 491   */
 492  require_once  __DIR__ . '/class-wp-block-parser-block.php';
 493  
 494  /**
 495   * WP_Block_Parser_Frame class.
 496   *
 497   * Required for backward compatibility in WordPress Core.
 498   */
 499  require_once  __DIR__ . '/class-wp-block-parser-frame.php';


Generated : Sat Nov 14 08:20:01 2026 Cross-referenced by PHPXref