blocks.php (36535B)
1 <?php 2 /** 3 * Functions related to registering and parsing blocks. 4 * 5 * @package WordPress 6 * @subpackage Blocks 7 * @since 5.0.0 8 */ 9 10 /** 11 * Removes the block asset's path prefix if provided. 12 * 13 * @since 5.5.0 14 * 15 * @param string $asset_handle_or_path Asset handle or prefixed path. 16 * @return string Path without the prefix or the original value. 17 */ 18 function remove_block_asset_path_prefix( $asset_handle_or_path ) { 19 $path_prefix = 'file:'; 20 if ( 0 !== strpos( $asset_handle_or_path, $path_prefix ) ) { 21 return $asset_handle_or_path; 22 } 23 return substr( 24 $asset_handle_or_path, 25 strlen( $path_prefix ) 26 ); 27 } 28 29 /** 30 * Generates the name for an asset based on the name of the block 31 * and the field name provided. 32 * 33 * @since 5.5.0 34 * 35 * @param string $block_name Name of the block. 36 * @param string $field_name Name of the metadata field. 37 * @return string Generated asset name for the block's field. 38 */ 39 function generate_block_asset_handle( $block_name, $field_name ) { 40 if ( 0 === strpos( $block_name, 'core/' ) ) { 41 $asset_handle = str_replace( 'core/', 'wp-block-', $block_name ); 42 if ( 0 === strpos( $field_name, 'editor' ) ) { 43 $asset_handle .= '-editor'; 44 } 45 return $asset_handle; 46 } 47 48 $field_mappings = array( 49 'editorScript' => 'editor-script', 50 'script' => 'script', 51 'editorStyle' => 'editor-style', 52 'style' => 'style', 53 ); 54 return str_replace( '/', '-', $block_name ) . 55 '-' . $field_mappings[ $field_name ]; 56 } 57 58 /** 59 * Finds a script handle for the selected block metadata field. It detects 60 * when a path to file was provided and finds a corresponding asset file 61 * with details necessary to register the script under automatically 62 * generated handle name. It returns unprocessed script handle otherwise. 63 * 64 * @since 5.5.0 65 * 66 * @param array $metadata Block metadata. 67 * @param string $field_name Field name to pick from metadata. 68 * @return string|false Script handle provided directly or created through 69 * script's registration, or false on failure. 70 */ 71 function register_block_script_handle( $metadata, $field_name ) { 72 if ( empty( $metadata[ $field_name ] ) ) { 73 return false; 74 } 75 $script_handle = $metadata[ $field_name ]; 76 $script_path = remove_block_asset_path_prefix( $metadata[ $field_name ] ); 77 if ( $script_handle === $script_path ) { 78 return $script_handle; 79 } 80 81 $script_handle = generate_block_asset_handle( $metadata['name'], $field_name ); 82 $script_asset_path = realpath( 83 dirname( $metadata['file'] ) . '/' . 84 substr_replace( $script_path, '.asset.php', - strlen( '.js' ) ) 85 ); 86 if ( ! file_exists( $script_asset_path ) ) { 87 _doing_it_wrong( 88 __FUNCTION__, 89 sprintf( 90 /* translators: 1: Field name, 2: Block name. */ 91 __( 'The asset file for the "%1$s" defined in "%2$s" block definition is missing.' ), 92 $field_name, 93 $metadata['name'] 94 ), 95 '5.5.0' 96 ); 97 return false; 98 } 99 $script_asset = require $script_asset_path; 100 $result = wp_register_script( 101 $script_handle, 102 plugins_url( $script_path, $metadata['file'] ), 103 $script_asset['dependencies'], 104 $script_asset['version'] 105 ); 106 if ( ! $result ) { 107 return false; 108 } 109 110 if ( ! empty( $metadata['textdomain'] ) ) { 111 wp_set_script_translations( $script_handle, $metadata['textdomain'] ); 112 } 113 114 return $script_handle; 115 } 116 117 /** 118 * Finds a style handle for the block metadata field. It detects when a path 119 * to file was provided and registers the style under automatically 120 * generated handle name. It returns unprocessed style handle otherwise. 121 * 122 * @since 5.5.0 123 * 124 * @param array $metadata Block metadata. 125 * @param string $field_name Field name to pick from metadata. 126 * @return string|false Style handle provided directly or created through 127 * style's registration, or false on failure. 128 */ 129 function register_block_style_handle( $metadata, $field_name ) { 130 if ( empty( $metadata[ $field_name ] ) ) { 131 return false; 132 } 133 $is_core_block = isset( $metadata['file'] ) && 0 === strpos( $metadata['file'], ABSPATH . WPINC ); 134 if ( $is_core_block && ! wp_should_load_separate_core_block_assets() ) { 135 return false; 136 } 137 138 // Check whether styles should have a ".min" suffix or not. 139 $suffix = SCRIPT_DEBUG ? '' : '.min'; 140 141 $style_handle = $metadata[ $field_name ]; 142 $style_path = remove_block_asset_path_prefix( $metadata[ $field_name ] ); 143 144 if ( $style_handle === $style_path && ! $is_core_block ) { 145 return $style_handle; 146 } 147 148 $style_uri = plugins_url( $style_path, $metadata['file'] ); 149 if ( $is_core_block ) { 150 $style_path = "style$suffix.css"; 151 $style_uri = includes_url( 'blocks/' . str_replace( 'core/', '', $metadata['name'] ) . "/style$suffix.css" ); 152 } 153 154 $style_handle = generate_block_asset_handle( $metadata['name'], $field_name ); 155 $block_dir = dirname( $metadata['file'] ); 156 $style_file = realpath( "$block_dir/$style_path" ); 157 $has_style_file = false !== $style_file; 158 $version = ! $is_core_block && isset( $metadata['version'] ) ? $metadata['version'] : false; 159 $style_uri = $has_style_file ? $style_uri : false; 160 $result = wp_register_style( 161 $style_handle, 162 $style_uri, 163 array(), 164 $version 165 ); 166 if ( file_exists( str_replace( '.css', '-rtl.css', $style_file ) ) ) { 167 wp_style_add_data( $style_handle, 'rtl', 'replace' ); 168 } 169 if ( $has_style_file ) { 170 wp_style_add_data( $style_handle, 'path', $style_file ); 171 } 172 173 $rtl_file = str_replace( "$suffix.css", "-rtl$suffix.css", $style_file ); 174 if ( is_rtl() && file_exists( $rtl_file ) ) { 175 wp_style_add_data( $style_handle, 'path', $rtl_file ); 176 } 177 178 return $result ? $style_handle : false; 179 } 180 181 /** 182 * Registers a block type from the metadata stored in the `block.json` file. 183 * 184 * @since 5.5.0 185 * 186 * @param string $file_or_folder Path to the JSON file with metadata definition for 187 * the block or path to the folder where the `block.json` file is located. 188 * @param array $args Optional. Array of block type arguments. Accepts any public property 189 * of `WP_Block_Type`. See WP_Block_Type::__construct() for information 190 * on accepted arguments. Default empty array. 191 * @return WP_Block_Type|false The registered block type on success, or false on failure. 192 */ 193 function register_block_type_from_metadata( $file_or_folder, $args = array() ) { 194 $filename = 'block.json'; 195 $metadata_file = ( substr( $file_or_folder, -strlen( $filename ) ) !== $filename ) ? 196 trailingslashit( $file_or_folder ) . $filename : 197 $file_or_folder; 198 if ( ! file_exists( $metadata_file ) ) { 199 return false; 200 } 201 202 $metadata = json_decode( file_get_contents( $metadata_file ), true ); 203 if ( ! is_array( $metadata ) || empty( $metadata['name'] ) ) { 204 return false; 205 } 206 $metadata['file'] = $metadata_file; 207 208 /** 209 * Filters the metadata provided for registering a block type. 210 * 211 * @since 5.7.0 212 * 213 * @param array $metadata Metadata for registering a block type. 214 */ 215 $metadata = apply_filters( 'block_type_metadata', $metadata ); 216 217 // Add `style` and `editor_style` for core blocks if missing. 218 if ( ! empty( $metadata['name'] ) && 0 === strpos( $metadata['name'], 'core/' ) ) { 219 $block_name = str_replace( 'core/', '', $metadata['name'] ); 220 221 if ( ! isset( $metadata['style'] ) ) { 222 $metadata['style'] = "wp-block-$block_name"; 223 } 224 if ( ! isset( $metadata['editorStyle'] ) ) { 225 $metadata['editorStyle'] = "wp-block-{$block_name}-editor"; 226 } 227 } 228 229 $settings = array(); 230 $property_mappings = array( 231 'title' => 'title', 232 'category' => 'category', 233 'parent' => 'parent', 234 'icon' => 'icon', 235 'description' => 'description', 236 'keywords' => 'keywords', 237 'attributes' => 'attributes', 238 'providesContext' => 'provides_context', 239 'usesContext' => 'uses_context', 240 'supports' => 'supports', 241 'styles' => 'styles', 242 'example' => 'example', 243 'apiVersion' => 'api_version', 244 ); 245 246 foreach ( $property_mappings as $key => $mapped_key ) { 247 if ( isset( $metadata[ $key ] ) ) { 248 $value = $metadata[ $key ]; 249 if ( empty( $metadata['textdomain'] ) ) { 250 $settings[ $mapped_key ] = $value; 251 continue; 252 } 253 $textdomain = $metadata['textdomain']; 254 switch ( $key ) { 255 case 'title': 256 case 'description': 257 // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralContext,WordPress.WP.I18n.NonSingularStringLiteralDomain 258 $settings[ $mapped_key ] = translate_with_gettext_context( $value, sprintf( 'block %s', $key ), $textdomain ); 259 break; 260 case 'keywords': 261 $settings[ $mapped_key ] = array(); 262 if ( ! is_array( $value ) ) { 263 continue 2; 264 } 265 266 foreach ( $value as $keyword ) { 267 // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain 268 $settings[ $mapped_key ][] = translate_with_gettext_context( $keyword, 'block keyword', $textdomain ); 269 } 270 271 break; 272 case 'styles': 273 $settings[ $mapped_key ] = array(); 274 if ( ! is_array( $value ) ) { 275 continue 2; 276 } 277 278 foreach ( $value as $style ) { 279 if ( ! empty( $style['label'] ) ) { 280 // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain 281 $style['label'] = translate_with_gettext_context( $style['label'], 'block style label', $textdomain ); 282 } 283 $settings[ $mapped_key ][] = $style; 284 } 285 286 break; 287 default: 288 $settings[ $mapped_key ] = $value; 289 } 290 } 291 } 292 293 if ( ! empty( $metadata['editorScript'] ) ) { 294 $settings['editor_script'] = register_block_script_handle( 295 $metadata, 296 'editorScript' 297 ); 298 } 299 300 if ( ! empty( $metadata['script'] ) ) { 301 $settings['script'] = register_block_script_handle( 302 $metadata, 303 'script' 304 ); 305 } 306 307 if ( ! empty( $metadata['editorStyle'] ) ) { 308 $settings['editor_style'] = register_block_style_handle( 309 $metadata, 310 'editorStyle' 311 ); 312 } 313 314 if ( ! empty( $metadata['style'] ) ) { 315 $settings['style'] = register_block_style_handle( 316 $metadata, 317 'style' 318 ); 319 } 320 321 /** 322 * Filters the settings determined from the block type metadata. 323 * 324 * @since 5.7.0 325 * 326 * @param array $settings Array of determined settings for registering a block type. 327 * @param array $metadata Metadata provided for registering a block type. 328 */ 329 $settings = apply_filters( 330 'block_type_metadata_settings', 331 array_merge( 332 $settings, 333 $args 334 ), 335 $metadata 336 ); 337 338 return WP_Block_Type_Registry::get_instance()->register( 339 $metadata['name'], 340 $settings 341 ); 342 } 343 344 /** 345 * Registers a block type. The recommended way is to register a block type using 346 * the metadata stored in the `block.json` file. 347 * 348 * @since 5.0.0 349 * @since 5.8.0 First param accepts a path to the `block.json` file. 350 * 351 * @param string|WP_Block_Type $block_type Block type name including namespace, or alternatively 352 * a path to the JSON file with metadata definition for the block, 353 * or a path to the folder where the `block.json` file is located, 354 * or a complete WP_Block_Type instance. 355 * In case a WP_Block_Type is provided, the $args parameter will be ignored. 356 * @param array $args Optional. Array of block type arguments. Accepts any public property 357 * of `WP_Block_Type`. See WP_Block_Type::__construct() for information 358 * on accepted arguments. Default empty array. 359 * 360 * @return WP_Block_Type|false The registered block type on success, or false on failure. 361 */ 362 function register_block_type( $block_type, $args = array() ) { 363 if ( is_string( $block_type ) && file_exists( $block_type ) ) { 364 return register_block_type_from_metadata( $block_type, $args ); 365 } 366 367 return WP_Block_Type_Registry::get_instance()->register( $block_type, $args ); 368 } 369 370 /** 371 * Unregisters a block type. 372 * 373 * @since 5.0.0 374 * 375 * @param string|WP_Block_Type $name Block type name including namespace, or alternatively 376 * a complete WP_Block_Type instance. 377 * @return WP_Block_Type|false The unregistered block type on success, or false on failure. 378 */ 379 function unregister_block_type( $name ) { 380 return WP_Block_Type_Registry::get_instance()->unregister( $name ); 381 } 382 383 /** 384 * Determine whether a post or content string has blocks. 385 * 386 * This test optimizes for performance rather than strict accuracy, detecting 387 * the pattern of a block but not validating its structure. For strict accuracy, 388 * you should use the block parser on post content. 389 * 390 * @since 5.0.0 391 * 392 * @see parse_blocks() 393 * 394 * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. 395 * Defaults to global $post. 396 * @return bool Whether the post has blocks. 397 */ 398 function has_blocks( $post = null ) { 399 if ( ! is_string( $post ) ) { 400 $wp_post = get_post( $post ); 401 if ( $wp_post instanceof WP_Post ) { 402 $post = $wp_post->post_content; 403 } 404 } 405 406 return false !== strpos( (string) $post, '<!-- wp:' ); 407 } 408 409 /** 410 * Determine whether a $post or a string contains a specific block type. 411 * 412 * This test optimizes for performance rather than strict accuracy, detecting 413 * whether the block type exists but not validating its structure and not checking 414 * reusable blocks. For strict accuracy, you should use the block parser on post content. 415 * 416 * @since 5.0.0 417 * 418 * @see parse_blocks() 419 * 420 * @param string $block_name Full block type to look for. 421 * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. 422 * Defaults to global $post. 423 * @return bool Whether the post content contains the specified block. 424 */ 425 function has_block( $block_name, $post = null ) { 426 if ( ! has_blocks( $post ) ) { 427 return false; 428 } 429 430 if ( ! is_string( $post ) ) { 431 $wp_post = get_post( $post ); 432 if ( $wp_post instanceof WP_Post ) { 433 $post = $wp_post->post_content; 434 } 435 } 436 437 /* 438 * Normalize block name to include namespace, if provided as non-namespaced. 439 * This matches behavior for WordPress 5.0.0 - 5.3.0 in matching blocks by 440 * their serialized names. 441 */ 442 if ( false === strpos( $block_name, '/' ) ) { 443 $block_name = 'core/' . $block_name; 444 } 445 446 // Test for existence of block by its fully qualified name. 447 $has_block = false !== strpos( $post, '<!-- wp:' . $block_name . ' ' ); 448 449 if ( ! $has_block ) { 450 /* 451 * If the given block name would serialize to a different name, test for 452 * existence by the serialized form. 453 */ 454 $serialized_block_name = strip_core_block_namespace( $block_name ); 455 if ( $serialized_block_name !== $block_name ) { 456 $has_block = false !== strpos( $post, '<!-- wp:' . $serialized_block_name . ' ' ); 457 } 458 } 459 460 return $has_block; 461 } 462 463 /** 464 * Returns an array of the names of all registered dynamic block types. 465 * 466 * @since 5.0.0 467 * 468 * @return string[] Array of dynamic block names. 469 */ 470 function get_dynamic_block_names() { 471 $dynamic_block_names = array(); 472 473 $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); 474 foreach ( $block_types as $block_type ) { 475 if ( $block_type->is_dynamic() ) { 476 $dynamic_block_names[] = $block_type->name; 477 } 478 } 479 480 return $dynamic_block_names; 481 } 482 483 /** 484 * Given an array of attributes, returns a string in the serialized attributes 485 * format prepared for post content. 486 * 487 * The serialized result is a JSON-encoded string, with unicode escape sequence 488 * substitution for characters which might otherwise interfere with embedding 489 * the result in an HTML comment. 490 * 491 * This function must produce output that remains in sync with the output of 492 * the serializeAttributes JavaScript function in the block editor in order 493 * to ensure consistent operation between PHP and JavaScript. 494 * 495 * @since 5.3.1 496 * 497 * @param array $block_attributes Attributes object. 498 * @return string Serialized attributes. 499 */ 500 function serialize_block_attributes( $block_attributes ) { 501 $encoded_attributes = wp_json_encode( $block_attributes, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); 502 $encoded_attributes = preg_replace( '/--/', '\\u002d\\u002d', $encoded_attributes ); 503 $encoded_attributes = preg_replace( '/</', '\\u003c', $encoded_attributes ); 504 $encoded_attributes = preg_replace( '/>/', '\\u003e', $encoded_attributes ); 505 $encoded_attributes = preg_replace( '/&/', '\\u0026', $encoded_attributes ); 506 // Regex: /\\"/ 507 $encoded_attributes = preg_replace( '/\\\\"/', '\\u0022', $encoded_attributes ); 508 509 return $encoded_attributes; 510 } 511 512 /** 513 * Returns the block name to use for serialization. This will remove the default 514 * "core/" namespace from a block name. 515 * 516 * @since 5.3.1 517 * 518 * @param string $block_name Original block name. 519 * @return string Block name to use for serialization. 520 */ 521 function strip_core_block_namespace( $block_name = null ) { 522 if ( is_string( $block_name ) && 0 === strpos( $block_name, 'core/' ) ) { 523 return substr( $block_name, 5 ); 524 } 525 526 return $block_name; 527 } 528 529 /** 530 * Returns the content of a block, including comment delimiters. 531 * 532 * @since 5.3.1 533 * 534 * @param string|null $block_name Block name. Null if the block name is unknown, 535 * e.g. Classic blocks have their name set to null. 536 * @param array $block_attributes Block attributes. 537 * @param string $block_content Block save content. 538 * @return string Comment-delimited block content. 539 */ 540 function get_comment_delimited_block_content( $block_name, $block_attributes, $block_content ) { 541 if ( is_null( $block_name ) ) { 542 return $block_content; 543 } 544 545 $serialized_block_name = strip_core_block_namespace( $block_name ); 546 $serialized_attributes = empty( $block_attributes ) ? '' : serialize_block_attributes( $block_attributes ) . ' '; 547 548 if ( empty( $block_content ) ) { 549 return sprintf( '<!-- wp:%s %s/-->', $serialized_block_name, $serialized_attributes ); 550 } 551 552 return sprintf( 553 '<!-- wp:%s %s-->%s<!-- /wp:%s -->', 554 $serialized_block_name, 555 $serialized_attributes, 556 $block_content, 557 $serialized_block_name 558 ); 559 } 560 561 /** 562 * Returns the content of a block, including comment delimiters, serializing all 563 * attributes from the given parsed block. 564 * 565 * This should be used when preparing a block to be saved to post content. 566 * Prefer `render_block` when preparing a block for display. Unlike 567 * `render_block`, this does not evaluate a block's `render_callback`, and will 568 * instead preserve the markup as parsed. 569 * 570 * @since 5.3.1 571 * 572 * @param WP_Block_Parser_Block $block A single parsed block object. 573 * @return string String of rendered HTML. 574 */ 575 function serialize_block( $block ) { 576 $block_content = ''; 577 578 $index = 0; 579 foreach ( $block['innerContent'] as $chunk ) { 580 $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] ); 581 } 582 583 if ( ! is_array( $block['attrs'] ) ) { 584 $block['attrs'] = array(); 585 } 586 587 return get_comment_delimited_block_content( 588 $block['blockName'], 589 $block['attrs'], 590 $block_content 591 ); 592 } 593 594 /** 595 * Returns a joined string of the aggregate serialization of the given parsed 596 * blocks. 597 * 598 * @since 5.3.1 599 * 600 * @param WP_Block_Parser_Block[] $blocks Parsed block objects. 601 * @return string String of rendered HTML. 602 */ 603 function serialize_blocks( $blocks ) { 604 return implode( '', array_map( 'serialize_block', $blocks ) ); 605 } 606 607 /** 608 * Filters and sanitizes block content to remove non-allowable HTML from 609 * parsed block attribute values. 610 * 611 * @since 5.3.1 612 * 613 * @param string $text Text that may contain block content. 614 * @param array[]|string $allowed_html An array of allowed HTML elements 615 * and attributes, or a context name 616 * such as 'post'. 617 * @param string[] $allowed_protocols Array of allowed URL protocols. 618 * @return string The filtered and sanitized content result. 619 */ 620 function filter_block_content( $text, $allowed_html = 'post', $allowed_protocols = array() ) { 621 $result = ''; 622 623 $blocks = parse_blocks( $text ); 624 foreach ( $blocks as $block ) { 625 $block = filter_block_kses( $block, $allowed_html, $allowed_protocols ); 626 $result .= serialize_block( $block ); 627 } 628 629 return $result; 630 } 631 632 /** 633 * Filters and sanitizes a parsed block to remove non-allowable HTML from block 634 * attribute values. 635 * 636 * @since 5.3.1 637 * 638 * @param WP_Block_Parser_Block $block The parsed block object. 639 * @param array[]|string $allowed_html An array of allowed HTML 640 * elements and attributes, or a 641 * context name such as 'post'. 642 * @param string[] $allowed_protocols Allowed URL protocols. 643 * @return array The filtered and sanitized block object result. 644 */ 645 function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) { 646 $block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols ); 647 648 if ( is_array( $block['innerBlocks'] ) ) { 649 foreach ( $block['innerBlocks'] as $i => $inner_block ) { 650 $block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols ); 651 } 652 } 653 654 return $block; 655 } 656 657 /** 658 * Filters and sanitizes a parsed block attribute value to remove non-allowable 659 * HTML. 660 * 661 * @since 5.3.1 662 * 663 * @param string[]|string $value The attribute value to filter. 664 * @param array[]|string $allowed_html An array of allowed HTML elements 665 * and attributes, or a context name 666 * such as 'post'. 667 * @param string[] $allowed_protocols Array of allowed URL protocols. 668 * @return string[]|string The filtered and sanitized result. 669 */ 670 function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = array() ) { 671 if ( is_array( $value ) ) { 672 foreach ( $value as $key => $inner_value ) { 673 $filtered_key = filter_block_kses_value( $key, $allowed_html, $allowed_protocols ); 674 $filtered_value = filter_block_kses_value( $inner_value, $allowed_html, $allowed_protocols ); 675 676 if ( $filtered_key !== $key ) { 677 unset( $value[ $key ] ); 678 } 679 680 $value[ $filtered_key ] = $filtered_value; 681 } 682 } elseif ( is_string( $value ) ) { 683 return wp_kses( $value, $allowed_html, $allowed_protocols ); 684 } 685 686 return $value; 687 } 688 689 /** 690 * Parses blocks out of a content string, and renders those appropriate for the excerpt. 691 * 692 * As the excerpt should be a small string of text relevant to the full post content, 693 * this function renders the blocks that are most likely to contain such text. 694 * 695 * @since 5.0.0 696 * 697 * @param string $content The content to parse. 698 * @return string The parsed and filtered content. 699 */ 700 function excerpt_remove_blocks( $content ) { 701 $allowed_inner_blocks = array( 702 // Classic blocks have their blockName set to null. 703 null, 704 'core/freeform', 705 'core/heading', 706 'core/html', 707 'core/list', 708 'core/media-text', 709 'core/paragraph', 710 'core/preformatted', 711 'core/pullquote', 712 'core/quote', 713 'core/table', 714 'core/verse', 715 ); 716 717 $allowed_wrapper_blocks = array( 718 'core/columns', 719 'core/column', 720 'core/group', 721 ); 722 723 /** 724 * Filters the list of blocks that can be used as wrapper blocks, allowing 725 * excerpts to be generated from the `innerBlocks` of these wrappers. 726 * 727 * @since 5.8.0 728 * 729 * @param array $allowed_wrapper_blocks The list of allowed wrapper blocks. 730 */ 731 $allowed_wrapper_blocks = apply_filters( 'excerpt_allowed_wrapper_blocks', $allowed_wrapper_blocks ); 732 733 $allowed_blocks = array_merge( $allowed_inner_blocks, $allowed_wrapper_blocks ); 734 735 /** 736 * Filters the list of blocks that can contribute to the excerpt. 737 * 738 * If a dynamic block is added to this list, it must not generate another 739 * excerpt, as this will cause an infinite loop to occur. 740 * 741 * @since 5.0.0 742 * 743 * @param array $allowed_blocks The list of allowed blocks. 744 */ 745 $allowed_blocks = apply_filters( 'excerpt_allowed_blocks', $allowed_blocks ); 746 $blocks = parse_blocks( $content ); 747 $output = ''; 748 749 foreach ( $blocks as $block ) { 750 if ( in_array( $block['blockName'], $allowed_blocks, true ) ) { 751 if ( ! empty( $block['innerBlocks'] ) ) { 752 if ( in_array( $block['blockName'], $allowed_wrapper_blocks, true ) ) { 753 $output .= _excerpt_render_inner_blocks( $block, $allowed_blocks ); 754 continue; 755 } 756 757 // Skip the block if it has disallowed or nested inner blocks. 758 foreach ( $block['innerBlocks'] as $inner_block ) { 759 if ( 760 ! in_array( $inner_block['blockName'], $allowed_inner_blocks, true ) || 761 ! empty( $inner_block['innerBlocks'] ) 762 ) { 763 continue 2; 764 } 765 } 766 } 767 768 $output .= render_block( $block ); 769 } 770 } 771 772 return $output; 773 } 774 775 /** 776 * Render inner blocks from the allowed wrapper blocks 777 * for generating an excerpt. 778 * 779 * @since 5.8 780 * @access private 781 * 782 * @param array $parsed_block The parsed block. 783 * @param array $allowed_blocks The list of allowed inner blocks. 784 * @return string The rendered inner blocks. 785 */ 786 function _excerpt_render_inner_blocks( $parsed_block, $allowed_blocks ) { 787 $output = ''; 788 789 foreach ( $parsed_block['innerBlocks'] as $inner_block ) { 790 if ( ! in_array( $inner_block['blockName'], $allowed_blocks, true ) ) { 791 continue; 792 } 793 794 if ( empty( $inner_block['innerBlocks'] ) ) { 795 $output .= render_block( $inner_block ); 796 } else { 797 $output .= _excerpt_render_inner_blocks( $inner_block, $allowed_blocks ); 798 } 799 } 800 801 return $output; 802 } 803 804 /** 805 * Renders a single block into a HTML string. 806 * 807 * @since 5.0.0 808 * 809 * @global WP_Post $post The post to edit. 810 * 811 * @param array $parsed_block A single parsed block object. 812 * @return string String of rendered HTML. 813 */ 814 function render_block( $parsed_block ) { 815 global $post; 816 817 /** 818 * Allows render_block() to be short-circuited, by returning a non-null value. 819 * 820 * @since 5.1.0 821 * 822 * @param string|null $pre_render The pre-rendered content. Default null. 823 * @param array $parsed_block The block being rendered. 824 */ 825 $pre_render = apply_filters( 'pre_render_block', null, $parsed_block ); 826 if ( ! is_null( $pre_render ) ) { 827 return $pre_render; 828 } 829 830 $source_block = $parsed_block; 831 832 /** 833 * Filters the block being rendered in render_block(), before it's processed. 834 * 835 * @since 5.1.0 836 * 837 * @param array $parsed_block The block being rendered. 838 * @param array $source_block An un-modified copy of $parsed_block, as it appeared in the source content. 839 */ 840 $parsed_block = apply_filters( 'render_block_data', $parsed_block, $source_block ); 841 842 $context = array(); 843 844 if ( $post instanceof WP_Post ) { 845 $context['postId'] = $post->ID; 846 847 /* 848 * The `postType` context is largely unnecessary server-side, since the ID 849 * is usually sufficient on its own. That being said, since a block's 850 * manifest is expected to be shared between the server and the client, 851 * it should be included to consistently fulfill the expectation. 852 */ 853 $context['postType'] = $post->post_type; 854 } 855 856 /** 857 * Filters the default context provided to a rendered block. 858 * 859 * @since 5.5.0 860 * 861 * @param array $context Default context. 862 * @param array $parsed_block Block being rendered, filtered by `render_block_data`. 863 */ 864 $context = apply_filters( 'render_block_context', $context, $parsed_block ); 865 866 $block = new WP_Block( $parsed_block, $context ); 867 868 return $block->render(); 869 } 870 871 /** 872 * Parses blocks out of a content string. 873 * 874 * @since 5.0.0 875 * 876 * @param string $content Post content. 877 * @return array[] Array of parsed block objects. 878 */ 879 function parse_blocks( $content ) { 880 /** 881 * Filter to allow plugins to replace the server-side block parser 882 * 883 * @since 5.0.0 884 * 885 * @param string $parser_class Name of block parser class. 886 */ 887 $parser_class = apply_filters( 'block_parser_class', 'WP_Block_Parser' ); 888 889 $parser = new $parser_class(); 890 return $parser->parse( $content ); 891 } 892 893 /** 894 * Parses dynamic blocks out of `post_content` and re-renders them. 895 * 896 * @since 5.0.0 897 * 898 * @param string $content Post content. 899 * @return string Updated post content. 900 */ 901 function do_blocks( $content ) { 902 $blocks = parse_blocks( $content ); 903 $output = ''; 904 905 foreach ( $blocks as $block ) { 906 $output .= render_block( $block ); 907 } 908 909 // If there are blocks in this content, we shouldn't run wpautop() on it later. 910 $priority = has_filter( 'the_content', 'wpautop' ); 911 if ( false !== $priority && doing_filter( 'the_content' ) && has_blocks( $content ) ) { 912 remove_filter( 'the_content', 'wpautop', $priority ); 913 add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 ); 914 } 915 916 return $output; 917 } 918 919 /** 920 * If do_blocks() needs to remove wpautop() from the `the_content` filter, this re-adds it afterwards, 921 * for subsequent `the_content` usage. 922 * 923 * @access private 924 * 925 * @since 5.0.0 926 * 927 * @param string $content The post content running through this filter. 928 * @return string The unmodified content. 929 */ 930 function _restore_wpautop_hook( $content ) { 931 $current_priority = has_filter( 'the_content', '_restore_wpautop_hook' ); 932 933 add_filter( 'the_content', 'wpautop', $current_priority - 1 ); 934 remove_filter( 'the_content', '_restore_wpautop_hook', $current_priority ); 935 936 return $content; 937 } 938 939 /** 940 * Returns the current version of the block format that the content string is using. 941 * 942 * If the string doesn't contain blocks, it returns 0. 943 * 944 * @since 5.0.0 945 * 946 * @param string $content Content to test. 947 * @return int The block format version is 1 if the content contains one or more blocks, 0 otherwise. 948 */ 949 function block_version( $content ) { 950 return has_blocks( $content ) ? 1 : 0; 951 } 952 953 /** 954 * Registers a new block style. 955 * 956 * @since 5.3.0 957 * 958 * @param string $block_name Block type name including namespace. 959 * @param array $style_properties Array containing the properties of the style name, 960 * label, style (name of the stylesheet to be enqueued), 961 * inline_style (string containing the CSS to be added). 962 * @return bool True if the block style was registered with success and false otherwise. 963 */ 964 function register_block_style( $block_name, $style_properties ) { 965 return WP_Block_Styles_Registry::get_instance()->register( $block_name, $style_properties ); 966 } 967 968 /** 969 * Unregisters a block style. 970 * 971 * @since 5.3.0 972 * 973 * @param string $block_name Block type name including namespace. 974 * @param string $block_style_name Block style name. 975 * @return bool True if the block style was unregistered with success and false otherwise. 976 */ 977 function unregister_block_style( $block_name, $block_style_name ) { 978 return WP_Block_Styles_Registry::get_instance()->unregister( $block_name, $block_style_name ); 979 } 980 981 /** 982 * Checks whether the current block type supports the feature requested. 983 * 984 * @since 5.8.0 985 * 986 * @param WP_Block_Type $block_type Block type to check for support. 987 * @param string $feature Name of the feature to check support for. 988 * @param mixed $default Fallback value for feature support, defaults to false. 989 * 990 * @return boolean Whether or not the feature is supported. 991 */ 992 function block_has_support( $block_type, $feature, $default = false ) { 993 $block_support = $default; 994 if ( $block_type && property_exists( $block_type, 'supports' ) ) { 995 $block_support = _wp_array_get( $block_type->supports, $feature, $default ); 996 } 997 998 return true === $block_support || is_array( $block_support ); 999 } 1000 1001 /** 1002 * Converts typography keys declared under `supports.*` to `supports.typography.*`. 1003 * 1004 * Displays a `_doing_it_wrong()` notice when a block using the older format is detected. 1005 * 1006 * @since 5.8.0 1007 * 1008 * @param array $metadata Metadata for registering a block type. 1009 * @return array Filtered metadata for registering a block type. 1010 */ 1011 function wp_migrate_old_typography_shape( $metadata ) { 1012 if ( ! isset( $metadata['supports'] ) ) { 1013 return $metadata; 1014 } 1015 1016 $typography_keys = array( 1017 '__experimentalFontFamily', 1018 '__experimentalFontStyle', 1019 '__experimentalFontWeight', 1020 '__experimentalLetterSpacing', 1021 '__experimentalTextDecoration', 1022 '__experimentalTextTransform', 1023 'fontSize', 1024 'lineHeight', 1025 ); 1026 1027 foreach ( $typography_keys as $typography_key ) { 1028 $support_for_key = _wp_array_get( $metadata['supports'], array( $typography_key ), null ); 1029 1030 if ( null !== $support_for_key ) { 1031 _doing_it_wrong( 1032 'register_block_type_from_metadata()', 1033 sprintf( 1034 /* translators: 1: Block type, 2: Typography supports key, e.g: fontSize, lineHeight, etc. 3: block.json, 4: Old metadata key, 5: New metadata key. */ 1035 __( 'Block "%1$s" is declaring %2$s support in %3$s file under %4$s. %2$s support is now declared under %5$s.' ), 1036 $metadata['name'], 1037 "<code>$typography_key</code>", 1038 '<code>block.json</code>', 1039 "<code>supports.$typography_key</code>", 1040 "<code>supports.typography.$typography_key</code>" 1041 ), 1042 '5.8.0' 1043 ); 1044 1045 _wp_array_set( $metadata['supports'], array( 'typography', $typography_key ), $support_for_key ); 1046 unset( $metadata['supports'][ $typography_key ] ); 1047 } 1048 } 1049 1050 return $metadata; 1051 } 1052 1053 /** 1054 * Helper function that constructs a WP_Query args array from 1055 * a `Query` block properties. 1056 * 1057 * It's used in Query Loop, Query Pagination Numbers and Query Pagination Next blocks. 1058 * 1059 * @since 5.8.0 1060 * 1061 * @param WP_Block $block Block instance. 1062 * @param int $page Current query's page. 1063 * 1064 * @return array Returns the constructed WP_Query arguments. 1065 */ 1066 function build_query_vars_from_query_block( $block, $page ) { 1067 $query = array( 1068 'post_type' => 'post', 1069 'order' => 'DESC', 1070 'orderby' => 'date', 1071 'post__not_in' => array(), 1072 ); 1073 1074 if ( isset( $block->context['query'] ) ) { 1075 if ( ! empty( $block->context['query']['postType'] ) ) { 1076 $post_type_param = $block->context['query']['postType']; 1077 if ( is_post_type_viewable( $post_type_param ) ) { 1078 $query['post_type'] = $post_type_param; 1079 } 1080 } 1081 if ( isset( $block->context['query']['sticky'] ) && ! empty( $block->context['query']['sticky'] ) ) { 1082 $sticky = get_option( 'sticky_posts' ); 1083 if ( 'only' === $block->context['query']['sticky'] ) { 1084 $query['post__in'] = $sticky; 1085 } else { 1086 $query['post__not_in'] = array_merge( $query['post__not_in'], $sticky ); 1087 } 1088 } 1089 if ( ! empty( $block->context['query']['exclude'] ) ) { 1090 $excluded_post_ids = array_map( 'intval', $block->context['query']['exclude'] ); 1091 $excluded_post_ids = array_filter( $excluded_post_ids ); 1092 $query['post__not_in'] = array_merge( $query['post__not_in'], $excluded_post_ids ); 1093 } 1094 if ( 1095 isset( $block->context['query']['perPage'] ) && 1096 is_numeric( $block->context['query']['perPage'] ) 1097 ) { 1098 $per_page = absint( $block->context['query']['perPage'] ); 1099 $offset = 0; 1100 1101 if ( 1102 isset( $block->context['query']['offset'] ) && 1103 is_numeric( $block->context['query']['offset'] ) 1104 ) { 1105 $offset = absint( $block->context['query']['offset'] ); 1106 } 1107 1108 $query['offset'] = ( $per_page * ( $page - 1 ) ) + $offset; 1109 $query['posts_per_page'] = $per_page; 1110 } 1111 if ( ! empty( $block->context['query']['categoryIds'] ) ) { 1112 $term_ids = array_map( 'intval', $block->context['query']['categoryIds'] ); 1113 $term_ids = array_filter( $term_ids ); 1114 $query['category__in'] = $term_ids; 1115 } 1116 if ( ! empty( $block->context['query']['tagIds'] ) ) { 1117 $term_ids = array_map( 'intval', $block->context['query']['tagIds'] ); 1118 $term_ids = array_filter( $term_ids ); 1119 $query['tag__in'] = $term_ids; 1120 } 1121 if ( 1122 isset( $block->context['query']['order'] ) && 1123 in_array( strtoupper( $block->context['query']['order'] ), array( 'ASC', 'DESC' ), true ) 1124 ) { 1125 $query['order'] = strtoupper( $block->context['query']['order'] ); 1126 } 1127 if ( isset( $block->context['query']['orderBy'] ) ) { 1128 $query['orderby'] = $block->context['query']['orderBy']; 1129 } 1130 if ( 1131 isset( $block->context['query']['author'] ) && 1132 (int) $block->context['query']['author'] > 0 1133 ) { 1134 $query['author'] = (int) $block->context['query']['author']; 1135 } 1136 if ( ! empty( $block->context['query']['search'] ) ) { 1137 $query['s'] = $block->context['query']['search']; 1138 } 1139 } 1140 return $query; 1141 }