ed_file = self::read_json_file( $path ); if ( is_array( $decoded_file ) && static::style_variation_has_scope( $decoded_file, $scope ) ) { $translated = static::translate( $decoded_file, wp_get_theme()->get( 'TextDomain' ) ); $variation = ( new WP_Theme_JSON( $translated ) )->get_raw_data(); if ( empty( $variation['title'] ) ) { $variation['title'] = basename( $path, '.json' ); } $variations[] = $variation; } } return $variations; } /** * Resolves relative paths in theme.json styles to theme absolute paths * and returns them in an array that can be embedded * as the value of `_link` object in REST API responses. * * @since 6.6.0 * * @param WP_Theme_JSON $theme_json A theme json instance. * @return array An array of resolved paths. */ public static function get_resolved_theme_uris( $theme_json ) { $resolved_theme_uris = array(); if ( ! $theme_json instanceof WP_Theme_JSON ) { return $resolved_theme_uris; } $theme_json_data = $theme_json->get_raw_data(); // Top level styles. $background_image_url = isset( $theme_json_data['styles']['background']['backgroundImage']['url'] ) ? $theme_json_data['styles']['background']['backgroundImage']['url'] : null; /* * The same file convention when registering web fonts. * See: WP_Font_Face_Resolver::to_theme_file_uri. */ $placeholder = 'file:./'; if ( isset( $background_image_url ) && is_string( $background_image_url ) && // Skip if the src doesn't start with the placeholder, as there's nothing to replace. str_starts_with( $background_image_url, $placeholder ) ) { $file_type = wp_check_filetype( $background_image_url ); $src_url = str_replace( $placeholder, '', $background_image_url ); $resolved_theme_uri = array( 'name' => $background_image_url, 'href' => sanitize_url( get_theme_file_uri( $src_url ) ), 'target' => 'styles.background.backgroundImage.url', ); if ( isset( $file_type['type'] ) ) { $resolved_theme_uri['type'] = $file_type['type']; } $resolved_theme_uris[] = $resolved_theme_uri; } return $resolved_theme_uris; } /** * Resolves relative paths in theme.json styles to theme absolute paths * and merges them with incoming theme JSON. * * @since 6.6.0 * * @param WP_Theme_JSON $theme_json A theme json instance. * @return WP_Theme_JSON Theme merged with resolved paths, if any found. */ public static function resolve_theme_file_uris( $theme_json ) { $resolved_urls = static::get_resolved_theme_uris( $theme_json ); if ( empty( $resolved_urls ) ) { return $theme_json; } $resolved_theme_json_data = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA, ); foreach ( $resolved_urls as $resolved_url ) { $path = explode( '.', $resolved_url['target'] ); _wp_array_set( $resolved_theme_json_data, $path, $resolved_url['href'] ); } $theme_json->merge( new WP_Theme_JSON( $resolved_theme_json_data ) ); return $theme_json; } /** * Adds variations sourced from block style variations files to the supplied theme.json data. * * @since 6.6.0 * * @param array $data Array following the theme.json specification. * @param array $variations Shared block style variations. * @return array Theme json data including shared block style variation definitions. */ private static function inject_variations_from_block_style_variation_files( $data, $variations ) { if ( empty( $variations ) ) { return $data; } foreach ( $variations as $variation ) { if ( empty( $variation['styles'] ) || empty( $variation['blockTypes'] ) ) { continue; } $variation_name = $variation['slug'] ?? _wp_to_kebab_case( $variation['title'] ); foreach ( $variation['blockTypes'] as $block_type ) { // First, override partial styles with any top-level styles. $top_level_data = $data['styles']['variations'][ $variation_name ] ?? array(); if ( ! empty( $top_level_data ) ) { $variation['styles'] = array_replace_recursive( $variation['styles'], $top_level_data ); } // Then, override styles so far with any block-level styles. $block_level_data = $data['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array(); if ( ! empty( $block_level_data ) ) { $variation['styles'] = array_replace_recursive( $variation['styles'], $block_level_data ); } $path = array( 'styles', 'blocks', $block_type, 'variations', $variation_name ); _wp_array_set( $data, $path, $variation['styles'] ); } } return $data; } /** * Adds variations sourced from the block styles registry to the supplied theme.json data. * * @since 6.6.0 * * @param array $data Array following the theme.json specification. * @return array Theme json data including shared block style variation definitions. */ private static function inject_variations_from_block_styles_registry( $data ) { $registry = WP_Block_Styles_Registry::get_instance(); $styles = $registry->get_all_registered(); foreach ( $styles as $block_type => $variations ) { foreach ( $variations as $variation_name => $variation ) { if ( empty( $variation['style_data'] ) ) { continue; } // First, override registry styles with any top-level styles. $top_level_data = $data['styles']['variations'][ $variation_name ] ?? array(); if ( ! empty( $top_level_data ) ) { $variation['style_data'] = array_replace_recursive( $variation['style_data'], $top_level_data ); } // Then, override styles so far with any block-level styles. $block_level_data = $data['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array(); if ( ! empty( $block_level_data ) ) { $variation['style_data'] = array_replace_recursive( $variation['style_data'], $block_level_data ); } $path = array( 'styles', 'blocks', $block_type, 'variations', $variation_name ); _wp_array_set( $data, $path, $variation['style_data'] ); } } return $data; } } * @param array $preset The duotone preset. * @return string The SVG for the filter definition. */ public static function get_filter_svg_from_preset( $preset ) { _deprecated_function( __FUNCTION__, '6.3.0' ); $filter_id = self::get_filter_id_from_preset( $preset ); return self::get_filter_svg( $filter_id, $preset['colors'] ); } /** * Get the SVGs for the duotone filters. * * Example output: * * * @internal * * @since 6.3.0 * * @param array $sources The duotone presets. * @return string The SVGs for the duotone filters. */ private static function get_svg_definitions( $sources ) { $svgs = ''; foreach ( $sources as $filter_id => $filter_data ) { $colors = $filter_data['colors']; $svgs .= self::get_filter_svg( $filter_id, $colors ); } return $svgs; } /** * Get the CSS for global styles. * * Example output: * body{--wp--preset--duotone--blue-orange:url('#wp-duotone-blue-orange');} * * @internal * * @since 6.3.0 * @since 6.6.0 Replaced body selector with `WP_Theme_JSON::ROOT_CSS_PROPERTIES_SELECTOR`. * * @param array $sources The duotone presets. * @return string The CSS for global styles. */ private static function get_global_styles_presets( $sources ) { $css = WP_Theme_JSON::ROOT_CSS_PROPERTIES_SELECTOR . '{'; foreach ( $sources as $filter_id => $filter_data ) { $slug = $filter_data['slug']; $colors = $filter_data['colors']; $css_property_name = self::get_css_custom_property_name( $slug ); $declaration_value = is_string( $colors ) ? $colors : self::get_filter_url( $filter_id ); $css .= "$css_property_name:$declaration_value;"; } $css .= '}'; return $css; } /** * Enqueue a block CSS declaration for the page. * * This does not include any SVGs. * * @internal * * @since 6.3.0 * * @param string $filter_id The filter ID. e.g. 'wp-duotone-000000-ffffff-2'. * @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'. * @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-000000-ffffff-2)' or 'unset'. */ private static function enqueue_block_css( $filter_id, $duotone_selector, $filter_value ) { // Build the CSS selectors to which the filter will be applied. $selectors = explode( ',', $duotone_selector ); $selectors_scoped = array(); foreach ( $selectors as $selector_part ) { /* * Assuming the selector part is a subclass selector (not a tag name) * so we can prepend the filter id class. If we want to support elements * such as `img` or namespaces, we'll need to add a case for that here. */ $selectors_scoped[] = '.' . $filter_id . trim( $selector_part ); } $selector = implode( ', ', $selectors_scoped ); self::$block_css_declarations[] = array( 'selector' => $selector, 'declarations' => array( 'filter' => $filter_value, ), ); } /** * Enqueue custom filter assets for the page. * * Includes an SVG filter and block CSS declaration. * * @internal * * @since 6.3.0 * * @param string $filter_id The filter ID. e.g. 'wp-duotone-000000-ffffff-2'. * @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'. * @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-000000-ffffff-2)' or 'unset'. * @param array $filter_data Duotone filter data with 'slug' and 'colors' keys. */ private static function enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $filter_data ) { self::$used_svg_filter_data[ $filter_id ] = $filter_data; self::enqueue_block_css( $filter_id, $duotone_selector, $filter_value ); } /** * Enqueue preset assets for the page. * * Includes a CSS custom property, SVG filter, and block CSS declaration. * * @internal * * @since 6.3.0 * * @param string $filter_id The filter ID. e.g. 'wp-duotone-blue-orange'. * @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'. * @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-blue-orange)' or 'unset'. */ private static function enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value ) { $global_styles_presets = self::get_all_global_styles_presets(); if ( ! array_key_exists( $filter_id, $global_styles_presets ) ) { $error_message = sprintf( /* translators: 1: Duotone filter ID, 2: theme.json */ __( 'The duotone id "%1$s" is not registered in %2$s settings' ), $filter_id, 'theme.json' ); _doing_it_wrong( __METHOD__, $error_message, '6.3.0' ); return; } self::$used_global_styles_presets[ $filter_id ] = $global_styles_presets[ $filter_id ]; self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $global_styles_presets[ $filter_id ] ); } /** * Registers the style and colors block attributes for block types that support it. * * Block support is added with `supports.filter.duotone` in block.json. * * @since 6.3.0 * * @param WP_Block_Type $block_type Block Type. */ public static function register_duotone_support( $block_type ) { /* * Previous `color.__experimentalDuotone` support flag is migrated * to `filter.duotone` via `block_type_metadata_settings` filter. */ if ( block_has_support( $block_type, array( 'filter', 'duotone' ), null ) ) { if ( ! $block_type->attributes ) { $block_type->attributes = array(); } if ( ! array_key_exists( 'style', $block_type->attributes ) ) { $block_type->attributes['style'] = array( 'type' => 'object', ); } } } /** * Get the CSS selector for a block type. * * This handles selectors defined in `color.__experimentalDuotone` support * if `filter.duotone` support is not defined. * * @internal * @since 6.3.0 * * @param WP_Block_Type $block_type Block type to check for support. * @return string|null The CSS selector or null if there is no support. */ private static function get_selector( $block_type ) { if ( ! ( $block_type instanceof WP_Block_Type ) ) { return null; } /* * Backward compatibility with `supports.color.__experimentalDuotone` * is provided via the `block_type_metadata_settings` filter. If * `supports.filter.duotone` has not been set and the experimental * property has been, the experimental property value is copied into * `supports.filter.duotone`. */ $duotone_support = block_has_support( $block_type, array( 'filter', 'duotone' ) ); if ( ! $duotone_support ) { return null; } /* * If the experimental duotone support was set, that value is to be * treated as a selector and requires scoping. */ $experimental_duotone = isset( $block_type->supports['color']['__experimentalDuotone'] ) ? $block_type->supports['color']['__experimentalDuotone'] : false; if ( $experimental_duotone ) { $root_selector = wp_get_block_css_selector( $block_type ); return is_string( $experimental_duotone ) ? WP_Theme_JSON::scope_selector( $root_selector, $experimental_duotone ) : $root_selector; } // Regular filter.duotone support uses filter.duotone selectors with fallbacks. return wp_get_block_css_selector( $block_type, array( 'filter', 'duotone' ), true ); } /** * Scrape all possible duotone presets from global and theme styles and * store them in self::$global_styles_presets. * * Used in conjunction with self::render_duotone_support for blocks that * use duotone preset filters. * * @since 6.3.0 * * @return array An array of global styles presets, keyed on the filter ID. */ private static function get_all_global_styles_presets() { if ( isset( self::$global_styles_presets ) ) { return self::$global_styles_presets; } // Get the per block settings from the theme.json. $tree = wp_get_global_settings(); $presets_by_origin = isset( $tree['color']['duotone'] ) ? $tree['color']['duotone'] : array(); self::$global_styles_presets = array(); foreach ( $presets_by_origin as $presets ) { foreach ( $presets as $preset ) { $filter_id = self::get_filter_id( _wp_to_kebab_case( $preset['slug'] ) ); self::$global_styles_presets[ $filter_id ] = $preset; } } return self::$global_styles_presets; } /** * Scrape all block names from global styles and store in self::$global_styles_block_names. * * Used in conjunction with self::render_duotone_support to output the * duotone filters defined in the theme.json global styles. * * @since 6.3.0 * * @return string[] An array of global style block slugs, keyed on the block name. */ private static function get_all_global_style_block_names() { if ( isset( self::$global_styles_block_names ) ) { return self::$global_styles_block_names; } // Get the per block settings from the theme.json. $tree = WP_Theme_JSON_Resolver::get_merged_data(); $block_nodes = $tree->get_styles_block_nodes(); $theme_json = $tree->get_raw_data(); self::$global_styles_block_names = array(); foreach ( $block_nodes as $block_node ) { // This block definition doesn't include any duotone settings. Skip it. if ( empty( $block_node['duotone'] ) ) { continue; } // Value looks like this: 'var(--wp--preset--duotone--blue-orange)' or 'var:preset|duotone|blue-orange'. $duotone_attr_path = array_merge( $block_node['path'], array( 'filter', 'duotone' ) ); $duotone_attr = _wp_array_get( $theme_json, $duotone_attr_path, array() ); if ( empty( $duotone_attr ) ) { continue; } // If it has a duotone filter preset, save the block name and the preset slug. $slug = self::get_slug_from_attribute( $duotone_attr ); if ( $slug && $slug !== $duotone_attr ) { self::$global_styles_block_names[ $block_node['name'] ] = $slug; } } return self::$global_styles_block_names; } /** * Render out the duotone CSS styles and SVG. * * The hooks self::set_global_style_block_names and self::set_global_styles_presets * must be called before this function. * * @since 6.3.0 * * @param string $block_content Rendered block content. * @param array $block Block object. * @param WP_Block $wp_block The block instance. * @return string Filtered block content. */ public static function render_duotone_support( $block_content, $block, $wp_block ) { if ( ! $block['blockName'] ) { return $block_content; } $duotone_selector = self::get_selector( $wp_block->block_type ); if ( ! $duotone_selector ) { return $block_content; } $global_styles_block_names = self::get_all_global_style_block_names(); // The block should have a duotone attribute or have duotone defined in its theme.json to be processed. $has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] ); $has_global_styles_duotone = array_key_exists( $block['blockName'], $global_styles_block_names ); if ( ! $has_duotone_attribute && ! $has_global_styles_duotone ) { return $block_content; } // Generate the pieces needed for rendering a duotone to the page. if ( $has_duotone_attribute ) { /* * Possible values for duotone attribute: * 1. Array of colors - e.g. array('#000000', '#ffffff'). * 2. Variable for an existing Duotone preset - e.g. 'var:preset|duotone|blue-orange' or 'var(--wp--preset--duotone--blue-orange)'' * 3. A CSS string - e.g. 'unset' to remove globally applied duotone. */ $duotone_attr = $block['attrs']['style']['color']['duotone']; $is_preset = is_string( $duotone_attr ) && self::is_preset( $duotone_attr ); $is_css = is_string( $duotone_attr ) && ! $is_preset; $is_custom = is_array( $duotone_attr ); if ( $is_preset ) { $slug = self::get_slug_from_attribute( $duotone_attr ); // e.g. 'blue-orange'. $filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-blue-orange'. $filter_value = self::get_css_var( $slug ); // e.g. 'var(--wp--preset--duotone--blue-orange)'. // CSS custom property, SVG filter, and block CSS. self::enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value ); } elseif ( $is_css ) { $slug = wp_unique_id( sanitize_key( $duotone_attr . '-' ) ); // e.g. 'unset-1'. $filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-unset-1'. $filter_value = $duotone_attr; // e.g. 'unset'. // Just block CSS. self::enqueue_block_css( $filter_id, $duotone_selector, $filter_value ); } elseif ( $is_custom ) { $slug = wp_unique_id( sanitize_key( implode( '-', $duotone_attr ) . '-' ) ); // e.g. '000000-ffffff-2'. $filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-000000-ffffff-2'. $filter_value = self::get_filter_url( $filter_id ); // e.g. 'url(#wp-duotone-filter-000000-ffffff-2)'. $filter_data = array( 'slug' => $slug, 'colors' => $duotone_attr, ); // SVG filter and block CSS. self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $filter_data ); } } elseif ( $has_global_styles_duotone ) { $slug = $global_styles_block_names[ $block['blockName'] ]; // e.g. 'blue-orange'. $filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-blue-orange'. $filter_value = self::get_css_var( $slug ); // e.g. 'var(--wp--preset--duotone--blue-orange)'. // CSS custom property, SVG filter, and block CSS. self::enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value ); } // Like the layout hook, this assumes the hook only applies to blocks with a single wrapper. $tags = new WP_HTML_Tag_Processor( $block_content ); if ( $tags->next_tag() ) { $tags->add_class( $filter_id ); } return $tags->get_updated_html(); } /** * Fixes the issue with our generated class name not being added to the block's outer container * in classic themes due to gutenberg_restore_image_outer_container from layout block supports. * * @since 6.6.0 * * @param string $block_content Rendered block content. * @return string Filtered block content. */ public static function restore_image_outer_container( $block_content ) { if ( wp_theme_has_theme_json() ) { return $block_content; } $tags = new WP_HTML_Tag_Processor( $block_content ); $wrapper_query = array( 'tag_name' => 'div', 'class_name' => 'wp-block-image', ); if ( ! $tags->next_tag( $wrapper_query ) ) { return $block_content; } $tags->set_bookmark( 'wrapper-div' ); $tags->next_tag(); $inner_classnames = explode( ' ', $tags->get_attribute( 'class' ) ); foreach ( $inner_classnames as $classname ) { if ( 0 === strpos( $classname, 'wp-duotone' ) ) { $tags->remove_class( $classname ); $tags->seek( 'wrapper-div' ); $tags->add_class( $classname ); break; } } return $tags->get_updated_html(); } /** * Appends the used block duotone filter declarations to the inline block supports CSS. * * Uses the declarations saved in earlier calls to self::enqueue_block_css. * * @since 6.3.0 */ public static function output_block_styles() { if ( ! empty( self::$block_css_declarations ) ) { wp_style_engine_get_stylesheet_from_css_rules( self::$block_css_declarations, array( 'context' => 'block-supports', ) ); } } /** * Appends the used global style duotone filter presets (CSS custom * properties) to the inline global styles CSS. * * Uses the declarations saved in earlier calls to self::enqueue_global_styles_preset. * * @since 6.3.0 */ public static function output_global_styles() { if ( ! empty( self::$used_global_styles_presets ) ) { wp_add_inline_style( 'global-styles', self::get_global_styles_presets( self::$used_global_styles_presets ) ); } } /** * Outputs all necessary SVG for duotone filters, CSS for classic themes. * * Uses the declarations saved in earlier calls to self::enqueue_global_styles_preset * and self::enqueue_custom_filter. * * @since 6.3.0 */ public static function output_footer_assets() { if ( ! empty( self::$used_svg_filter_data ) ) { echo self::get_svg_definitions( self::$used_svg_filter_data ); } // In block themes, the CSS is added in the head via wp_add_inline_style in the wp_enqueue_scripts action. if ( ! wp_is_block_theme() ) { $style_tag_id = 'core-block-supports-duotone'; wp_register_style( $style_tag_id, false ); if ( ! empty( self::$used_global_styles_presets ) ) { wp_add_inline_style( $style_tag_id, self::get_global_styles_presets( self::$used_global_styles_presets ) ); } if ( ! empty( self::$block_css_declarations ) ) { wp_add_inline_style( $style_tag_id, wp_style_engine_get_stylesheet_from_css_rules( self::$block_css_declarations ) ); } wp_enqueue_style( $style_tag_id ); } } /** * Adds the duotone SVGs and CSS custom properties to the editor settings. * * This allows the properties to be pulled in by the EditorStyles component * in JS and rendered in the post editor. * * @since 6.3.0 * * @param array $settings The block editor settings from the `block_editor_settings_all` filter. * @return array The editor settings with duotone SVGs and CSS custom properties. */ public static function add_editor_settings( $settings ) { $global_styles_presets = self::get_all_global_styles_presets(); if ( ! empty( $global_styles_presets ) ) { if ( ! isset( $settings['styles'] ) ) { $settings['styles'] = array(); } $settings['styles'][] = array( // For the editor we can add all of the presets by default. 'assets' => self::get_svg_definitions( $global_styles_presets ), // The 'svgs' type is new in 6.3 and requires the corresponding JS changes in the EditorStyles component to work. '__unstableType' => 'svgs', // These styles not generated by global styles, so this must be false or they will be stripped out in wp_get_block_editor_settings. 'isGlobalStyles' => false, ); $settings['styles'][] = array( // For the editor we can add all of the presets by default. 'css' => self::get_global_styles_presets( $global_styles_presets ), // This must be set and must be something other than 'theme' or they will be stripped out in the post editor component. '__unstableType' => 'presets', // These styles are no longer generated by global styles, so this must be false or they will be stripped out in wp_get_block_editor_settings. 'isGlobalStyles' => false, ); } return $settings; } /** * Migrates the experimental duotone support flag to the stabilized location. * * This moves `supports.color.__experimentalDuotone` to `supports.filter.duotone`. * * @since 6.3.0 * * @param array $settings Current block type settings. * @param array $metadata Block metadata as read in via block.json. * @return array Filtered block type settings. */ public static function migrate_experimental_duotone_support_flag( $settings, $metadata ) { $duotone_support = isset( $metadata['supports']['color']['__experimentalDuotone'] ) ? $metadata['supports']['color']['__experimentalDuotone'] : null; if ( ! isset( $settings['supports']['filter']['duotone'] ) && null !== $duotone_support ) { _wp_array_set( $settings, array( 'supports', 'filter', 'duotone' ), (bool) $duotone_support ); } return $settings; } /** * Gets the CSS filter property value from a preset. * * Exported for the deprecated function wp_get_duotone_filter_id(). * * @internal * * @since 6.3.0 * @deprecated 6.3.0 * * @param array $preset The duotone preset. * @return string The CSS filter property value. */ public static function get_filter_css_property_value_from_preset( $preset ) { _deprecated_function( __FUNCTION__, '6.3.0' ); if ( isset( $preset['colors'] ) && is_string( $preset['colors'] ) ) { return $preset['colors']; } $filter_id = self::get_filter_id_from_preset( $preset ); return 'url(#' . $filter_id . ')'; } }