wp/wp-includes/class-wp-theme-json.php
changeset 19 3d72ae0968f4
parent 18 be944660c56a
child 21 48c4eec2b7e6
--- a/wp/wp-includes/class-wp-theme-json.php	Wed Sep 21 18:19:35 2022 +0200
+++ b/wp/wp-includes/class-wp-theme-json.php	Tue Sep 27 16:37:53 2022 +0200
@@ -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 {
@@ -20,7 +24,7 @@
 	 * @since 5.8.0
 	 * @var array
 	 */
-	private $theme_json = null;
+	protected $theme_json = null;
 
 	/**
 	 * Holds block metadata extracted from block.json
@@ -30,7 +34,7 @@
 	 * @since 5.8.0
 	 * @var array
 	 */
-	private static $blocks_metadata = null;
+	protected static $blocks_metadata = null;
 
 	/**
 	 * The CSS selector for the top-level styles.
@@ -44,12 +48,12 @@
 	 * The sources of data this object can represent.
 	 *
 	 * @since 5.8.0
-	 * @var array
+	 * @var string[]
 	 */
 	const VALID_ORIGINS = array(
-		'core',
+		'default',
 		'theme',
-		'user',
+		'custom',
 	);
 
 	/**
@@ -70,181 +74,280 @@
 	 *
 	 * 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--<preset_infix>--<slug>: <preset_value>
-	 *
-	 * - 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-<slug>-<class_suffix> {
-	 *                       <property_name>: <preset_value>
-	 *                   }
+	 * - path             => Where to find the preset within the settings section.
+	 * - prevent_override => Disables override of default presets by theme presets.
+	 *                       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 overriden
+	 *                         - If defaults are disabled => theme presets should be overriden
+	 *                       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 by setting this false.
+	 * - use_default_names => whether to use the default names
+	 * - value_key        => the key that represents the value
+	 * - value_func       => optionally, instead of value_key, a function to generate
+	 *                       the value that takes a preset as an argument
+	 *                       (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: <value>" 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, where for each array item
+	 *                       the key is the class name and the value the property name.
+	 *                       The "$slug" substring will be replaced by the slug of each preset.
+	 *                       For example:
+	 *                       'classes' => array(
+	 *                         '.has-$slug-color'            => 'color',
+	 *                         '.has-$slug-background-color' => 'background-color',
+	 *                         '.has-$slug-border-color'     => 'border-color',
+	 *                       )
+	 * - properties       => array of CSS properties to be used by kses to
+	 *                       validate the content of each preset
+	 *                       by means of the remove_insecure_properties method.
 	 *
 	 * @since 5.8.0
+	 * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets,
+	 *              `use_default_names` preset key, and simplified the metadata structure.
+	 * @since 6.0.0 Replaced `override` with `prevent_override` and updated the
+	 *              `prevent_overried` value for `color.duotone` to use `color.defaultDuotone`.
 	 * @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' ),
+			'prevent_override'  => array( 'color', 'defaultPalette' ),
+			'use_default_names' => false,
+			'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' ),
+			'prevent_override'  => array( 'color', 'defaultGradients' ),
+			'use_default_names' => false,
+			'value_key'         => 'gradient',
+			'css_vars'          => '--wp--preset--gradient--$slug',
+			'classes'           => array( '.has-$slug-gradient-background' => 'background' ),
+			'properties'        => array( 'background' ),
 		),
 		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', 'duotone' ),
+			'prevent_override'  => array( 'color', 'defaultDuotone' ),
+			'use_default_names' => false,
+			'value_func'        => 'wp_get_duotone_filter_property',
+			'css_vars'          => '--wp--preset--duotone--$slug',
+			'classes'           => array(),
+			'properties'        => array( 'filter' ),
 		),
 		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( 'typography', 'fontSizes' ),
+			'prevent_override'  => false,
+			'use_default_names' => true,
+			'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' ),
+			'prevent_override'  => false,
+			'use_default_names' => false,
+			'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 the `border-*`, `font-family`, `font-style`, `font-weight`,
+	 *              `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`,
+	 *              `text-decoration`, `text-transform`, and `filter` properties,
+	 *              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' ),
+		'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 As `ALLOWED_TOP_LEVEL_KEYS`.
+	 * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`,
+	 *              added the `customTemplates` and `templateParts` values.
+	 * @var string[]
+	 */
+	const VALID_TOP_LEVEL_KEYS = array(
+		'customTemplates',
+		'patterns',
+		'settings',
+		'styles',
+		'templateParts',
+		'version',
+		'title',
+	);
+
+	/**
+	 * The valid properties under the settings key.
+	 *
+	 * @since 5.8.0 As `ALLOWED_SETTINGS`.
+	 * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`,
+	 *              added new properties for `border`, `color`, `spacing`,
+	 *              and `typography`, and renamed others according to the new schema.
+	 * @since 6.0.0 Added `color.defaultDuotone`.
+	 * @var array
+	 */
+	const VALID_SETTINGS = array(
+		'appearanceTools' => null,
+		'border'          => array(
+			'color'  => null,
+			'radius' => null,
+			'style'  => null,
+			'width'  => null,
 		),
-		'font-size'        => array(
-			'value' => array( 'typography', 'fontSize' ),
-		),
-		'line-height'      => array(
-			'value' => array( 'typography', 'lineHeight' ),
+		'color'           => array(
+			'background'       => null,
+			'custom'           => null,
+			'customDuotone'    => null,
+			'customGradient'   => null,
+			'defaultDuotone'   => null,
+			'defaultGradients' => null,
+			'defaultPalette'   => null,
+			'duotone'          => null,
+			'gradients'        => null,
+			'link'             => null,
+			'palette'          => null,
+			'text'             => null,
 		),
-		'margin'           => array(
-			'value'      => array( 'spacing', 'margin' ),
-			'properties' => array( 'top', 'right', 'bottom', 'left' ),
+		'custom'          => null,
+		'layout'          => array(
+			'contentSize' => null,
+			'wideSize'    => null,
+		),
+		'spacing'         => array(
+			'blockGap' => null,
+			'margin'   => null,
+			'padding'  => null,
+			'units'    => null,
 		),
-		'padding'          => array(
-			'value'      => array( 'spacing', 'padding' ),
-			'properties' => array( 'top', 'right', 'bottom', 'left' ),
+		'typography'      => array(
+			'customFontSize' => null,
+			'dropCap'        => null,
+			'fontFamilies'   => null,
+			'fontSizes'      => null,
+			'fontStyle'      => null,
+			'fontWeight'     => null,
+			'letterSpacing'  => null,
+			'lineHeight'     => null,
+			'textDecoration' => null,
+			'textTransform'  => null,
 		),
 	);
 
 	/**
-	 * @since 5.8.0
-	 * @var array
-	 */
-	const ALLOWED_TOP_LEVEL_KEYS = array(
-		'settings',
-		'styles',
-		'version',
-	);
-
-	/**
-	 * @since 5.8.0
+	 * The valid properties under the styles key.
+	 *
+	 * @since 5.8.0 As `ALLOWED_STYLES`.
+	 * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`,
+	 *              added new properties for `border`, `filter`, `spacing`,
+	 *              and `typography`.
 	 * @var array
 	 */
-	const ALLOWED_SETTINGS = array(
+	const VALID_STYLES = array(
 		'border'     => array(
-			'customRadius' => null,
-		),
-		'color'      => array(
-			'custom'         => null,
-			'customDuotone'  => null,
-			'customGradient' => null,
-			'duotone'        => null,
-			'gradients'      => null,
-			'link'           => null,
-			'palette'        => null,
-		),
-		'custom'     => null,
-		'layout'     => array(
-			'contentSize' => null,
-			'wideSize'    => null,
-		),
-		'spacing'    => array(
-			'customMargin'  => null,
-			'customPadding' => null,
-			'units'         => null,
-		),
-		'typography' => array(
-			'customFontSize'   => null,
-			'customLineHeight' => null,
-			'dropCap'          => null,
-			'fontSizes'        => null,
-		),
-	);
-
-	/**
-	 * @since 5.8.0
-	 * @var array
-	 */
-	const ALLOWED_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' => 'top',
 		),
 		'typography' => array(
-			'fontSize'   => null,
-			'lineHeight' => null,
+			'fontFamily'     => null,
+			'fontSize'       => null,
+			'fontStyle'      => null,
+			'fontWeight'     => null,
+			'letterSpacing'  => null,
+			'lineHeight'     => null,
+			'textDecoration' => null,
+			'textTransform'  => null,
 		),
 	);
 
 	/**
+	 * The valid elements that can be found under styles.
+	 *
 	 * @since 5.8.0
-	 * @var array
+	 * @var string[]
 	 */
 	const ELEMENTS = array(
 		'link' => 'a',
@@ -257,85 +360,164 @@
 	);
 
 	/**
+	 * Options that settings.appearanceTools enables.
+	 *
+	 * @since 6.0.0
+	 * @var array
+	 */
+	const APPEARANCE_TOOLS_OPT_INS = array(
+		array( 'border', 'color' ),
+		array( 'border', 'radius' ),
+		array( 'border', 'style' ),
+		array( 'border', 'width' ),
+		array( 'color', 'link' ),
+		array( 'spacing', 'blockGap' ),
+		array( 'spacing', 'margin' ),
+		array( 'spacing', 'padding' ),
+		array( 'typography', 'lineHeight' ),
+	);
+
+	/**
+	 * The latest version of the schema in use.
+	 *
 	 * @since 5.8.0
+	 * @since 5.9.0 Changed value from 1 to 2.
 	 * @var int
 	 */
-	const LATEST_SCHEMA = 1;
+	const LATEST_SCHEMA = 2;
 
 	/**
 	 * Constructor.
 	 *
 	 * @since 5.8.0
 	 *
-	 * @param array $theme_json A structure that follows the theme.json schema.
-	 * @param string $origin    Optional. What source of data this object represents.
-	 *                          One of 'core', 'theme', or 'user'. Default 'theme'.
+	 * @param array  $theme_json A structure that follows the theme.json schema.
+	 * @param string $origin     Optional. What source of data this object represents.
+	 *                           One of 'default', 'theme', or 'custom'. Default 'theme'.
 	 */
 	public function __construct( $theme_json = array(), $origin = 'theme' ) {
-		if ( ! in_array( $origin, self::VALID_ORIGINS, true ) ) {
+		if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
 			$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( static::get_blocks_metadata() );
+		$valid_element_names = array_keys( static::ELEMENTS );
+		$theme_json          = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names );
+		$this->theme_json    = static::maybe_opt_in_into_settings( $theme_json );
 
 		// Internally, presets are keyed by origin.
-		$nodes = self::get_setting_nodes( $this->theme_json );
+		$nodes = static::get_setting_nodes( $this->theme_json );
 		foreach ( $nodes as $node ) {
-			foreach ( self::PRESETS_METADATA as $preset ) {
-				$path   = array_merge( $node['path'], $preset['path'] );
+			foreach ( static::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 ) );
+					// If the preset is not already keyed by origin.
+					if ( isset( $preset[0] ) || empty( $preset ) ) {
+						_wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
+					}
 				}
 			}
 		}
 	}
 
 	/**
+	 * Enables some opt-in settings if theme declared support.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @param array $theme_json A theme.json structure to modify.
+	 * @return array The modified theme.json structure.
+	 */
+	protected static function maybe_opt_in_into_settings( $theme_json ) {
+		$new_theme_json = $theme_json;
+
+		if (
+			isset( $new_theme_json['settings']['appearanceTools'] ) &&
+			true === $new_theme_json['settings']['appearanceTools']
+		) {
+			static::do_opt_in_into_settings( $new_theme_json['settings'] );
+		}
+
+		if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) {
+			foreach ( $new_theme_json['settings']['blocks'] as &$block ) {
+				if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) {
+					static::do_opt_in_into_settings( $block );
+				}
+			}
+		}
+
+		return $new_theme_json;
+	}
+
+	/**
+	 * Enables some settings.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @param array $context The context to which the settings belong.
+	 */
+	protected static function do_opt_in_into_settings( &$context ) {
+		foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) {
+			// Use "unset prop" as a marker instead of "null" because
+			// "null" can be a valid value for some props (e.g. blockGap).
+			if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) {
+				_wp_array_set( $context, $path, true );
+			}
+		}
+
+		unset( $context['appearanceTools'] );
+	}
+
+	/**
 	 * Sanitizes the input according to the schemas.
 	 *
 	 * @since 5.8.0
+	 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
 	 *
-	 * @param array $input Structure to sanitize.
+	 * @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 ) {
+	protected 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( static::VALID_TOP_LEVEL_KEYS ) );
 
-		$output = array_intersect_key( $input, array_flip( $allowed_top_level_keys ) );
+		// Some styles are only meant to be available at the top-level (e.g.: blockGap),
+		// hence, the schema for blocks & elements should not have them.
+		$styles_non_top_level = static::VALID_STYLES;
+		foreach ( array_keys( $styles_non_top_level ) as $section ) {
+			foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) {
+				if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) {
+					unset( $styles_non_top_level[ $section ][ $prop ] );
+				}
+			}
+		}
 
-		// 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 ] = $styles_non_top_level;
 		}
 		$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 ]           = static::VALID_SETTINGS;
+			$schema_styles_blocks[ $block ]             = $styles_non_top_level;
 			$schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
 		}
-		$schema['styles']             = $allowed_styles;
+		$schema['styles']             = static::VALID_STYLES;
 		$schema['styles']['blocks']   = $schema_styles_blocks;
 		$schema['styles']['elements'] = $schema_styles_elements;
-		$schema['settings']           = $allowed_settings;
+		$schema['settings']           = static::VALID_SETTINGS;
 		$schema['settings']['blocks'] = $schema_settings_blocks;
 
 		// Remove anything that's not present in the schema.
@@ -349,7 +531,7 @@
 				continue;
 			}
 
-			$result = self::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
+			$result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
 
 			if ( empty( $result ) ) {
 				unset( $output[ $subtree ] );
@@ -377,23 +559,25 @@
 	 *       '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.
 	 */
-	private static function get_blocks_metadata() {
-		if ( null !== self::$blocks_metadata ) {
-			return self::$blocks_metadata;
+	protected static function get_blocks_metadata() {
+		if ( null !== static::$blocks_metadata ) {
+			return static::$blocks_metadata;
 		}
 
-		self::$blocks_metadata = array();
+		static::$blocks_metadata = array();
 
 		$registry = WP_Block_Type_Registry::get_instance();
 		$blocks   = $registry->get_all_registered();
@@ -402,27 +586,32 @@
 				isset( $block_type->supports['__experimentalSelector'] ) &&
 				is_string( $block_type->supports['__experimentalSelector'] )
 			) {
-				self::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector'];
+				static::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector'];
 			} else {
-				self::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) );
+				static::$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.
-			 */
-			$block_selectors = explode( ',', self::$blocks_metadata[ $block_name ]['selector'] );
-			foreach ( self::ELEMENTS as $el_name => $el_selector ) {
+			if (
+				isset( $block_type->supports['color']['__experimentalDuotone'] ) &&
+				is_string( $block_type->supports['color']['__experimentalDuotone'] )
+			) {
+				static::$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( ',', static::$blocks_metadata[ $block_name ]['selector'] );
+			foreach ( static::ELEMENTS as $el_name => $el_selector ) {
 				$element_selector = array();
 				foreach ( $block_selectors as $selector ) {
 					$element_selector[] = $selector . ' ' . $el_selector;
 				}
-				self::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector );
+				static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector );
 			}
 		}
 
-		return self::$blocks_metadata;
+		return static::$blocks_metadata;
 	}
 
 	/**
@@ -436,7 +625,7 @@
 	 * @param array $schema Schema to adhere to.
 	 * @return array Returns the modified $tree.
 	 */
-	private static function remove_keys_not_in_schema( $tree, $schema ) {
+	protected static function remove_keys_not_in_schema( $tree, $schema ) {
 		$tree = array_intersect_key( $tree, $schema );
 
 		foreach ( $schema as $key => $data ) {
@@ -445,7 +634,7 @@
 			}
 
 			if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) {
-				$tree[ $key ] = self::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
+				$tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
 
 				if ( empty( $tree[ $key ] ) ) {
 					unset( $tree[ $key ] );
@@ -493,25 +682,99 @@
 	 * the theme.json structure this object represents.
 	 *
 	 * @since 5.8.0
+	 * @since 5.9.0 Removed the `$type` parameter`, added the `$types` and `$origins` parameters.
 	 *
-	 * @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 VALID_ORIGINS.
 	 * @return string Stylesheet.
 	 */
-	public function get_stylesheet( $type = 'all' ) {
-		$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 );
+	public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null ) {
+		if ( null === $origins ) {
+			$origins = static::VALID_ORIGINS;
+		}
+
+		if ( is_string( $types ) ) {
+			// Dispatch error and map old arguments to new ones.
+			_deprecated_argument( __FUNCTION__, '5.9.0' );
+			if ( 'block_styles' === $types ) {
+				$types = array( 'styles', 'presets' );
+			} elseif ( 'css_variables' === $types ) {
+				$types = array( 'variables' );
+			} else {
+				$types = array( 'variables', 'styles', 'presets' );
+			}
+		}
 
-		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 );
+		$blocks_metadata = static::get_blocks_metadata();
+		$style_nodes     = static::get_style_nodes( $this->theme_json, $blocks_metadata );
+		$setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
+
+		$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 active theme.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @return array
+	 */
+	public function get_custom_templates() {
+		$custom_templates = array();
+		if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $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 active theme.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @return array
+	 */
+	public function get_template_parts() {
+		$template_parts = array();
+		if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $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 +789,17 @@
 	 *     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;
-	 *     }
+	 * @since 5.8.0 As `get_block_styles()`.
+	 * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()`
+	 *              and no longer returns preset classes.
+	 *              Removed the `$setting_nodes` parameter.
 	 *
-	 *     .has-value-font-size {
-	 *       font-size: value;
-	 *     }
-	 *
-	 *     .has-value-gradient-background {
-	 *       background: value;
-	 *     }
-	 *
-	 *     p.has-value-gradient-background {
-	 *       background: value;
-	 *     }
-	 *
-	 * @since 5.8.0
-	 *
-	 * @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 ) {
+	protected function get_block_classes( $style_nodes ) {
 		$block_rules = '';
+
 		foreach ( $style_nodes as $metadata ) {
 			if ( null === $metadata['selector'] ) {
 				continue;
@@ -564,11 +807,88 @@
 
 			$node         = _wp_array_get( $this->theme_json, $metadata['path'], array() );
 			$selector     = $metadata['selector'];
-			$declarations = self::compute_style_properties( $node );
-			$block_rules .= self::to_ruleset( $selector, $declarations );
+			$settings     = _wp_array_get( $this->theme_json, array( 'settings' ) );
+			$declarations = static::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;
+				}
+			}
+
+			/*
+			 * 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.
+			 */
+			if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
+				$block_rules .= 'body { margin: 0; }';
+			}
+
+			// 2. Generate the rules that use the general selector.
+			$block_rules .= static::to_ruleset( $selector, $declarations );
+
+			// 3. Generate the rules that use the duotone selector.
+			if ( isset( $metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
+				$selector_duotone = static::scope_selector( $metadata['selector'], $metadata['duotone'] );
+				$block_rules     .= static::to_ruleset( $selector_duotone, $declarations_duotone );
+			}
+
+			if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
+				$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-block-start: 0; margin-block-end: 0; }';
+					$block_rules .= '.wp-site-blocks > * + * { margin-block-start: 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.
+	 */
+	protected function get_preset_classes( $setting_nodes, $origins ) {
 		$preset_rules = '';
+
 		foreach ( $setting_nodes as $metadata ) {
 			if ( null === $metadata['selector'] ) {
 				continue;
@@ -576,10 +896,10 @@
 
 			$selector      = $metadata['selector'];
 			$node          = _wp_array_get( $this->theme_json, $metadata['path'], array() );
-			$preset_rules .= self::compute_preset_classes( $node, $selector );
+			$preset_rules .= static::compute_preset_classes( $node, $selector, $origins );
 		}
 
-		return $block_rules . $preset_rules;
+		return $preset_rules;
 	}
 
 	/**
@@ -597,11 +917,13 @@
 	 *     }
 	 *
 	 * @since 5.8.0
+	 * @since 5.9.0 Added the `$origins` parameter.
 	 *
-	 * @param array $nodes Nodes with settings.
+	 * @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 ) {
+	protected function get_css_variables( $nodes, $origins ) {
 		$stylesheet = '';
 		foreach ( $nodes as $metadata ) {
 			if ( null === $metadata['selector'] ) {
@@ -611,9 +933,9 @@
 			$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( static::compute_preset_vars( $node, $origins ), static::compute_theme_vars( $node ) );
 
-			$stylesheet .= self::to_ruleset( $selector, $declarations );
+			$stylesheet .= static::to_ruleset( $selector, $declarations );
 		}
 
 		return $stylesheet;
@@ -629,14 +951,14 @@
 	 * @param array  $declarations List of declarations.
 	 * @return string CSS ruleset.
 	 */
-	private static function to_ruleset( $selector, $declarations ) {
+	protected static function to_ruleset( $selector, $declarations ) {
 		if ( empty( $declarations ) ) {
 			return '';
 		}
 
 		$declaration_block = array_reduce(
 			$declarations,
-			function ( $carry, $element ) {
+			static function ( $carry, $element ) {
 				return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
 			''
 		);
@@ -657,7 +979,7 @@
 	 * @param string $to_append Selector to append.
 	 * @return string
 	 */
-	private static function append_to_selector( $selector, $to_append ) {
+	protected static function append_to_selector( $selector, $to_append ) {
 		$new_selectors = array();
 		$selectors     = explode( ',', $selector );
 		foreach ( $selectors as $sel ) {
@@ -668,63 +990,37 @@
 	}
 
 	/**
-	 * 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 the `$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 ) {
-		if ( self::ROOT_BLOCK_SELECTOR === $selector ) {
+	protected static function compute_preset_classes( $settings, $selector, $origins ) {
+		if ( static::ROOT_BLOCK_SELECTOR === $selector ) {
 			// Classes at the global level do not need any CSS prefixed,
 			// and we don't want to increase its specificity.
 			$selector = '';
 		}
 
 		$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 ) {
-					$stylesheet .= self::to_ruleset(
-						self::append_to_selector( $selector, '.has-' . _wp_to_kebab_case( $slug ) . '-' . $class['class_suffix'] ),
+		foreach ( static::PRESETS_METADATA as $preset_metadata ) {
+			$slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins );
+			foreach ( $preset_metadata['classes'] as $class => $property ) {
+				foreach ( $slugs as $slug ) {
+					$css_var     = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
+					$class_name  = static::replace_slug_in_string( $class, $slug );
+					$stylesheet .= static::to_ruleset(
+						static::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',
 							),
 						)
 					);
@@ -736,6 +1032,150 @@
 	}
 
 	/**
+	 * Function that scopes a selector with another one. This works a bit like
+	 * SCSS nesting except the `&` operator isn't supported.
+	 *
+	 * <code>
+	 * $scope = '.a, .b .c';
+	 * $selector = '> .x, .y';
+	 * $merged = scope_selector( $scope, $selector );
+	 * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
+	 * </code>
+	 *
+	 * @since 5.9.0
+	 *
+	 * @param string $scope    Selector to scope to.
+	 * @param string $selector Original selector.
+	 * @return string Scoped selector.
+	 */
+	protected 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.
+	 *
+	 * <code>
+	 * $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',
+	 * // );
+	 * </code>
+	 *
+	 * @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.
+	 */
+	protected 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'], $preset[ $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.
+	 */
+	protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) {
+		if ( null === $origins ) {
+			$origins = static::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`.
+	 */
+	protected 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
 	 * following the format:
@@ -746,18 +1186,19 @@
 	 *     )
 	 *
 	 * @since 5.8.0
+	 * @since 5.9.0 Added the `$origins` parameter.
 	 *
 	 * @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 ) {
+	protected 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 ( static::PRESETS_METADATA as $preset_metadata ) {
+			$values_by_slug = static::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'  => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
 					'value' => $value,
 				);
 			}
@@ -781,10 +1222,10 @@
 	 * @param array $settings Settings to process.
 	 * @return array Returns the modified $declarations.
 	 */
-	private static function compute_theme_vars( $settings ) {
+	protected static function compute_theme_vars( $settings ) {
 		$declarations  = array();
 		$custom_values = _wp_array_get( $settings, array( 'custom' ), array() );
-		$css_vars      = self::flatten_tree( $custom_values );
+		$css_vars      = static::flatten_tree( $custom_values );
 		foreach ( $css_vars as $key => $value ) {
 			$declarations[] = array(
 				'name'  => '--wp--custom--' . $key,
@@ -832,20 +1273,20 @@
 	 * @param string $token  Optional. Token to use between levels. Default '--'.
 	 * @return array The flattened tree.
 	 */
-	private static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
+	protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
 		$result = array();
 		foreach ( $tree as $property => $value ) {
 			$new_key = $prefix . str_replace(
 				'/',
 				'-',
-				strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $property ) ) // CamelCase to kebab-case.
+				strtolower( _wp_to_kebab_case( $property ) )
 			);
 
 			if ( is_array( $value ) ) {
 				$new_prefix = $new_key . $token;
 				$result     = array_merge(
 					$result,
-					self::flatten_tree( $value, $new_prefix, $token )
+					static::flatten_tree( $value, $new_prefix, $token )
 				);
 			} else {
 				$result[ $new_key ] = $value;
@@ -864,46 +1305,46 @@
 	 *     )
 	 *
 	 * @since 5.8.0
+	 * @since 5.9.0 Added the `$settings` and `$properties` parameters.
 	 *
-	 * @param array $styles Styles to process.
+	 * @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 ) {
+	protected static function compute_style_properties( $styles, $settings = array(), $properties = null ) {
+		if ( null === $properties ) {
+			$properties = static::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 ) ),
-					);
+		foreach ( $properties as $css_property => $value_path ) {
+			$value = static::get_property_value( $styles, $value_path );
+
+			// 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, static::PROTECTED_PROPERTIES ) &&
+					_wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
+				) {
+					continue;
 				}
-			} else {
-				$properties[] = array(
-					'name'  => $name,
-					'value' => $metadata['value'],
-				);
 			}
-		}
 
-		foreach ( $properties as $prop ) {
-			$value = self::get_property_value( $styles, $prop['value'] );
-			if ( empty( $value ) ) {
+			// 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,
 			);
 		}
@@ -912,22 +1353,6 @@
 	}
 
 	/**
-	 * 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.
 	 *
 	 * It also converts CSS Custom Property stored as
@@ -935,15 +1360,16 @@
 	 * "--wp--preset--color--secondary".
 	 *
 	 * @since 5.8.0
+	 * @since 5.9.0 Added support for values of array type, which are returned as is.
 	 *
 	 * @param array $styles Styles subtree.
 	 * @param array $path   Which property to process.
-	 * @return string Style property value.
+	 * @return string|array Style property value.
 	 */
-	private static function get_property_value( $styles, $path ) {
+	protected static function get_property_value( $styles, $path ) {
 		$value = _wp_array_get( $styles, $path, '' );
 
-		if ( '' === $value ) {
+		if ( '' === $value || is_array( $value ) ) {
 			return $value;
 		}
 
@@ -983,7 +1409,7 @@
 	 * @param array $selectors  List of selectors per block.
 	 * @return array
 	 */
-	private static function get_setting_nodes( $theme_json, $selectors = array() ) {
+	protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
 		$nodes = array();
 		if ( ! isset( $theme_json['settings'] ) ) {
 			return $nodes;
@@ -992,7 +1418,7 @@
 		// Top-level.
 		$nodes[] = array(
 			'path'     => array( 'settings' ),
-			'selector' => self::ROOT_BLOCK_SELECTOR,
+			'selector' => static::ROOT_BLOCK_SELECTOR,
 		);
 
 		// Calculate paths for blocks.
@@ -1015,18 +1441,19 @@
 		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
 	 *       ],
 	 *     ]
 	 *
@@ -1036,7 +1463,7 @@
 	 * @param array $selectors  List of selectors per block.
 	 * @return array
 	 */
-	private static function get_style_nodes( $theme_json, $selectors = array() ) {
+	protected static function get_style_nodes( $theme_json, $selectors = array() ) {
 		$nodes = array();
 		if ( ! isset( $theme_json['styles'] ) ) {
 			return $nodes;
@@ -1045,14 +1472,14 @@
 		// Top-level.
 		$nodes[] = array(
 			'path'     => array( 'styles' ),
-			'selector' => self::ROOT_BLOCK_SELECTOR,
+			'selector' => static::ROOT_BLOCK_SELECTOR,
 		);
 
 		if ( isset( $theme_json['styles']['elements'] ) ) {
 			foreach ( $theme_json['styles']['elements'] as $element => $node ) {
 				$nodes[] = array(
 					'path'     => array( 'styles', 'elements', $element ),
-					'selector' => self::ELEMENTS[ $element ],
+					'selector' => static::ELEMENTS[ $element ],
 				);
 			}
 		}
@@ -1068,9 +1495,15 @@
 				$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'] ) ) {
@@ -1087,9 +1520,50 @@
 	}
 
 	/**
+	 * For metadata values that can either be booleans or paths to booleans, gets the value.
+	 *
+	 * ```php
+	 * $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 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 = false ) {
+		if ( is_bool( $path ) ) {
+			return $path;
+		}
+
+		if ( is_array( $path ) ) {
+			$value = _wp_array_get( $data, $path );
+			if ( null !== $value ) {
+				return $value;
+			}
+		}
+
+		return $default;
+	}
+
+	/**
 	 * 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,30 +1572,404 @@
 		$this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
 
 		/*
-		 * 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.
+		 * 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.
 		 */
-		$to_replace   = array();
-		$to_replace[] = array( 'spacing', 'units' );
-		$to_replace[] = array( 'color', 'duotone' );
-		foreach ( self::VALID_ORIGINS as $origin ) {
-			$to_replace[] = array( 'color', 'palette', $origin );
-			$to_replace[] = array( 'color', 'gradients', $origin );
-			$to_replace[] = array( 'typography', 'fontSizes', $origin );
-			$to_replace[] = array( 'typography', 'fontFamilies', $origin );
+		$nodes        = static::get_setting_nodes( $incoming_data );
+		$slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
+		foreach ( $nodes as $node ) {
+			$slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] );
+			$slugs      = array_merge_recursive( $slugs_global, $slugs_node );
+
+			// Replace the spacing.units.
+			$path    = array_merge( $node['path'], array( 'spacing', '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 = array_merge( $node['path'], $preset['path'] );
+					$path      = array_merge( $base_path, array( $origin ) );
+					$content   = _wp_array_get( $incoming_data, $path, null );
+					if ( ! isset( $content ) ) {
+						continue;
+					}
+
+					if ( 'theme' === $origin && $preset['use_default_names'] ) {
+						foreach ( $content as &$item ) {
+							if ( ! array_key_exists( 'name', $item ) ) {
+								$name = static::get_name_from_defaults( $item['slug'], $base_path );
+								if ( null !== $name ) {
+									$item['name'] = $name;
+								}
+							}
+						}
+					}
+
+					if (
+						( 'theme' !== $origin ) ||
+						( 'theme' === $origin && $override_preset )
+					) {
+						_wp_array_set( $this->theme_json, $path, $content );
+					} else {
+						$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 );
+					}
+				}
+			}
 		}
+	}
 
-		$nodes = self::get_setting_nodes( $this->theme_json );
-		foreach ( $nodes as $metadata ) {
-			foreach ( $to_replace as $property_path ) {
-				$path = array_merge( $metadata['path'], $property_path );
-				$node = _wp_array_get( $incoming_data, $path, null );
-				if ( isset( $node ) ) {
-					_wp_array_set( $this->theme_json, $path, $node );
+	/**
+	 * 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;
+	}
+
+	/**
+	 * Returns 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 boolean
+	 */
+	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   = array_merge( $node_path, $metadata['path'], array( 'default' ) );
+			$preset = _wp_array_get( $data, $path, null );
+			if ( ! isset( $preset ) ) {
+				continue;
+			}
+
+			$slugs_for_preset = array();
+			$slugs_for_preset = array_map(
+				static function( $value ) {
+					return isset( $value['slug'] ) ? $value['slug'] : null;
+				},
+				$preset
+			);
+			_wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
+		}
+
+		return $slugs;
+	}
+
+	/**
+	 * Get 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            = array_merge( $base_path, array( '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
+	 *
+	 * @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 );
+		$theme_json          = static::sanitize( $theme_json, $valid_block_names, $valid_element_names );
+
+		$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;
+			}
+
+			$output = static::remove_insecure_styles( $input );
+			if ( ! empty( $output ) ) {
+				_wp_array_set( $sanitized, $metadata['path'], $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 = array_merge( $preset_metadata['path'], array( $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 );
+				}
+			}
+		}
+		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 );
+				}
+			}
+		}
+		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 ) );
 	}
 
 	/**
@@ -1146,7 +1994,7 @@
 	 */
 	public static function get_from_editor_settings( $settings ) {
 		$theme_settings = array(
-			'version'  => self::LATEST_SCHEMA,
+			'version'  => static::LATEST_SCHEMA,
 			'settings' => array(),
 		);
 
@@ -1176,7 +2024,7 @@
 			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,18 +2068,157 @@
 			if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
 				$theme_settings['settings']['spacing'] = array();
 			}
-			$theme_settings['settings']['spacing']['customPadding'] = $settings['enableCustomSpacing'];
-		}
-
-		// Things that didn't land in core yet, so didn't have a setting assigned.
-		if ( current( (array) get_theme_support( 'experimental-link-color' ) ) ) {
-			if ( ! isset( $theme_settings['settings']['color'] ) ) {
-				$theme_settings['settings']['color'] = array();
-			}
-			$theme_settings['settings']['color']['link'] = true;
+			$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   = array_merge( $node['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' => $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 = array_merge( $node['path'], $opt_in_path );
+				// 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 ) {
+				_wp_array_set( $output, array_merge( $node['path'], array( 'appearanceTools' ) ), true );
+				foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) {
+					$full_path = array_merge( $node['path'], $opt_in_path );
+					// 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;
+	}
+
 }