<?php /** * Temporary compatibility shims for block APIs present in Gutenberg. * * @package gutenberg */ /** * Shim for the `variation_callback` block type argument. * * @param array $args The block type arguments. * @return array The updated block type arguments. */ function gutenberg_register_block_type_args_shim( $args ) { if ( isset( $args['variation_callback'] ) && is_callable( $args['variation_callback'] ) ) { $args['variations'] = call_user_func( $args['variation_callback'] ); unset( $args['variation_callback'] ); } return $args; } if ( ! method_exists( 'WP_Block_Type', 'get_variations' ) ) { add_filter( 'register_block_type_args', 'gutenberg_register_block_type_args_shim' ); } /** * Registers the metadata block attribute for all block types. * * @param array $args Array of arguments for registering a block type. * @return array $args */ function gutenberg_register_metadata_attribute( $args ) { // Setup attributes if needed. if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) { $args['attributes'] = array(); } if ( ! array_key_exists( 'metadata', $args['attributes'] ) ) { $args['attributes']['metadata'] = array( 'type' => 'object', ); } return $args; } add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' ); // Only process block bindings if they are not processed in core. if ( ! class_exists( 'WP_Block_Bindings_Registry' ) ) { /** * Depending on the block attribute name, replace its value in the HTML based on the value provided. * * @param string $block_content Block Content. * @param string $block_name The name of the block to process. * @param string $attribute_name The attribute name to replace. * @param mixed $source_value The value used to replace in the HTML. * @return string The modified block content. */ function gutenberg_block_bindings_replace_html( $block_content, $block_name, string $attribute_name, $source_value ) { $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) { return $block_content; } // Depending on the attribute source, the processing will be different. switch ( $block_type->attributes[ $attribute_name ]['source'] ) { case 'html': case 'rich-text': $block_reader = new WP_HTML_Tag_Processor( $block_content ); // TODO: Support for CSS selectors whenever they are ready in the HTML API. // In the meantime, support comma-separated selectors by exploding them into an array. $selectors = explode( ',', $block_type->attributes[ $attribute_name ]['selector'] ); // Add a bookmark to the first tag to be able to iterate over the selectors. $block_reader->next_tag(); $block_reader->set_bookmark( 'iterate-selectors' ); // TODO: This shouldn't be needed when the `set_inner_html` function is ready. // Store the parent tag and its attributes to be able to restore them later in the button. // The button block has a wrapper while the paragraph and heading blocks don't. if ( 'core/button' === $block_name ) { $button_wrapper = $block_reader->get_tag(); $button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); $button_wrapper_attrs = array(); foreach ( $button_wrapper_attribute_names as $name ) { $button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name ); } } foreach ( $selectors as $selector ) { // If the parent tag, or any of its children, matches the selector, replace the HTML. if ( strcasecmp( $block_reader->get_tag( $selector ), $selector ) === 0 || $block_reader->next_tag( array( 'tag_name' => $selector, ) ) ) { $block_reader->release_bookmark( 'iterate-selectors' ); // TODO: Use `set_inner_html` method whenever it's ready in the HTML API. // Until then, it is hardcoded for the paragraph, heading, and button blocks. // Store the tag and its attributes to be able to restore them later. $selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); $selector_attrs = array(); foreach ( $selector_attribute_names as $name ) { $selector_attrs[ $name ] = $block_reader->get_attribute( $name ); } $selector_markup = "<$selector>" . wp_kses_post( $source_value ) . "</$selector>"; $amended_content = new WP_HTML_Tag_Processor( $selector_markup ); $amended_content->next_tag(); foreach ( $selector_attrs as $attribute_key => $attribute_value ) { $amended_content->set_attribute( $attribute_key, $attribute_value ); } if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) { return $amended_content->get_updated_html(); } if ( 'core/button' === $block_name ) { $button_markup = "<$button_wrapper>{$amended_content->get_updated_html()}</$button_wrapper>"; $amended_button = new WP_HTML_Tag_Processor( $button_markup ); $amended_button->next_tag(); foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) { $amended_button->set_attribute( $attribute_key, $attribute_value ); } return $amended_button->get_updated_html(); } } else { $block_reader->seek( 'iterate-selectors' ); } } $block_reader->release_bookmark( 'iterate-selectors' ); return $block_content; case 'attribute': $amended_content = new WP_HTML_Tag_Processor( $block_content ); if ( ! $amended_content->next_tag( array( // TODO: build the query from CSS selector. 'tag_name' => $block_type->attributes[ $attribute_name ]['selector'], ) ) ) { return $block_content; } $amended_content->set_attribute( $block_type->attributes[ $attribute_name ]['attribute'], $source_value ); return $amended_content->get_updated_html(); default: return $block_content; } } /** * Process the block bindings attribute. * * @param string $block_content Block Content. * @param array $parsed_block The full block, including name and attributes. * @param WP_Block $block_instance The block instance. * @return string Block content with the bind applied. */ function gutenberg_process_block_bindings( $block_content, $parsed_block, $block_instance ) { $supported_block_attrs = array( 'core/paragraph' => array( 'content' ), 'core/heading' => array( 'content' ), 'core/image' => array( 'id', 'url', 'title', 'alt' ), 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), ); // If the block doesn't have the bindings property or isn't one of the supported block types, return. if ( ! isset( $supported_block_attrs[ $block_instance->name ] ) || empty( $parsed_block['attrs']['metadata']['bindings'] ) || ! is_array( $parsed_block['attrs']['metadata']['bindings'] ) ) { return $block_content; } /* * Assuming the following format for the bindings property of the "metadata" attribute: * * "bindings": { * "title": { * "source": "core/post-meta", * "args": { "key": "text_custom_field" } * }, * "url": { * "source": "core/post-meta", * "args": { "key": "url_custom_field" } * } * } */ $modified_block_content = $block_content; foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) { // If the attribute is not in the supported list, process next attribute. if ( ! in_array( $attribute_name, $supported_block_attrs[ $block_instance->name ], true ) ) { continue; } // If no source is provided, or that source is not registered, process next attribute. if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) { continue; } $block_binding_source = get_block_bindings_source( $block_binding['source'] ); if ( null === $block_binding_source ) { continue; } $source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array(); $source_value = $block_binding_source->get_value( $source_args, $block_instance, $attribute_name ); // If the value is not null, process the HTML based on the block and the attribute. if ( ! is_null( $source_value ) ) { $modified_block_content = gutenberg_block_bindings_replace_html( $modified_block_content, $block_instance->name, $attribute_name, $source_value ); } } return $modified_block_content; } add_filter( 'render_block', 'gutenberg_process_block_bindings', 20, 3 ); } /** * Enable the viewStyle block API for core versions < 6.5 * */ if ( ! property_exists( 'WP_Block_Type', 'view_style_handles' ) ) { /** * Registers viewStyle assets on block type registration (=same moment core would do it). * * @param array $settings Array of determined settings for registering a block type. * @param array $metadata Metadata provided for registering a block type. * @return array The block type settings */ function gutenberg_filter_block_type_metadata_settings_register_view_styles( $settings, $metadata ) { if ( empty( $metadata['viewStyle'] ) ) { return $settings; } $styles = $metadata['viewStyle']; $processed_styles = array(); if ( is_array( $styles ) ) { for ( $index = 0; $index < count( $styles ); $index++ ) { $result = gutenberg_register_block_view_style_handle( $metadata, $index ); if ( $result ) { $processed_styles[] = $result; } } } else { $result = gutenberg_register_block_view_style_handle( $metadata ); if ( $result ) { $processed_styles[] = $result; } } if ( ! empty( $processed_styles ) ) { $settings['view_style_handles'] = $processed_styles; } return $settings; } add_filter( 'block_type_metadata_settings', 'gutenberg_filter_block_type_metadata_settings_register_view_styles', 10, 2 ); /** * Enqueue view styles associated with the block. * * @param string $block_content The block content. * @param array $block The full block, including name and attributes. * @param WP_Block $instance The block instance. */ function gutenberg_filter_render_block_enqueue_view_styles( $block_content, $parsed_block, $block_instance ) { $block_type = $block_instance->block_type; if ( ! empty( $block_type->view_style_handles ) ) { foreach ( $block_type->view_style_handles as $view_style_handle ) { wp_enqueue_style( $view_style_handle ); } } return $block_content; } add_filter( 'render_block', 'gutenberg_filter_render_block_enqueue_view_styles', 10, 3 ); /** * Registers a REST field for block types to provide view styles. * * Adds the `view_style_handles` field to block type objects in the REST API, which * lists the style handles associated with the block's viewStyle key. */ function gutenberg_register_view_style_rest_field() { register_rest_field( 'block-type', 'view_style_handles', array( 'get_callback' => function ( $item ) { $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $item['name'] ); if ( isset( $block_type->view_style_handles ) ) { return $block_type->view_style_handles; } return array(); }, ) ); } add_action( 'rest_api_init', 'gutenberg_register_view_style_rest_field' ); /** * A compat version of `register_block_style_handle()` to use the viewStyle handle. * Required for core versions < 6.5, since a custom version of generate_block_asset_handle has to be used * that supports the viewStyle property and correctly creates the handle * * @param array $metadata Block metadata. * @param int $index Optional. Index of the style to register when multiple items passed. * Default 0. * @return string|false Style handle provided directly or created through * style's registration, or false on failure. */ function gutenberg_register_block_view_style_handle( $metadata, $index = 0 ) { $style_handle = $metadata['viewStyle']; if ( is_array( $style_handle ) ) { if ( empty( $style_handle[ $index ] ) ) { return false; } $style_handle = $style_handle[ $index ]; } $style_handle_name = gutenberg_generate_view_style_block_asset_handle( $metadata['name'], $index ); // If the style handle is already registered, skip re-registering. if ( wp_style_is( $style_handle_name, 'registered' ) ) { return $style_handle_name; } static $wpinc_path_norm = ''; if ( ! $wpinc_path_norm ) { $wpinc_path_norm = wp_normalize_path( realpath( ABSPATH . WPINC ) ); } $is_core_block = isset( $metadata['file'] ) && str_starts_with( $metadata['file'], $wpinc_path_norm ); // Skip registering individual styles for each core block when a bundled version provided. if ( $is_core_block && ! wp_should_load_separate_core_block_assets() ) { return false; } $style_path = remove_block_asset_path_prefix( $style_handle ); $is_style_handle = $style_handle === $style_path; // Allow only passing style handles for core blocks. if ( $is_core_block && ! $is_style_handle ) { return false; } // Return the style handle unless it's the first item for every core block that requires special treatment. if ( $is_style_handle && ! ( $is_core_block && 0 === $index ) ) { return $style_handle; } // Check whether styles should have a ".min" suffix or not. $suffix = SCRIPT_DEBUG ? '' : '.min'; if ( $is_core_block ) { $style_path = "view{$suffix}.css"; // Use view.css for viewStyle of core blocks } $style_path_norm = wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $style_path ) ); $style_uri = get_block_asset_url( $style_path_norm ); $version = ! $is_core_block && isset( $metadata['version'] ) ? $metadata['version'] : false; $result = wp_register_style( $style_handle_name, $style_uri, array(), $version ); if ( ! $result ) { return false; } if ( $style_uri ) { wp_style_add_data( $style_handle_name, 'path', $style_path_norm ); if ( $is_core_block ) { $rtl_file = str_replace( "{$suffix}.css", "-rtl{$suffix}.css", $style_path_norm ); } else { $rtl_file = str_replace( '.css', '-rtl.css', $style_path_norm ); } if ( is_rtl() && file_exists( $rtl_file ) ) { wp_style_add_data( $style_handle_name, 'rtl', 'replace' ); wp_style_add_data( $style_handle_name, 'suffix', $suffix ); wp_style_add_data( $style_handle_name, 'path', $rtl_file ); } } return $style_handle_name; } /** * A compat version of `generate_block_asset_handle()` to use the viewStyle asset handle. * * @param string $block_name Name of the block. * @param string $field_name Name of the metadata field. * @param int $index Optional. Index of the asset when multiple items passed. * Default 0. * @return string Generated asset name for the block's field. */ function gutenberg_generate_view_style_block_asset_handle( $block_name, $index = 0 ) { if ( str_starts_with( $block_name, 'core/' ) ) { $asset_handle = str_replace( 'core/', 'wp-block-', $block_name ); $asset_handle .= '-view'; if ( $index > 0 ) { $asset_handle .= '-' . ( $index + 1 ); } return $asset_handle; } $asset_handle = str_replace( '/', '-', $block_name ) . '-view-style'; if ( $index > 0 ) { $asset_handle .= '-' . ( $index + 1 ); } return $asset_handle; } }