| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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';
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Nov 14 08:20:01 2026 | Cross-referenced by PHPXref |