[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 ( ! str_starts_with( $asset_handle_or_path, $path_prefix ) ) { 21 return $asset_handle_or_path; 22 } 23 $path = substr( 24 $asset_handle_or_path, 25 strlen( $path_prefix ) 26 ); 27 if ( str_starts_with( $path, './' ) ) { 28 $path = substr( $path, 2 ); 29 } 30 return $path; 31 } 32 33 /** 34 * Generates the name for an asset based on the name of the block 35 * and the field name provided. 36 * 37 * @since 5.5.0 38 * @since 6.1.0 Added `$index` parameter. 39 * @since 6.5.0 Added support for `viewScriptModule` field. 40 * 41 * @param string $block_name Name of the block. 42 * @param string $field_name Name of the metadata field. 43 * @param int $index Optional. Index of the asset when multiple items passed. 44 * Default 0. 45 * @return string Generated asset name for the block's field. 46 */ 47 function generate_block_asset_handle( $block_name, $field_name, $index = 0 ) { 48 if ( str_starts_with( $block_name, 'core/' ) ) { 49 $asset_handle = str_replace( 'core/', 'wp-block-', $block_name ); 50 if ( str_starts_with( $field_name, 'editor' ) ) { 51 $asset_handle .= '-editor'; 52 } 53 if ( str_starts_with( $field_name, 'view' ) ) { 54 $asset_handle .= '-view'; 55 } 56 if ( str_ends_with( strtolower( $field_name ), 'scriptmodule' ) ) { 57 $asset_handle .= '-script-module'; 58 } 59 if ( $index > 0 ) { 60 $asset_handle .= '-' . ( $index + 1 ); 61 } 62 return $asset_handle; 63 } 64 65 $field_mappings = array( 66 'editorScript' => 'editor-script', 67 'editorStyle' => 'editor-style', 68 'script' => 'script', 69 'style' => 'style', 70 'viewScript' => 'view-script', 71 'viewScriptModule' => 'view-script-module', 72 'viewStyle' => 'view-style', 73 ); 74 $asset_handle = str_replace( '/', '-', $block_name ) . 75 '-' . $field_mappings[ $field_name ]; 76 if ( $index > 0 ) { 77 $asset_handle .= '-' . ( $index + 1 ); 78 } 79 return $asset_handle; 80 } 81 82 /** 83 * Gets the URL to a block asset. 84 * 85 * @since 6.4.0 86 * 87 * @param string $path A normalized path to a block asset. 88 * @return string|false The URL to the block asset or false on failure. 89 */ 90 function get_block_asset_url( $path ) { 91 if ( empty( $path ) ) { 92 return false; 93 } 94 95 // Path needs to be normalized to work in Windows env. 96 static $wpinc_path_norm = ''; 97 if ( ! $wpinc_path_norm ) { 98 $wpinc_path_norm = wp_normalize_path( realpath( ABSPATH . WPINC ) ); 99 } 100 101 if ( str_starts_with( $path, $wpinc_path_norm ) ) { 102 return includes_url( str_replace( $wpinc_path_norm, '', $path ) ); 103 } 104 105 static $template_paths_norm = array(); 106 107 $template = get_template(); 108 if ( ! isset( $template_paths_norm[ $template ] ) ) { 109 $template_paths_norm[ $template ] = wp_normalize_path( realpath( get_template_directory() ) ); 110 } 111 112 if ( str_starts_with( $path, trailingslashit( $template_paths_norm[ $template ] ) ) ) { 113 return get_theme_file_uri( str_replace( $template_paths_norm[ $template ], '', $path ) ); 114 } 115 116 if ( is_child_theme() ) { 117 $stylesheet = get_stylesheet(); 118 if ( ! isset( $template_paths_norm[ $stylesheet ] ) ) { 119 $template_paths_norm[ $stylesheet ] = wp_normalize_path( realpath( get_stylesheet_directory() ) ); 120 } 121 122 if ( str_starts_with( $path, trailingslashit( $template_paths_norm[ $stylesheet ] ) ) ) { 123 return get_theme_file_uri( str_replace( $template_paths_norm[ $stylesheet ], '', $path ) ); 124 } 125 } 126 127 return plugins_url( basename( $path ), $path ); 128 } 129 130 /** 131 * Finds a script module ID for the selected block metadata field. It detects 132 * when a path to file was provided and optionally finds a corresponding asset 133 * file with details necessary to register the script module under with an 134 * automatically generated module ID. It returns unprocessed script module 135 * ID otherwise. 136 * 137 * @since 6.5.0 138 * 139 * @param array $metadata Block metadata. 140 * @param string $field_name Field name to pick from metadata. 141 * @param int $index Optional. Index of the script module ID to register when multiple 142 * items passed. Default 0. 143 * @return string|false Script module ID or false on failure. 144 */ 145 function register_block_script_module_id( $metadata, $field_name, $index = 0 ) { 146 if ( empty( $metadata[ $field_name ] ) ) { 147 return false; 148 } 149 150 $module_id = $metadata[ $field_name ]; 151 if ( is_array( $module_id ) ) { 152 if ( empty( $module_id[ $index ] ) ) { 153 return false; 154 } 155 $module_id = $module_id[ $index ]; 156 } 157 158 $module_path = remove_block_asset_path_prefix( $module_id ); 159 if ( $module_id === $module_path ) { 160 return $module_id; 161 } 162 163 $path = dirname( $metadata['file'] ); 164 $module_asset_raw_path = $path . '/' . substr_replace( $module_path, '.asset.php', - strlen( '.js' ) ); 165 $module_id = generate_block_asset_handle( $metadata['name'], $field_name, $index ); 166 $module_asset_path = wp_normalize_path( 167 realpath( $module_asset_raw_path ) 168 ); 169 170 $module_path_norm = wp_normalize_path( realpath( $path . '/' . $module_path ) ); 171 $module_uri = get_block_asset_url( $module_path_norm ); 172 173 $module_asset = ! empty( $module_asset_path ) ? require $module_asset_path : array(); 174 $module_dependencies = isset( $module_asset['dependencies'] ) ? $module_asset['dependencies'] : array(); 175 $block_version = isset( $metadata['version'] ) ? $metadata['version'] : false; 176 $module_version = isset( $module_asset['version'] ) ? $module_asset['version'] : $block_version; 177 178 wp_register_script_module( 179 $module_id, 180 $module_uri, 181 $module_dependencies, 182 $module_version 183 ); 184 185 return $module_id; 186 } 187 188 /** 189 * Finds a script handle for the selected block metadata field. It detects 190 * when a path to file was provided and optionally finds a corresponding asset 191 * file with details necessary to register the script under automatically 192 * generated handle name. It returns unprocessed script handle otherwise. 193 * 194 * @since 5.5.0 195 * @since 6.1.0 Added `$index` parameter. 196 * @since 6.5.0 The asset file is optional. Added script handle support in the asset file. 197 * 198 * @param array $metadata Block metadata. 199 * @param string $field_name Field name to pick from metadata. 200 * @param int $index Optional. Index of the script to register when multiple items passed. 201 * Default 0. 202 * @return string|false Script handle provided directly or created through 203 * script's registration, or false on failure. 204 */ 205 function register_block_script_handle( $metadata, $field_name, $index = 0 ) { 206 if ( empty( $metadata[ $field_name ] ) ) { 207 return false; 208 } 209 210 $script_handle_or_path = $metadata[ $field_name ]; 211 if ( is_array( $script_handle_or_path ) ) { 212 if ( empty( $script_handle_or_path[ $index ] ) ) { 213 return false; 214 } 215 $script_handle_or_path = $script_handle_or_path[ $index ]; 216 } 217 218 $script_path = remove_block_asset_path_prefix( $script_handle_or_path ); 219 if ( $script_handle_or_path === $script_path ) { 220 return $script_handle_or_path; 221 } 222 223 $path = dirname( $metadata['file'] ); 224 $script_asset_raw_path = $path . '/' . substr_replace( $script_path, '.asset.php', - strlen( '.js' ) ); 225 $script_asset_path = wp_normalize_path( 226 realpath( $script_asset_raw_path ) 227 ); 228 229 // Asset file for blocks is optional. See https://core.trac.wordpress.org/ticket/60460. 230 $script_asset = ! empty( $script_asset_path ) ? require $script_asset_path : array(); 231 $script_handle = isset( $script_asset['handle'] ) ? 232 $script_asset['handle'] : 233 generate_block_asset_handle( $metadata['name'], $field_name, $index ); 234 if ( wp_script_is( $script_handle, 'registered' ) ) { 235 return $script_handle; 236 } 237 238 $script_path_norm = wp_normalize_path( realpath( $path . '/' . $script_path ) ); 239 $script_uri = get_block_asset_url( $script_path_norm ); 240 $script_dependencies = isset( $script_asset['dependencies'] ) ? $script_asset['dependencies'] : array(); 241 $block_version = isset( $metadata['version'] ) ? $metadata['version'] : false; 242 $script_version = isset( $script_asset['version'] ) ? $script_asset['version'] : $block_version; 243 $script_args = array(); 244 if ( 'viewScript' === $field_name && $script_uri ) { 245 $script_args['strategy'] = 'defer'; 246 } 247 248 $result = wp_register_script( 249 $script_handle, 250 $script_uri, 251 $script_dependencies, 252 $script_version, 253 $script_args 254 ); 255 if ( ! $result ) { 256 return false; 257 } 258 259 if ( ! empty( $metadata['textdomain'] ) && in_array( 'wp-i18n', $script_dependencies, true ) ) { 260 wp_set_script_translations( $script_handle, $metadata['textdomain'] ); 261 } 262 263 return $script_handle; 264 } 265 266 /** 267 * Finds a style handle for the block metadata field. It detects when a path 268 * to file was provided and registers the style under automatically 269 * generated handle name. It returns unprocessed style handle otherwise. 270 * 271 * @since 5.5.0 272 * @since 6.1.0 Added `$index` parameter. 273 * 274 * @param array $metadata Block metadata. 275 * @param string $field_name Field name to pick from metadata. 276 * @param int $index Optional. Index of the style to register when multiple items passed. 277 * Default 0. 278 * @return string|false Style handle provided directly or created through 279 * style's registration, or false on failure. 280 */ 281 function register_block_style_handle( $metadata, $field_name, $index = 0 ) { 282 if ( empty( $metadata[ $field_name ] ) ) { 283 return false; 284 } 285 286 $style_handle = $metadata[ $field_name ]; 287 if ( is_array( $style_handle ) ) { 288 if ( empty( $style_handle[ $index ] ) ) { 289 return false; 290 } 291 $style_handle = $style_handle[ $index ]; 292 } 293 294 $style_handle_name = generate_block_asset_handle( $metadata['name'], $field_name, $index ); 295 // If the style handle is already registered, skip re-registering. 296 if ( wp_style_is( $style_handle_name, 'registered' ) ) { 297 return $style_handle_name; 298 } 299 300 static $wpinc_path_norm = ''; 301 if ( ! $wpinc_path_norm ) { 302 $wpinc_path_norm = wp_normalize_path( realpath( ABSPATH . WPINC ) ); 303 } 304 305 $is_core_block = isset( $metadata['file'] ) && str_starts_with( $metadata['file'], $wpinc_path_norm ); 306 // Skip registering individual styles for each core block when a bundled version provided. 307 if ( $is_core_block && ! wp_should_load_separate_core_block_assets() ) { 308 return false; 309 } 310 311 $style_path = remove_block_asset_path_prefix( $style_handle ); 312 $is_style_handle = $style_handle === $style_path; 313 // Allow only passing style handles for core blocks. 314 if ( $is_core_block && ! $is_style_handle ) { 315 return false; 316 } 317 // Return the style handle unless it's the first item for every core block that requires special treatment. 318 if ( $is_style_handle && ! ( $is_core_block && 0 === $index ) ) { 319 return $style_handle; 320 } 321 322 // Check whether styles should have a ".min" suffix or not. 323 $suffix = SCRIPT_DEBUG ? '' : '.min'; 324 if ( $is_core_block ) { 325 $style_path = ( 'editorStyle' === $field_name ) ? "editor{$suffix}.css" : "style{$suffix}.css"; 326 } 327 328 $style_path_norm = wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $style_path ) ); 329 $style_uri = get_block_asset_url( $style_path_norm ); 330 331 $version = ! $is_core_block && isset( $metadata['version'] ) ? $metadata['version'] : false; 332 $result = wp_register_style( 333 $style_handle_name, 334 $style_uri, 335 array(), 336 $version 337 ); 338 if ( ! $result ) { 339 return false; 340 } 341 342 if ( $style_uri ) { 343 wp_style_add_data( $style_handle_name, 'path', $style_path_norm ); 344 345 if ( $is_core_block ) { 346 $rtl_file = str_replace( "{$suffix}.css", "-rtl{$suffix}.css", $style_path_norm ); 347 } else { 348 $rtl_file = str_replace( '.css', '-rtl.css', $style_path_norm ); 349 } 350 351 if ( is_rtl() && file_exists( $rtl_file ) ) { 352 wp_style_add_data( $style_handle_name, 'rtl', 'replace' ); 353 wp_style_add_data( $style_handle_name, 'suffix', $suffix ); 354 wp_style_add_data( $style_handle_name, 'path', $rtl_file ); 355 } 356 } 357 358 return $style_handle_name; 359 } 360 361 /** 362 * Gets i18n schema for block's metadata read from `block.json` file. 363 * 364 * @since 5.9.0 365 * 366 * @return object The schema for block's metadata. 367 */ 368 function get_block_metadata_i18n_schema() { 369 static $i18n_block_schema; 370 371 if ( ! isset( $i18n_block_schema ) ) { 372 $i18n_block_schema = wp_json_file_decode( __DIR__ . '/block-i18n.json' ); 373 } 374 375 return $i18n_block_schema; 376 } 377 378 /** 379 * Registers a block type from the metadata stored in the `block.json` file. 380 * 381 * @since 5.5.0 382 * @since 5.7.0 Added support for `textdomain` field and i18n handling for all translatable fields. 383 * @since 5.9.0 Added support for `variations` and `viewScript` fields. 384 * @since 6.1.0 Added support for `render` field. 385 * @since 6.3.0 Added `selectors` field. 386 * @since 6.4.0 Added support for `blockHooks` field. 387 * @since 6.5.0 Added support for `allowedBlocks`, `viewScriptModule`, and `viewStyle` fields. 388 * 389 * @param string $file_or_folder Path to the JSON file with metadata definition for 390 * the block or path to the folder where the `block.json` file is located. 391 * If providing the path to a JSON file, the filename must end with `block.json`. 392 * @param array $args Optional. Array of block type arguments. Accepts any public property 393 * of `WP_Block_Type`. See WP_Block_Type::__construct() for information 394 * on accepted arguments. Default empty array. 395 * @return WP_Block_Type|false The registered block type on success, or false on failure. 396 */ 397 function register_block_type_from_metadata( $file_or_folder, $args = array() ) { 398 /* 399 * Get an array of metadata from a PHP file. 400 * This improves performance for core blocks as it's only necessary to read a single PHP file 401 * instead of reading a JSON file per-block, and then decoding from JSON to PHP. 402 * Using a static variable ensures that the metadata is only read once per request. 403 */ 404 static $core_blocks_meta; 405 if ( ! $core_blocks_meta ) { 406 $core_blocks_meta = require ABSPATH . WPINC . '/blocks/blocks-json.php'; 407 } 408 409 $metadata_file = ( ! str_ends_with( $file_or_folder, 'block.json' ) ) ? 410 trailingslashit( $file_or_folder ) . 'block.json' : 411 $file_or_folder; 412 413 $is_core_block = str_starts_with( $file_or_folder, ABSPATH . WPINC ); 414 // If the block is not a core block, the metadata file must exist. 415 $metadata_file_exists = $is_core_block || file_exists( $metadata_file ); 416 if ( ! $metadata_file_exists && empty( $args['name'] ) ) { 417 return false; 418 } 419 420 // Try to get metadata from the static cache for core blocks. 421 $metadata = array(); 422 if ( $is_core_block ) { 423 $core_block_name = str_replace( ABSPATH . WPINC . '/blocks/', '', $file_or_folder ); 424 if ( ! empty( $core_blocks_meta[ $core_block_name ] ) ) { 425 $metadata = $core_blocks_meta[ $core_block_name ]; 426 } 427 } 428 429 // If metadata is not found in the static cache, read it from the file. 430 if ( $metadata_file_exists && empty( $metadata ) ) { 431 $metadata = wp_json_file_decode( $metadata_file, array( 'associative' => true ) ); 432 } 433 434 if ( ! is_array( $metadata ) || ( empty( $metadata['name'] ) && empty( $args['name'] ) ) ) { 435 return false; 436 } 437 438 $metadata['file'] = $metadata_file_exists ? wp_normalize_path( realpath( $metadata_file ) ) : null; 439 440 /** 441 * Filters the metadata provided for registering a block type. 442 * 443 * @since 5.7.0 444 * 445 * @param array $metadata Metadata for registering a block type. 446 */ 447 $metadata = apply_filters( 'block_type_metadata', $metadata ); 448 449 // Add `style` and `editor_style` for core blocks if missing. 450 if ( ! empty( $metadata['name'] ) && str_starts_with( $metadata['name'], 'core/' ) ) { 451 $block_name = str_replace( 'core/', '', $metadata['name'] ); 452 453 if ( ! isset( $metadata['style'] ) ) { 454 $metadata['style'] = "wp-block-$block_name"; 455 } 456 if ( current_theme_supports( 'wp-block-styles' ) && wp_should_load_separate_core_block_assets() ) { 457 $metadata['style'] = (array) $metadata['style']; 458 $metadata['style'][] = "wp-block-{$block_name}-theme"; 459 } 460 if ( ! isset( $metadata['editorStyle'] ) ) { 461 $metadata['editorStyle'] = "wp-block-{$block_name}-editor"; 462 } 463 } 464 465 $settings = array(); 466 $property_mappings = array( 467 'apiVersion' => 'api_version', 468 'name' => 'name', 469 'title' => 'title', 470 'category' => 'category', 471 'parent' => 'parent', 472 'ancestor' => 'ancestor', 473 'icon' => 'icon', 474 'description' => 'description', 475 'keywords' => 'keywords', 476 'attributes' => 'attributes', 477 'providesContext' => 'provides_context', 478 'usesContext' => 'uses_context', 479 'selectors' => 'selectors', 480 'supports' => 'supports', 481 'styles' => 'styles', 482 'variations' => 'variations', 483 'example' => 'example', 484 'allowedBlocks' => 'allowed_blocks', 485 ); 486 $textdomain = ! empty( $metadata['textdomain'] ) ? $metadata['textdomain'] : null; 487 $i18n_schema = get_block_metadata_i18n_schema(); 488 489 foreach ( $property_mappings as $key => $mapped_key ) { 490 if ( isset( $metadata[ $key ] ) ) { 491 $settings[ $mapped_key ] = $metadata[ $key ]; 492 if ( $metadata_file_exists && $textdomain && isset( $i18n_schema->$key ) ) { 493 $settings[ $mapped_key ] = translate_settings_using_i18n_schema( $i18n_schema->$key, $settings[ $key ], $textdomain ); 494 } 495 } 496 } 497 498 if ( ! empty( $metadata['render'] ) ) { 499 $template_path = wp_normalize_path( 500 realpath( 501 dirname( $metadata['file'] ) . '/' . 502 remove_block_asset_path_prefix( $metadata['render'] ) 503 ) 504 ); 505 if ( $template_path ) { 506 /** 507 * Renders the block on the server. 508 * 509 * @since 6.1.0 510 * 511 * @param array $attributes Block attributes. 512 * @param string $content Block default content. 513 * @param WP_Block $block Block instance. 514 * 515 * @return string Returns the block content. 516 */ 517 $settings['render_callback'] = static function ( $attributes, $content, $block ) use ( $template_path ) { 518 ob_start(); 519 require $template_path; 520 return ob_get_clean(); 521 }; 522 } 523 } 524 525 $settings = array_merge( $settings, $args ); 526 527 $script_fields = array( 528 'editorScript' => 'editor_script_handles', 529 'script' => 'script_handles', 530 'viewScript' => 'view_script_handles', 531 ); 532 foreach ( $script_fields as $metadata_field_name => $settings_field_name ) { 533 if ( ! empty( $settings[ $metadata_field_name ] ) ) { 534 $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; 535 } 536 if ( ! empty( $metadata[ $metadata_field_name ] ) ) { 537 $scripts = $metadata[ $metadata_field_name ]; 538 $processed_scripts = array(); 539 if ( is_array( $scripts ) ) { 540 for ( $index = 0; $index < count( $scripts ); $index++ ) { 541 $result = register_block_script_handle( 542 $metadata, 543 $metadata_field_name, 544 $index 545 ); 546 if ( $result ) { 547 $processed_scripts[] = $result; 548 } 549 } 550 } else { 551 $result = register_block_script_handle( 552 $metadata, 553 $metadata_field_name 554 ); 555 if ( $result ) { 556 $processed_scripts[] = $result; 557 } 558 } 559 $settings[ $settings_field_name ] = $processed_scripts; 560 } 561 } 562 563 $module_fields = array( 564 'viewScriptModule' => 'view_script_module_ids', 565 ); 566 foreach ( $module_fields as $metadata_field_name => $settings_field_name ) { 567 if ( ! empty( $settings[ $metadata_field_name ] ) ) { 568 $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; 569 } 570 if ( ! empty( $metadata[ $metadata_field_name ] ) ) { 571 $modules = $metadata[ $metadata_field_name ]; 572 $processed_modules = array(); 573 if ( is_array( $modules ) ) { 574 for ( $index = 0; $index < count( $modules ); $index++ ) { 575 $result = register_block_script_module_id( 576 $metadata, 577 $metadata_field_name, 578 $index 579 ); 580 if ( $result ) { 581 $processed_modules[] = $result; 582 } 583 } 584 } else { 585 $result = register_block_script_module_id( 586 $metadata, 587 $metadata_field_name 588 ); 589 if ( $result ) { 590 $processed_modules[] = $result; 591 } 592 } 593 $settings[ $settings_field_name ] = $processed_modules; 594 } 595 } 596 597 $style_fields = array( 598 'editorStyle' => 'editor_style_handles', 599 'style' => 'style_handles', 600 'viewStyle' => 'view_style_handles', 601 ); 602 foreach ( $style_fields as $metadata_field_name => $settings_field_name ) { 603 if ( ! empty( $settings[ $metadata_field_name ] ) ) { 604 $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; 605 } 606 if ( ! empty( $metadata[ $metadata_field_name ] ) ) { 607 $styles = $metadata[ $metadata_field_name ]; 608 $processed_styles = array(); 609 if ( is_array( $styles ) ) { 610 for ( $index = 0; $index < count( $styles ); $index++ ) { 611 $result = register_block_style_handle( 612 $metadata, 613 $metadata_field_name, 614 $index 615 ); 616 if ( $result ) { 617 $processed_styles[] = $result; 618 } 619 } 620 } else { 621 $result = register_block_style_handle( 622 $metadata, 623 $metadata_field_name 624 ); 625 if ( $result ) { 626 $processed_styles[] = $result; 627 } 628 } 629 $settings[ $settings_field_name ] = $processed_styles; 630 } 631 } 632 633 if ( ! empty( $metadata['blockHooks'] ) ) { 634 /** 635 * Map camelCased position string (from block.json) to snake_cased block type position. 636 * 637 * @var array 638 */ 639 $position_mappings = array( 640 'before' => 'before', 641 'after' => 'after', 642 'firstChild' => 'first_child', 643 'lastChild' => 'last_child', 644 ); 645 646 $settings['block_hooks'] = array(); 647 foreach ( $metadata['blockHooks'] as $anchor_block_name => $position ) { 648 // Avoid infinite recursion (hooking to itself). 649 if ( $metadata['name'] === $anchor_block_name ) { 650 _doing_it_wrong( 651 __METHOD__, 652 __( 'Cannot hook block to itself.' ), 653 '6.4.0' 654 ); 655 continue; 656 } 657 658 if ( ! isset( $position_mappings[ $position ] ) ) { 659 continue; 660 } 661 662 $settings['block_hooks'][ $anchor_block_name ] = $position_mappings[ $position ]; 663 } 664 } 665 666 /** 667 * Filters the settings determined from the block type metadata. 668 * 669 * @since 5.7.0 670 * 671 * @param array $settings Array of determined settings for registering a block type. 672 * @param array $metadata Metadata provided for registering a block type. 673 */ 674 $settings = apply_filters( 'block_type_metadata_settings', $settings, $metadata ); 675 676 $metadata['name'] = ! empty( $settings['name'] ) ? $settings['name'] : $metadata['name']; 677 678 return WP_Block_Type_Registry::get_instance()->register( 679 $metadata['name'], 680 $settings 681 ); 682 } 683 684 /** 685 * Registers a block type. The recommended way is to register a block type using 686 * the metadata stored in the `block.json` file. 687 * 688 * @since 5.0.0 689 * @since 5.8.0 First parameter now accepts a path to the `block.json` file. 690 * 691 * @param string|WP_Block_Type $block_type Block type name including namespace, or alternatively 692 * a path to the JSON file with metadata definition for the block, 693 * or a path to the folder where the `block.json` file is located, 694 * or a complete WP_Block_Type instance. 695 * In case a WP_Block_Type is provided, the $args parameter will be ignored. 696 * @param array $args Optional. Array of block type arguments. Accepts any public property 697 * of `WP_Block_Type`. See WP_Block_Type::__construct() for information 698 * on accepted arguments. Default empty array. 699 * 700 * @return WP_Block_Type|false The registered block type on success, or false on failure. 701 */ 702 function register_block_type( $block_type, $args = array() ) { 703 if ( is_string( $block_type ) && file_exists( $block_type ) ) { 704 return register_block_type_from_metadata( $block_type, $args ); 705 } 706 707 return WP_Block_Type_Registry::get_instance()->register( $block_type, $args ); 708 } 709 710 /** 711 * Unregisters a block type. 712 * 713 * @since 5.0.0 714 * 715 * @param string|WP_Block_Type $name Block type name including namespace, or alternatively 716 * a complete WP_Block_Type instance. 717 * @return WP_Block_Type|false The unregistered block type on success, or false on failure. 718 */ 719 function unregister_block_type( $name ) { 720 return WP_Block_Type_Registry::get_instance()->unregister( $name ); 721 } 722 723 /** 724 * Determines whether a post or content string has blocks. 725 * 726 * This test optimizes for performance rather than strict accuracy, detecting 727 * the pattern of a block but not validating its structure. For strict accuracy, 728 * you should use the block parser on post content. 729 * 730 * @since 5.0.0 731 * 732 * @see parse_blocks() 733 * 734 * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. 735 * Defaults to global $post. 736 * @return bool Whether the post has blocks. 737 */ 738 function has_blocks( $post = null ) { 739 if ( ! is_string( $post ) ) { 740 $wp_post = get_post( $post ); 741 742 if ( ! $wp_post instanceof WP_Post ) { 743 return false; 744 } 745 746 $post = $wp_post->post_content; 747 } 748 749 return str_contains( (string) $post, '<!-- wp:' ); 750 } 751 752 /** 753 * Determines whether a $post or a string contains a specific block type. 754 * 755 * This test optimizes for performance rather than strict accuracy, detecting 756 * whether the block type exists but not validating its structure and not checking 757 * synced patterns (formerly called reusable blocks). For strict accuracy, 758 * you should use the block parser on post content. 759 * 760 * @since 5.0.0 761 * 762 * @see parse_blocks() 763 * 764 * @param string $block_name Full block type to look for. 765 * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. 766 * Defaults to global $post. 767 * @return bool Whether the post content contains the specified block. 768 */ 769 function has_block( $block_name, $post = null ) { 770 if ( ! has_blocks( $post ) ) { 771 return false; 772 } 773 774 if ( ! is_string( $post ) ) { 775 $wp_post = get_post( $post ); 776 if ( $wp_post instanceof WP_Post ) { 777 $post = $wp_post->post_content; 778 } 779 } 780 781 /* 782 * Normalize block name to include namespace, if provided as non-namespaced. 783 * This matches behavior for WordPress 5.0.0 - 5.3.0 in matching blocks by 784 * their serialized names. 785 */ 786 if ( ! str_contains( $block_name, '/' ) ) { 787 $block_name = 'core/' . $block_name; 788 } 789 790 // Test for existence of block by its fully qualified name. 791 $has_block = str_contains( $post, '<!-- wp:' . $block_name . ' ' ); 792 793 if ( ! $has_block ) { 794 /* 795 * If the given block name would serialize to a different name, test for 796 * existence by the serialized form. 797 */ 798 $serialized_block_name = strip_core_block_namespace( $block_name ); 799 if ( $serialized_block_name !== $block_name ) { 800 $has_block = str_contains( $post, '<!-- wp:' . $serialized_block_name . ' ' ); 801 } 802 } 803 804 return $has_block; 805 } 806 807 /** 808 * Returns an array of the names of all registered dynamic block types. 809 * 810 * @since 5.0.0 811 * 812 * @return string[] Array of dynamic block names. 813 */ 814 function get_dynamic_block_names() { 815 $dynamic_block_names = array(); 816 817 $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); 818 foreach ( $block_types as $block_type ) { 819 if ( $block_type->is_dynamic() ) { 820 $dynamic_block_names[] = $block_type->name; 821 } 822 } 823 824 return $dynamic_block_names; 825 } 826 827 /** 828 * Retrieves block types hooked into the given block, grouped by anchor block type and the relative position. 829 * 830 * @since 6.4.0 831 * 832 * @return array[] Array of block types grouped by anchor block type and the relative position. 833 */ 834 function get_hooked_blocks() { 835 $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); 836 $hooked_blocks = array(); 837 foreach ( $block_types as $block_type ) { 838 if ( ! ( $block_type instanceof WP_Block_Type ) || ! is_array( $block_type->block_hooks ) ) { 839 continue; 840 } 841 foreach ( $block_type->block_hooks as $anchor_block_type => $relative_position ) { 842 if ( ! isset( $hooked_blocks[ $anchor_block_type ] ) ) { 843 $hooked_blocks[ $anchor_block_type ] = array(); 844 } 845 if ( ! isset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) ) { 846 $hooked_blocks[ $anchor_block_type ][ $relative_position ] = array(); 847 } 848 $hooked_blocks[ $anchor_block_type ][ $relative_position ][] = $block_type->name; 849 } 850 } 851 852 return $hooked_blocks; 853 } 854 855 /** 856 * Returns the markup for blocks hooked to the given anchor block in a specific relative position. 857 * 858 * @since 6.5.0 859 * @access private 860 * 861 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 862 * @param string $relative_position The relative position of the hooked blocks. 863 * Can be one of 'before', 'after', 'first_child', or 'last_child'. 864 * @param array $hooked_blocks An array of hooked block types, grouped by anchor block and relative position. 865 * @param WP_Block_Template|array $context The block template, template part, or pattern that the anchor block belongs to. 866 * @return string 867 */ 868 function insert_hooked_blocks( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) { 869 $anchor_block_type = $parsed_anchor_block['blockName']; 870 $hooked_block_types = isset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) 871 ? $hooked_blocks[ $anchor_block_type ][ $relative_position ] 872 : array(); 873 874 /** 875 * Filters the list of hooked block types for a given anchor block type and relative position. 876 * 877 * @since 6.4.0 878 * 879 * @param string[] $hooked_block_types The list of hooked block types. 880 * @param string $relative_position The relative position of the hooked blocks. 881 * Can be one of 'before', 'after', 'first_child', or 'last_child'. 882 * @param string $anchor_block_type The anchor block type. 883 * @param WP_Block_Template|WP_Post|array $context The block template, template part, `wp_navigation` post type, 884 * or pattern that the anchor block belongs to. 885 */ 886 $hooked_block_types = apply_filters( 'hooked_block_types', $hooked_block_types, $relative_position, $anchor_block_type, $context ); 887 888 $markup = ''; 889 foreach ( $hooked_block_types as $hooked_block_type ) { 890 $parsed_hooked_block = array( 891 'blockName' => $hooked_block_type, 892 'attrs' => array(), 893 'innerBlocks' => array(), 894 'innerContent' => array(), 895 ); 896 897 /** 898 * Filters the parsed block array for a given hooked block. 899 * 900 * @since 6.5.0 901 * 902 * @param array|null $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block. 903 * @param string $hooked_block_type The hooked block type name. 904 * @param string $relative_position The relative position of the hooked block. 905 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 906 * @param WP_Block_Template|WP_Post|array $context The block template, template part, `wp_navigation` post type, 907 * or pattern that the anchor block belongs to. 908 */ 909 $parsed_hooked_block = apply_filters( 'hooked_block', $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 910 911 /** 912 * Filters the parsed block array for a given hooked block. 913 * 914 * The dynamic portion of the hook name, `$hooked_block_type`, refers to the block type name of the specific hooked block. 915 * 916 * @since 6.5.0 917 * 918 * @param array|null $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block. 919 * @param string $hooked_block_type The hooked block type name. 920 * @param string $relative_position The relative position of the hooked block. 921 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 922 * @param WP_Block_Template|WP_Post|array $context The block template, template part, `wp_navigation` post type, 923 * or pattern that the anchor block belongs to. 924 */ 925 $parsed_hooked_block = apply_filters( "hooked_block_{$hooked_block_type}", $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 926 927 if ( null === $parsed_hooked_block ) { 928 continue; 929 } 930 931 // It's possible that the filter returned a block of a different type, so we explicitly 932 // look for the original `$hooked_block_type` in the `ignoredHookedBlocks` metadata. 933 if ( 934 ! isset( $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] ) || 935 ! in_array( $hooked_block_type, $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'], true ) 936 ) { 937 $markup .= serialize_block( $parsed_hooked_block ); 938 } 939 } 940 941 return $markup; 942 } 943 944 /** 945 * Adds a list of hooked block types to an anchor block's ignored hooked block types. 946 * 947 * This function is meant for internal use only. 948 * 949 * @since 6.5.0 950 * @access private 951 * 952 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 953 * @param string $relative_position The relative position of the hooked blocks. 954 * Can be one of 'before', 'after', 'first_child', or 'last_child'. 955 * @param array $hooked_blocks An array of hooked block types, grouped by anchor block and relative position. 956 * @param WP_Block_Template|array $context The block template, template part, or pattern that the anchor block belongs to. 957 * @return string An empty string. 958 */ 959 function set_ignored_hooked_blocks_metadata( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) { 960 $anchor_block_type = $parsed_anchor_block['blockName']; 961 $hooked_block_types = isset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) 962 ? $hooked_blocks[ $anchor_block_type ][ $relative_position ] 963 : array(); 964 965 /** This filter is documented in wp-includes/blocks.php */ 966 $hooked_block_types = apply_filters( 'hooked_block_types', $hooked_block_types, $relative_position, $anchor_block_type, $context ); 967 if ( empty( $hooked_block_types ) ) { 968 return ''; 969 } 970 971 foreach ( $hooked_block_types as $index => $hooked_block_type ) { 972 $parsed_hooked_block = array( 973 'blockName' => $hooked_block_type, 974 'attrs' => array(), 975 'innerBlocks' => array(), 976 'innerContent' => array(), 977 ); 978 979 /** This filter is documented in wp-includes/blocks.php */ 980 $parsed_hooked_block = apply_filters( 'hooked_block', $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 981 982 /** This filter is documented in wp-includes/blocks.php */ 983 $parsed_hooked_block = apply_filters( "hooked_block_{$hooked_block_type}", $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 984 985 if ( null === $parsed_hooked_block ) { 986 unset( $hooked_block_types[ $index ] ); 987 } 988 } 989 990 $previously_ignored_hooked_blocks = isset( $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] ) 991 ? $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] 992 : array(); 993 994 $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] = array_unique( 995 array_merge( 996 $previously_ignored_hooked_blocks, 997 $hooked_block_types 998 ) 999 ); 1000 1001 // Markup for the hooked blocks has already been created (in `insert_hooked_blocks`). 1002 return ''; 1003 } 1004 1005 /** 1006 * Returns a function that injects the theme attribute into, and hooked blocks before, a given block. 1007 * 1008 * The returned function can be used as `$pre_callback` argument to `traverse_and_serialize_block(s)`, 1009 * where it will inject the `theme` attribute into all Template Part blocks, and prepend the markup for 1010 * any blocks hooked `before` the given block and as its parent's `first_child`, respectively. 1011 * 1012 * This function is meant for internal use only. 1013 * 1014 * @since 6.4.0 1015 * @since 6.5.0 Added $callback argument. 1016 * @access private 1017 * 1018 * @param array $hooked_blocks An array of blocks hooked to another given block. 1019 * @param WP_Block_Template|WP_Post|array $context A block template, template part, `wp_navigation` post object, 1020 * or pattern that the blocks belong to. 1021 * @param callable $callback A function that will be called for each block to generate 1022 * the markup for a given list of blocks that are hooked to it. 1023 * Default: 'insert_hooked_blocks'. 1024 * @return callable A function that returns the serialized markup for the given block, 1025 * including the markup for any hooked blocks before it. 1026 */ 1027 function make_before_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) { 1028 /** 1029 * Injects hooked blocks before the given block, injects the `theme` attribute into Template Part blocks, and returns the serialized markup. 1030 * 1031 * If the current block is a Template Part block, inject the `theme` attribute. 1032 * Furthermore, prepend the markup for any blocks hooked `before` the given block and as its parent's 1033 * `first_child`, respectively, to the serialized markup for the given block. 1034 * 1035 * @param array $block The block to inject the theme attribute into, and hooked blocks before. Passed by reference. 1036 * @param array $parent_block The parent block of the given block. Passed by reference. Default null. 1037 * @param array $prev The previous sibling block of the given block. Default null. 1038 * @return string The serialized markup for the given block, with the markup for any hooked blocks prepended to it. 1039 */ 1040 return function ( &$block, &$parent_block = null, $prev = null ) use ( $hooked_blocks, $context, $callback ) { 1041 _inject_theme_attribute_in_template_part_block( $block ); 1042 1043 $markup = ''; 1044 1045 if ( $parent_block && ! $prev ) { 1046 // Candidate for first-child insertion. 1047 $markup .= call_user_func_array( 1048 $callback, 1049 array( &$parent_block, 'first_child', $hooked_blocks, $context ) 1050 ); 1051 } 1052 1053 $markup .= call_user_func_array( 1054 $callback, 1055 array( &$block, 'before', $hooked_blocks, $context ) 1056 ); 1057 1058 return $markup; 1059 }; 1060 } 1061 1062 /** 1063 * Returns a function that injects the hooked blocks after a given block. 1064 * 1065 * The returned function can be used as `$post_callback` argument to `traverse_and_serialize_block(s)`, 1066 * where it will append the markup for any blocks hooked `after` the given block and as its parent's 1067 * `last_child`, respectively. 1068 * 1069 * This function is meant for internal use only. 1070 * 1071 * @since 6.4.0 1072 * @since 6.5.0 Added $callback argument. 1073 * @access private 1074 * 1075 * @param array $hooked_blocks An array of blocks hooked to another block. 1076 * @param WP_Block_Template|WP_Post|array $context A block template, template part, `wp_navigation` post object, 1077 * or pattern that the blocks belong to. 1078 * @param callable $callback A function that will be called for each block to generate 1079 * the markup for a given list of blocks that are hooked to it. 1080 * Default: 'insert_hooked_blocks'. 1081 * @return callable A function that returns the serialized markup for the given block, 1082 * including the markup for any hooked blocks after it. 1083 */ 1084 function make_after_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) { 1085 /** 1086 * Injects hooked blocks after the given block, and returns the serialized markup. 1087 * 1088 * Append the markup for any blocks hooked `after` the given block and as its parent's 1089 * `last_child`, respectively, to the serialized markup for the given block. 1090 * 1091 * @param array $block The block to inject the hooked blocks after. Passed by reference. 1092 * @param array $parent_block The parent block of the given block. Passed by reference. Default null. 1093 * @param array $next The next sibling block of the given block. Default null. 1094 * @return string The serialized markup for the given block, with the markup for any hooked blocks appended to it. 1095 */ 1096 return function ( &$block, &$parent_block = null, $next = null ) use ( $hooked_blocks, $context, $callback ) { 1097 $markup = call_user_func_array( 1098 $callback, 1099 array( &$block, 'after', $hooked_blocks, $context ) 1100 ); 1101 1102 if ( $parent_block && ! $next ) { 1103 // Candidate for last-child insertion. 1104 $markup .= call_user_func_array( 1105 $callback, 1106 array( &$parent_block, 'last_child', $hooked_blocks, $context ) 1107 ); 1108 } 1109 1110 return $markup; 1111 }; 1112 } 1113 1114 /** 1115 * Given an array of attributes, returns a string in the serialized attributes 1116 * format prepared for post content. 1117 * 1118 * The serialized result is a JSON-encoded string, with unicode escape sequence 1119 * substitution for characters which might otherwise interfere with embedding 1120 * the result in an HTML comment. 1121 * 1122 * This function must produce output that remains in sync with the output of 1123 * the serializeAttributes JavaScript function in the block editor in order 1124 * to ensure consistent operation between PHP and JavaScript. 1125 * 1126 * @since 5.3.1 1127 * 1128 * @param array $block_attributes Attributes object. 1129 * @return string Serialized attributes. 1130 */ 1131 function serialize_block_attributes( $block_attributes ) { 1132 $encoded_attributes = wp_json_encode( $block_attributes, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); 1133 $encoded_attributes = preg_replace( '/--/', '\\u002d\\u002d', $encoded_attributes ); 1134 $encoded_attributes = preg_replace( '/</', '\\u003c', $encoded_attributes ); 1135 $encoded_attributes = preg_replace( '/>/', '\\u003e', $encoded_attributes ); 1136 $encoded_attributes = preg_replace( '/&/', '\\u0026', $encoded_attributes ); 1137 // Regex: /\\"/ 1138 $encoded_attributes = preg_replace( '/\\\\"/', '\\u0022', $encoded_attributes ); 1139 1140 return $encoded_attributes; 1141 } 1142 1143 /** 1144 * Returns the block name to use for serialization. This will remove the default 1145 * "core/" namespace from a block name. 1146 * 1147 * @since 5.3.1 1148 * 1149 * @param string|null $block_name Optional. Original block name. Null if the block name is unknown, 1150 * e.g. Classic blocks have their name set to null. Default null. 1151 * @return string Block name to use for serialization. 1152 */ 1153 function strip_core_block_namespace( $block_name = null ) { 1154 if ( is_string( $block_name ) && str_starts_with( $block_name, 'core/' ) ) { 1155 return substr( $block_name, 5 ); 1156 } 1157 1158 return $block_name; 1159 } 1160 1161 /** 1162 * Returns the content of a block, including comment delimiters. 1163 * 1164 * @since 5.3.1 1165 * 1166 * @param string|null $block_name Block name. Null if the block name is unknown, 1167 * e.g. Classic blocks have their name set to null. 1168 * @param array $block_attributes Block attributes. 1169 * @param string $block_content Block save content. 1170 * @return string Comment-delimited block content. 1171 */ 1172 function get_comment_delimited_block_content( $block_name, $block_attributes, $block_content ) { 1173 if ( is_null( $block_name ) ) { 1174 return $block_content; 1175 } 1176 1177 $serialized_block_name = strip_core_block_namespace( $block_name ); 1178 $serialized_attributes = empty( $block_attributes ) ? '' : serialize_block_attributes( $block_attributes ) . ' '; 1179 1180 if ( empty( $block_content ) ) { 1181 return sprintf( '<!-- wp:%s %s/-->', $serialized_block_name, $serialized_attributes ); 1182 } 1183 1184 return sprintf( 1185 '<!-- wp:%s %s-->%s<!-- /wp:%s -->', 1186 $serialized_block_name, 1187 $serialized_attributes, 1188 $block_content, 1189 $serialized_block_name 1190 ); 1191 } 1192 1193 /** 1194 * Returns the content of a block, including comment delimiters, serializing all 1195 * attributes from the given parsed block. 1196 * 1197 * This should be used when preparing a block to be saved to post content. 1198 * Prefer `render_block` when preparing a block for display. Unlike 1199 * `render_block`, this does not evaluate a block's `render_callback`, and will 1200 * instead preserve the markup as parsed. 1201 * 1202 * @since 5.3.1 1203 * 1204 * @param array $block { 1205 * A representative array of a single parsed block object. See WP_Block_Parser_Block. 1206 * 1207 * @type string $blockName Name of block. 1208 * @type array $attrs Attributes from block comment delimiters. 1209 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1210 * have the same structure as this one. 1211 * @type string $innerHTML HTML from inside block comment delimiters. 1212 * @type array $innerContent List of string fragments and null markers where 1213 * inner blocks were found. 1214 * } 1215 * @return string String of rendered HTML. 1216 */ 1217 function serialize_block( $block ) { 1218 $block_content = ''; 1219 1220 $index = 0; 1221 foreach ( $block['innerContent'] as $chunk ) { 1222 $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] ); 1223 } 1224 1225 if ( ! is_array( $block['attrs'] ) ) { 1226 $block['attrs'] = array(); 1227 } 1228 1229 return get_comment_delimited_block_content( 1230 $block['blockName'], 1231 $block['attrs'], 1232 $block_content 1233 ); 1234 } 1235 1236 /** 1237 * Returns a joined string of the aggregate serialization of the given 1238 * parsed blocks. 1239 * 1240 * @since 5.3.1 1241 * 1242 * @param array[] $blocks { 1243 * Array of block structures. 1244 * 1245 * @type array ...$0 { 1246 * A representative array of a single parsed block object. See WP_Block_Parser_Block. 1247 * 1248 * @type string $blockName Name of block. 1249 * @type array $attrs Attributes from block comment delimiters. 1250 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1251 * have the same structure as this one. 1252 * @type string $innerHTML HTML from inside block comment delimiters. 1253 * @type array $innerContent List of string fragments and null markers where 1254 * inner blocks were found. 1255 * } 1256 * } 1257 * @return string String of rendered HTML. 1258 */ 1259 function serialize_blocks( $blocks ) { 1260 return implode( '', array_map( 'serialize_block', $blocks ) ); 1261 } 1262 1263 /** 1264 * Traverses a parsed block tree and applies callbacks before and after serializing it. 1265 * 1266 * Recursively traverses the block and its inner blocks and applies the two callbacks provided as 1267 * arguments, the first one before serializing the block, and the second one after serializing it. 1268 * If either callback returns a string value, it will be prepended and appended to the serialized 1269 * block markup, respectively. 1270 * 1271 * The callbacks will receive a reference to the current block as their first argument, so that they 1272 * can also modify it, and the current block's parent block as second argument. Finally, the 1273 * `$pre_callback` receives the previous block, whereas the `$post_callback` receives 1274 * the next block as third argument. 1275 * 1276 * Serialized blocks are returned including comment delimiters, and with all attributes serialized. 1277 * 1278 * This function should be used when there is a need to modify the saved block, or to inject markup 1279 * into the return value. Prefer `serialize_block` when preparing a block to be saved to post content. 1280 * 1281 * This function is meant for internal use only. 1282 * 1283 * @since 6.4.0 1284 * @access private 1285 * 1286 * @see serialize_block() 1287 * 1288 * @param array $block A representative array of a single parsed block object. See WP_Block_Parser_Block. 1289 * @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized. 1290 * It is called with the following arguments: &$block, $parent_block, $previous_block. 1291 * Its string return value will be prepended to the serialized block markup. 1292 * @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized. 1293 * It is called with the following arguments: &$block, $parent_block, $next_block. 1294 * Its string return value will be appended to the serialized block markup. 1295 * @return string Serialized block markup. 1296 */ 1297 function traverse_and_serialize_block( $block, $pre_callback = null, $post_callback = null ) { 1298 $block_content = ''; 1299 $block_index = 0; 1300 1301 foreach ( $block['innerContent'] as $chunk ) { 1302 if ( is_string( $chunk ) ) { 1303 $block_content .= $chunk; 1304 } else { 1305 $inner_block = $block['innerBlocks'][ $block_index ]; 1306 1307 if ( is_callable( $pre_callback ) ) { 1308 $prev = 0 === $block_index 1309 ? null 1310 : $block['innerBlocks'][ $block_index - 1 ]; 1311 1312 $block_content .= call_user_func_array( 1313 $pre_callback, 1314 array( &$inner_block, &$block, $prev ) 1315 ); 1316 } 1317 1318 if ( is_callable( $post_callback ) ) { 1319 $next = count( $block['innerBlocks'] ) - 1 === $block_index 1320 ? null 1321 : $block['innerBlocks'][ $block_index + 1 ]; 1322 1323 $post_markup = call_user_func_array( 1324 $post_callback, 1325 array( &$inner_block, &$block, $next ) 1326 ); 1327 } 1328 1329 $block_content .= traverse_and_serialize_block( $inner_block, $pre_callback, $post_callback ); 1330 $block_content .= isset( $post_markup ) ? $post_markup : ''; 1331 1332 ++$block_index; 1333 } 1334 } 1335 1336 if ( ! is_array( $block['attrs'] ) ) { 1337 $block['attrs'] = array(); 1338 } 1339 1340 return get_comment_delimited_block_content( 1341 $block['blockName'], 1342 $block['attrs'], 1343 $block_content 1344 ); 1345 } 1346 1347 /** 1348 * Given an array of parsed block trees, applies callbacks before and after serializing them and 1349 * returns their concatenated output. 1350 * 1351 * Recursively traverses the blocks and their inner blocks and applies the two callbacks provided as 1352 * arguments, the first one before serializing a block, and the second one after serializing. 1353 * If either callback returns a string value, it will be prepended and appended to the serialized 1354 * block markup, respectively. 1355 * 1356 * The callbacks will receive a reference to the current block as their first argument, so that they 1357 * can also modify it, and the current block's parent block as second argument. Finally, the 1358 * `$pre_callback` receives the previous block, whereas the `$post_callback` receives 1359 * the next block as third argument. 1360 * 1361 * Serialized blocks are returned including comment delimiters, and with all attributes serialized. 1362 * 1363 * This function should be used when there is a need to modify the saved blocks, or to inject markup 1364 * into the return value. Prefer `serialize_blocks` when preparing blocks to be saved to post content. 1365 * 1366 * This function is meant for internal use only. 1367 * 1368 * @since 6.4.0 1369 * @access private 1370 * 1371 * @see serialize_blocks() 1372 * 1373 * @param array[] $blocks An array of parsed blocks. See WP_Block_Parser_Block. 1374 * @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized. 1375 * It is called with the following arguments: &$block, $parent_block, $previous_block. 1376 * Its string return value will be prepended to the serialized block markup. 1377 * @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized. 1378 * It is called with the following arguments: &$block, $parent_block, $next_block. 1379 * Its string return value will be appended to the serialized block markup. 1380 * @return string Serialized block markup. 1381 */ 1382 function traverse_and_serialize_blocks( $blocks, $pre_callback = null, $post_callback = null ) { 1383 $result = ''; 1384 $parent_block = null; // At the top level, there is no parent block to pass to the callbacks; yet the callbacks expect a reference. 1385 1386 foreach ( $blocks as $index => $block ) { 1387 if ( is_callable( $pre_callback ) ) { 1388 $prev = 0 === $index 1389 ? null 1390 : $blocks[ $index - 1 ]; 1391 1392 $result .= call_user_func_array( 1393 $pre_callback, 1394 array( &$block, &$parent_block, $prev ) 1395 ); 1396 } 1397 1398 if ( is_callable( $post_callback ) ) { 1399 $next = count( $blocks ) - 1 === $index 1400 ? null 1401 : $blocks[ $index + 1 ]; 1402 1403 $post_markup = call_user_func_array( 1404 $post_callback, 1405 array( &$block, &$parent_block, $next ) 1406 ); 1407 } 1408 1409 $result .= traverse_and_serialize_block( $block, $pre_callback, $post_callback ); 1410 $result .= isset( $post_markup ) ? $post_markup : ''; 1411 } 1412 1413 return $result; 1414 } 1415 1416 /** 1417 * Filters and sanitizes block content to remove non-allowable HTML 1418 * from parsed block attribute values. 1419 * 1420 * @since 5.3.1 1421 * 1422 * @param string $text Text that may contain block content. 1423 * @param array[]|string $allowed_html Optional. An array of allowed HTML elements and attributes, 1424 * or a context name such as 'post'. See wp_kses_allowed_html() 1425 * for the list of accepted context names. Default 'post'. 1426 * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. 1427 * Defaults to the result of wp_allowed_protocols(). 1428 * @return string The filtered and sanitized content result. 1429 */ 1430 function filter_block_content( $text, $allowed_html = 'post', $allowed_protocols = array() ) { 1431 $result = ''; 1432 1433 if ( str_contains( $text, '<!--' ) && str_contains( $text, '--->' ) ) { 1434 $text = preg_replace_callback( '%<!--(.*?)--->%', '_filter_block_content_callback', $text ); 1435 } 1436 1437 $blocks = parse_blocks( $text ); 1438 foreach ( $blocks as $block ) { 1439 $block = filter_block_kses( $block, $allowed_html, $allowed_protocols ); 1440 $result .= serialize_block( $block ); 1441 } 1442 1443 return $result; 1444 } 1445 1446 /** 1447 * Callback used for regular expression replacement in filter_block_content(). 1448 * 1449 * @since 6.2.1 1450 * @access private 1451 * 1452 * @param array $matches Array of preg_replace_callback matches. 1453 * @return string Replacement string. 1454 */ 1455 function _filter_block_content_callback( $matches ) { 1456 return '<!--' . rtrim( $matches[1], '-' ) . '-->'; 1457 } 1458 1459 /** 1460 * Filters and sanitizes a parsed block to remove non-allowable HTML 1461 * from block attribute values. 1462 * 1463 * @since 5.3.1 1464 * 1465 * @param WP_Block_Parser_Block $block The parsed block object. 1466 * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, 1467 * or a context name such as 'post'. See wp_kses_allowed_html() 1468 * for the list of accepted context names. 1469 * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. 1470 * Defaults to the result of wp_allowed_protocols(). 1471 * @return array The filtered and sanitized block object result. 1472 */ 1473 function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) { 1474 $block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols ); 1475 1476 if ( is_array( $block['innerBlocks'] ) ) { 1477 foreach ( $block['innerBlocks'] as $i => $inner_block ) { 1478 $block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols ); 1479 } 1480 } 1481 1482 return $block; 1483 } 1484 1485 /** 1486 * Filters and sanitizes a parsed block attribute value to remove 1487 * non-allowable HTML. 1488 * 1489 * @since 5.3.1 1490 * 1491 * @param string[]|string $value The attribute value to filter. 1492 * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, 1493 * or a context name such as 'post'. See wp_kses_allowed_html() 1494 * for the list of accepted context names. 1495 * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. 1496 * Defaults to the result of wp_allowed_protocols(). 1497 * @return string[]|string The filtered and sanitized result. 1498 */ 1499 function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = array() ) { 1500 if ( is_array( $value ) ) { 1501 foreach ( $value as $key => $inner_value ) { 1502 $filtered_key = filter_block_kses_value( $key, $allowed_html, $allowed_protocols ); 1503 $filtered_value = filter_block_kses_value( $inner_value, $allowed_html, $allowed_protocols ); 1504 1505 if ( $filtered_key !== $key ) { 1506 unset( $value[ $key ] ); 1507 } 1508 1509 $value[ $filtered_key ] = $filtered_value; 1510 } 1511 } elseif ( is_string( $value ) ) { 1512 return wp_kses( $value, $allowed_html, $allowed_protocols ); 1513 } 1514 1515 return $value; 1516 } 1517 1518 /** 1519 * Parses blocks out of a content string, and renders those appropriate for the excerpt. 1520 * 1521 * As the excerpt should be a small string of text relevant to the full post content, 1522 * this function renders the blocks that are most likely to contain such text. 1523 * 1524 * @since 5.0.0 1525 * 1526 * @param string $content The content to parse. 1527 * @return string The parsed and filtered content. 1528 */ 1529 function excerpt_remove_blocks( $content ) { 1530 if ( ! has_blocks( $content ) ) { 1531 return $content; 1532 } 1533 1534 $allowed_inner_blocks = array( 1535 // Classic blocks have their blockName set to null. 1536 null, 1537 'core/freeform', 1538 'core/heading', 1539 'core/html', 1540 'core/list', 1541 'core/media-text', 1542 'core/paragraph', 1543 'core/preformatted', 1544 'core/pullquote', 1545 'core/quote', 1546 'core/table', 1547 'core/verse', 1548 ); 1549 1550 $allowed_wrapper_blocks = array( 1551 'core/columns', 1552 'core/column', 1553 'core/group', 1554 ); 1555 1556 /** 1557 * Filters the list of blocks that can be used as wrapper blocks, allowing 1558 * excerpts to be generated from the `innerBlocks` of these wrappers. 1559 * 1560 * @since 5.8.0 1561 * 1562 * @param string[] $allowed_wrapper_blocks The list of names of allowed wrapper blocks. 1563 */ 1564 $allowed_wrapper_blocks = apply_filters( 'excerpt_allowed_wrapper_blocks', $allowed_wrapper_blocks ); 1565 1566 $allowed_blocks = array_merge( $allowed_inner_blocks, $allowed_wrapper_blocks ); 1567 1568 /** 1569 * Filters the list of blocks that can contribute to the excerpt. 1570 * 1571 * If a dynamic block is added to this list, it must not generate another 1572 * excerpt, as this will cause an infinite loop to occur. 1573 * 1574 * @since 5.0.0 1575 * 1576 * @param string[] $allowed_blocks The list of names of allowed blocks. 1577 */ 1578 $allowed_blocks = apply_filters( 'excerpt_allowed_blocks', $allowed_blocks ); 1579 $blocks = parse_blocks( $content ); 1580 $output = ''; 1581 1582 foreach ( $blocks as $block ) { 1583 if ( in_array( $block['blockName'], $allowed_blocks, true ) ) { 1584 if ( ! empty( $block['innerBlocks'] ) ) { 1585 if ( in_array( $block['blockName'], $allowed_wrapper_blocks, true ) ) { 1586 $output .= _excerpt_render_inner_blocks( $block, $allowed_blocks ); 1587 continue; 1588 } 1589 1590 // Skip the block if it has disallowed or nested inner blocks. 1591 foreach ( $block['innerBlocks'] as $inner_block ) { 1592 if ( 1593 ! in_array( $inner_block['blockName'], $allowed_inner_blocks, true ) || 1594 ! empty( $inner_block['innerBlocks'] ) 1595 ) { 1596 continue 2; 1597 } 1598 } 1599 } 1600 1601 $output .= render_block( $block ); 1602 } 1603 } 1604 1605 return $output; 1606 } 1607 1608 /** 1609 * Parses footnotes markup out of a content string, 1610 * and renders those appropriate for the excerpt. 1611 * 1612 * @since 6.3.0 1613 * 1614 * @param string $content The content to parse. 1615 * @return string The parsed and filtered content. 1616 */ 1617 function excerpt_remove_footnotes( $content ) { 1618 if ( ! str_contains( $content, 'data-fn=' ) ) { 1619 return $content; 1620 } 1621 1622 return preg_replace( 1623 '_<sup data-fn="[^"]+" class="[^"]+">\s*<a href="[^"]+" id="[^"]+">\d+</a>\s*</sup>_', 1624 '', 1625 $content 1626 ); 1627 } 1628 1629 /** 1630 * Renders inner blocks from the allowed wrapper blocks 1631 * for generating an excerpt. 1632 * 1633 * @since 5.8.0 1634 * @access private 1635 * 1636 * @param array $parsed_block The parsed block. 1637 * @param array $allowed_blocks The list of allowed inner blocks. 1638 * @return string The rendered inner blocks. 1639 */ 1640 function _excerpt_render_inner_blocks( $parsed_block, $allowed_blocks ) { 1641 $output = ''; 1642 1643 foreach ( $parsed_block['innerBlocks'] as $inner_block ) { 1644 if ( ! in_array( $inner_block['blockName'], $allowed_blocks, true ) ) { 1645 continue; 1646 } 1647 1648 if ( empty( $inner_block['innerBlocks'] ) ) { 1649 $output .= render_block( $inner_block ); 1650 } else { 1651 $output .= _excerpt_render_inner_blocks( $inner_block, $allowed_blocks ); 1652 } 1653 } 1654 1655 return $output; 1656 } 1657 1658 /** 1659 * Renders a single block into a HTML string. 1660 * 1661 * @since 5.0.0 1662 * 1663 * @global WP_Post $post The post to edit. 1664 * 1665 * @param array $parsed_block { 1666 * A representative array of the block being rendered. See WP_Block_Parser_Block. 1667 * 1668 * @type string $blockName Name of block. 1669 * @type array $attrs Attributes from block comment delimiters. 1670 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1671 * have the same structure as this one. 1672 * @type string $innerHTML HTML from inside block comment delimiters. 1673 * @type array $innerContent List of string fragments and null markers where 1674 * inner blocks were found. 1675 * } 1676 * @return string String of rendered HTML. 1677 */ 1678 function render_block( $parsed_block ) { 1679 global $post; 1680 $parent_block = null; 1681 1682 /** 1683 * Allows render_block() to be short-circuited, by returning a non-null value. 1684 * 1685 * @since 5.1.0 1686 * @since 5.9.0 The `$parent_block` parameter was added. 1687 * 1688 * @param string|null $pre_render The pre-rendered content. Default null. 1689 * @param array $parsed_block { 1690 * A representative array of the block being rendered. See WP_Block_Parser_Block. 1691 * 1692 * @type string $blockName Name of block. 1693 * @type array $attrs Attributes from block comment delimiters. 1694 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1695 * have the same structure as this one. 1696 * @type string $innerHTML HTML from inside block comment delimiters. 1697 * @type array $innerContent List of string fragments and null markers where 1698 * inner blocks were found. 1699 * } 1700 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. 1701 */ 1702 $pre_render = apply_filters( 'pre_render_block', null, $parsed_block, $parent_block ); 1703 if ( ! is_null( $pre_render ) ) { 1704 return $pre_render; 1705 } 1706 1707 $source_block = $parsed_block; 1708 1709 /** 1710 * Filters the block being rendered in render_block(), before it's processed. 1711 * 1712 * @since 5.1.0 1713 * @since 5.9.0 The `$parent_block` parameter was added. 1714 * 1715 * @param array $parsed_block { 1716 * A representative array of the block being rendered. See WP_Block_Parser_Block. 1717 * 1718 * @type string $blockName Name of block. 1719 * @type array $attrs Attributes from block comment delimiters. 1720 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1721 * have the same structure as this one. 1722 * @type string $innerHTML HTML from inside block comment delimiters. 1723 * @type array $innerContent List of string fragments and null markers where 1724 * inner blocks were found. 1725 * } 1726 * @param array $source_block { 1727 * An un-modified copy of `$parsed_block`, as it appeared in the source content. 1728 * See WP_Block_Parser_Block. 1729 * 1730 * @type string $blockName Name of block. 1731 * @type array $attrs Attributes from block comment delimiters. 1732 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1733 * have the same structure as this one. 1734 * @type string $innerHTML HTML from inside block comment delimiters. 1735 * @type array $innerContent List of string fragments and null markers where 1736 * inner blocks were found. 1737 * } 1738 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. 1739 */ 1740 $parsed_block = apply_filters( 'render_block_data', $parsed_block, $source_block, $parent_block ); 1741 1742 $context = array(); 1743 1744 if ( $post instanceof WP_Post ) { 1745 $context['postId'] = $post->ID; 1746 1747 /* 1748 * The `postType` context is largely unnecessary server-side, since the ID 1749 * is usually sufficient on its own. That being said, since a block's 1750 * manifest is expected to be shared between the server and the client, 1751 * it should be included to consistently fulfill the expectation. 1752 */ 1753 $context['postType'] = $post->post_type; 1754 } 1755 1756 /** 1757 * Filters the default context provided to a rendered block. 1758 * 1759 * @since 5.5.0 1760 * @since 5.9.0 The `$parent_block` parameter was added. 1761 * 1762 * @param array $context Default context. 1763 * @param array $parsed_block { 1764 * A representative array of the block being rendered. See WP_Block_Parser_Block. 1765 * 1766 * @type string $blockName Name of block. 1767 * @type array $attrs Attributes from block comment delimiters. 1768 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1769 * have the same structure as this one. 1770 * @type string $innerHTML HTML from inside block comment delimiters. 1771 * @type array $innerContent List of string fragments and null markers where 1772 * inner blocks were found. 1773 * } 1774 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. 1775 */ 1776 $context = apply_filters( 'render_block_context', $context, $parsed_block, $parent_block ); 1777 1778 $block = new WP_Block( $parsed_block, $context ); 1779 1780 return $block->render(); 1781 } 1782 1783 /** 1784 * Parses blocks out of a content string. 1785 * 1786 * @since 5.0.0 1787 * 1788 * @param string $content Post content. 1789 * @return array[] { 1790 * Array of block structures. 1791 * 1792 * @type array ...$0 { 1793 * A representative array of a single parsed block object. See WP_Block_Parser_Block. 1794 * 1795 * @type string $blockName Name of block. 1796 * @type array $attrs Attributes from block comment delimiters. 1797 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1798 * have the same structure as this one. 1799 * @type string $innerHTML HTML from inside block comment delimiters. 1800 * @type array $innerContent List of string fragments and null markers where 1801 * inner blocks were found. 1802 * } 1803 * } 1804 */ 1805 function parse_blocks( $content ) { 1806 /** 1807 * Filter to allow plugins to replace the server-side block parser. 1808 * 1809 * @since 5.0.0 1810 * 1811 * @param string $parser_class Name of block parser class. 1812 */ 1813 $parser_class = apply_filters( 'block_parser_class', 'WP_Block_Parser' ); 1814 1815 $parser = new $parser_class(); 1816 return $parser->parse( $content ); 1817 } 1818 1819 /** 1820 * Parses dynamic blocks out of `post_content` and re-renders them. 1821 * 1822 * @since 5.0.0 1823 * 1824 * @param string $content Post content. 1825 * @return string Updated post content. 1826 */ 1827 function do_blocks( $content ) { 1828 $blocks = parse_blocks( $content ); 1829 $output = ''; 1830 1831 foreach ( $blocks as $block ) { 1832 $output .= render_block( $block ); 1833 } 1834 1835 // If there are blocks in this content, we shouldn't run wpautop() on it later. 1836 $priority = has_filter( 'the_content', 'wpautop' ); 1837 if ( false !== $priority && doing_filter( 'the_content' ) && has_blocks( $content ) ) { 1838 remove_filter( 'the_content', 'wpautop', $priority ); 1839 add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 ); 1840 } 1841 1842 return $output; 1843 } 1844 1845 /** 1846 * If do_blocks() needs to remove wpautop() from the `the_content` filter, this re-adds it afterwards, 1847 * for subsequent `the_content` usage. 1848 * 1849 * @since 5.0.0 1850 * @access private 1851 * 1852 * @param string $content The post content running through this filter. 1853 * @return string The unmodified content. 1854 */ 1855 function _restore_wpautop_hook( $content ) { 1856 $current_priority = has_filter( 'the_content', '_restore_wpautop_hook' ); 1857 1858 add_filter( 'the_content', 'wpautop', $current_priority - 1 ); 1859 remove_filter( 'the_content', '_restore_wpautop_hook', $current_priority ); 1860 1861 return $content; 1862 } 1863 1864 /** 1865 * Returns the current version of the block format that the content string is using. 1866 * 1867 * If the string doesn't contain blocks, it returns 0. 1868 * 1869 * @since 5.0.0 1870 * 1871 * @param string $content Content to test. 1872 * @return int The block format version is 1 if the content contains one or more blocks, 0 otherwise. 1873 */ 1874 function block_version( $content ) { 1875 return has_blocks( $content ) ? 1 : 0; 1876 } 1877 1878 /** 1879 * Registers a new block style. 1880 * 1881 * @since 5.3.0 1882 * 1883 * @link https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/ 1884 * 1885 * @param string $block_name Block type name including namespace. 1886 * @param array $style_properties Array containing the properties of the style name, label, 1887 * style_handle (name of the stylesheet to be enqueued), 1888 * inline_style (string containing the CSS to be added). 1889 * See WP_Block_Styles_Registry::register(). 1890 * @return bool True if the block style was registered with success and false otherwise. 1891 */ 1892 function register_block_style( $block_name, $style_properties ) { 1893 return WP_Block_Styles_Registry::get_instance()->register( $block_name, $style_properties ); 1894 } 1895 1896 /** 1897 * Unregisters a block style. 1898 * 1899 * @since 5.3.0 1900 * 1901 * @param string $block_name Block type name including namespace. 1902 * @param string $block_style_name Block style name. 1903 * @return bool True if the block style was unregistered with success and false otherwise. 1904 */ 1905 function unregister_block_style( $block_name, $block_style_name ) { 1906 return WP_Block_Styles_Registry::get_instance()->unregister( $block_name, $block_style_name ); 1907 } 1908 1909 /** 1910 * Checks whether the current block type supports the feature requested. 1911 * 1912 * @since 5.8.0 1913 * @since 6.4.0 The `$feature` parameter now supports a string. 1914 * 1915 * @param WP_Block_Type $block_type Block type to check for support. 1916 * @param string|array $feature Feature slug, or path to a specific feature to check support for. 1917 * @param mixed $default_value Optional. Fallback value for feature support. Default false. 1918 * @return bool Whether the feature is supported. 1919 */ 1920 function block_has_support( $block_type, $feature, $default_value = false ) { 1921 $block_support = $default_value; 1922 if ( $block_type instanceof WP_Block_Type ) { 1923 if ( is_array( $feature ) && count( $feature ) === 1 ) { 1924 $feature = $feature[0]; 1925 } 1926 1927 if ( is_array( $feature ) ) { 1928 $block_support = _wp_array_get( $block_type->supports, $feature, $default_value ); 1929 } elseif ( isset( $block_type->supports[ $feature ] ) ) { 1930 $block_support = $block_type->supports[ $feature ]; 1931 } 1932 } 1933 1934 return true === $block_support || is_array( $block_support ); 1935 } 1936 1937 /** 1938 * Converts typography keys declared under `supports.*` to `supports.typography.*`. 1939 * 1940 * Displays a `_doing_it_wrong()` notice when a block using the older format is detected. 1941 * 1942 * @since 5.8.0 1943 * 1944 * @param array $metadata Metadata for registering a block type. 1945 * @return array Filtered metadata for registering a block type. 1946 */ 1947 function wp_migrate_old_typography_shape( $metadata ) { 1948 if ( ! isset( $metadata['supports'] ) ) { 1949 return $metadata; 1950 } 1951 1952 $typography_keys = array( 1953 '__experimentalFontFamily', 1954 '__experimentalFontStyle', 1955 '__experimentalFontWeight', 1956 '__experimentalLetterSpacing', 1957 '__experimentalTextDecoration', 1958 '__experimentalTextTransform', 1959 'fontSize', 1960 'lineHeight', 1961 ); 1962 1963 foreach ( $typography_keys as $typography_key ) { 1964 $support_for_key = isset( $metadata['supports'][ $typography_key ] ) ? $metadata['supports'][ $typography_key ] : null; 1965 1966 if ( null !== $support_for_key ) { 1967 _doing_it_wrong( 1968 'register_block_type_from_metadata()', 1969 sprintf( 1970 /* translators: 1: Block type, 2: Typography supports key, e.g: fontSize, lineHeight, etc. 3: block.json, 4: Old metadata key, 5: New metadata key. */ 1971 __( 'Block "%1$s" is declaring %2$s support in %3$s file under %4$s. %2$s support is now declared under %5$s.' ), 1972 $metadata['name'], 1973 "<code>$typography_key</code>", 1974 '<code>block.json</code>', 1975 "<code>supports.$typography_key</code>", 1976 "<code>supports.typography.$typography_key</code>" 1977 ), 1978 '5.8.0' 1979 ); 1980 1981 _wp_array_set( $metadata['supports'], array( 'typography', $typography_key ), $support_for_key ); 1982 unset( $metadata['supports'][ $typography_key ] ); 1983 } 1984 } 1985 1986 return $metadata; 1987 } 1988 1989 /** 1990 * Helper function that constructs a WP_Query args array from 1991 * a `Query` block properties. 1992 * 1993 * It's used in Query Loop, Query Pagination Numbers and Query Pagination Next blocks. 1994 * 1995 * @since 5.8.0 1996 * @since 6.1.0 Added `query_loop_block_query_vars` filter and `parents` support in query. 1997 * 1998 * @param WP_Block $block Block instance. 1999 * @param int $page Current query's page. 2000 * 2001 * @return array Returns the constructed WP_Query arguments. 2002 */ 2003 function build_query_vars_from_query_block( $block, $page ) { 2004 $query = array( 2005 'post_type' => 'post', 2006 'order' => 'DESC', 2007 'orderby' => 'date', 2008 'post__not_in' => array(), 2009 ); 2010 2011 if ( isset( $block->context['query'] ) ) { 2012 if ( ! empty( $block->context['query']['postType'] ) ) { 2013 $post_type_param = $block->context['query']['postType']; 2014 if ( is_post_type_viewable( $post_type_param ) ) { 2015 $query['post_type'] = $post_type_param; 2016 } 2017 } 2018 if ( isset( $block->context['query']['sticky'] ) && ! empty( $block->context['query']['sticky'] ) ) { 2019 $sticky = get_option( 'sticky_posts' ); 2020 if ( 'only' === $block->context['query']['sticky'] ) { 2021 /* 2022 * Passing an empty array to post__in will return have_posts() as true (and all posts will be returned). 2023 * Logic should be used before hand to determine if WP_Query should be used in the event that the array 2024 * being passed to post__in is empty. 2025 * 2026 * @see https://core.trac.wordpress.org/ticket/28099 2027 */ 2028 $query['post__in'] = ! empty( $sticky ) ? $sticky : array( 0 ); 2029 $query['ignore_sticky_posts'] = 1; 2030 } else { 2031 $query['post__not_in'] = array_merge( $query['post__not_in'], $sticky ); 2032 } 2033 } 2034 if ( ! empty( $block->context['query']['exclude'] ) ) { 2035 $excluded_post_ids = array_map( 'intval', $block->context['query']['exclude'] ); 2036 $excluded_post_ids = array_filter( $excluded_post_ids ); 2037 $query['post__not_in'] = array_merge( $query['post__not_in'], $excluded_post_ids ); 2038 } 2039 if ( 2040 isset( $block->context['query']['perPage'] ) && 2041 is_numeric( $block->context['query']['perPage'] ) 2042 ) { 2043 $per_page = absint( $block->context['query']['perPage'] ); 2044 $offset = 0; 2045 2046 if ( 2047 isset( $block->context['query']['offset'] ) && 2048 is_numeric( $block->context['query']['offset'] ) 2049 ) { 2050 $offset = absint( $block->context['query']['offset'] ); 2051 } 2052 2053 $query['offset'] = ( $per_page * ( $page - 1 ) ) + $offset; 2054 $query['posts_per_page'] = $per_page; 2055 } 2056 // Migrate `categoryIds` and `tagIds` to `tax_query` for backwards compatibility. 2057 if ( ! empty( $block->context['query']['categoryIds'] ) || ! empty( $block->context['query']['tagIds'] ) ) { 2058 $tax_query = array(); 2059 if ( ! empty( $block->context['query']['categoryIds'] ) ) { 2060 $tax_query[] = array( 2061 'taxonomy' => 'category', 2062 'terms' => array_filter( array_map( 'intval', $block->context['query']['categoryIds'] ) ), 2063 'include_children' => false, 2064 ); 2065 } 2066 if ( ! empty( $block->context['query']['tagIds'] ) ) { 2067 $tax_query[] = array( 2068 'taxonomy' => 'post_tag', 2069 'terms' => array_filter( array_map( 'intval', $block->context['query']['tagIds'] ) ), 2070 'include_children' => false, 2071 ); 2072 } 2073 $query['tax_query'] = $tax_query; 2074 } 2075 if ( ! empty( $block->context['query']['taxQuery'] ) ) { 2076 $query['tax_query'] = array(); 2077 foreach ( $block->context['query']['taxQuery'] as $taxonomy => $terms ) { 2078 if ( is_taxonomy_viewable( $taxonomy ) && ! empty( $terms ) ) { 2079 $query['tax_query'][] = array( 2080 'taxonomy' => $taxonomy, 2081 'terms' => array_filter( array_map( 'intval', $terms ) ), 2082 'include_children' => false, 2083 ); 2084 } 2085 } 2086 } 2087 if ( 2088 isset( $block->context['query']['order'] ) && 2089 in_array( strtoupper( $block->context['query']['order'] ), array( 'ASC', 'DESC' ), true ) 2090 ) { 2091 $query['order'] = strtoupper( $block->context['query']['order'] ); 2092 } 2093 if ( isset( $block->context['query']['orderBy'] ) ) { 2094 $query['orderby'] = $block->context['query']['orderBy']; 2095 } 2096 if ( 2097 isset( $block->context['query']['author'] ) 2098 ) { 2099 if ( is_array( $block->context['query']['author'] ) ) { 2100 $query['author__in'] = array_filter( array_map( 'intval', $block->context['query']['author'] ) ); 2101 } elseif ( is_string( $block->context['query']['author'] ) ) { 2102 $query['author__in'] = array_filter( array_map( 'intval', explode( ',', $block->context['query']['author'] ) ) ); 2103 } elseif ( is_int( $block->context['query']['author'] ) && $block->context['query']['author'] > 0 ) { 2104 $query['author'] = $block->context['query']['author']; 2105 } 2106 } 2107 if ( ! empty( $block->context['query']['search'] ) ) { 2108 $query['s'] = $block->context['query']['search']; 2109 } 2110 if ( ! empty( $block->context['query']['parents'] ) && is_post_type_hierarchical( $query['post_type'] ) ) { 2111 $query['post_parent__in'] = array_filter( array_map( 'intval', $block->context['query']['parents'] ) ); 2112 } 2113 } 2114 2115 /** 2116 * Filters the arguments which will be passed to `WP_Query` for the Query Loop Block. 2117 * 2118 * Anything to this filter should be compatible with the `WP_Query` API to form 2119 * the query context which will be passed down to the Query Loop Block's children. 2120 * This can help, for example, to include additional settings or meta queries not 2121 * directly supported by the core Query Loop Block, and extend its capabilities. 2122 * 2123 * Please note that this will only influence the query that will be rendered on the 2124 * front-end. The editor preview is not affected by this filter. Also, worth noting 2125 * that the editor preview uses the REST API, so, ideally, one should aim to provide 2126 * attributes which are also compatible with the REST API, in order to be able to 2127 * implement identical queries on both sides. 2128 * 2129 * @since 6.1.0 2130 * 2131 * @param array $query Array containing parameters for `WP_Query` as parsed by the block context. 2132 * @param WP_Block $block Block instance. 2133 * @param int $page Current query's page. 2134 */ 2135 return apply_filters( 'query_loop_block_query_vars', $query, $block, $page ); 2136 } 2137 2138 /** 2139 * Helper function that returns the proper pagination arrow HTML for 2140 * `QueryPaginationNext` and `QueryPaginationPrevious` blocks based 2141 * on the provided `paginationArrow` from `QueryPagination` context. 2142 * 2143 * It's used in QueryPaginationNext and QueryPaginationPrevious blocks. 2144 * 2145 * @since 5.9.0 2146 * 2147 * @param WP_Block $block Block instance. 2148 * @param bool $is_next Flag for handling `next/previous` blocks. 2149 * @return string|null The pagination arrow HTML or null if there is none. 2150 */ 2151 function get_query_pagination_arrow( $block, $is_next ) { 2152 $arrow_map = array( 2153 'none' => '', 2154 'arrow' => array( 2155 'next' => '→', 2156 'previous' => '←', 2157 ), 2158 'chevron' => array( 2159 'next' => '»', 2160 'previous' => '«', 2161 ), 2162 ); 2163 if ( ! empty( $block->context['paginationArrow'] ) && array_key_exists( $block->context['paginationArrow'], $arrow_map ) && ! empty( $arrow_map[ $block->context['paginationArrow'] ] ) ) { 2164 $pagination_type = $is_next ? 'next' : 'previous'; 2165 $arrow_attribute = $block->context['paginationArrow']; 2166 $arrow = $arrow_map[ $block->context['paginationArrow'] ][ $pagination_type ]; 2167 $arrow_classes = "wp-block-query-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; 2168 return "<span class='$arrow_classes' aria-hidden='true'>$arrow</span>"; 2169 } 2170 return null; 2171 } 2172 2173 /** 2174 * Helper function that constructs a comment query vars array from the passed 2175 * block properties. 2176 * 2177 * It's used with the Comment Query Loop inner blocks. 2178 * 2179 * @since 6.0.0 2180 * 2181 * @param WP_Block $block Block instance. 2182 * @return array Returns the comment query parameters to use with the 2183 * WP_Comment_Query constructor. 2184 */ 2185 function build_comment_query_vars_from_block( $block ) { 2186 2187 $comment_args = array( 2188 'orderby' => 'comment_date_gmt', 2189 'order' => 'ASC', 2190 'status' => 'approve', 2191 'no_found_rows' => false, 2192 ); 2193 2194 if ( is_user_logged_in() ) { 2195 $comment_args['include_unapproved'] = array( get_current_user_id() ); 2196 } else { 2197 $unapproved_email = wp_get_unapproved_comment_author_email(); 2198 2199 if ( $unapproved_email ) { 2200 $comment_args['include_unapproved'] = array( $unapproved_email ); 2201 } 2202 } 2203 2204 if ( ! empty( $block->context['postId'] ) ) { 2205 $comment_args['post_id'] = (int) $block->context['postId']; 2206 } 2207 2208 if ( get_option( 'thread_comments' ) ) { 2209 $comment_args['hierarchical'] = 'threaded'; 2210 } else { 2211 $comment_args['hierarchical'] = false; 2212 } 2213 2214 if ( get_option( 'page_comments' ) === '1' || get_option( 'page_comments' ) === true ) { 2215 $per_page = get_option( 'comments_per_page' ); 2216 $default_page = get_option( 'default_comments_page' ); 2217 if ( $per_page > 0 ) { 2218 $comment_args['number'] = $per_page; 2219 2220 $page = (int) get_query_var( 'cpage' ); 2221 if ( $page ) { 2222 $comment_args['paged'] = $page; 2223 } elseif ( 'oldest' === $default_page ) { 2224 $comment_args['paged'] = 1; 2225 } elseif ( 'newest' === $default_page ) { 2226 $max_num_pages = (int) ( new WP_Comment_Query( $comment_args ) )->max_num_pages; 2227 if ( 0 !== $max_num_pages ) { 2228 $comment_args['paged'] = $max_num_pages; 2229 } 2230 } 2231 // Set the `cpage` query var to ensure the previous and next pagination links are correct 2232 // when inheriting the Discussion Settings. 2233 if ( 0 === $page && isset( $comment_args['paged'] ) && $comment_args['paged'] > 0 ) { 2234 set_query_var( 'cpage', $comment_args['paged'] ); 2235 } 2236 } 2237 } 2238 2239 return $comment_args; 2240 } 2241 2242 /** 2243 * Helper function that returns the proper pagination arrow HTML for 2244 * `CommentsPaginationNext` and `CommentsPaginationPrevious` blocks based on the 2245 * provided `paginationArrow` from `CommentsPagination` context. 2246 * 2247 * It's used in CommentsPaginationNext and CommentsPaginationPrevious blocks. 2248 * 2249 * @since 6.0.0 2250 * 2251 * @param WP_Block $block Block instance. 2252 * @param string $pagination_type Optional. Type of the arrow we will be rendering. 2253 * Accepts 'next' or 'previous'. Default 'next'. 2254 * @return string|null The pagination arrow HTML or null if there is none. 2255 */ 2256 function get_comments_pagination_arrow( $block, $pagination_type = 'next' ) { 2257 $arrow_map = array( 2258 'none' => '', 2259 'arrow' => array( 2260 'next' => '→', 2261 'previous' => '←', 2262 ), 2263 'chevron' => array( 2264 'next' => '»', 2265 'previous' => '«', 2266 ), 2267 ); 2268 if ( ! empty( $block->context['comments/paginationArrow'] ) && ! empty( $arrow_map[ $block->context['comments/paginationArrow'] ][ $pagination_type ] ) ) { 2269 $arrow_attribute = $block->context['comments/paginationArrow']; 2270 $arrow = $arrow_map[ $block->context['comments/paginationArrow'] ][ $pagination_type ]; 2271 $arrow_classes = "wp-block-comments-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; 2272 return "<span class='$arrow_classes' aria-hidden='true'>$arrow</span>"; 2273 } 2274 return null; 2275 } 2276 2277 /** 2278 * Strips all HTML from the content of footnotes, and sanitizes the ID. 2279 * 2280 * This function expects slashed data on the footnotes content. 2281 * 2282 * @access private 2283 * @since 6.3.2 2284 * 2285 * @param string $footnotes JSON-encoded string of an array containing the content and ID of each footnote. 2286 * @return string Filtered content without any HTML on the footnote content and with the sanitized ID. 2287 */ 2288 function _wp_filter_post_meta_footnotes( $footnotes ) { 2289 $footnotes_decoded = json_decode( $footnotes, true ); 2290 if ( ! is_array( $footnotes_decoded ) ) { 2291 return ''; 2292 } 2293 $footnotes_sanitized = array(); 2294 foreach ( $footnotes_decoded as $footnote ) { 2295 if ( ! empty( $footnote['content'] ) && ! empty( $footnote['id'] ) ) { 2296 $footnotes_sanitized[] = array( 2297 'id' => sanitize_key( $footnote['id'] ), 2298 'content' => wp_unslash( wp_filter_post_kses( wp_slash( $footnote['content'] ) ) ), 2299 ); 2300 } 2301 } 2302 return wp_json_encode( $footnotes_sanitized ); 2303 } 2304 2305 /** 2306 * Adds the filters for footnotes meta field. 2307 * 2308 * @access private 2309 * @since 6.3.2 2310 */ 2311 function _wp_footnotes_kses_init_filters() { 2312 add_filter( 'sanitize_post_meta_footnotes', '_wp_filter_post_meta_footnotes' ); 2313 } 2314 2315 /** 2316 * Removes the filters for footnotes meta field. 2317 * 2318 * @access private 2319 * @since 6.3.2 2320 */ 2321 function _wp_footnotes_remove_filters() { 2322 remove_filter( 'sanitize_post_meta_footnotes', '_wp_filter_post_meta_footnotes' ); 2323 } 2324 2325 /** 2326 * Registers the filter of footnotes meta field if the user does not have `unfiltered_html` capability. 2327 * 2328 * @access private 2329 * @since 6.3.2 2330 */ 2331 function _wp_footnotes_kses_init() { 2332 _wp_footnotes_remove_filters(); 2333 if ( ! current_user_can( 'unfiltered_html' ) ) { 2334 _wp_footnotes_kses_init_filters(); 2335 } 2336 } 2337 2338 /** 2339 * Initializes the filters for footnotes meta field when imported data should be filtered. 2340 * 2341 * This filter is the last one being executed on {@see 'force_filtered_html_on_import'}. 2342 * If the input of the filter is true, it means we are in an import situation and should 2343 * enable kses, independently of the user capabilities. So in that case we call 2344 * _wp_footnotes_kses_init_filters(). 2345 * 2346 * @access private 2347 * @since 6.3.2 2348 * 2349 * @param string $arg Input argument of the filter. 2350 * @return string Input argument of the filter. 2351 */ 2352 function _wp_footnotes_force_filtered_html_on_import_filter( $arg ) { 2353 // If `force_filtered_html_on_import` is true, we need to init the global styles kses filters. 2354 if ( $arg ) { 2355 _wp_footnotes_kses_init_filters(); 2356 } 2357 return $arg; 2358 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Wed May 8 08:20:02 2024 | Cross-referenced by PHPXref |