| [ 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 * @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';
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Mon Sep 14 08:20:31 2026 | Cross-referenced by PHPXref |