[ 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       * @since 5.0.0
  39       * @var array[]
  40       */
  41      public $output;
  42  
  43      /**
  44       * Stack of partially-parsed structures in memory during parse
  45       *
  46       * @since 5.0.0
  47       * @var WP_Block_Parser_Frame[]
  48       */
  49      public $stack;
  50  
  51      /**
  52       * Parses a document and returns a list of block structures
  53       *
  54       * When encountering an invalid parse will return a best-effort
  55       * parse. In contrast to the specification parser this does not
  56       * return an error on invalid inputs.
  57       *
  58       * @since 5.0.0
  59       *
  60       * @param string $document Input document being parsed.
  61       * @return array[]
  62       */
  63  	public function parse( $document ) {
  64          $this->document = $document;
  65          $this->offset   = 0;
  66          $this->output   = array();
  67          $this->stack    = array();
  68  
  69          while ( $this->proceed() ) {
  70              continue;
  71          }
  72  
  73          return $this->output;
  74      }
  75  
  76      /**
  77       * Processes the next token from the input document
  78       * and returns whether to proceed eating more tokens
  79       *
  80       * This is the "next step" function that essentially
  81       * takes a token as its input and decides what to do
  82       * with that token before descending deeper into a
  83       * nested block tree or continuing along the document
  84       * or breaking out of a level of nesting.
  85       *
  86       * @internal
  87       * @since 5.0.0
  88       *
  89       * @return bool
  90       */
  91  	public function proceed() {
  92          $next_token = $this->next_token();
  93          list( $token_type, $block_name, $attrs, $start_offset, $token_length ) = $next_token;
  94          $stack_depth = count( $this->stack );
  95  
  96          // we may have some HTML soup before the next block.
  97          $leading_html_start = $start_offset > $this->offset ? $this->offset : null;
  98  
  99          switch ( $token_type ) {
 100              case 'no-more-tokens':
 101                  // if not in a block then flush output.
 102                  if ( 0 === $stack_depth ) {
 103                      $this->add_freeform();
 104                      return false;
 105                  }
 106  
 107                  /*
 108                   * Otherwise we have a problem
 109                   * This is an error
 110                   *
 111                   * we have options
 112                   * - treat it all as freeform text
 113                   * - assume an implicit closer (easiest when not nesting)
 114                   */
 115  
 116                  // for the easy case we'll assume an implicit closer.
 117                  if ( 1 === $stack_depth ) {
 118                      $this->add_block_from_stack();
 119                      return false;
 120                  }
 121  
 122                  /*
 123                   * for the nested case where it's more difficult we'll
 124                   * have to assume that multiple closers are missing
 125                   * and so we'll collapse the whole stack piecewise
 126                   */
 127                  while ( 0 < count( $this->stack ) ) {
 128                      $this->add_block_from_stack();
 129                  }
 130                  return false;
 131  
 132              case 'void-block':
 133                  /*
 134                   * easy case is if we stumbled upon a void block
 135                   * in the top-level of the document
 136                   */
 137                  if ( 0 === $stack_depth ) {
 138                      if ( isset( $leading_html_start ) ) {
 139                          $this->output[] = (array) $this->freeform(
 140                              substr(
 141                                  $this->document,
 142                                  $leading_html_start,
 143                                  $start_offset - $leading_html_start
 144                              )
 145                          );
 146                      }
 147  
 148                      $this->output[] = (array) new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() );
 149                      $this->offset   = $start_offset + $token_length;
 150                      return true;
 151                  }
 152  
 153                  // otherwise we found an inner block.
 154                  $this->add_inner_block(
 155                      new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ),
 156                      $start_offset,
 157                      $token_length
 158                  );
 159                  $this->offset = $start_offset + $token_length;
 160                  return true;
 161  
 162              case 'block-opener':
 163                  // track all newly-opened blocks on the stack.
 164                  array_push(
 165                      $this->stack,
 166                      new WP_Block_Parser_Frame(
 167                          new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ),
 168                          $start_offset,
 169                          $token_length,
 170                          $start_offset + $token_length,
 171                          $leading_html_start
 172                      )
 173                  );
 174                  $this->offset = $start_offset + $token_length;
 175                  return true;
 176  
 177              case 'block-closer':
 178                  /*
 179                   * if we're missing an opener we're in trouble
 180                   * This is an error
 181                   */
 182                  if ( 0 === $stack_depth ) {
 183                      /*
 184                       * we have options
 185                       * - assume an implicit opener
 186                       * - assume _this_ is the opener
 187                       * - give up and close out the document
 188                       */
 189                      $this->add_freeform();
 190                      return false;
 191                  }
 192  
 193                  // if we're not nesting then this is easy - close the block.
 194                  if ( 1 === $stack_depth ) {
 195                      $this->add_block_from_stack( $start_offset );
 196                      $this->offset = $start_offset + $token_length;
 197                      return true;
 198                  }
 199  
 200                  /*
 201                   * otherwise we're nested and we have to close out the current
 202                   * block and add it as a new innerBlock to the parent
 203                   */
 204                  $stack_top                        = array_pop( $this->stack );
 205                  $html                             = substr( $this->document, $stack_top->prev_offset, $start_offset - $stack_top->prev_offset );
 206                  $stack_top->block->innerHTML     .= $html;
 207                  $stack_top->block->innerContent[] = $html;
 208                  $stack_top->prev_offset           = $start_offset + $token_length;
 209  
 210                  $this->add_inner_block(
 211                      $stack_top->block,
 212                      $stack_top->token_start,
 213                      $stack_top->token_length,
 214                      $start_offset + $token_length
 215                  );
 216                  $this->offset = $start_offset + $token_length;
 217                  return true;
 218  
 219              default:
 220                  // This is an error.
 221                  $this->add_freeform();
 222                  return false;
 223          }
 224      }
 225  
 226      /**
 227       * Scans the document from where we last left off
 228       * and finds the next valid token to parse if it exists
 229       *
 230       * Returns the type of the find: kind of find, block information, attributes
 231       *
 232       * @internal
 233       * @since 5.0.0
 234       * @since 4.6.1 fixed a bug in attribute parsing which caused catastrophic backtracking on invalid block comments
 235       *
 236       * @return array
 237       */
 238  	public function next_token() {
 239          $matches = null;
 240  
 241          /*
 242           * aye the magic
 243           * we're using a single RegExp to tokenize the block comment delimiters
 244           * we're also using a trick here because the only difference between a
 245           * block opener and a block closer is the leading `/` before `wp:` (and
 246           * a closer has no attributes). we can trap them both and process the
 247           * match back in PHP to see which one it was.
 248           */
 249          $has_match = preg_match(
 250              '/<!--\s+(?P<closer>\/)?wp:(?P<namespace>[a-z][a-z0-9_-]*\/)?(?P<name>[a-z][a-z0-9_-]*)\s+(?P<attrs>{(?:(?:[^}]+|}+(?=})|(?!}\s+\/?-->).)*+)?}\s+)?(?P<void>\/)?-->/s',
 251              $this->document,
 252              $matches,
 253              PREG_OFFSET_CAPTURE,
 254              $this->offset
 255          );
 256  
 257          // if we get here we probably have catastrophic backtracking or out-of-memory in the PCRE.
 258          if ( false === $has_match ) {
 259              return array( 'no-more-tokens', null, null, null, null );
 260          }
 261  
 262          // we have no more tokens.
 263          if ( 0 === $has_match ) {
 264              return array( 'no-more-tokens', null, null, null, null );
 265          }
 266  
 267          list( $match, $started_at ) = $matches[0];
 268  
 269          $length    = strlen( $match );
 270          $is_closer = isset( $matches['closer'] ) && -1 !== $matches['closer'][1];
 271          $is_void   = isset( $matches['void'] ) && -1 !== $matches['void'][1];
 272          $namespace = $matches['namespace'];
 273          $namespace = ( -1 !== $namespace[1] ) ? $namespace[0] : 'core/';
 274          $name      = $namespace . $matches['name'][0];
 275          $has_attrs = isset( $matches['attrs'] ) && -1 !== $matches['attrs'][1];
 276  
 277          /*
 278           * Fun fact! It's not trivial in PHP to create "an empty associative array" since all arrays
 279           * are associative arrays. If we use `array()` we get a JSON `[]`
 280           */
 281          $attrs = $has_attrs
 282              ? json_decode( $matches['attrs'][0], /* as-associative */ true )
 283              : array();
 284  
 285          /*
 286           * This state isn't allowed
 287           * This is an error
 288           */
 289          if ( $is_closer && ( $is_void || $has_attrs ) ) {
 290              // we can ignore them since they don't hurt anything.
 291          }
 292  
 293          if ( $is_void ) {
 294              return array( 'void-block', $name, $attrs, $started_at, $length );
 295          }
 296  
 297          if ( $is_closer ) {
 298              return array( 'block-closer', $name, null, $started_at, $length );
 299          }
 300  
 301          return array( 'block-opener', $name, $attrs, $started_at, $length );
 302      }
 303  
 304      /**
 305       * Returns a new block object for freeform HTML
 306       *
 307       * @internal
 308       * @since 5.0.0
 309       *
 310       * @param string $inner_html HTML content of block.
 311       * @return WP_Block_Parser_Block freeform block object.
 312       */
 313  	public function freeform( $inner_html ) {
 314          return new WP_Block_Parser_Block( null, array(), array(), $inner_html, array( $inner_html ) );
 315      }
 316  
 317      /**
 318       * Pushes a length of text from the input document
 319       * to the output list as a freeform block.
 320       *
 321       * @internal
 322       * @since 5.0.0
 323       *
 324       * @param null|int $length How many bytes of document text to output.
 325       */
 326  	public function add_freeform( $length = null ) {
 327          $length = $length ?? strlen( $this->document ) - $this->offset;
 328  
 329          if ( 0 === $length ) {
 330              return;
 331          }
 332  
 333          $this->output[] = (array) $this->freeform( substr( $this->document, $this->offset, $length ) );
 334      }
 335  
 336      /**
 337       * Given a block structure from memory pushes
 338       * a new block to the output list.
 339       *
 340       * @internal
 341       * @since 5.0.0
 342       *
 343       * @param WP_Block_Parser_Block $block        The block to add to the output.
 344       * @param int                   $token_start  Byte offset into the document where the first token for the block starts.
 345       * @param int                   $token_length Byte length of entire block from start of opening token to end of closing token.
 346       * @param int|null              $last_offset  Last byte offset into document if continuing form earlier output.
 347       */
 348  	public function add_inner_block( WP_Block_Parser_Block $block, $token_start, $token_length, $last_offset = null ) {
 349          $parent                       = $this->stack[ array_key_last( $this->stack ) ];
 350          $parent->block->innerBlocks[] = (array) $block;
 351          $html                         = substr( $this->document, $parent->prev_offset, $token_start - $parent->prev_offset );
 352  
 353          if ( ! empty( $html ) ) {
 354              $parent->block->innerHTML     .= $html;
 355              $parent->block->innerContent[] = $html;
 356          }
 357  
 358          $parent->block->innerContent[] = null;
 359          $parent->prev_offset           = $last_offset ?? $token_start + $token_length;
 360      }
 361  
 362      /**
 363       * Pushes the top block from the parsing stack to the output list.
 364       *
 365       * @internal
 366       * @since 5.0.0
 367       *
 368       * @param int|null $end_offset byte offset into document for where we should stop sending text output as HTML.
 369       */
 370  	public function add_block_from_stack( $end_offset = null ) {
 371          $stack_top   = array_pop( $this->stack );
 372          $prev_offset = $stack_top->prev_offset;
 373  
 374          $html = isset( $end_offset )
 375              ? substr( $this->document, $prev_offset, $end_offset - $prev_offset )
 376              : substr( $this->document, $prev_offset );
 377  
 378          if ( ! empty( $html ) ) {
 379              $stack_top->block->innerHTML     .= $html;
 380              $stack_top->block->innerContent[] = $html;
 381          }
 382  
 383          if ( isset( $stack_top->leading_html_start ) ) {
 384              $this->output[] = (array) $this->freeform(
 385                  substr(
 386                      $this->document,
 387                      $stack_top->leading_html_start,
 388                      $stack_top->token_start - $stack_top->leading_html_start
 389                  )
 390              );
 391          }
 392  
 393          $this->output[] = (array) $stack_top->block;
 394      }
 395  }
 396  
 397  /**
 398   * WP_Block_Parser_Block class.
 399   *
 400   * Required for backward compatibility in WordPress Core.
 401   */
 402  require_once  __DIR__ . '/class-wp-block-parser-block.php';
 403  
 404  /**
 405   * WP_Block_Parser_Frame class.
 406   *
 407   * Required for backward compatibility in WordPress Core.
 408   */
 409  require_once  __DIR__ . '/class-wp-block-parser-frame.php';


Generated : Mon Sep 14 08:20:31 2026 Cross-referenced by PHPXref