diff --git a/wp-includes/block-editor.php b/wp-includes/block-editor.php
index 4d9707ed05..96339d5e62 100644
--- a/wp-includes/block-editor.php
+++ b/wp-includes/block-editor.php
@@ -303,15 +303,15 @@ function get_block_editor_settings( array $custom_settings, $block_editor_contex
$custom_settings
);
- $theme_json = WP_Theme_JSON_Resolver::get_merged_data( $editor_settings );
+ $theme_json = WP_Theme_JSON_Resolver::get_merged_data();
if ( WP_Theme_JSON_Resolver::theme_has_support() ) {
$editor_settings['styles'][] = array(
- 'css' => $theme_json->get_stylesheet( 'block_styles' ),
+ 'css' => $theme_json->get_stylesheet( array( 'styles', 'presets' ) ),
'__unstableType' => 'globalStyles',
);
$editor_settings['styles'][] = array(
- 'css' => $theme_json->get_stylesheet( 'css_variables' ),
+ 'css' => $theme_json->get_stylesheet( array( 'variables' ) ),
'__experimentalNoWrapper' => true,
'__unstableType' => 'globalStyles',
);
@@ -358,17 +358,17 @@ function get_block_editor_settings( array $custom_settings, $block_editor_contex
$editor_settings['disableCustomFontSizes'] = ! $editor_settings['__experimentalFeatures']['typography']['customFontSize'];
unset( $editor_settings['__experimentalFeatures']['typography']['customFontSize'] );
}
- if ( isset( $editor_settings['__experimentalFeatures']['typography']['customLineHeight'] ) ) {
- $editor_settings['enableCustomLineHeight'] = $editor_settings['__experimentalFeatures']['typography']['customLineHeight'];
- unset( $editor_settings['__experimentalFeatures']['typography']['customLineHeight'] );
+ if ( isset( $editor_settings['__experimentalFeatures']['typography']['lineHeight'] ) ) {
+ $editor_settings['enableCustomLineHeight'] = $editor_settings['__experimentalFeatures']['typography']['lineHeight'];
+ unset( $editor_settings['__experimentalFeatures']['typography']['lineHeight'] );
}
if ( isset( $editor_settings['__experimentalFeatures']['spacing']['units'] ) ) {
$editor_settings['enableCustomUnits'] = $editor_settings['__experimentalFeatures']['spacing']['units'];
unset( $editor_settings['__experimentalFeatures']['spacing']['units'] );
}
- if ( isset( $editor_settings['__experimentalFeatures']['spacing']['customPadding'] ) ) {
- $editor_settings['enableCustomSpacing'] = $editor_settings['__experimentalFeatures']['spacing']['customPadding'];
- unset( $editor_settings['__experimentalFeatures']['spacing']['customPadding'] );
+ if ( isset( $editor_settings['__experimentalFeatures']['spacing']['padding'] ) ) {
+ $editor_settings['enableCustomSpacing'] = $editor_settings['__experimentalFeatures']['spacing']['padding'];
+ unset( $editor_settings['__experimentalFeatures']['spacing']['padding'] );
}
/**
diff --git a/wp-includes/block-supports/duotone.php b/wp-includes/block-supports/duotone.php
index 04aac0ef1e..c2f2d5256d 100644
--- a/wp-includes/block-supports/duotone.php
+++ b/wp-includes/block-supports/duotone.php
@@ -69,6 +69,28 @@ function wp_tinycolor_bound01( $n, $max ) {
return ( $n % $max ) / (float) $max;
}
+/**
+ * Direct port of tinycolor's boundAlpha function to maintain consistency with
+ * how tinycolor works.
+ *
+ * @see https://github.com/bgrins/TinyColor
+ *
+ * @since 5.9.0
+ * @access private
+ *
+ * @param mixed $n Number of unknown type.
+ * @return float Value in the range [0,1].
+ */
+function _wp_tinycolor_bound_alpha( $n ) {
+ if ( is_numeric( $n ) ) {
+ $n = (float) $n;
+ if ( $n >= 0 && $n <= 1 ) {
+ return $n;
+ }
+ }
+ return 1;
+}
+
/**
* Round and convert values of an RGB object.
*
@@ -170,8 +192,7 @@ function wp_tinycolor_hsl_to_rgb( $hsl_color ) {
/**
* Parses hex, hsl, and rgb CSS strings using the same regex as TinyColor v1.4.2
- * used in the JavaScript. Only colors output from react-color are implemented
- * and the alpha value is ignored as it is not used in duotone.
+ * used in the JavaScript. Only colors output from react-color are implemented.
*
* Direct port of TinyColor's function, lightly simplified to maintain
* consistency with TinyColor.
@@ -180,6 +201,7 @@ function wp_tinycolor_hsl_to_rgb( $hsl_color ) {
* @see https://github.com/casesandberg/react-color/
*
* @since 5.8.0
+ * @since 5.9.0 Added alpha processing.
* @access private
*
* @param string $color_str CSS color string.
@@ -199,35 +221,47 @@ function wp_tinycolor_string_to_rgb( $color_str ) {
$rgb_regexp = '/^rgb' . $permissive_match3 . '$/';
if ( preg_match( $rgb_regexp, $color_str, $match ) ) {
- return wp_tinycolor_rgb_to_rgb(
+ $rgb = wp_tinycolor_rgb_to_rgb(
array(
'r' => $match[1],
'g' => $match[2],
'b' => $match[3],
)
);
+
+ $rgb['a'] = 1;
+
+ return $rgb;
}
$rgba_regexp = '/^rgba' . $permissive_match4 . '$/';
if ( preg_match( $rgba_regexp, $color_str, $match ) ) {
- return wp_tinycolor_rgb_to_rgb(
+ $rgb = wp_tinycolor_rgb_to_rgb(
array(
'r' => $match[1],
'g' => $match[2],
'b' => $match[3],
)
);
+
+ $rgb['a'] = _wp_tinycolor_bound_alpha( $match[4] );
+
+ return $rgb;
}
$hsl_regexp = '/^hsl' . $permissive_match3 . '$/';
if ( preg_match( $hsl_regexp, $color_str, $match ) ) {
- return wp_tinycolor_hsl_to_rgb(
+ $rgb = wp_tinycolor_hsl_to_rgb(
array(
'h' => $match[1],
's' => $match[2],
'l' => $match[3],
)
);
+
+ $rgb['a'] = 1;
+
+ return $rgb;
}
$hsla_regexp = '/^hsla' . $permissive_match4 . '$/';
@@ -239,50 +273,87 @@ function wp_tinycolor_string_to_rgb( $color_str ) {
'l' => $match[3],
)
);
+
+ $rgb['a'] = _wp_tinycolor_bound_alpha( $match[4] );
+
+ return $rgb;
}
$hex8_regexp = '/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/';
if ( preg_match( $hex8_regexp, $color_str, $match ) ) {
- return wp_tinycolor_rgb_to_rgb(
+ $rgb = wp_tinycolor_rgb_to_rgb(
array(
'r' => base_convert( $match[1], 16, 10 ),
'g' => base_convert( $match[2], 16, 10 ),
'b' => base_convert( $match[3], 16, 10 ),
)
);
+
+ $rgb['a'] = _wp_tinycolor_bound_alpha(
+ base_convert( $match[4], 16, 10 ) / 255
+ );
+
+ return $rgb;
}
$hex6_regexp = '/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/';
if ( preg_match( $hex6_regexp, $color_str, $match ) ) {
- return wp_tinycolor_rgb_to_rgb(
+ $rgb = wp_tinycolor_rgb_to_rgb(
array(
'r' => base_convert( $match[1], 16, 10 ),
'g' => base_convert( $match[2], 16, 10 ),
'b' => base_convert( $match[3], 16, 10 ),
)
);
+
+ $rgb['a'] = 1;
+
+ return $rgb;
}
$hex4_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/';
if ( preg_match( $hex4_regexp, $color_str, $match ) ) {
- return wp_tinycolor_rgb_to_rgb(
+ $rgb = wp_tinycolor_rgb_to_rgb(
array(
'r' => base_convert( $match[1] . $match[1], 16, 10 ),
'g' => base_convert( $match[2] . $match[2], 16, 10 ),
'b' => base_convert( $match[3] . $match[3], 16, 10 ),
)
);
+
+ $rgb['a'] = _wp_tinycolor_bound_alpha(
+ base_convert( $match[4] . $match[4], 16, 10 ) / 255
+ );
+
+ return $rgb;
}
$hex3_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/';
if ( preg_match( $hex3_regexp, $color_str, $match ) ) {
- return wp_tinycolor_rgb_to_rgb(
+ $rgb = wp_tinycolor_rgb_to_rgb(
array(
'r' => base_convert( $match[1] . $match[1], 16, 10 ),
'g' => base_convert( $match[2] . $match[2], 16, 10 ),
'b' => base_convert( $match[3] . $match[3], 16, 10 ),
)
);
+
+ $rgb['a'] = 1;
+
+ return $rgb;
+ }
+
+ /*
+ * The JS color picker considers the string "transparent" to be a hex value,
+ * so we need to handle it here as a special case.
+ */
+ if ( 'transparent' === $color_str ) {
+ return array(
+ 'r' => 0,
+ 'g' => 0,
+ 'b' => 0,
+ 'a' => 0,
+ );
}
}
@@ -313,6 +384,95 @@ function wp_register_duotone_support( $block_type ) {
}
}
}
+/**
+ * Renders the duotone filter SVG and returns the CSS filter property to
+ * reference the rendered SVG.
+ *
+ * @since 5.9.0
+ *
+ * @param array $preset Duotone preset value as seen in theme.json.
+ * @return string Duotone CSS filter property.
+ */
+function wp_render_duotone_filter_preset( $preset ) {
+ $duotone_id = $preset['slug'];
+ $duotone_colors = $preset['colors'];
+ $filter_id = 'wp-duotone-' . $duotone_id;
+ $duotone_values = array(
+ 'r' => array(),
+ 'g' => array(),
+ 'b' => array(),
+ 'a' => array(),
+ );
+ foreach ( $duotone_colors as $color_str ) {
+ $color = wp_tinycolor_string_to_rgb( $color_str );
+
+ $duotone_values['r'][] = $color['r'] / 255;
+ $duotone_values['g'][] = $color['g'] / 255;
+ $duotone_values['b'][] = $color['b'] / 255;
+ $duotone_values['a'][] = $color['a'];
+ }
+
+ ob_start();
+
+ ?>
+
+
+
+ ', '><', $svg );
+ $svg = trim( $svg );
+ }
+
+ add_action(
+ /*
+ * Safari doesn't render SVG filters defined in data URIs,
+ * and SVG filters won't render in the head of a document,
+ * so the next best place to put the SVG is in the footer.
+ */
+ is_admin() ? 'admin_footer' : 'wp_footer',
+ static function () use ( $svg ) {
+ echo $svg;
+ }
+ );
+
+ return "url('#" . $filter_id . "')";
+}
/**
* Render out the duotone stylesheet and SVG.
diff --git a/wp-includes/class-wp-theme-json-resolver.php b/wp-includes/class-wp-theme-json-resolver.php
index 47427c27e3..34d3ab9fcf 100644
--- a/wp-includes/class-wp-theme-json-resolver.php
+++ b/wp-includes/class-wp-theme-json-resolver.php
@@ -11,6 +11,10 @@
* Class that abstracts the processing of the different data sources
* for site-level config and offers an API to work with them.
*
+ * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes).
+ * This is a low-level API that may need to do breaking changes. Please,
+ * use get_global_settings, get_global_styles, and get_global_stylesheet instead.
+ *
* @access private
*/
class WP_Theme_JSON_Resolver {
@@ -40,9 +44,27 @@ class WP_Theme_JSON_Resolver {
private static $theme_has_support = null;
/**
- * Container to keep loaded i18n schema for `theme.json`.
+ * Container for data coming from the user.
*
* @since 5.9.0
+ * @var WP_Theme_JSON
+ */
+ private static $user = null;
+
+ /**
+ * Stores the ID of the custom post type
+ * that holds the user data.
+ *
+ * @since 5.9.0
+ * @var integer
+ */
+ private static $user_custom_post_type_id = null;
+
+ /**
+ * Container to keep loaded i18n schema for `theme.json`.
+ *
+ * @since 5.8.0
+ * @since 5.9.0 Renamed from $theme_json_i18n
* @var array
*/
private static $i18n_schema = null;
@@ -122,34 +144,45 @@ class WP_Theme_JSON_Resolver {
/**
* Returns the theme's data.
*
- * Data from theme.json can be augmented via the $theme_support_data variable.
- * This is useful, for example, to backfill the gaps in theme.json that a theme
- * has declared via add_theme_supports.
- *
- * Note that if the same data is present in theme.json and in $theme_support_data,
- * the theme.json's is not overwritten.
+ * Data from theme.json will be backfilled from existing
+ * theme supports, if any. Note that if the same data
+ * is present in theme.json and in theme supports,
+ * the theme.json takes precendence.
*
* @since 5.8.0
+ * @since 5.9.0 Theme supports have been inlined and the argument removed.
*
- * @param array $theme_support_data Optional. Theme support data in theme.json format.
- * Default empty array.
* @return WP_Theme_JSON Entity that holds theme data.
*/
- public static function get_theme_data( $theme_support_data = array() ) {
+ public static function get_theme_data( $deprecated = array() ) {
+ if ( ! empty( $deprecated ) ) {
+ _deprecated_argument( __METHOD__, '5.9' );
+ }
if ( null === self::$theme ) {
$theme_json_data = self::read_json_file( self::get_file_path_from_theme( 'theme.json' ) );
$theme_json_data = self::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) );
self::$theme = new WP_Theme_JSON( $theme_json_data );
- }
- if ( empty( $theme_support_data ) ) {
- return self::$theme;
+ if ( wp_get_theme()->parent() ) {
+ // Get parent theme.json.
+ $parent_theme_json_data = self::read_json_file( self::get_file_path_from_theme( 'theme.json', true ) );
+ $parent_theme_json_data = self::translate( $parent_theme_json_data, wp_get_theme()->parent()->get( 'TextDomain' ) );
+ $parent_theme = new WP_Theme_JSON( $parent_theme_json_data );
+
+ // Merge the child theme.json into the parent theme.json.
+ // The child theme takes precedence over the parent.
+ $parent_theme->merge( self::$theme );
+ self::$theme = $parent_theme;
+ }
}
/*
- * We want the presets and settings declared in theme.json
- * to override the ones declared via add_theme_support.
- */
+ * We want the presets and settings declared in theme.json
+ * to override the ones declared via theme supports.
+ * So we take theme supports, transform it to theme.json shape
+ * and merge the self::$theme upon that.
+ */
+ $theme_support_data = WP_Theme_JSON::get_from_editor_settings( get_default_block_editor_settings() );
$with_theme_supports = new WP_Theme_JSON( $theme_support_data );
$with_theme_supports->merge( self::$theme );
@@ -157,40 +190,180 @@ class WP_Theme_JSON_Resolver {
}
/**
- * There are different sources of data for a site: core and theme.
+ * Returns the CPT that contains the user's origin config
+ * for the current theme or a void array if none found.
*
- * While the getters {@link get_core_data}, {@link get_theme_data} return the raw data
- * from the respective origins, this method merges them all together.
+ * It can also create and return a new draft CPT.
*
- * If the same piece of data is declared in different origins (core and theme),
- * the last origin overrides the previous. For example, if core disables custom colors
- * but a theme enables them, the theme config wins.
+ * @since 5.9.0
+ *
+ * @param bool $should_create_cpt Optional. Whether a new CPT should be created if no one was found.
+ * False by default.
+ * @param array $post_status_filter Filter Optional. CPT by post status.
+ * ['publish'] by default, so it only fetches published posts.
+ *
+ * @return array Custom Post Type for the user's origin config.
+ */
+ private static function get_user_data_from_custom_post_type( $should_create_cpt = false, $post_status_filter = array( 'publish' ) ) {
+ $user_cpt = array();
+ $post_type_filter = 'wp_global_styles';
+ $query = new WP_Query(
+ array(
+ 'posts_per_page' => 1,
+ 'orderby' => 'date',
+ 'order' => 'desc',
+ 'post_type' => $post_type_filter,
+ 'post_status' => $post_status_filter,
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'wp_theme',
+ 'field' => 'name',
+ 'terms' => wp_get_theme()->get_stylesheet(),
+ ),
+ ),
+ )
+ );
+
+ if ( is_array( $query->posts ) && ( 1 === $query->post_count ) ) {
+ $user_cpt = $query->posts[0]->to_array();
+ } elseif ( $should_create_cpt ) {
+ $cpt_post_id = wp_insert_post(
+ array(
+ 'post_content' => '{"version": ' . WP_Theme_JSON::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }',
+ 'post_status' => 'publish',
+ 'post_title' => __( 'Custom Styles', 'default' ),
+ 'post_type' => $post_type_filter,
+ 'post_name' => 'wp-global-styles-' . urlencode( wp_get_theme()->get_stylesheet() ),
+ 'tax_input' => array(
+ 'wp_theme' => array( wp_get_theme()->get_stylesheet() ),
+ ),
+ ),
+ true
+ );
+
+ if ( is_wp_error( $cpt_post_id ) ) {
+ $user_cpt = array();
+ } else {
+ $user_cpt = get_post( $cpt_post_id, ARRAY_A );
+ }
+ }
+
+ return $user_cpt;
+ }
+
+ /**
+ * Returns the user's origin config.
+ *
+ * @since 5.9.0
+ *
+ * @return WP_Theme_JSON Entity that holds user data.
+ */
+ public static function get_user_data() {
+ if ( null !== self::$user ) {
+ return self::$user;
+ }
+
+ $config = array();
+ $user_cpt = self::get_user_data_from_custom_post_type();
+
+ if ( array_key_exists( 'post_content', $user_cpt ) ) {
+ $decoded_data = json_decode( $user_cpt['post_content'], true );
+
+ $json_decoding_error = json_last_error();
+ if ( JSON_ERROR_NONE !== $json_decoding_error ) {
+ trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() );
+ return new WP_Theme_JSON( $config, 'user' );
+ }
+
+ // Very important to verify if the flag isGlobalStylesUserThemeJSON is true.
+ // If is not true the content was not escaped and is not safe.
+ if (
+ is_array( $decoded_data ) &&
+ isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
+ $decoded_data['isGlobalStylesUserThemeJSON']
+ ) {
+ unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
+ $config = $decoded_data;
+ }
+ }
+ self::$user = new WP_Theme_JSON( $config, 'user' );
+
+ return self::$user;
+ }
+
+ /**
+ * There are three sources of data (origins) for a site:
+ * core, theme, and user. The user's has higher priority
+ * than the theme's, and the theme's higher than core's.
+ *
+ * Unlike the getters {@link get_core_data},
+ * {@link get_theme_data}, and {@link get_user_data},
+ * this method returns data after it has been merged
+ * with the previous origins. This means that if the same piece of data
+ * is declared in different origins (user, theme, and core),
+ * the last origin overrides the previous.
+ *
+ * For example, if the user has set a background color
+ * for the paragraph block, and the theme has done it as well,
+ * the user preference wins.
*
* @since 5.8.0
+ * @since 5.9.0 Add user data and change the arguments.
*
- * @param array $settings Optional. Existing block editor settings. Default empty array.
+ * @param string $origin Optional. To what level should we merge data.
+ * Valid values are 'theme' or 'user'.
+ * Default is 'user'.
* @return WP_Theme_JSON
*/
- public static function get_merged_data( $settings = array() ) {
- $theme_support_data = WP_Theme_JSON::get_from_editor_settings( $settings );
+ public static function get_merged_data( $origin = 'user' ) {
+ if ( is_array( $origin ) ) {
+ _deprecated_argument( __FUNCTION__, '5.9' );
+ }
$result = new WP_Theme_JSON();
$result->merge( self::get_core_data() );
- $result->merge( self::get_theme_data( $theme_support_data ) );
+ $result->merge( self::get_theme_data() );
+
+ if ( 'user' === $origin ) {
+ $result->merge( self::get_user_data() );
+ }
return $result;
}
+ /**
+ * Returns the ID of the custom post type
+ * that stores user data.
+ *
+ * @since 5.9.0
+ *
+ * @return integer|null
+ */
+ public static function get_user_custom_post_type_id() {
+ if ( null !== self::$user_custom_post_type_id ) {
+ return self::$user_custom_post_type_id;
+ }
+
+ $user_cpt = self::get_user_data_from_custom_post_type( true );
+
+ if ( array_key_exists( 'ID', $user_cpt ) ) {
+ self::$user_custom_post_type_id = $user_cpt['ID'];
+ }
+
+ return self::$user_custom_post_type_id;
+ }
+
/**
* Whether the current theme has a theme.json file.
*
* @since 5.8.0
+ * @since 5.9.0 Also check in the parent theme.
*
* @return bool
*/
public static function theme_has_support() {
if ( ! isset( self::$theme_has_support ) ) {
- self::$theme_has_support = (bool) self::get_file_path_from_theme( 'theme.json' );
+ self::$theme_has_support = is_readable( get_theme_file_path( 'theme.json' ) );
}
return self::$theme_has_support;
@@ -202,35 +375,32 @@ class WP_Theme_JSON_Resolver {
* If it isn't, returns an empty string, otherwise returns the whole file path.
*
* @since 5.8.0
+ * @since 5.9.0 Adapt to work with child themes.
*
* @param string $file_name Name of the file.
+ * @param bool $template Optional. Use template theme directory. Default false.
* @return string The whole file path or empty if the file doesn't exist.
*/
- private static function get_file_path_from_theme( $file_name ) {
- /*
- * This used to be a locate_template call. However, that method proved problematic
- * due to its use of constants (STYLESHEETPATH) that threw errors in some scenarios.
- *
- * When the theme.json merge algorithm properly supports child themes,
- * this should also fall back to the template path, as locate_template did.
- */
- $located = '';
- $candidate = get_stylesheet_directory() . '/' . $file_name;
- if ( is_readable( $candidate ) ) {
- $located = $candidate;
- }
- return $located;
+ private static function get_file_path_from_theme( $file_name, $template = false ) {
+ $path = $template ? get_template_directory() : get_stylesheet_directory();
+ $candidate = $path . '/' . $file_name;
+
+ return is_readable( $candidate ) ? $candidate : '';
}
/**
* Cleans the cached data so it can be recalculated.
*
* @since 5.8.0
+ * @since 5.9.0 Added new variables to reset.
*/
public static function clean_cached_data() {
- self::$core = null;
- self::$theme = null;
- self::$theme_has_support = null;
+ self::$core = null;
+ self::$theme = null;
+ self::$user = null;
+ self::$user_custom_post_type_id = null;
+ self::$theme_has_support = null;
+ self::$i18n_schema = null;
}
}
diff --git a/wp-includes/class-wp-theme-json-schema.php b/wp-includes/class-wp-theme-json-schema.php
new file mode 100644
index 0000000000..6374a911bb
--- /dev/null
+++ b/wp-includes/class-wp-theme-json-schema.php
@@ -0,0 +1,141 @@
+ 'border.radius',
+ 'spacing.customMargin' => 'spacing.margin',
+ 'spacing.customPadding' => 'spacing.padding',
+ 'typography.customLineHeight' => 'typography.lineHeight',
+ );
+
+ /**
+ * Function that migrates a given theme.json structure to the last version.
+ *
+ * @param array $theme_json The structure to migrate.
+ *
+ * @return array The structure in the last version.
+ */
+ public static function migrate( $theme_json ) {
+ if ( ! isset( $theme_json['version'] ) ) {
+ $theme_json = array(
+ 'version' => WP_Theme_JSON::LATEST_SCHEMA,
+ );
+ }
+
+ if ( 1 === $theme_json['version'] ) {
+ $theme_json = self::migrate_v1_to_v2( $theme_json );
+ }
+
+ return $theme_json;
+ }
+
+ /**
+ * Removes the custom prefixes for a few properties
+ * that were part of v1:
+ *
+ * 'border.customRadius' => 'border.radius',
+ * 'spacing.customMargin' => 'spacing.margin',
+ * 'spacing.customPadding' => 'spacing.padding',
+ * 'typography.customLineHeight' => 'typography.lineHeight',
+ *
+ * @param array $old Data to migrate.
+ *
+ * @return array Data without the custom prefixes.
+ */
+ private static function migrate_v1_to_v2( $old ) {
+ // Copy everything.
+ $new = $old;
+
+ // Overwrite the things that changed.
+ if ( isset( $old['settings'] ) ) {
+ $new['settings'] = self::rename_paths( $old['settings'], self::V1_TO_V2_RENAMED_PATHS );
+ }
+
+ // Set the new version.
+ $new['version'] = 2;
+
+ return $new;
+ }
+
+ /**
+ * Processes the settings subtree.
+ *
+ * @param array $settings Array to process.
+ * @param array $paths_to_rename Paths to rename.
+ *
+ * @return array The settings in the new format.
+ */
+ private static function rename_paths( $settings, $paths_to_rename ) {
+ $new_settings = $settings;
+
+ // Process any renamed/moved paths within default settings.
+ self::rename_settings( $new_settings, $paths_to_rename );
+
+ // Process individual block settings.
+ if ( isset( $new_settings['blocks'] ) && is_array( $new_settings['blocks'] ) ) {
+ foreach ( $new_settings['blocks'] as &$block_settings ) {
+ self::rename_settings( $block_settings, $paths_to_rename );
+ }
+ }
+
+ return $new_settings;
+ }
+
+ /**
+ * Processes a settings array, renaming or moving properties.
+ *
+ * @param array $settings Reference to settings either defaults or an individual block's.
+ * @param arary $paths_to_rename Paths to rename.
+ */
+ private static function rename_settings( &$settings, $paths_to_rename ) {
+ foreach ( $paths_to_rename as $original => $renamed ) {
+ $original_path = explode( '.', $original );
+ $renamed_path = explode( '.', $renamed );
+ $current_value = _wp_array_get( $settings, $original_path, null );
+
+ if ( null !== $current_value ) {
+ _wp_array_set( $settings, $renamed_path, $current_value );
+ self::unset_setting_by_path( $settings, $original_path );
+ }
+ }
+ }
+
+ /**
+ * Removes a property from within the provided settings by its path.
+ *
+ * @param array $settings Reference to the current settings array.
+ * @param array $path Path to the property to be removed.
+ *
+ * @return void
+ */
+ private static function unset_setting_by_path( &$settings, $path ) {
+ $tmp_settings = &$settings; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ $last_key = array_pop( $path );
+ foreach ( $path as $key ) {
+ $tmp_settings = &$tmp_settings[ $key ];
+ }
+
+ unset( $tmp_settings[ $last_key ] );
+ }
+}
diff --git a/wp-includes/class-wp-theme-json.php b/wp-includes/class-wp-theme-json.php
index d394bd3301..cd80d24d3c 100644
--- a/wp-includes/class-wp-theme-json.php
+++ b/wp-includes/class-wp-theme-json.php
@@ -10,6 +10,10 @@
/**
* Class that encapsulates the processing of structures that adhere to the theme.json spec.
*
+ * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes).
+ * This is a low-level API that may need to do breaking changes. Please,
+ * use get_global_settings, get_global_styles, and get_global_stylesheet instead.
+ *
* @access private
*/
class WP_Theme_JSON {
@@ -70,119 +74,153 @@ class WP_Theme_JSON {
*
* This contains the necessary metadata to process them:
*
- * - path => where to find the preset within the settings section
- *
- * - value_key => the key that represents the value
- *
- * - css_var_infix => infix to use in generating the CSS Custom Property. Example:
- * --wp--preset----:
- *
- * - classes => array containing a structure with the classes to
- * generate for the presets. Each class should have
- * the class suffix and the property name. Example:
- *
- * .has-- {
- * :
- * }
+ * - path => where to find the preset within the settings section
+ * - value_key => the key that represents the value
+ * - value_func => the callback to render the value (either value_key or value_func should be present)
+ * - css_vars => template string to use in generating the CSS Custom Property.
+ * Example output: "--wp--preset--duotone--blue: " will generate as many CSS Custom Properties as presets defined
+ * substituting the $slug for the slug's value for each preset value.
+ * - classes => array containing a structure with the classes to generate for the presets.
+ * Each key is a template string to resolve similarly to the css_vars and each value is the CSS property to use for that class.
+ * Example output: ".has-blue-color { color: }"
+ * - properties => a list of CSS properties to be used by kses to check the preset value is safe.
*
* @since 5.8.0
+ * @since 5.9.0 Added new presets and simplified the metadata structure.
* @var array
*/
const PRESETS_METADATA = array(
array(
- 'path' => array( 'color', 'palette' ),
- 'value_key' => 'color',
- 'css_var_infix' => 'color',
- 'classes' => array(
- array(
- 'class_suffix' => 'color',
- 'property_name' => 'color',
- ),
- array(
- 'class_suffix' => 'background-color',
- 'property_name' => 'background-color',
- ),
+ 'path' => array( 'color', 'palette' ),
+ 'value_key' => 'color',
+ 'css_vars' => '--wp--preset--color--$slug',
+ 'classes' => array(
+ '.has-$slug-color' => 'color',
+ '.has-$slug-background-color' => 'background-color',
+ '.has-$slug-border-color' => 'border-color',
),
+ 'properties' => array( 'color', 'background-color', 'border-color' ),
),
array(
- 'path' => array( 'color', 'gradients' ),
- 'value_key' => 'gradient',
- 'css_var_infix' => 'gradient',
- 'classes' => array(
- array(
- 'class_suffix' => 'gradient-background',
- 'property_name' => 'background',
- ),
- ),
+ 'path' => array( 'color', 'gradients' ),
+ 'value_key' => 'gradient',
+ 'css_vars' => '--wp--preset--gradient--$slug',
+ 'classes' => array( '.has-$slug-gradient-background' => 'background' ),
+ 'properties' => array( 'background' ),
),
array(
- 'path' => array( 'typography', 'fontSizes' ),
- 'value_key' => 'size',
- 'css_var_infix' => 'font-size',
- 'classes' => array(
- array(
- 'class_suffix' => 'font-size',
- 'property_name' => 'font-size',
- ),
- ),
+ 'path' => array( 'color', 'duotone' ),
+ 'value_func' => 'wp_render_duotone_filter_preset',
+ 'css_vars' => '--wp--preset--duotone--$slug',
+ 'classes' => array(),
+ 'properties' => array( 'filter' ),
+ ),
+ array(
+ 'path' => array( 'typography', 'fontSizes' ),
+ 'value_key' => 'size',
+ 'css_vars' => '--wp--preset--font-size--$slug',
+ 'classes' => array( '.has-$slug-font-size' => 'font-size' ),
+ 'properties' => array( 'font-size' ),
+ ),
+ array(
+ 'path' => array( 'typography', 'fontFamilies' ),
+ 'value_key' => 'fontFamily',
+ 'css_vars' => '--wp--preset--font-family--$slug',
+ 'classes' => array( '.has-$slug-font-family' => 'font-family' ),
+ 'properties' => array( 'font-family' ),
),
);
/**
* Metadata for style properties.
*
- * Each property declares:
- *
- * - 'value': path to the value in theme.json and block attributes.
+ * Each element is a direct mapping from the CSS property name to the
+ * path to the value in theme.json & block attributes.
*
* @since 5.8.0
+ * @since 5.9.0 Added new properties and simplified the metadata structure.
* @var array
*/
const PROPERTIES_METADATA = array(
- 'background' => array(
- 'value' => array( 'color', 'gradient' ),
- ),
- 'background-color' => array(
- 'value' => array( 'color', 'background' ),
- ),
- 'color' => array(
- 'value' => array( 'color', 'text' ),
- ),
- 'font-size' => array(
- 'value' => array( 'typography', 'fontSize' ),
- ),
- 'line-height' => array(
- 'value' => array( 'typography', 'lineHeight' ),
- ),
- 'margin' => array(
- 'value' => array( 'spacing', 'margin' ),
- 'properties' => array( 'top', 'right', 'bottom', 'left' ),
- ),
- 'padding' => array(
- 'value' => array( 'spacing', 'padding' ),
- 'properties' => array( 'top', 'right', 'bottom', 'left' ),
- ),
+ 'background' => array( 'color', 'gradient' ),
+ 'background-color' => array( 'color', 'background' ),
+ 'border-radius' => array( 'border', 'radius' ),
+ 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ),
+ 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ),
+ 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ),
+ 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ),
+ 'border-color' => array( 'border', 'color' ),
+ 'border-width' => array( 'border', 'width' ),
+ 'border-style' => array( 'border', 'style' ),
+ 'color' => array( 'color', 'text' ),
+ 'font-family' => array( 'typography', 'fontFamily' ),
+ 'font-size' => array( 'typography', 'fontSize' ),
+ 'font-style' => array( 'typography', 'fontStyle' ),
+ 'font-weight' => array( 'typography', 'fontWeight' ),
+ 'letter-spacing' => array( 'typography', 'letterSpacing' ),
+ 'line-height' => array( 'typography', 'lineHeight' ),
+ 'margin' => array( 'spacing', 'margin' ),
+ 'margin-top' => array( 'spacing', 'margin', 'top' ),
+ 'margin-right' => array( 'spacing', 'margin', 'right' ),
+ 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ),
+ 'margin-left' => array( 'spacing', 'margin', 'left' ),
+ 'padding' => array( 'spacing', 'padding' ),
+ 'padding-top' => array( 'spacing', 'padding', 'top' ),
+ 'padding-right' => array( 'spacing', 'padding', 'right' ),
+ 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
+ 'padding-left' => array( 'spacing', 'padding', 'left' ),
+ '--wp--style--block-gap' => array( 'spacing', 'blockGap' ),
+ 'text-decoration' => array( 'typography', 'textDecoration' ),
+ 'text-transform' => array( 'typography', 'textTransform' ),
+ 'filter' => array( 'filter', 'duotone' ),
);
/**
+ * Protected style properties.
+ *
+ * These style properties are only rendered if a setting enables it
+ * via a value other than `null`.
+ *
+ * Each element maps the style property to the corresponding theme.json
+ * setting key.
+ *
+ * @since 5.9.0
+ */
+ const PROTECTED_PROPERTIES = array(
+ 'spacing.blockGap' => array( 'spacing', 'blockGap' ),
+ );
+
+ /**
+ * The top-level keys a theme.json can have.
+ *
* @since 5.8.0
+ * @since 5.9.0 Renamed from ALLOWED_TOP_LEVEL_KEYS and added new values.
* @var string[]
*/
- const ALLOWED_TOP_LEVEL_KEYS = array(
+ const VALID_TOP_LEVEL_KEYS = array(
+ 'customTemplates',
'settings',
'styles',
+ 'templateParts',
'version',
);
/**
+ * The valid properties under the settings key.
+ *
* @since 5.8.0
+ * @since 5.9.0 Renamed from ALLOWED_SETTINGS, gained new properties, and renamed others according to the new schema.
* @var array
*/
- const ALLOWED_SETTINGS = array(
+ const VALID_SETTINGS = array(
'border' => array(
- 'customRadius' => null,
+ 'color' => null,
+ 'radius' => null,
+ 'style' => null,
+ 'width' => null,
),
'color' => array(
+ 'background' => null,
'custom' => null,
'customDuotone' => null,
'customGradient' => null,
@@ -190,6 +228,7 @@ class WP_Theme_JSON {
'gradients' => null,
'link' => null,
'palette' => null,
+ 'text' => null,
),
'custom' => null,
'layout' => array(
@@ -197,48 +236,61 @@ class WP_Theme_JSON {
'wideSize' => null,
),
'spacing' => array(
- 'customMargin' => null,
- 'customPadding' => null,
- 'units' => null,
+ 'blockGap' => null,
+ 'margin' => null,
+ 'padding' => null,
+ 'units' => null,
),
'typography' => array(
- 'customFontSize' => null,
- 'customLineHeight' => null,
- 'dropCap' => null,
- 'fontSizes' => null,
+ 'customFontSize' => null,
+ 'dropCap' => null,
+ 'fontFamilies' => null,
+ 'fontSizes' => null,
+ 'fontStyle' => null,
+ 'fontWeight' => null,
+ 'letterSpacing' => null,
+ 'lineHeight' => null,
+ 'textDecoration' => null,
+ 'textTransform' => null,
),
);
/**
+ * The valid properties under the styles key.
+ *
* @since 5.8.0
+ * @since 5.9.0 Renamed from ALLOWED_SETTINGS, gained new properties.
* @var array
*/
- const ALLOWED_STYLES = array(
+ const VALID_STYLES = array(
'border' => array(
+ 'color' => null,
'radius' => null,
+ 'style' => null,
+ 'width' => null,
),
'color' => array(
'background' => null,
'gradient' => null,
'text' => null,
),
+ 'filter' => array(
+ 'duotone' => null,
+ ),
'spacing' => array(
- 'margin' => array(
- 'top' => null,
- 'right' => null,
- 'bottom' => null,
- 'left' => null,
- ),
- 'padding' => array(
- 'bottom' => null,
- 'left' => null,
- 'right' => null,
- 'top' => null,
- ),
+ 'margin' => null,
+ 'padding' => null,
+ 'blockGap' => null,
),
'typography' => array(
- 'fontSize' => null,
- 'lineHeight' => null,
+ 'fontFamily' => null,
+ 'fontSize' => null,
+ 'fontStyle' => null,
+ 'fontWeight' => null,
+ 'letterSpacing' => null,
+ 'lineHeight' => null,
+ 'textDecoration' => null,
+ 'textTransform' => null,
),
);
@@ -257,10 +309,13 @@ class WP_Theme_JSON {
);
/**
+ * The latest version of the schema in use.
+ *
* @since 5.8.0
+ * @since 5.9.0 Changed value.
* @var int
*/
- const LATEST_SCHEMA = 1;
+ const LATEST_SCHEMA = 2;
/**
* Constructor.
@@ -276,18 +331,16 @@ class WP_Theme_JSON {
$origin = 'theme';
}
- if ( ! isset( $theme_json['version'] ) || self::LATEST_SCHEMA !== $theme_json['version'] ) {
- $this->theme_json = array();
- return;
- }
-
- $this->theme_json = self::sanitize( $theme_json );
+ $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json );
+ $valid_block_names = array_keys( self::get_blocks_metadata() );
+ $valid_element_names = array_keys( self::ELEMENTS );
+ $this->theme_json = self::sanitize( $this->theme_json, $valid_block_names, $valid_element_names );
// Internally, presets are keyed by origin.
$nodes = self::get_setting_nodes( $this->theme_json );
foreach ( $nodes as $node ) {
- foreach ( self::PRESETS_METADATA as $preset ) {
- $path = array_merge( $node['path'], $preset['path'] );
+ foreach ( self::PRESETS_METADATA as $preset_metadata ) {
+ $path = array_merge( $node['path'], $preset_metadata['path'] );
$preset = _wp_array_get( $this->theme_json, $path, null );
if ( null !== $preset ) {
_wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
@@ -300,42 +353,39 @@ class WP_Theme_JSON {
* Sanitizes the input according to the schemas.
*
* @since 5.8.0
+ * @since 5.9.0 Has new parameters.
*
* @param array $input Structure to sanitize.
+ * @param array $valid_block_names List of valid block names.
+ * @param array $valid_element_names List of valid element names.
* @return array The sanitized output.
*/
- private static function sanitize( $input ) {
+ private static function sanitize( $input, $valid_block_names, $valid_element_names ) {
$output = array();
if ( ! is_array( $input ) ) {
return $output;
}
- $allowed_top_level_keys = self::ALLOWED_TOP_LEVEL_KEYS;
- $allowed_settings = self::ALLOWED_SETTINGS;
- $allowed_styles = self::ALLOWED_STYLES;
- $allowed_blocks = array_keys( self::get_blocks_metadata() );
- $allowed_elements = array_keys( self::ELEMENTS );
+ $output = array_intersect_key( $input, array_flip( self::VALID_TOP_LEVEL_KEYS ) );
- $output = array_intersect_key( $input, array_flip( $allowed_top_level_keys ) );
-
- // Build the schema.
+ // Build the schema based on valid block & element names.
$schema = array();
$schema_styles_elements = array();
- foreach ( $allowed_elements as $element ) {
- $schema_styles_elements[ $element ] = $allowed_styles;
+ foreach ( $valid_element_names as $element ) {
+ $schema_styles_elements[ $element ] = self::VALID_STYLES;
}
$schema_styles_blocks = array();
$schema_settings_blocks = array();
- foreach ( $allowed_blocks as $block ) {
- $schema_settings_blocks[ $block ] = $allowed_settings;
- $schema_styles_blocks[ $block ] = $allowed_styles;
+ foreach ( $valid_block_names as $block ) {
+ $schema_settings_blocks[ $block ] = self::VALID_SETTINGS;
+ $schema_styles_blocks[ $block ] = self::VALID_STYLES;
$schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
}
- $schema['styles'] = $allowed_styles;
+ $schema['styles'] = self::VALID_STYLES;
$schema['styles']['blocks'] = $schema_styles_blocks;
$schema['styles']['elements'] = $schema_styles_elements;
- $schema['settings'] = $allowed_settings;
+ $schema['settings'] = self::VALID_SETTINGS;
$schema['settings']['blocks'] = $schema_settings_blocks;
// Remove anything that's not present in the schema.
@@ -360,7 +410,6 @@ class WP_Theme_JSON {
return $output;
}
-
/**
* Returns the metadata for each block.
*
@@ -377,14 +426,16 @@ class WP_Theme_JSON {
* 'core/heading': {
* 'selector': 'h1',
* 'elements': {}
- * }
- * 'core/group': {
- * 'selector': '.wp-block-group',
+ * },
+ * 'core/image': {
+ * 'selector': '.wp-block-image',
+ * 'duotone': 'img',
* 'elements': {}
* }
* }
*
* @since 5.8.0
+ * @since 5.9.0 Added duotone key with CSS selector.
*
* @return array Block metadata.
*/
@@ -407,11 +458,16 @@ class WP_Theme_JSON {
self::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) );
}
- /*
- * Assign defaults, then overwrite those that the block sets by itself.
- * If the block selector is compounded, will append the element to each
- * individual block selector.
- */
+ if (
+ isset( $block_type->supports['color']['__experimentalDuotone'] ) &&
+ is_string( $block_type->supports['color']['__experimentalDuotone'] )
+ ) {
+ self::$blocks_metadata[ $block_name ]['duotone'] = $block_type->supports['color']['__experimentalDuotone'];
+ }
+
+ // Assign defaults, then overwrite 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( ',', self::$blocks_metadata[ $block_name ]['selector'] );
foreach ( self::ELEMENTS as $el_name => $el_selector ) {
$element_selector = array();
@@ -493,25 +549,95 @@ class WP_Theme_JSON {
* the theme.json structure this object represents.
*
* @since 5.8.0
+ * @since 5.9.0 Changed the arguments passed to the function.
*
- * @param string $type Optional. Type of stylesheet we want. Accepts 'all',
- * 'block_styles', and 'css_variables'. Default 'all'.
+ * @param array $types Types of styles to load. Will load all by default. It accepts:
+ * 'variables': only the CSS Custom Properties for presets & custom ones.
+ * 'styles': only the styles section in theme.json.
+ * 'presets': only the classes for the presets.
+ * @param array $origins A list of origins to include. By default it includes self::VALID_ORIGINS.
* @return string Stylesheet.
*/
- public function get_stylesheet( $type = 'all' ) {
+ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = self::VALID_ORIGINS ) {
+ if ( is_string( $types ) ) {
+ // Dispatch error and map old arguments to new ones.
+ _deprecated_argument( __FUNCTION__, '5.9' );
+ if ( 'block_styles' === $types ) {
+ $types = array( 'styles', 'presets' );
+ } elseif ( 'css_variables' === $types ) {
+ $types = array( 'variables' );
+ } else {
+ $types = array( 'variables', 'styles', 'presets' );
+ }
+ }
+
$blocks_metadata = self::get_blocks_metadata();
$style_nodes = self::get_style_nodes( $this->theme_json, $blocks_metadata );
$setting_nodes = self::get_setting_nodes( $this->theme_json, $blocks_metadata );
- switch ( $type ) {
- case 'block_styles':
- return $this->get_block_styles( $style_nodes, $setting_nodes );
- case 'css_variables':
- return $this->get_css_variables( $setting_nodes );
- default:
- return $this->get_css_variables( $setting_nodes ) . $this->get_block_styles( $style_nodes, $setting_nodes );
+ $stylesheet = '';
+
+ if ( in_array( 'variables', $types, true ) ) {
+ $stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
}
+ if ( in_array( 'styles', $types, true ) ) {
+ $stylesheet .= $this->get_block_classes( $style_nodes );
+ }
+
+ if ( in_array( 'presets', $types, true ) ) {
+ $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
+ }
+
+ return $stylesheet;
+ }
+
+ /**
+ * Returns the page templates of the current theme.
+ *
+ * @since 5.9.0
+ *
+ * @return array
+ */
+ public function get_custom_templates() {
+ $custom_templates = array();
+ if ( ! isset( $this->theme_json['customTemplates'] ) ) {
+ return $custom_templates;
+ }
+
+ foreach ( $this->theme_json['customTemplates'] as $item ) {
+ if ( isset( $item['name'] ) ) {
+ $custom_templates[ $item['name'] ] = array(
+ 'title' => isset( $item['title'] ) ? $item['title'] : '',
+ 'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ),
+ );
+ }
+ }
+ return $custom_templates;
+ }
+
+ /**
+ * Returns the template part data of current theme.
+ *
+ * @since 5.9.0
+ *
+ * @return array
+ */
+ public function get_template_parts() {
+ $template_parts = array();
+ if ( ! isset( $this->theme_json['templateParts'] ) ) {
+ return $template_parts;
+ }
+
+ foreach ( $this->theme_json['templateParts'] as $item ) {
+ if ( isset( $item['name'] ) ) {
+ $template_parts[ $item['name'] ] = array(
+ 'title' => isset( $item['title'] ) ? $item['title'] : '',
+ 'area' => isset( $item['area'] ) ? $item['area'] : '',
+ );
+ }
+ }
+ return $template_parts;
}
/**
@@ -526,37 +652,15 @@ class WP_Theme_JSON {
* style-property-one: value;
* }
*
- * Additionally, it'll also create new rulesets
- * as classes for each preset value such as:
- *
- * .has-value-color {
- * color: value;
- * }
- *
- * .has-value-background-color {
- * background-color: value;
- * }
- *
- * .has-value-font-size {
- * font-size: value;
- * }
- *
- * .has-value-gradient-background {
- * background: value;
- * }
- *
- * p.has-value-gradient-background {
- * background: value;
- * }
- *
* @since 5.8.0
+ * @since 5.9.0 Renamed to get_block_classes and no longer returns preset classes.
*
- * @param array $style_nodes Nodes with styles.
- * @param array $setting_nodes Nodes with settings.
+ * @param array $style_nodes Nodes with styles.
* @return string The new stylesheet.
*/
- private function get_block_styles( $style_nodes, $setting_nodes ) {
+ private function get_block_classes( $style_nodes ) {
$block_rules = '';
+
foreach ( $style_nodes as $metadata ) {
if ( null === $metadata['selector'] ) {
continue;
@@ -564,11 +668,77 @@ class WP_Theme_JSON {
$node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
$selector = $metadata['selector'];
- $declarations = self::compute_style_properties( $node );
+ $settings = _wp_array_get( $this->theme_json, array( 'settings' ) );
+ $declarations = self::compute_style_properties( $node, $settings );
+
+ // 1. Separate the ones who use the general selector
+ // and the ones who use the duotone selector.
+ $declarations_duotone = array();
+ foreach ( $declarations as $index => $declaration ) {
+ if ( 'filter' === $declaration['name'] ) {
+ unset( $declarations[ $index ] );
+ $declarations_duotone[] = $declaration;
+ }
+ }
+
+ // 2. Generate the rules that use the general selector.
$block_rules .= self::to_ruleset( $selector, $declarations );
+
+ // 3. Generate the rules that use the duotone selector.
+ if ( isset( $metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
+ $selector_duotone = self::scope_selector( $metadata['selector'], $metadata['duotone'] );
+ $block_rules .= self::to_ruleset( $selector_duotone, $declarations_duotone );
+ }
+
+ if ( self::ROOT_BLOCK_SELECTOR === $selector ) {
+ $block_rules .= 'body { margin: 0; }';
+ $block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
+ $block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
+ $block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
+
+ $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
+ if ( $has_block_gap_support ) {
+ $block_rules .= '.wp-site-blocks > * { margin-top: 0; margin-bottom: 0; }';
+ $block_rules .= '.wp-site-blocks > * + * { margin-top: var( --wp--style--block-gap ); }';
+ }
+ }
}
+ return $block_rules;
+ }
+
+ /**
+ * Creates new rulesets as classes for each preset value such as:
+ *
+ * .has-value-color {
+ * color: value;
+ * }
+ *
+ * .has-value-background-color {
+ * background-color: value;
+ * }
+ *
+ * .has-value-font-size {
+ * font-size: value;
+ * }
+ *
+ * .has-value-gradient-background {
+ * background: value;
+ * }
+ *
+ * p.has-value-gradient-background {
+ * background: value;
+ * }
+ *
+ * @since 5.9.0
+ *
+ * @param array $setting_nodes Nodes with settings.
+ * @param array $origins List of origins to process presets from.
+ * @return string The new stylesheet.
+ */
+ private function get_preset_classes( $setting_nodes, $origins ) {
$preset_rules = '';
+
foreach ( $setting_nodes as $metadata ) {
if ( null === $metadata['selector'] ) {
continue;
@@ -576,10 +746,10 @@ class WP_Theme_JSON {
$selector = $metadata['selector'];
$node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
- $preset_rules .= self::compute_preset_classes( $node, $selector );
+ $preset_rules .= self::compute_preset_classes( $node, $selector, $origins );
}
- return $block_rules . $preset_rules;
+ return $preset_rules;
}
/**
@@ -597,11 +767,13 @@ class WP_Theme_JSON {
* }
*
* @since 5.8.0
+ * @since 5.9.0 Added origins parameter.
*
* @param array $nodes Nodes with settings.
+ * @param array $origins List of origins to process.
* @return string The new stylesheet.
*/
- private function get_css_variables( $nodes ) {
+ private function get_css_variables( $nodes, $origins ) {
$stylesheet = '';
foreach ( $nodes as $metadata ) {
if ( null === $metadata['selector'] ) {
@@ -611,7 +783,7 @@ class WP_Theme_JSON {
$selector = $metadata['selector'];
$node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
- $declarations = array_merge( self::compute_preset_vars( $node ), self::compute_theme_vars( $node ) );
+ $declarations = array_merge( self::compute_preset_vars( $node, $origins ), self::compute_theme_vars( $node ) );
$stylesheet .= self::to_ruleset( $selector, $declarations );
}
@@ -667,46 +839,19 @@ class WP_Theme_JSON {
return implode( ',', $new_selectors );
}
- /**
- * Given an array of presets keyed by origin and the value key of the preset,
- * it returns an array where each key is the preset slug and each value the preset value.
- *
- * @since 5.8.0
- *
- * @param array $preset_per_origin Array of presets keyed by origin.
- * @param string $value_key The property of the preset that contains its value.
- * @return array Array of presets where each key is a slug and each value is the preset value.
- */
- private static function get_merged_preset_by_slug( $preset_per_origin, $value_key ) {
- $result = array();
- foreach ( self::VALID_ORIGINS as $origin ) {
- if ( ! isset( $preset_per_origin[ $origin ] ) ) {
- continue;
- }
- foreach ( $preset_per_origin[ $origin ] as $preset ) {
- /*
- * We don't want to use kebabCase here,
- * see https://github.com/WordPress/gutenberg/issues/32347
- * However, we need to make sure the generated class or CSS variable
- * doesn't contain spaces.
- */
- $result[ preg_replace( '/\s+/', '-', $preset['slug'] ) ] = $preset[ $value_key ];
- }
- }
- return $result;
- }
-
/**
* Given a settings array, it returns the generated rulesets
* for the preset classes.
*
* @since 5.8.0
+ * @since 5.9.0 Added origins parameter.
*
* @param array $settings Settings to process.
* @param string $selector Selector wrapping the classes.
+ * @param array $origins List of origins to process.
* @return string The result of processing the presets.
*/
- private static function compute_preset_classes( $settings, $selector ) {
+ private static function compute_preset_classes( $settings, $selector, $origins ) {
if ( self::ROOT_BLOCK_SELECTOR === $selector ) {
// Classes at the global level do not need any CSS prefixed,
// and we don't want to increase its specificity.
@@ -714,17 +859,18 @@ class WP_Theme_JSON {
}
$stylesheet = '';
- foreach ( self::PRESETS_METADATA as $preset ) {
- $preset_per_origin = _wp_array_get( $settings, $preset['path'], array() );
- $preset_by_slug = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] );
- foreach ( $preset['classes'] as $class ) {
- foreach ( $preset_by_slug as $slug => $value ) {
+ foreach ( self::PRESETS_METADATA as $preset_metadata ) {
+ $slugs = self::get_settings_slugs( $settings, $preset_metadata, $origins );
+ foreach ( $preset_metadata['classes'] as $class => $property ) {
+ foreach ( $slugs as $slug ) {
+ $css_var = self::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
+ $class_name = self::replace_slug_in_string( $class, $slug );
$stylesheet .= self::to_ruleset(
- self::append_to_selector( $selector, '.has-' . _wp_to_kebab_case( $slug ) . '-' . $class['class_suffix'] ),
+ self::append_to_selector( $selector, $class_name ),
array(
array(
- 'name' => $class['property_name'],
- 'value' => 'var(--wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case( $slug ) . ') !important',
+ 'name' => $property,
+ 'value' => 'var(' . $css_var . ') !important',
),
)
);
@@ -735,6 +881,147 @@ class WP_Theme_JSON {
return $stylesheet;
}
+ /**
+ * Function that scopes a selector with another one. This works a bit like
+ * SCSS nesting except the `&` operator isn't supported.
+ *
+ *
+ * $scope = '.a, .b .c';
+ * $selector = '> .x, .y';
+ * $merged = scope_selector( $scope, $selector );
+ * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
+ *
+ *
+ * @since 5.9.0
+ *
+ * @param string $scope Selector to scope to.
+ * @param string $selector Original selector.
+ *
+ * @return string Scoped selector.
+ */
+ private static function scope_selector( $scope, $selector ) {
+ $scopes = explode( ',', $scope );
+ $selectors = explode( ',', $selector );
+
+ $selectors_scoped = array();
+ foreach ( $scopes as $outer ) {
+ foreach ( $selectors as $inner ) {
+ $selectors_scoped[] = trim( $outer ) . ' ' . trim( $inner );
+ }
+ }
+
+ return implode( ', ', $selectors_scoped );
+ }
+
+ /**
+ * Gets preset values keyed by slugs based on settings and metadata.
+ *
+ *
+ * $settings = array(
+ * 'typography' => array(
+ * 'fontFamilies' => array(
+ * array(
+ * 'slug' => 'sansSerif',
+ * 'fontFamily' => '"Helvetica Neue", sans-serif',
+ * ),
+ * array(
+ * 'slug' => 'serif',
+ * 'colors' => 'Georgia, serif',
+ * )
+ * ),
+ * ),
+ * );
+ * $meta = array(
+ * 'path' => array( 'typography', 'fontFamilies' ),
+ * 'value_key' => 'fontFamily',
+ * );
+ * $values_by_slug = get_settings_values_by_slug();
+ * // $values_by_slug === array(
+ * // 'sans-serif' => '"Helvetica Neue", sans-serif',
+ * // 'serif' => 'Georgia, serif',
+ * // );
+ *
+ *
+ * @since 5.9.0
+ *
+ * @param array $settings Settings to process.
+ * @param array $preset_metadata One of the PRESETS_METADATA values.
+ * @param array $origins List of origins to process.
+ * @return array Array of presets where each key is a slug and each value is the preset value.
+ */
+ private static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
+ $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
+
+ $result = array();
+ foreach ( $origins as $origin ) {
+ if ( ! isset( $preset_per_origin[ $origin ] ) ) {
+ continue;
+ }
+ foreach ( $preset_per_origin[ $origin ] as $preset ) {
+ $slug = _wp_to_kebab_case( $preset['slug'] );
+
+ $value = '';
+ if ( isset( $preset_metadata['value_key'] ) ) {
+ $value_key = $preset_metadata['value_key'];
+ $value = $preset[ $value_key ];
+ } elseif (
+ isset( $preset_metadata['value_func'] ) &&
+ is_callable( $preset_metadata['value_func'] )
+ ) {
+ $value_func = $preset_metadata['value_func'];
+ $value = call_user_func( $value_func, $preset );
+ } else {
+ // If we don't have a value, then don't add it to the result.
+ continue;
+ }
+
+ $result[ $slug ] = $value;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Similar to get_settings_values_by_slug, but doesn't compute the value.
+ *
+ * @since 5.9.0
+ *
+ * @param array $settings Settings to process.
+ * @param array $preset_metadata One of the PRESETS_METADATA values.
+ * @param array $origins List of origins to process.
+ * @return array Array of presets where the key and value are both the slug.
+ */
+ private static function get_settings_slugs( $settings, $preset_metadata, $origins = self::VALID_ORIGINS ) {
+ $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );
+
+ $result = array();
+ foreach ( $origins as $origin ) {
+ if ( ! isset( $preset_per_origin[ $origin ] ) ) {
+ continue;
+ }
+ foreach ( $preset_per_origin[ $origin ] as $preset ) {
+ $slug = _wp_to_kebab_case( $preset['slug'] );
+
+ // Use the array as a set so we don't get duplicates.
+ $result[ $slug ] = $slug;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Transform a slug into a CSS Custom Property.
+ *
+ * @since 5.9.0
+ *
+ * @param string $input String to replace.
+ * @param string $slug The slug value to use to generate the custom property.
+ * @return string The CSS Custom Property. Something along the lines of --wp--preset--color--black.
+ */
+ private static function replace_slug_in_string( $input, $slug ) {
+ return strtr( $input, array( '$slug' => $slug ) );
+ }
+
/**
* Given the block settings, it extracts the CSS Custom Properties
* for the presets and adds them to the $declarations array
@@ -748,16 +1035,16 @@ class WP_Theme_JSON {
* @since 5.8.0
*
* @param array $settings Settings to process.
+ * @param array $origins List of origins to process.
* @return array Returns the modified $declarations.
*/
- private static function compute_preset_vars( $settings ) {
+ private static function compute_preset_vars( $settings, $origins ) {
$declarations = array();
- foreach ( self::PRESETS_METADATA as $preset ) {
- $preset_per_origin = _wp_array_get( $settings, $preset['path'], array() );
- $preset_by_slug = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] );
- foreach ( $preset_by_slug as $slug => $value ) {
+ foreach ( self::PRESETS_METADATA as $preset_metadata ) {
+ $values_by_slug = self::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
+ foreach ( $values_by_slug as $slug => $value ) {
$declarations[] = array(
- 'name' => '--wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case( $slug ),
+ 'name' => self::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
'value' => $value,
);
}
@@ -864,46 +1151,42 @@ class WP_Theme_JSON {
* )
*
* @since 5.8.0
+ * @since 5.9.0 Added theme setting and properties parameters.
*
* @param array $styles Styles to process.
+ * @param array $settings Theme settings.
+ * @param array $properties Properties metadata.
* @return array Returns the modified $declarations.
*/
- private static function compute_style_properties( $styles ) {
+ private static function compute_style_properties( $styles, $settings = array(), $properties = self::PROPERTIES_METADATA ) {
$declarations = array();
if ( empty( $styles ) ) {
return $declarations;
}
- $properties = array();
- foreach ( self::PROPERTIES_METADATA as $name => $metadata ) {
- /*
- * Some properties can be shorthand properties, meaning that
- * they contain multiple values instead of a single one.
- * An example of this is the padding property.
- */
- if ( self::has_properties( $metadata ) ) {
- foreach ( $metadata['properties'] as $property ) {
- $properties[] = array(
- 'name' => $name . '-' . $property,
- 'value' => array_merge( $metadata['value'], array( $property ) ),
- );
- }
- } else {
- $properties[] = array(
- 'name' => $name,
- 'value' => $metadata['value'],
- );
- }
- }
+ foreach ( $properties as $css_property => $value_path ) {
+ $value = self::get_property_value( $styles, $value_path );
- foreach ( $properties as $prop ) {
- $value = self::get_property_value( $styles, $prop['value'] );
- if ( empty( $value ) ) {
+ // Look up protected properties, keyed by value path.
+ // Skip protected properties that are explicitly set to `null`.
+ if ( is_array( $value_path ) ) {
+ $path_string = implode( '.', $value_path );
+ if (
+ array_key_exists( $path_string, self::PROTECTED_PROPERTIES ) &&
+ _wp_array_get( $settings, self::PROTECTED_PROPERTIES[ $path_string ], null ) === null
+ ) {
+ continue;
+ }
+ }
+
+ // Skip if empty and not "0" or value represents array of longhand values.
+ $has_missing_value = empty( $value ) && ! is_numeric( $value );
+ if ( $has_missing_value || is_array( $value ) ) {
continue;
}
$declarations[] = array(
- 'name' => $prop['name'],
+ 'name' => $css_property,
'value' => $value,
);
}
@@ -911,22 +1194,6 @@ class WP_Theme_JSON {
return $declarations;
}
- /**
- * Whether the metadata contains a key named properties.
- *
- * @since 5.8.0
- *
- * @param array $metadata Description of the style property.
- * @return bool True if properties exists, false otherwise.
- */
- private static function has_properties( $metadata ) {
- if ( array_key_exists( 'properties', $metadata ) ) {
- return true;
- }
-
- return false;
- }
-
/**
* Returns the style property for the given path.
*
@@ -935,6 +1202,7 @@ class WP_Theme_JSON {
* "--wp--preset--color--secondary".
*
* @since 5.8.0
+ * @since 5.9.0 Consider $value that are arrays as well.
*
* @param array $styles Styles subtree.
* @param array $path Which property to process.
@@ -943,7 +1211,7 @@ class WP_Theme_JSON {
private static function get_property_value( $styles, $path ) {
$value = _wp_array_get( $styles, $path, '' );
- if ( '' === $value ) {
+ if ( '' === $value || is_array( $value ) ) {
return $value;
}
@@ -1015,18 +1283,19 @@ class WP_Theme_JSON {
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'
+ * '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'
+ * 'selector' => 'CSS selector for other node',
+ * 'duotone' => null
* ],
* ]
*
@@ -1068,9 +1337,15 @@ class WP_Theme_JSON {
$selector = $selectors[ $name ]['selector'];
}
+ $duotone_selector = null;
+ if ( isset( $selectors[ $name ]['duotone'] ) ) {
+ $duotone_selector = $selectors[ $name ]['duotone'];
+ }
+
$nodes[] = array(
'path' => array( 'styles', 'blocks', $name ),
'selector' => $selector,
+ 'duotone' => $duotone_selector,
);
if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
@@ -1090,6 +1365,7 @@ class WP_Theme_JSON {
* Merge new incoming data.
*
* @since 5.8.0
+ * @since 5.9.0 Duotone preset also has origins.
*
* @param WP_Theme_JSON $incoming Data to merge.
*/
@@ -1098,14 +1374,14 @@ class WP_Theme_JSON {
$this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
/*
- * The array_replace_recursive() algorithm merges at the leaf level.
+ * The array_replace_recursive algorithm merges at the leaf level.
* For leaf values that are arrays it will use the numeric indexes for replacement.
* In those cases, we want to replace the existing with the incoming value, if it exists.
*/
$to_replace = array();
$to_replace[] = array( 'spacing', 'units' );
- $to_replace[] = array( 'color', 'duotone' );
foreach ( self::VALID_ORIGINS as $origin ) {
+ $to_replace[] = array( 'color', 'duotone', $origin );
$to_replace[] = array( 'color', 'palette', $origin );
$to_replace[] = array( 'color', 'gradients', $origin );
$to_replace[] = array( 'typography', 'fontSizes', $origin );
@@ -1122,6 +1398,164 @@ class WP_Theme_JSON {
}
}
}
+
+ }
+
+ /**
+ * Removes insecure data from theme.json.
+ *
+ * @since 5.9.0
+ *
+ * @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( self::get_blocks_metadata() );
+ $valid_element_names = array_keys( self::ELEMENTS );
+ $theme_json = self::sanitize( $theme_json, $valid_block_names, $valid_element_names );
+
+ $blocks_metadata = self::get_blocks_metadata();
+ $style_nodes = self::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;
+ }
+
+ $output = self::remove_insecure_styles( $input );
+ if ( ! empty( $output ) ) {
+ _wp_array_set( $sanitized, $metadata['path'], $output );
+ }
+ }
+
+ $setting_nodes = self::get_setting_nodes( $theme_json );
+ foreach ( $setting_nodes as $metadata ) {
+ $input = _wp_array_get( $theme_json, $metadata['path'], array() );
+ if ( empty( $input ) ) {
+ continue;
+ }
+
+ $output = self::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
+ */
+ private static function remove_insecure_settings( $input ) {
+ $output = array();
+ foreach ( self::PRESETS_METADATA as $preset_metadata ) {
+ $presets = _wp_array_get( $input, $preset_metadata['path'], 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'] ) ) {
+ $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 ( ! self::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, $preset_metadata['path'], $escaped_preset );
+ }
+ }
+
+ 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
+ */
+ private static function remove_insecure_styles( $input ) {
+ $output = array();
+ $declarations = self::compute_style_properties( $input );
+
+ foreach ( $declarations as $declaration ) {
+ if ( self::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
+ $path = self::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 );
+ }
+ }
+ }
+ 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 boolean
+ */
+ private 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 ) );
}
/**
@@ -1176,7 +1610,7 @@ class WP_Theme_JSON {
if ( ! isset( $theme_settings['settings']['typography'] ) ) {
$theme_settings['settings']['typography'] = array();
}
- $theme_settings['settings']['typography']['customLineHeight'] = $settings['enableCustomLineHeight'];
+ $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight'];
}
if ( isset( $settings['enableCustomUnits'] ) ) {
@@ -1220,7 +1654,7 @@ class WP_Theme_JSON {
if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
$theme_settings['settings']['spacing'] = array();
}
- $theme_settings['settings']['spacing']['customPadding'] = $settings['enableCustomSpacing'];
+ $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing'];
}
return $theme_settings;
diff --git a/wp-includes/default-filters.php b/wp-includes/default-filters.php
index a446eed1c2..c4a88b69d2 100644
--- a/wp-includes/default-filters.php
+++ b/wp-includes/default-filters.php
@@ -336,6 +336,7 @@ add_action( 'current_screen', '_load_remote_block_patterns' );
add_action( 'init', 'check_theme_switched', 99 );
add_action( 'init', array( 'WP_Block_Supports', 'init' ), 22 );
add_action( 'switch_theme', array( 'WP_Theme_JSON_Resolver', 'clean_cached_data' ) );
+add_action( 'start_previewing_theme', array( 'WP_Theme_JSON_Resolver', 'clean_cached_data' ) );
add_action( 'after_switch_theme', '_wp_menus_changed' );
add_action( 'after_switch_theme', '_wp_sidebars_changed' );
add_action( 'wp_print_styles', 'print_emoji_styles' );
diff --git a/wp-includes/kses.php b/wp-includes/kses.php
index 3a7f99dd1a..ed480c242a 100644
--- a/wp-includes/kses.php
+++ b/wp-includes/kses.php
@@ -2260,6 +2260,8 @@ function safecss_filter_attr( $css, $deprecated = '' ) {
'border-bottom-color',
'border-bottom-style',
'border-bottom-width',
+ 'border-bottom-right-radius',
+ 'border-bottom-left-radius',
'border-left',
'border-left-color',
'border-left-style',
@@ -2268,6 +2270,8 @@ function safecss_filter_attr( $css, $deprecated = '' ) {
'border-top-color',
'border-top-style',
'border-top-width',
+ 'border-top-left-radius',
+ 'border-top-right-radius',
'border-spacing',
'border-collapse',
@@ -2282,6 +2286,7 @@ function safecss_filter_attr( $css, $deprecated = '' ) {
'column-width',
'color',
+ 'filter',
'font',
'font-family',
'font-size',
diff --git a/wp-includes/script-loader.php b/wp-includes/script-loader.php
index 44e9263b78..5ab358facb 100644
--- a/wp-includes/script-loader.php
+++ b/wp-includes/script-loader.php
@@ -2321,8 +2321,7 @@ function wp_enqueue_global_styles() {
}
if ( null === $stylesheet ) {
- $settings = get_default_block_editor_settings();
- $theme_json = WP_Theme_JSON_Resolver::get_merged_data( $settings );
+ $theme_json = WP_Theme_JSON_Resolver::get_merged_data();
$stylesheet = $theme_json->get_stylesheet();
if ( $can_use_cache ) {
diff --git a/wp-includes/theme-i18n.json b/wp-includes/theme-i18n.json
index a01a5aa566..98b2680792 100644
--- a/wp-includes/theme-i18n.json
+++ b/wp-includes/theme-i18n.json
@@ -5,6 +5,11 @@
{
"name": "Font size name"
}
+ ],
+ "fontFamilies": [
+ {
+ "name": "Font family name"
+ }
]
},
"color": {
@@ -31,6 +36,11 @@
{
"name": "Font size name"
}
+ ],
+ "fontFamilies": [
+ {
+ "name": "Font family name"
+ }
]
},
"color": {
@@ -47,5 +57,15 @@
}
}
}
- }
+ },
+ "customTemplates": [
+ {
+ "title": "Custom template name"
+ }
+ ],
+ "templateParts": [
+ {
+ "title": "Template part name"
+ }
+ ]
}
diff --git a/wp-includes/theme.json b/wp-includes/theme.json
index 17389579ea..f2c71be11a 100644
--- a/wp-includes/theme.json
+++ b/wp-includes/theme.json
@@ -1,14 +1,19 @@
{
- "version": 1,
+ "version": 2,
"settings": {
"border": {
- "customRadius": false
+ "color": false,
+ "radius": false,
+ "style": false,
+ "width": false
},
"color": {
"custom": true,
"customDuotone": true,
"customGradient": true,
"link": false,
+ "background": true,
+ "text": true,
"duotone": [
{
"name": "Dark grayscale" ,
@@ -177,14 +182,20 @@
]
},
"spacing": {
- "customMargin": false,
- "customPadding": false,
+ "blockGap": null,
+ "margin": false,
+ "padding": false,
"units": [ "px", "em", "rem", "vh", "vw", "%" ]
},
"typography": {
"customFontSize": true,
- "customLineHeight": false,
"dropCap": true,
+ "fontStyle": true,
+ "fontWeight": true,
+ "letterSpacing": true,
+ "lineHeight": false,
+ "textDecoration": true,
+ "textTransform": true,
"fontSizes": [
{
"name": "Small",
@@ -216,9 +227,20 @@
"blocks": {
"core/button": {
"border": {
- "customRadius": true
+ "radius": true
+ }
+ },
+ "core/pullquote": {
+ "border": {
+ "color": true,
+ "radius": true,
+ "style": true,
+ "width": true
}
}
}
+ },
+ "styles": {
+ "spacing": { "blockGap": "24px" }
}
}
diff --git a/wp-includes/version.php b/wp-includes/version.php
index bfab917a7d..f70675a27a 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '5.9-alpha-52048';
+$wp_version = '5.9-alpha-52049';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.
diff --git a/wp-settings.php b/wp-settings.php
index 33fe2bfcb3..d2778166c1 100644
--- a/wp-settings.php
+++ b/wp-settings.php
@@ -170,6 +170,7 @@ require ABSPATH . WPINC . '/query.php';
require ABSPATH . WPINC . '/class-wp-date-query.php';
require ABSPATH . WPINC . '/theme.php';
require ABSPATH . WPINC . '/class-wp-theme.php';
+require ABSPATH . WPINC . '/class-wp-theme-json-schema.php';
require ABSPATH . WPINC . '/class-wp-theme-json.php';
require ABSPATH . WPINC . '/class-wp-theme-json-resolver.php';
require ABSPATH . WPINC . '/class-wp-block-template.php';