} // 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; } }