ce 5.9.0 Added support for values of array type, which are returned as is. * @since 6.1.0 Added the `$theme_json` parameter. * @since 6.3.0 It no longer converts the internal format "var:preset|color|secondary" * to the standard form "--wp--preset--color--secondary". * This is already done by the sanitize method, * so every property will be in the standard form. * * @param array $styles Styles subtree. * @param array $path Which property to process. * @param array $theme_json Theme JSON array. * @return string|array Style property value. */ protected static function get_property_value( $styles, $path, $theme_json = null ) { $value = _wp_array_get( $styles, $path, '' ); if ( '' === $value || null === $value ) { // No need to process the value further. return ''; } /* * This converts references to a path to the value at that path * where the values is an array with a "ref" key, pointing to a path. * For example: { "ref": "style.color.background" } => "#fff". */ if ( is_array( $value ) && isset( $value['ref'] ) ) { $value_path = explode( '.', $value['ref'] ); $ref_value = _wp_array_get( $theme_json, $value_path ); // Only use the ref value if we find anything. if ( ! empty( $ref_value ) && is_string( $ref_value ) ) { $value = $ref_value; } if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) { $path_string = json_encode( $path ); $ref_value_string = json_encode( $ref_value ); _doing_it_wrong( 'get_property_value', sprintf( /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */ __( 'Your %1$s file uses a dynamic value (%2$s) for the path at %3$s. However, the value at %3$s is also a dynamic value (pointing to %4$s) and pointing to another dynamic value is not supported. Please update %3$s to point directly to %4$s.' ), 'theme.json', $ref_value_string, $path_string, $ref_value['ref'] ), '6.1.0' ); } } if ( is_array( $value ) ) { return $value; } return $value; } /** * Builds metadata for the setting nodes, which returns in the form of: * * [ * [ * 'path' => ['path', 'to', 'some', 'node' ], * 'selector' => 'CSS selector for some node' * ], * [ * 'path' => [ 'path', 'to', 'other', 'node' ], * 'selector' => 'CSS selector for other node' * ], * ] * * @since 5.8.0 * * @param array $theme_json The tree to extract setting nodes from. * @param array $selectors List of selectors per block. * @return array An array of setting nodes metadata. */ protected static function get_setting_nodes( $theme_json, $selectors = array() ) { $nodes = array(); if ( ! isset( $theme_json['settings'] ) ) { return $nodes; } // Top-level. $nodes[] = array( 'path' => array( 'settings' ), 'selector' => static::ROOT_BLOCK_SELECTOR, ); // Calculate paths for blocks. if ( ! isset( $theme_json['settings']['blocks'] ) ) { return $nodes; } foreach ( $theme_json['settings']['blocks'] as $name => $node ) { $selector = null; if ( isset( $selectors[ $name ]['selector'] ) ) { $selector = $selectors[ $name ]['selector']; } $nodes[] = array( 'path' => array( 'settings', 'blocks', $name ), 'selector' => $selector, ); } return $nodes; } /** * Builds metadata for the style nodes, which returns in the form of: * * [ * [ * 'path' => [ 'path', 'to', 'some', 'node' ], * 'selector' => 'CSS selector for some node', * 'duotone' => 'CSS selector for duotone for some node' * ], * [ * 'path' => ['path', 'to', 'other', 'node' ], * 'selector' => 'CSS selector for other node', * 'duotone' => null * ], * ] * * @since 5.8.0 * * @param array $theme_json The tree to extract style nodes from. * @param array $selectors List of selectors per block. * @return array An array of style nodes metadata. */ protected static function get_style_nodes( $theme_json, $selectors = array() ) { $nodes = array(); if ( ! isset( $theme_json['styles'] ) ) { return $nodes; } // Top-level. $nodes[] = array( 'path' => array( 'styles' ), 'selector' => static::ROOT_BLOCK_SELECTOR, ); if ( isset( $theme_json['styles']['elements'] ) ) { foreach ( self::ELEMENTS as $element => $selector ) { if ( ! isset( $theme_json['styles']['elements'][ $element ] ) ) { continue; } $nodes[] = array( 'path' => array( 'styles', 'elements', $element ), 'selector' => static::ELEMENTS[ $element ], ); // Handle any pseudo selectors for the element. if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) { foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) { $nodes[] = array( 'path' => array( 'styles', 'elements', $element ), 'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ), ); } } } } } // Blocks. if ( ! isset( $theme_json['styles']['blocks'] ) ) { return $nodes; } $block_nodes = static::get_block_nodes( $theme_json ); foreach ( $block_nodes as $block_node ) { $nodes[] = $block_node; } /** * Filters the list of style nodes with metadata. * * This allows for things like loading block CSS independently. * * @since 6.1.0 * * @param array $nodes Style nodes with metadata. */ return apply_filters( 'wp_theme_json_get_style_nodes', $nodes ); } /** * A public helper to get the block nodes from a theme.json file. * * @since 6.1.0 * * @return array The block nodes in theme.json. */ public function get_styles_block_nodes() { return static::get_block_nodes( $this->theme_json ); } /** * Returns a filtered declarations array if there is a separator block with only a background * style defined in theme.json by adding a color attribute to reflect the changes in the front. * * @since 6.1.1 * * @param array $declarations List of declarations. * @return array $declarations List of declarations filtered. */ private static function update_separator_declarations( $declarations ) { $background_color = ''; $border_color_matches = false; $text_color_matches = false; foreach ( $declarations as $declaration ) { if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) { $background_color = $declaration['value']; } elseif ( 'border-color' === $declaration['name'] ) { $border_color_matches = true; } elseif ( 'color' === $declaration['name'] ) { $text_color_matches = true; } if ( $background_color && $border_color_matches && $text_color_matches ) { break; } } if ( $background_color && ! $border_color_matches && ! $text_color_matches ) { $declarations[] = array( 'name' => 'color', 'value' => $background_color, ); } return $declarations; } /** * An internal method to get the block nodes from a theme.json file. * * @since 6.1.0 * @since 6.3.0 Refactored and stabilized selectors API. * * @param array $theme_json The theme.json converted to an array. * @return array The block nodes in theme.json. */ private static function get_block_nodes( $theme_json ) { $selectors = static::get_blocks_metadata(); $nodes = array(); if ( ! isset( $theme_json['styles'] ) ) { return $nodes; } // Blocks. if ( ! isset( $theme_json['styles']['blocks'] ) ) { return $nodes; } foreach ( $theme_json['styles']['blocks'] as $name => $node ) { $selector = null; if ( isset( $selectors[ $name ]['selector'] ) ) { $selector = $selectors[ $name ]['selector']; } $duotone_selector = null; if ( isset( $selectors[ $name ]['duotone'] ) ) { $duotone_selector = $selectors[ $name ]['duotone']; } $feature_selectors = null; if ( isset( $selectors[ $name ]['selectors'] ) ) { $feature_selectors = $selectors[ $name ]['selectors']; } $variation_selectors = array(); if ( isset( $node['variations'] ) ) { foreach ( $node['variations'] as $variation => $node ) { $variation_selectors[] = array( 'path' => array( 'styles', 'blocks', $name, 'variations', $variation ), 'selector' => $selectors[ $name ]['styleVariations'][ $variation ], ); } } $nodes[] = array( 'name' => $name, 'path' => array( 'styles', 'blocks', $name ), 'selector' => $selector, 'selectors' => $feature_selectors, 'duotone' => $duotone_selector, 'features' => $feature_selectors, 'variations' => $variation_selectors, ); if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { $nodes[] = array( 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), 'selector' => $selectors[ $name ]['elements'][ $element ], ); // Handle any pseudo selectors for the element. if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) { foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) { $nodes[] = array( 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ), ); } } } } } } return $nodes; } /** * Gets the CSS rules for a particular block from theme.json. * * @since 6.1.0 * * @param array $block_metadata Metadata about the block to get styles for. * * @return string Styles for the block. */ public function get_styles_for_block( $block_metadata ) { $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; $selector = $block_metadata['selector']; $settings = isset( $this->theme_json['settings'] ) ? $this->theme_json['settings'] : array(); $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node ); // If there are style variations, generate the declarations for them, including any feature selectors the block may have. $style_variation_declarations = array(); if ( ! empty( $block_metadata['variations'] ) ) { foreach ( $block_metadata['variations'] as $style_variation ) { $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); $clean_style_variation_selector = trim( $style_variation['selector'] ); // Generate any feature/subfeature style declarations for the current style variation. $variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node ); // Combine selectors with style variation's selector and add to overall style variation declarations. foreach ( $variation_declarations as $current_selector => $new_declarations ) { // If current selector includes block classname, remove it but leave the whitespace in. $shortened_selector = str_replace( $block_metadata['selector'] . ' ', ' ', $current_selector ); // Prepend the variation selector to the current selector. $split_selectors = explode( ',', $shortened_selector ); $updated_selectors = array_map( static function ( $split_selector ) use ( $clean_style_variation_selector ) { return $clean_style_variation_selector . $split_selector; }, $split_selectors ); $combined_selectors = implode( ',', $updated_selectors ); // Add the new declarations to the overall results under the modified selector. $style_variation_declarations[ $combined_selectors ] = $new_declarations; } // Compute declarations for remaining styles not covered by feature level selectors. $style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json ); } } /* * Get a reference to element name from path. * $block_metadata['path'] = array( 'styles','elements','link' ); * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ]. * Skip non-element paths like just ['styles']. */ $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; $element_pseudo_allowed = array(); if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) { $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ]; } /* * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover"). * This also resets the array keys. */ $pseudo_matches = array_values( array_filter( $element_pseudo_allowed, static function ( $pseudo_selector ) use ( $selector ) { return str_contains( $selector, $pseudo_selector ); } ) ); $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null; /* * If the current selector is a pseudo selector that's defined in the allow list for the current * element then compute the style properties for it. * Otherwise just compute the styles for the default selector as normal. */ if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) ) { $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding ); } else { $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding ); } $block_rules = ''; /* * 1. Separate the declarations that use the general selector * from the ones using the duotone selector. */ $declarations_duotone = array(); foreach ( $declarations as $index => $declaration ) { if ( 'filter' === $declaration['name'] ) { unset( $declarations[ $index ] ); $declarations_duotone[] = $declaration; } } // Update declarations if there are separators with only background color defined. if ( '.wp-block-separator' === $selector ) { $declarations = static::update_separator_declarations( $declarations ); } // 2. Generate and append the rules that use the general selector. $block_rules .= static::to_ruleset( $selector, $declarations ); // 3. Generate and append the rules that use the duotone selector. if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { $block_rules .= static::to_ruleset( $block_metadata['duotone'], $declarations_duotone ); } // 4. Generate Layout block gap styles. if ( static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $block_metadata['name'] ) ) { $block_rules .= $this->get_layout_styles( $block_metadata ); } // 5. Generate and append the feature level rulesets. foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) { $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations ); } // 6. Generate and append the style variation rulesets. foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) { $block_rules .= static::to_ruleset( $style_variation_selector, $individual_style_variation_declarations ); } return $block_rules; } /** * Outputs the CSS for layout rules on the root. * * @since 6.1.0 * * @param string $selector The root node selector. * @param array $block_metadata The metadata for the root block. * @return string The additional root rules CSS. */ public function get_root_layout_rules( $selector, $block_metadata ) { $css = ''; $settings = isset( $this->theme_json['settings'] ) ? $this->theme_json['settings'] : array(); $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; /* * Reset default browser margin on the root body element. * This is set on the root selector **before** generating the ruleset * from the `theme.json`. This is to ensure that if the `theme.json` declares * `margin` in its `spacing` declaration for the `body` element then these * user-generated values take precedence in the CSS cascade. * @link https://github.com/WordPress/gutenberg/issues/36147. */ $css .= 'body { margin: 0;'; /* * If there are content and wide widths in theme.json, output them * as custom properties on the body element so all blocks can use them. */ if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) { $content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize']; $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial'; $wide_size = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize']; $wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial'; $css .= '--wp--style--global--content-size: ' . $content_size . ';'; $css .= '--wp--style--global--wide-size: ' . $wide_size . ';'; } $css .= ' }'; if ( $use_root_padding ) { // Top and bottom padding are applied to the outer block container. $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }'; // Right and left padding are applied to the first container with `.has-global-padding` class. $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; // Nested containers with `.has-global-padding` class do not get padding. $css .= '.has-global-padding :where(.has-global-padding:not(.wp-block-block)) { padding-right: 0; padding-left: 0; }'; // Alignfull children of the container with left and right padding have negative margins so they can still be full width. $css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }'; // The above rule is negated for alignfull children of nested containers. $css .= '.has-global-padding :where(.has-global-padding:not(.wp-block-block)) > .alignfull { margin-right: 0; margin-left: 0; }'; // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks. $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding):not(.is-layout-flex):not(.is-layout-grid)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks. $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }'; } $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; $block_gap_value = isset( $this->theme_json['styles']['spacing']['blockGap'] ) ? $this->theme_json['styles']['spacing']['blockGap'] : '0.5em'; $has_block_gap_support = isset( $this->theme_json['settings']['spacing']['blockGap'] ); if ( $has_block_gap_support ) { $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) ); $css .= ":where(.wp-site-blocks) > * { margin-block-start: $block_gap_value; margin-block-end: 0; }"; $css .= ':where(.wp-site-blocks) > :first-child:first-child { margin-block-start: 0; }'; $css .= ':where(.wp-site-blocks) > :last-child:last-child { margin-block-end: 0; }'; // For backwards compatibility, ensure the legacy block gap CSS variable is still available. $css .= "$selector { --wp--style--block-gap: $block_gap_value; }"; } $css .= $this->get_layout_styles( $block_metadata ); return $css; } /** * For metadata values that can either be booleans or paths to booleans, gets the value. * * $data = array( * 'color' => array( * 'defaultPalette' => true * ) * ); * * static::get_metadata_boolean( $data, false ); * // => false * * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) ); * // => true * * @since 6.0.0 * * @param array $data The data to inspect. * @param bool|array $path Boolean or path to a boolean. * @param bool $default_value Default value if the referenced path is missing. * Default false. * @return bool Value of boolean metadata. */ protected static function get_metadata_boolean( $data, $path, $default_value = false ) { if ( is_bool( $path ) ) { return $path; } if ( is_array( $path ) ) { $value = _wp_array_get( $data, $path ); if ( null !== $value ) { return $value; } } return $default_value; } /** * Merges new incoming data. * * @since 5.8.0 * @since 5.9.0 Duotone preset also has origins. * * @param WP_Theme_JSON $incoming Data to merge. */ public function merge( $incoming ) { $incoming_data = $incoming->get_raw_data(); $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); /* * The array_replace_recursive algorithm merges at the leaf level, * but we don't want leaf arrays to be merged, so we overwrite it. * * For leaf values that are sequential arrays it will use the numeric indexes for replacement. * We rather replace the existing with the incoming value, if it exists. * This is the case of spacing.units. * * For leaf values that are associative arrays it will merge them as expected. * This is also not the behavior we want for the current associative arrays (presets). * We rather replace the existing with the incoming value, if it exists. * This happens, for example, when we merge data from theme.json upon existing * theme supports or when we merge anything coming from the same source twice. * This is the case of color.palette, color.gradients, color.duotone, * typography.fontSizes, or typography.fontFamilies. * * Additionally, for some preset types, we also want to make sure the * values they introduce don't conflict with default values. We do so * by checking the incoming slugs for theme presets and compare them * with the equivalent default presets: if a slug is present as a default * we remove it from the theme presets. */ $nodes = static::get_setting_nodes( $incoming_data ); $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) ); foreach ( $nodes as $node ) { // Replace the spacing.units. $path = $node['path']; $path[] = 'spacing'; $path[] = 'units'; $content = _wp_array_get( $incoming_data, $path, null ); if ( isset( $content ) ) { _wp_array_set( $this->theme_json, $path, $content ); } // Replace the presets. foreach ( static::PRESETS_METADATA as $preset ) { $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true ); foreach ( static::VALID_ORIGINS as $origin ) { $base_path = $node['path']; foreach ( $preset['path'] as $leaf ) { $base_path[] = $leaf; } $path = $base_path; $path[] = $origin; $content = _wp_array_get( $incoming_data, $path, null ); if ( ! isset( $content ) ) { continue; } if ( 'theme' === $origin && $preset['use_default_names'] ) { foreach ( $content as $key => $item ) { if ( ! isset( $item['name'] ) ) { $name = static::get_name_from_defaults( $item['slug'], $base_path ); if ( null !== $name ) { $content[ $key ]['name'] = $name; } } } } if ( ( 'theme' !== $origin ) || ( 'theme' === $origin && $override_preset ) ) { _wp_array_set( $this->theme_json, $path, $content ); } else { $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] ); $slugs = array_merge_recursive( $slugs_global, $slugs_node ); $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() ); $content = static::filter_slugs( $content, $slugs_for_preset ); _wp_array_set( $this->theme_json, $path, $content ); } } } } } /** * Converts all filter (duotone) presets into SVGs. * * @since 5.9.1 * * @param array $origins List of origins to process. * @return string SVG filters. */ public function get_svg_filters( $origins ) { $blocks_metadata = static::get_blocks_metadata(); $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); $filters = ''; foreach ( $setting_nodes as $metadata ) { $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); if ( empty( $node['color']['duotone'] ) ) { continue; } $duotone_presets = $node['color']['duotone']; foreach ( $origins as $origin ) { if ( ! isset( $duotone_presets[ $origin ] ) ) { continue; } foreach ( $duotone_presets[ $origin ] as $duotone_preset ) { $filters .= wp_get_duotone_filter_svg( $duotone_preset ); } } } return $filters; } /** * Determines whether a presets should be overridden or not. * * @since 5.9.0 * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead. * * @param array $theme_json The theme.json like structure to inspect. * @param array $path Path to inspect. * @param bool|array $override Data to compute whether to override the preset. * @return bool */ protected static function should_override_preset( $theme_json, $path, $override ) { _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' ); if ( is_bool( $override ) ) { return $override; } /* * The relationship between whether to override the defaults * and whether the defaults are enabled is inverse: * * - If defaults are enabled => theme presets should not be overridden * - If defaults are disabled => theme presets should be overridden * * For example, a theme sets defaultPalette to false, * making the default palette hidden from the user. * In that case, we want all the theme presets to be present, * so they should override the defaults. */ if ( is_array( $override ) ) { $value = _wp_array_get( $theme_json, array_merge( $path, $override ) ); if ( isset( $value ) ) { return ! $value; } // Search the top-level key if none was found for this node. $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) ); if ( isset( $value ) ) { return ! $value; } return true; } } /** * Returns the default slugs for all the presets in an associative array * whose keys are the preset paths and the leafs is the list of slugs. * * For example: * * array( * 'color' => array( * 'palette' => array( 'slug-1', 'slug-2' ), * 'gradients' => array( 'slug-3', 'slug-4' ), * ), * ) * * @since 5.9.0 * * @param array $data A theme.json like structure. * @param array $node_path The path to inspect. It's 'settings' by default. * @return array */ protected static function get_default_slugs( $data, $node_path ) { $slugs = array(); foreach ( static::PRESETS_METADATA as $metadata ) { $path = $node_path; foreach ( $metadata['path'] as $leaf ) { $path[] = $leaf; } $path[] = 'default'; $preset = _wp_array_get( $data, $path, null ); if ( ! isset( $preset ) ) { continue; } $slugs_for_preset = array(); foreach ( $preset as $item ) { if ( isset( $item['slug'] ) ) { $slugs_for_preset[] = $item['slug']; } } _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset ); } return $slugs; } /** * Gets a `default`'s preset name by a provided slug. * * @since 5.9.0 * * @param string $slug The slug we want to find a match from default presets. * @param array $base_path The path to inspect. It's 'settings' by default. * @return string|null */ protected function get_name_from_defaults( $slug, $base_path ) { $path = $base_path; $path[] = 'default'; $default_content = _wp_array_get( $this->theme_json, $path, null ); if ( ! $default_content ) { return null; } foreach ( $default_content as $item ) { if ( $slug === $item['slug'] ) { return $item['name']; } } return null; } /** * Removes the preset values whose slug is equal to any of given slugs. * * @since 5.9.0 * * @param array $node The node with the presets to validate. * @param array $slugs The slugs that should not be overridden. * @return array The new node. */ protected static function filter_slugs( $node, $slugs ) { if ( empty( $slugs ) ) { return $node; } $new_node = array(); foreach ( $node as $value ) { if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) { $new_node[] = $value; } } return $new_node; } /** * Removes insecure data from theme.json. * * @since 5.9.0 * @since 6.3.2 Preserves global styles block variations when securing styles. * * @param array $theme_json Structure to sanitize. * @return array Sanitized structure. */ public static function remove_insecure_properties( $theme_json ) { $sanitized = array(); $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); $valid_block_names = array_keys( static::get_blocks_metadata() ); $valid_element_names = array_keys( static::ELEMENTS ); $valid_variations = array(); foreach ( self::get_blocks_metadata() as $block_name => $block_meta ) { if ( ! isset( $block_meta['styleVariations'] ) ) { continue; } $valid_variations[ $block_name ] = array_keys( $block_meta['styleVariations'] ); } $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations ); $blocks_metadata = static::get_blocks_metadata(); $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata ); foreach ( $style_nodes as $metadata ) { $input = _wp_array_get( $theme_json, $metadata['path'], array() ); if ( empty( $input ) ) { continue; } // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability. if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) { $output = $input; } else { $output = static::remove_insecure_styles( $input ); } /* * Get a reference to element name from path. * $metadata['path'] = array( 'styles', 'elements', 'link' ); */ $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ]; /* * $output is stripped of pseudo selectors. Re-add and process them * or insecure styles here. */ if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) { foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) { if ( isset( $input[ $pseudo_selector ] ) ) { $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] ); } } } if ( ! empty( $output ) ) { _wp_array_set( $sanitized, $metadata['path'], $output ); } if ( isset( $metadata['variations'] ) ) { foreach ( $metadata['variations'] as $variation ) { $variation_input = _wp_array_get( $theme_json, $variation['path'], array() ); if ( empty( $variation_input ) ) { continue; } $variation_output = static::remove_insecure_styles( $variation_input ); if ( ! empty( $variation_output ) ) { _wp_array_set( $sanitized, $variation['path'], $variation_output ); } } } } $setting_nodes = static::get_setting_nodes( $theme_json ); foreach ( $setting_nodes as $metadata ) { $input = _wp_array_get( $theme_json, $metadata['path'], array() ); if ( empty( $input ) ) { continue; } $output = static::remove_insecure_settings( $input ); if ( ! empty( $output ) ) { _wp_array_set( $sanitized, $metadata['path'], $output ); } } if ( empty( $sanitized['styles'] ) ) { unset( $theme_json['styles'] ); } else { $theme_json['styles'] = $sanitized['styles']; } if ( empty( $sanitized['settings'] ) ) { unset( $theme_json['settings'] ); } else { $theme_json['settings'] = $sanitized['settings']; } return $theme_json; } /** * Processes a setting node and returns the same node * without the insecure settings. * * @since 5.9.0 * * @param array $input Node to process. * @return array */ protected static function remove_insecure_settings( $input ) { $output = array(); foreach ( static::PRESETS_METADATA as $preset_metadata ) { foreach ( static::VALID_ORIGINS as $origin ) { $path_with_origin = $preset_metadata['path']; $path_with_origin[] = $origin; $presets = _wp_array_get( $input, $path_with_origin, null ); if ( null === $presets ) { continue; } $escaped_preset = array(); foreach ( $presets as $preset ) { if ( esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] && sanitize_html_class( $preset['slug'] ) === $preset['slug'] ) { $value = null; if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { $value = $preset[ $preset_metadata['value_key'] ]; } elseif ( isset( $preset_metadata['value_func'] ) && is_callable( $preset_metadata['value_func'] ) ) { $value = call_user_func( $preset_metadata['value_func'], $preset ); } $preset_is_valid = true; foreach ( $preset_metadata['properties'] as $property ) { if ( ! static::is_safe_css_declaration( $property, $value ) ) { $preset_is_valid = false; break; } } if ( $preset_is_valid ) { $escaped_preset[] = $preset; } } } if ( ! empty( $escaped_preset ) ) { _wp_array_set( $output, $path_with_origin, $escaped_preset ); } } } // Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed. static::remove_indirect_properties( $input, $output ); return $output; } /** * Processes a style node and returns the same node * without the insecure styles. * * @since 5.9.0 * * @param array $input Node to process. * @return array */ protected static function remove_insecure_styles( $input ) { $output = array(); $declarations = static::compute_style_properties( $input ); foreach ( $declarations as $declaration ) { if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) { $path = static::PROPERTIES_METADATA[ $declaration['name'] ]; /* * Check the value isn't an array before adding so as to not * double up shorthand and longhand styles. */ $value = _wp_array_get( $input, $path, array() ); if ( ! is_array( $value ) ) { _wp_array_set( $output, $path, $value ); } } } // Ensure indirect properties not handled by `compute_style_properties` are allowed. static::remove_indirect_properties( $input, $output ); return $output; } /** * Checks that a declaration provided by the user is safe. * * @since 5.9.0 * * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`. * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`. * @return bool */ protected static function is_safe_css_declaration( $property_name, $property_value ) { $style_to_validate = $property_name . ': ' . $property_value; $filtered = esc_html( safecss_filter_attr( $style_to_validate ) ); return ! empty( trim( $filtered ) ); } /** * Removes indirect properties from the given input node and * sets in the given output node. * * @since 6.2.0 * * @param array $input Node to process. * @param array $output The processed node. Passed by reference. */ private static function remove_indirect_properties( $input, &$output ) { foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) { foreach ( $paths as $path ) { $value = _wp_array_get( $input, $path ); if ( is_string( $value ) && static::is_safe_css_declaration( $property, $value ) ) { _wp_array_set( $output, $path, $value ); } } } } /** * Returns the raw data. * * @since 5.8.0 * * @return array Raw data. */ public function get_raw_data() { return $this->theme_json; } /** * Transforms the given editor settings according the * add_theme_support format to the theme.json format. * * @since 5.8.0 * * @param array $settings Existing editor settings. * @return array Config that adheres to the theme.json schema. */ public static function get_from_editor_settings( $settings ) { $theme_settings = array( 'version' => static::LATEST_SCHEMA, 'settings' => array(), ); // Deprecated theme supports. if ( isset( $settings['disableCustomColors'] ) ) { if ( ! isset( $theme_settings['settings']['color'] ) ) { $theme_settings['settings']['color'] = array(); } $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors']; } if ( isset( $settings['disableCustomGradients'] ) ) { if ( ! isset( $theme_settings['settings']['color'] ) ) { $theme_settings['settings']['color'] = array(); } $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients']; } if ( isset( $settings['disableCustomFontSizes'] ) ) { if ( ! isset( $theme_settings['settings']['typography'] ) ) { $theme_settings['settings']['typography'] = array(); } $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes']; } if ( isset( $settings['enableCustomLineHeight'] ) ) { if ( ! isset( $theme_settings['settings']['typography'] ) ) { $theme_settings['settings']['typography'] = array(); } $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight']; } if ( isset( $settings['enableCustomUnits'] ) ) { if ( ! isset( $theme_settings['settings']['spacing'] ) ) { $theme_settings['settings']['spacing'] = array(); } $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) : $settings['enableCustomUnits']; } if ( isset( $settings['colors'] ) ) { if ( ! isset( $theme_settings['settings']['color'] ) ) { $theme_settings['settings']['color'] = array(); } $theme_settings['settings']['color']['palette'] = $settings['colors']; } if ( isset( $settings['gradients'] ) ) { if ( ! isset( $theme_settings['settings']['color'] ) ) { $theme_settings['settings']['color'] = array(); } $theme_settings['settings']['color']['gradients'] = $settings['gradients']; } if ( isset( $settings['fontSizes'] ) ) { $font_sizes = $settings['fontSizes']; // Back-compatibility for presets without units. foreach ( $font_sizes as $key => $font_size ) { if ( is_numeric( $font_size['size'] ) ) { $font_sizes[ $key ]['size'] = $font_size['size'] . 'px'; } } if ( ! isset( $theme_settings['settings']['typography'] ) ) { $theme_settings['settings']['typography'] = array(); } $theme_settings['settings']['typography']['fontSizes'] = $font_sizes; } if ( isset( $settings['enableCustomSpacing'] ) ) { if ( ! isset( $theme_settings['settings']['spacing'] ) ) { $theme_settings['settings']['spacing'] = array(); } $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing']; } return $theme_settings; } /** * Returns the current theme's wanted patterns(slugs) to be * registered from Pattern Directory. * * @since 6.0.0 * * @return string[] */ public function get_patterns() { if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) { return $this->theme_json['patterns']; } return array(); } /** * Returns a valid theme.json as provided by a theme. * * Unlike get_raw_data() this returns the presets flattened, as provided by a theme. * This also uses appearanceTools instead of their opt-ins if all of them are true. * * @since 6.0.0 * * @return array */ public function get_data() { $output = $this->theme_json; $nodes = static::get_setting_nodes( $output ); /** * Flatten the theme & custom origins into a single one. * * For example, the following: * * { * "settings": { * "color": { * "palette": { * "theme": [ {} ], * "custom": [ {} ] * } * } * } * } * * will be converted to: * * { * "settings": { * "color": { * "palette": [ {} ] * } * } * } */ foreach ( $nodes as $node ) { foreach ( static::PRESETS_METADATA as $preset_metadata ) { $path = $node['path']; foreach ( $preset_metadata['path'] as $preset_metadata_path ) { $path[] = $preset_metadata_path; } $preset = _wp_array_get( $output, $path, null ); if ( null === $preset ) { continue; } $items = array(); if ( isset( $preset['theme'] ) ) { foreach ( $preset['theme'] as $item ) { $slug = $item['slug']; unset( $item['slug'] ); $items[ $slug ] = $item; } } if ( isset( $preset['custom'] ) ) { foreach ( $preset['custom'] as $item ) { $slug = $item['slug']; unset( $item['slug'] ); $items[ $slug ] = $item; } } $flattened_preset = array(); foreach ( $items as $slug => $value ) { $flattened_preset[] = array_merge( array( 'slug' => (string) $slug ), $value ); } _wp_array_set( $output, $path, $flattened_preset ); } } /* * If all of the static::APPEARANCE_TOOLS_OPT_INS are true, * this code unsets them and sets 'appearanceTools' instead. */ foreach ( $nodes as $node ) { $all_opt_ins_are_set = true; foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { $full_path = $node['path']; foreach ( $opt_in_path as $opt_in_path_item ) { $full_path[] = $opt_in_path_item; } /* * Use "unset prop" as a marker instead of "null" because * "null" can be a valid value for some props (e.g. blockGap). */ $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); if ( 'unset prop' === $opt_in_value ) { $all_opt_ins_are_set = false; break; } } if ( $all_opt_ins_are_set ) { $node_path_with_appearance_tools = $node['path']; $node_path_with_appearance_tools[] = 'appearanceTools'; _wp_array_set( $output, $node_path_with_appearance_tools, true ); foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { $full_path = $node['path']; foreach ( $opt_in_path as $opt_in_path_item ) { $full_path[] = $opt_in_path_item; } /* * Use "unset prop" as a marker instead of "null" because * "null" can be a valid value for some props (e.g. blockGap). */ $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); if ( true !== $opt_in_value ) { continue; } /* * The following could be improved to be path independent. * At the moment it relies on a couple of assumptions: * * - all opt-ins having a path of size 2. * - there's two sources of settings: the top-level and the block-level. */ if ( ( 1 === count( $node['path'] ) ) && ( 'settings' === $node['path'][0] ) ) { // Top-level settings. unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] ); if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) { unset( $output['settings'][ $opt_in_path[0] ] ); } } elseif ( ( 3 === count( $node['path'] ) ) && ( 'settings' === $node['path'][0] ) && ( 'blocks' === $node['path'][1] ) ) { // Block-level settings. $block_name = $node['path'][2]; unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] ); if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) { unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ); } } } } } wp_recursive_ksort( $output ); return $output; } /** * Sets the spacingSizes array based on the spacingScale values from theme.json. * * @since 6.1.0 * * @return null|void */ public function set_spacing_sizes() { $spacing_scale = isset( $this->theme_json['settings']['spacing']['spacingScale'] ) ? $this->theme_json['settings']['spacing']['spacingScale'] : array(); if ( ! isset( $spacing_scale['steps'] ) || ! is_numeric( $spacing_scale['steps'] ) || ! isset( $spacing_scale['mediumStep'] ) || ! isset( $spacing_scale['unit'] ) || ! isset( $spacing_scale['operator'] ) || ! isset( $spacing_scale['increment'] ) || ! isset( $spacing_scale['steps'] ) || ! is_numeric( $spacing_scale['increment'] ) || ! is_numeric( $spacing_scale['mediumStep'] ) || ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) { if ( ! empty( $spacing_scale ) ) { trigger_error( __( 'Some of the theme.json settings.spacing.spacingScale values are invalid' ), E_USER_NOTICE ); } return null; } // If theme authors want to prevent the generation of the core spacing scale they can set their theme.json spacingScale.steps to 0. if ( 0 === $spacing_scale['steps'] ) { return null; } $unit = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] ); $current_step = $spacing_scale['mediumStep']; $steps_mid_point = round( $spacing_scale['steps'] / 2, 0 ); $x_small_count = null; $below_sizes = array(); $slug = 40; $remainder = 0; for ( $below_midpoint_count = $steps_mid_point - 1; $spacing_scale['steps'] > 1 && $slug > 0 && $below_midpoint_count > 0; $below_midpoint_count-- ) { if ( '+' === $spacing_scale['operator'] ) { $current_step -= $spacing_scale['increment']; } elseif ( $spacing_scale['increment'] > 1 ) { $current_step /= $spacing_scale['increment']; } else { $current_step *= $spacing_scale['increment']; } if ( $current_step <= 0 ) { $remainder = $below_midpoint_count; break; } $below_sizes[] = array( /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */ 'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small' ) : sprintf( __( '%sX-Small' ), (string) $x_small_count ), 'slug' => (string) $slug, 'size' => round( $current_step, 2 ) . $unit, ); if ( $below_midpoint_count === $steps_mid_point - 2 ) { $x_small_count = 2; } if ( $below_midpoint_count < $steps_mid_point - 2 ) { ++$x_small_count; } $slug -= 10; } $below_sizes = array_reverse( $below_sizes ); $below_sizes[] = array( 'name' => __( 'Medium' ), 'slug' => '50', 'size' => $spacing_scale['mediumStep'] . $unit, ); $current_step = $spacing_scale['mediumStep']; $x_large_count = null; $above_sizes = array(); $slug = 60; $steps_above = ( $spacing_scale['steps'] - $steps_mid_point ) + $remainder; for ( $above_midpoint_count = 0; $above_midpoint_count < $steps_above; $above_midpoint_count++ ) { $current_step = '+' === $spacing_scale['operator'] ? $current_step + $spacing_scale['increment'] : ( $spacing_scale['increment'] >= 1 ? $current_step * $spacing_scale['increment'] : $current_step / $spacing_scale['increment'] ); $above_sizes[] = array( /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */ 'name' => 0 === $above_midpoint_count ? __( 'Large' ) : sprintf( __( '%sX-Large' ), (string) $x_large_count ), 'slug' => (string) $slug, 'size' => round( $current_step, 2 ) . $unit, ); if ( 1 === $above_midpoint_count ) { $x_large_count = 2; } if ( $above_midpoint_count > 1 ) { ++$x_large_count; } $slug += 10; } $spacing_sizes = $below_sizes; foreach ( $above_sizes as $above_sizes_item ) { $spacing_sizes[] = $above_sizes_item; } // If there are 7 or fewer steps in the scale revert to numbers for labels instead of t-shirt sizes. if ( $spacing_scale['steps'] <= 7 ) { for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) { $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 ); } } _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes ); } /** * This is used to convert the internal representation of variables to the CSS representation. * For example, `var:preset|color|vivid-green-cyan` becomes `var(--wp--preset--color--vivid-green-cyan)`. * * @since 6.3.0 * @param string $value The variable such as var:preset|color|vivid-green-cyan to convert. * @return string The converted variable. */ private static function convert_custom_properties( $value ) { $prefix = 'var:'; $prefix_len = strlen( $prefix ); $token_in = '|'; $token_out = '--'; if ( str_starts_with( $value, $prefix ) ) { $unwrapped_name = str_replace( $token_in, $token_out, substr( $value, $prefix_len ) ); $value = "var(--wp--$unwrapped_name)"; } return $value; } /** * Given a tree, converts the internal representation of variables to the CSS representation. * It is recursive and modifies the input in-place. * * @since 6.3.0 * @param array $tree Input to process. * @return array The modified $tree. */ private static function resolve_custom_css_format( $tree ) { $prefix = 'var:'; foreach ( $tree as $key => $data ) { if ( is_string( $data ) && str_starts_with( $data, $prefix ) ) { $tree[ $key ] = self::convert_custom_properties( $data ); } elseif ( is_array( $data ) ) { $tree[ $key ] = self::resolve_custom_css_format( $data ); } } return $tree; } /** * Returns the selectors metadata for a block. * * @since 6.3.0 * * @param object $block_type The block type. * @param string $root_selector The block's root selector. * * @return array The custom selectors set by the block. */ protected static function get_block_selectors( $block_type, $root_selector ) { if ( ! empty( $block_type->selectors ) ) { return $block_type->selectors; } $selectors = array( 'root' => $root_selector ); foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) { $feature_selector = wp_get_block_css_selector( $block_type, $key ); if ( null !== $feature_selector ) { $selectors[ $feature ] = array( 'root' => $feature_selector ); } } return $selectors; } /** * Generates all the element selectors for a block. * * @since 6.3.0 * * @param string $root_selector The block's root CSS selector. * @return array The block's element selectors. */ protected static function get_block_element_selectors( $root_selector ) { /* * Assign defaults, then override those that the block sets by itself. * If the block selector is compounded, will append the element to each * individual block selector. */ $block_selectors = explode( ',', $root_selector ); $element_selectors = array(); foreach ( static::ELEMENTS as $el_name => $el_selector ) { $element_selector = array(); foreach ( $block_selectors as $selector ) { if ( $selector === $el_selector ) { $element_selector = array( $el_selector ); break; } $element_selector[] = static::prepend_to_selector( $el_selector, $selector . ' ' ); } $element_selectors[ $el_name ] = implode( ',', $element_selector ); } return $element_selectors; } /** * Generates style declarations for a node's features e.g., color, border, * typography etc. that have custom selectors in their related block's * metadata. * * @since 6.3.0 * * @param object $metadata The related block metadata containing selectors. * @param object $node A merged theme.json node for block or variation. * * @return array The style declarations for the node's features with custom * selectors. */ protected function get_feature_declarations_for_node( $metadata, &$node ) { $declarations = array(); if ( ! isset( $metadata['selectors'] ) ) { return $declarations; } $settings = isset( $this->theme_json['settings'] ) ? $this->theme_json['settings'] : array(); foreach ( $metadata['selectors'] as $feature => $feature_selectors ) { /* * Skip if this is the block's root selector or the block doesn't * have any styles for the feature. */ if ( 'root' === $feature || empty( $node[ $feature ] ) ) { continue; } if ( is_array( $feature_selectors ) ) { foreach ( $feature_selectors as $subfeature => $subfeature_selector ) { if ( 'root' === $subfeature || empty( $node[ $feature ][ $subfeature ] ) ) { continue; } /* * Create temporary node containing only the subfeature data * to leverage existing `compute_style_properties` function. */ $subfeature_node = array( $feature => array( $subfeature => $node[ $feature ][ $subfeature ], ), ); // Generate style declarations. $new_declarations = static::compute_style_properties( $subfeature_node, $settings, null, $this->theme_json ); // Merge subfeature declarations into feature declarations. if ( isset( $declarations[ $subfeature_selector ] ) ) { foreach ( $new_declarations as $new_declaration ) { $declarations[ $subfeature_selector ][] = $new_declaration; } } else { $declarations[ $subfeature_selector ] = $new_declarations; } /* * Remove the subfeature from the block's node now its * styles will be included under its own selector not the * block's. */ unset( $node[ $feature ][ $subfeature ] ); } } /* * Now subfeatures have been processed and removed we can process * feature root selector or simple string selector. */ if ( is_string( $feature_selectors ) || ( isset( $feature_selectors['root'] ) && $feature_selectors['root'] ) ) { $feature_selector = is_string( $feature_selectors ) ? $feature_selectors : $feature_selectors['root']; /* * Create temporary node containing only the feature data * to leverage existing `compute_style_properties` function. */ $feature_node = array( $feature => $node[ $feature ] ); // Generate the style declarations. $new_declarations = static::compute_style_properties( $feature_node, $settings, null, $this->theme_json ); /* * Merge new declarations with any that already exist for * the feature selector. This may occur when multiple block * support features use the same custom selector. */ if ( isset( $declarations[ $feature_selector ] ) ) { foreach ( $new_declarations as $new_declaration ) { $declarations[ $feature_selector ][] = $new_declaration; } } else { $declarations[ $feature_selector ] = $new_declarations; } /* * Remove the feature from the block's node now its styles * will be included under its own selector not the block's. */ unset( $node[ $feature ] ); } } return $declarations; } /** * Replaces CSS variables with their values in place. * * @since 6.3.0 * @param array $styles CSS declarations to convert. * @param array $values key => value pairs to use for replacement. * @return array */ private static function convert_variables_to_value( $styles, $values ) { foreach ( $styles as $key => $style ) { if ( is_array( $style ) ) { $styles[ $key ] = self::convert_variables_to_value( $style, $values ); continue; } if ( 0 <= strpos( $style, 'var(' ) ) { // find all the variables in the string in the form of var(--variable-name, fallback), with fallback in the second capture group. $has_matches = preg_match_all( '/var\(([^),]+)?,?\s?(\S+)?\)/', $style, $var_parts ); if ( $has_matches ) { $resolved_style = $styles[ $key ]; foreach ( $var_parts[1] as $index => $var_part ) { $key_in_values = 'var(' . $var_part . ')'; $rule_to_replace = $var_parts[0][ $index ]; // the css rule to replace e.g. var(--wp--preset--color--vivid-green-cyan). $fallback = $var_parts[2][ $index ]; // the fallback value. $resolved_style = str_replace( array( $rule_to_replace, $fallback, ), array( isset( $values[ $key_in_values ] ) ? $values[ $key_in_values ] : $rule_to_replace, isset( $values[ $fallback ] ) ? $values[ $fallback ] : $fallback, ), $resolved_style ); } $styles[ $key ] = $resolved_style; } } } return $styles; } /** * Resolves the values of CSS variables in the given styles. * * @since 6.3.0 * @param WP_Theme_JSON $theme_json The theme json resolver. * * @return WP_Theme_JSON The $theme_json with resolved variables. */ public static function resolve_variables( $theme_json ) { $settings = $theme_json->get_settings(); $styles = $theme_json->get_raw_data()['styles']; $preset_vars = static::compute_preset_vars( $settings, static::VALID_ORIGINS ); $theme_vars = static::compute_theme_vars( $settings ); $vars = array_reduce( array_merge( $preset_vars, $theme_vars ), function ( $carry, $item ) { $name = $item['name']; $carry[ "var({$name})" ] = $item['value']; return $carry; }, array() ); $theme_json->theme_json['styles'] = self::convert_variables_to_value( $styles, $vars ); return $theme_json; } } mua mật ong rừng ở hà nội – Chân gà Đông Tảo và Gà Đông Tảo biếu tại Hà Nội và HCM