diff --git a/docs/designers-developers/developers/themes/theme-json.md b/docs/designers-developers/developers/themes/theme-json.md index dad83773b1e3c..222040b251485 100644 --- a/docs/designers-developers/developers/themes/theme-json.md +++ b/docs/designers-developers/developers/themes/theme-json.md @@ -34,44 +34,18 @@ By providing the block style properties in a structured way, the Block Editor ca ## Specification -The `experimental-theme.json` file is divided into sections known as "contexts", that represent a different CSS selector. For example, the `core/paragraph` context maps to `p` while `core/group` maps to `.wp-block-group`. In general, one block will map to a single context as in the cases mentioned. There are cases where one block can generate multiple contexts (different CSS selectors). For example, the heading block generates six different contexts (`core/heading/h1`, `core/heading/h2`, etc), one for each different selector (h1, h2, etc). +The `experimental-theme.json` file declares how a theme wants the editor configured (`settings`) as well as the style properties it sets (`styles`). ``` { - "global": { ... }, - "core/paragraph": { ... }, - "core/group": { ... }, - "core/heading/h1": { ... }, - "core/heading/h2": { ... }, - "core/heading/h3": { ... }, - "core/heading/h4": { ... }, - "core/heading/h5": { ... }, - "core/heading/h6": { ... } + "settings": { ... }, + "styles": { ... } } ``` -Every context has the same structure, divided in two sections: `settings` and `styles`. The `settings` are used to control the editor (enable/disable certain features, declare presets), taking over what was previously declared via `add_theme_support`. The `styles` will be used to create new style rules to be appended to a new stylesheet `global-styles-inline-css` enqueued in the front-end and post editor. +Each one of these sections is sub-divided into "contexts" that loosely map to a block. In general, one block will create one single context ―the paragraph block can be addressed via `core/paragraph`― but there are also cases where one block will create multiple contexts ―the heading block represents different HTML elements, h1 to h6, so it creates one context for each such `core/heading/h1`, `core/heading/h2`, etc. Every context has the same inner structure. -``` -{ - "some/context": { - "settings": { - "border": [ ... ], - "color": [ ... ], - "custom": [ ... ], - "spacing": [ ... ], - "typography": [ ... ] - }, - "styles": { - "border": { ... }, - "color": { ... }, - "typography": { ... } - } - } -} -``` - -This structure is the same for the three different origins that exist: core, themes, and users. Themes can override core's defaults by creating a file called `experimental-theme.json`. Users, via the site editor, will also be also to override theme's or core's preferences via an user interface that is being worked on. +This specification is the same for the three different origins that use this format: core, themes, and users. Themes can override core's defaults by creating a file called `experimental-theme.json`. Users, via the site editor, will also be also to override theme's or core's preferences via an user interface that is being worked on. ### Settings @@ -79,8 +53,8 @@ The settings section has the following structure and default values: ``` { - "some/context": { - "settings": { + "settings": { + "some/context": { "border": { "customRadius": false /* true to opt-in */ }, @@ -114,26 +88,25 @@ The settings section has the following structure and default values: } ``` -To retain backward compatibility, `add_theme_support` declarations are retrofit in the proper categories. If a theme uses `add_theme_support('disable-custom-colors')`, it'll be the same as set `global.settings.color.custom` to `false`. If the `experimental-theme.json` contains any settings, these will take precedence over the values declared via `add_theme_support`. +To retain backward compatibility, `add_theme_support` declarations are retrofit in the proper categories. If a theme uses `add_theme_support('disable-custom-colors')`, it'll be the same as set `settings.global.color.custom` to `false`. If the `experimental-theme.json` contains any settings, these will take precedence over the values declared via `add_theme_support`. Settings can also be controlled by context, providing a more fine-grained control over what exists via `add_theme_support`. As an example, let's say that a theme author wants to enable custom colors for the paragraph block exclusively. This is how it'd be done: ```json { - "global": { - "settings": { + "settings": { + "global": { "color": { "custom": false } - } - }, - "core/paragraph": { - "settings": { + }, + "core/paragraph": { "color": { "custom": true } } } +} ``` Note, however, that not all settings are relevant for all contexts and the blocks they represent. The settings section provides an opt-in/opt-out mechanism for themes, but it's the block's responsibility to add support for the features that are relevant to it. For example, if a block doesn't implement the `dropCap` feature, a theme can't enable it for such a block through `experimental-theme.json`. @@ -146,8 +119,8 @@ For example, for this input: ```json { - "global": { - "settings": { + "settings": { + "global": { "color": { "palette": [ { @@ -204,14 +177,14 @@ The goal is that presets can be defined using this format, although, right now, #### Free-form CSS Custom Properties -In addition to create CSS Custom Properties for the presets, the theme.json also allows for themes to create their own, so they don't have to be enqueued separately. Any values declared within the `settings.custom` section will be transformed to CSS Custom Properties following this naming schema: `--wp--custom--`. +In addition to create CSS Custom Properties for the presets, the theme.json also allows for themes to create their own, so they don't have to be enqueued separately. Any values declared within the `settings..custom` section will be transformed to CSS Custom Properties following this naming schema: `--wp--custom--`. For example, for this input: ```json { - "global": { - "settings": { + "settings": { + "global": { "custom": { "base-font": 16, "line-height": { @@ -244,8 +217,8 @@ Each block declares which style properties it exposes. This has been coined as " ```json { - "some/context": { - "styles": { + "styles": { + "some/context": { "border": { "radius": "value" }, @@ -281,18 +254,16 @@ For example, an input like this: ```json { - "core/heading/h1": { - "styles": { + "styles": { + "core/heading/h1": { "color": { "text": "var(--wp--preset--color--primary)" }, "typography": { "fontSize": "calc(1px * var(--wp--preset--font-size--huge))" } - } - }, - "core/heading/h4": { - "styles": { + }, + "core/heading/h4": { "color": { "text": "var(--wp--preset--color--secondary)" }, diff --git a/lib/class-wp-theme-json-resolver.php b/lib/class-wp-theme-json-resolver.php index 721aa77b04629..36bcb603cccd0 100644 --- a/lib/class-wp-theme-json-resolver.php +++ b/lib/class-wp-theme-json-resolver.php @@ -76,6 +76,32 @@ private static function get_from_file( $file_path ) { * containing the a translatable path from theme.json and an array * of properties that are translatable. * + * For example, given this input: + * + * { + * "settings": { + * "*": { + * "typography": { + * "fontSizes": [ "name" ], + * "fontStyles": [ "name" ] + * } + * } + * } + * } + * + * will return this output: + * + * [ + * 0 => [ + * 'path' => [ 'settings', '*', 'typography', 'fontSizes' ], + * 'translatable_keys' => [ 'name' ] + * ], + * 1 => [ + * 'path' => [ 'settings', '*', 'typography', 'fontStyles' ], + * 'translatable_keys' => [ 'name'] + * ] + * ] + * * @param array $file_structure_partial A part of a theme.json i18n tree. * @param array $current_path An array with a path on the theme.json i18n tree. * @@ -110,7 +136,6 @@ private static function get_presets_to_translate() { if ( null === $theme_json_i18n ) { $file_structure = self::get_from_file( __DIR__ . '/experimental-i18n-theme.json' ); $theme_json_i18n = self::theme_json_i18_file_structure_to_preset_paths( $file_structure ); - } return $theme_json_i18n; } @@ -123,29 +148,37 @@ private static function get_presets_to_translate() { * Default 'default'. */ private static function translate_presets( &$theme_json_structure, $domain = 'default' ) { + if ( ! isset( $theme_json_structure['settings'] ) ) { + return; + } + $preset_to_translate = self::get_presets_to_translate(); - foreach ( $theme_json_structure as &$context_value ) { - if ( empty( $context_value ) ) { + foreach ( $theme_json_structure['settings'] as &$settings ) { + if ( empty( $settings ) ) { continue; } + foreach ( $preset_to_translate as $preset ) { - $path = $preset['path']; + $path = array_slice( $preset['path'], 2 ); $translatable_keys = $preset['translatable_keys']; - $array_to_translate = gutenberg_experimental_get( $context_value, $path, null ); + $array_to_translate = gutenberg_experimental_get( $settings, $path, null ); if ( null === $array_to_translate ) { continue; } + foreach ( $array_to_translate as &$item_to_translate ) { foreach ( $translatable_keys as $translatable_key ) { if ( empty( $item_to_translate[ $translatable_key ] ) ) { continue; } + // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain $item_to_translate[ $translatable_key ] = translate( $item_to_translate[ $translatable_key ], $domain ); // phpcs:enable } } - gutenberg_experimental_set( $context_value, $path, $array_to_translate ); + + gutenberg_experimental_set( $settings, $path, $array_to_translate ); } } } @@ -178,8 +211,8 @@ private static function get_core_origin() { 'vivid-cyan-blue' => __( 'Vivid cyan blue', 'gutenberg' ), 'vivid-purple' => __( 'Vivid purple', 'gutenberg' ), ); - if ( ! empty( $config['global']['settings']['color']['palette'] ) ) { - foreach ( $config['global']['settings']['color']['palette'] as &$color ) { + if ( ! empty( $config['settings']['global']['color']['palette'] ) ) { + foreach ( $config['settings']['global']['color']['palette'] as &$color ) { $color['name'] = $default_colors_i18n[ $color['slug'] ]; } } @@ -198,8 +231,8 @@ private static function get_core_origin() { 'electric-grass' => __( 'Electric grass', 'gutenberg' ), 'midnight' => __( 'Midnight', 'gutenberg' ), ); - if ( ! empty( $config['global']['settings']['color']['gradients'] ) ) { - foreach ( $config['global']['settings']['color']['gradients'] as &$gradient ) { + if ( ! empty( $config['settings']['global']['color']['gradients'] ) ) { + foreach ( $config['settings']['global']['color']['gradients'] as &$gradient ) { $gradient['name'] = $default_gradients_i18n[ $gradient['slug'] ]; } } @@ -211,8 +244,8 @@ private static function get_core_origin() { 'large' => __( 'Large', 'gutenberg' ), 'huge' => __( 'Huge', 'gutenberg' ), ); - if ( ! empty( $config['global']['settings']['typography']['fontSizes'] ) ) { - foreach ( $config['global']['settings']['typography']['fontSizes'] as &$font_size ) { + if ( ! empty( $config['settings']['global']['typography']['fontSizes'] ) ) { + foreach ( $config['settings']['global']['typography']['fontSizes'] as &$font_size ) { $font_size['name'] = $default_font_sizes_i18n[ $font_size['slug'] ]; } } diff --git a/lib/class-wp-theme-json.php b/lib/class-wp-theme-json.php index a4ecd2760e6c0..4d117715f0cd1 100644 --- a/lib/class-wp-theme-json.php +++ b/lib/class-wp-theme-json.php @@ -16,7 +16,7 @@ class WP_Theme_JSON { * * @var array */ - private $contexts = null; + private $theme_json = null; /** * Holds block metadata extracted from block.json @@ -28,21 +28,21 @@ class WP_Theme_JSON { private static $blocks_metadata = null; /** - * The name of the global context. + * The name of the global block. * * @var string */ const GLOBAL_NAME = 'global'; /** - * The CSS selector for the global context. + * The CSS selector for the global block. * * @var string */ const GLOBAL_SELECTOR = ':root'; /** - * The supported properties of the global context. + * The supported properties of the global block. * * @var array */ @@ -61,12 +61,12 @@ class WP_Theme_JSON { ); /** - * Data schema of each context within a theme.json. + * Data schema of each block within a theme.json. * * Example: * * { - * 'context-one': { + * 'block-one': { * 'styles': { * 'color': { * 'background': 'color' @@ -78,7 +78,7 @@ class WP_Theme_JSON { * } * } * }, - * 'context-two': { + * 'block-two': { * 'styles': { * 'color': { * 'link': 'color' @@ -164,7 +164,7 @@ class WP_Theme_JSON { * * This contains the necessary metadata to process them: * - * - path => where to find the preset in a theme.json context + * - path => where to find the preset within the settings section * * - value_key => the key that represents the value * @@ -181,7 +181,7 @@ class WP_Theme_JSON { */ const PRESETS_METADATA = array( array( - 'path' => array( 'settings', 'color', 'palette' ), + 'path' => array( 'color', 'palette' ), 'value_key' => 'color', 'css_var_infix' => 'color', 'classes' => array( @@ -196,7 +196,7 @@ class WP_Theme_JSON { ), ), array( - 'path' => array( 'settings', 'color', 'gradients' ), + 'path' => array( 'color', 'gradients' ), 'value_key' => 'gradient', 'css_var_infix' => 'gradient', 'classes' => array( @@ -207,7 +207,7 @@ class WP_Theme_JSON { ), ), array( - 'path' => array( 'settings', 'typography', 'fontSizes' ), + 'path' => array( 'typography', 'fontSizes' ), 'value_key' => 'size', 'css_var_infix' => 'font-size', 'classes' => array( @@ -218,7 +218,7 @@ class WP_Theme_JSON { ), ), array( - 'path' => array( 'settings', 'typography', 'fontFamilies' ), + 'path' => array( 'typography', 'fontFamilies' ), 'value_key' => 'fontFamily', 'css_var_infix' => 'font-family', 'classes' => array(), @@ -292,56 +292,85 @@ class WP_Theme_JSON { /** * Constructor. * - * @param array $contexts A structure that follows the theme.json schema. + * @param array $theme_json A structure that follows the theme.json schema. */ - public function __construct( $contexts = array() ) { - $this->contexts = array(); + public function __construct( $theme_json = array() ) { + $this->theme_json = array(); - if ( ! is_array( $contexts ) ) { + if ( ! is_array( $theme_json ) ) { return; } - $metadata = $this->get_blocks_metadata(); - foreach ( $contexts as $key => $context ) { - if ( ! isset( $metadata[ $key ] ) ) { - // Skip incoming contexts that can't be found - // within the contexts registered. - continue; + // Remove top-level keys that aren't present in the schema. + $this->theme_json = array_intersect_key( $theme_json, self::SCHEMA ); + + $block_metadata = $this->get_blocks_metadata(); + foreach ( array( 'settings', 'styles' ) as $subtree ) { + // Remove settings & styles subtrees if they aren't arrays. + if ( isset( $this->theme_json[ $subtree ] ) && ! is_array( $this->theme_json[ $subtree ] ) ) { + unset( $this->theme_json[ $subtree ] ); } - // Filter out top-level keys that aren't valid according to the schema. - $context = array_intersect_key( $context, self::SCHEMA ); - - // Process styles subtree. - $this->process_key( 'styles', $context, self::SCHEMA ); - if ( isset( $context['styles'] ) ) { - $this->process_key( 'border', $context['styles'], self::SCHEMA['styles'] ); - $this->process_key( 'color', $context['styles'], self::SCHEMA['styles'] ); - $this->process_key( 'spacing', $context['styles'], self::SCHEMA['styles'] ); - $this->process_key( 'typography', $context['styles'], self::SCHEMA['styles'] ); - - if ( empty( $context['styles'] ) ) { - unset( $context['styles'] ); - } else { - $this->contexts[ $key ]['styles'] = $context['styles']; + // Remove block selectors subtrees declared within settings & styles if that aren't registered. + if ( isset( $this->theme_json[ $subtree ] ) ) { + $this->theme_json[ $subtree ] = array_intersect_key( $this->theme_json[ $subtree ], $block_metadata ); + } + } + + foreach ( $block_metadata as $block_selector => $metadata ) { + if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) { + // Remove the block selector subtree if it's not an array. + if ( ! is_array( $this->theme_json['styles'][ $block_selector ] ) ) { + unset( $this->theme_json['styles'][ $block_selector ] ); + continue; + } + + // Remove the properties the block doesn't support. + // This is a subset of the full styles schema. + $styles_schema = self::SCHEMA['styles']; + foreach ( self::PROPERTIES_METADATA as $prop_name => $prop_meta ) { + if ( ! in_array( $prop_name, $metadata['supports'], true ) ) { + unset( $styles_schema[ $prop_meta['value'][0] ][ $prop_meta['value'][1] ] ); + } + } + self::remove_keys_not_in_schema( + $this->theme_json['styles'][ $block_selector ], + $styles_schema + ); + + // Remove the block selector subtree if it is empty after having processed it. + if ( empty( $this->theme_json['styles'][ $block_selector ] ) ) { + unset( $this->theme_json['styles'][ $block_selector ] ); } } - // Process settings subtree. - $this->process_key( 'settings', $context, self::SCHEMA ); - if ( isset( $context['settings'] ) ) { - $this->process_key( 'border', $context['settings'], self::SCHEMA['settings'] ); - $this->process_key( 'color', $context['settings'], self::SCHEMA['settings'] ); - $this->process_key( 'spacing', $context['settings'], self::SCHEMA['settings'] ); - $this->process_key( 'typography', $context['settings'], self::SCHEMA['settings'] ); - - if ( empty( $context['settings'] ) ) { - unset( $context['settings'] ); - } else { - $this->contexts[ $key ]['settings'] = $context['settings']; + if ( isset( $this->theme_json['settings'][ $block_selector ] ) ) { + // Remove the block selector subtree if it's not an array. + if ( ! is_array( $this->theme_json['settings'][ $block_selector ] ) ) { + unset( $this->theme_json['settings'][ $block_selector ] ); + continue; + } + + // Remove the properties that aren't present in the schema. + self::remove_keys_not_in_schema( + $this->theme_json['settings'][ $block_selector ], + self::SCHEMA['settings'] + ); + + // Remove the block selector subtree if it is empty after having processed it. + if ( empty( $this->theme_json['settings'][ $block_selector ] ) ) { + unset( $this->theme_json['settings'][ $block_selector ] ); } } } + + // Remove the settings & styles subtrees if they're empty after having processed them. + foreach ( array( 'settings', 'styles' ) as $subtree ) { + if ( empty( $this->theme_json[ $subtree ] ) ) { + unset( $this->theme_json[ $subtree ] ); + } + } + } /** @@ -370,10 +399,12 @@ private static function to_property( $property ) { * Returns a mapping on metadata properties to avoid having to constantly * transforms properties between camel case and kebab. * - * @return array Containing three mappings - * "to_kebab_case" mapping properties in camel case to + * @return array Containing two mappings: + * + * - "to_kebab_case" mapping properties in camel case to * properties in kebab case e.g: "paddingTop" to "padding-top". - * "to_property" mapping properties in kebab case to + * + * - "to_property" mapping properties in kebab case to * the main properties in camel case e.g: "padding-top" to "padding". */ private static function get_case_mappings() { @@ -517,52 +548,25 @@ private static function get_blocks_metadata() { } /** - * Normalize the subtree according to the given schema. - * This function modifies the given input by removing - * the nodes that aren't valid per the schema. + * Given a tree, removes the keys that are not present in the schema. * - * @param string $key Key of the subtree to normalize. - * @param array $input Whole tree to normalize. - * @param array $schema Schema to use for normalization. + * It is recursive and modifies the input in-place. + * + * @param array $tree Input to process. + * @param array $schema Schema to adhere to. */ - private static function process_key( $key, &$input, $schema ) { - if ( ! isset( $input[ $key ] ) ) { - return; - } - - // Consider valid the input value. - if ( null === $schema[ $key ] ) { - return; - } + private static function remove_keys_not_in_schema( &$tree, $schema ) { + $tree = array_intersect_key( $tree, $schema ); - if ( ! is_array( $input[ $key ] ) ) { - unset( $input[ $key ] ); - return; - } - - $input[ $key ] = array_intersect_key( - $input[ $key ], - $schema[ $key ] - ); + foreach ( $schema as $key => $data ) { + if ( is_array( $schema[ $key ] ) && isset( $tree[ $key ] ) ) { + self::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] ); - if ( 0 === count( $input[ $key ] ) ) { - unset( $input[ $key ] ); - } - } - - /** - * Given a context, it returns its settings subtree. - * - * @param array $context Context adhering to the theme.json schema. - * - * @return array|null The settings subtree. - */ - private static function extract_settings( $context ) { - if ( empty( $context['settings'] ) ) { - return null; + if ( empty( $tree[ $key ] ) ) { + unset( $tree[ $key ] ); + } + } } - - return $context['settings']; } /** @@ -674,7 +678,7 @@ private static function has_properties( $metadata ) { } /** - * Given a context, it extracts the style properties + * Given a styles array, it extracts the style properties * and adds them to the $declarations array following the format: * * ```php @@ -686,22 +690,24 @@ private static function has_properties( $metadata ) { * * Note that this modifies the $declarations in place. * - * @param array $declarations Holds the existing declarations. - * @param array $context Input context to process. - * @param array $context_supports Supports information for this context. + * @param array $declarations Holds the existing declarations. + * @param array $styles Styles to process. + * @param array $supports Supports information for this block. */ - private static function compute_style_properties( &$declarations, $context, $context_supports ) { - if ( empty( $context['styles'] ) ) { + private static function compute_style_properties( &$declarations, $styles, $supports ) { + if ( empty( $styles ) ) { return; } + $properties = array(); foreach ( self::PROPERTIES_METADATA as $name => $metadata ) { - if ( ! in_array( $name, $context_supports, true ) ) { + if ( ! in_array( $name, $supports, true ) ) { continue; } // 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, see self::SCHEMA. if ( self::has_properties( $metadata ) ) { foreach ( $metadata['properties'] as $property ) { $properties[] = array( @@ -718,7 +724,7 @@ private static function compute_style_properties( &$declarations, $context, $con } foreach ( $properties as $prop ) { - $value = self::get_property_value( $context['styles'], $prop['value'] ); + $value = self::get_property_value( $styles, $prop['value'] ); if ( ! empty( $value ) ) { $kebab_cased_name = self::to_kebab_case( $prop['name'] ); $declarations[] = array( @@ -730,16 +736,16 @@ private static function compute_style_properties( &$declarations, $context, $con } /** - * Given a context, it extracts its presets + * Given a settings array, it extracts its presets * and adds them to the given input $stylesheet. * * Note this function modifies $stylesheet in place. * * @param string $stylesheet Input stylesheet to add the presets to. - * @param array $context Context to process. + * @param array $settings Settings to process. * @param string $selector Selector wrapping the classes. */ - private static function compute_preset_classes( &$stylesheet, $context, $selector ) { + private static function compute_preset_classes( &$stylesheet, $settings, $selector ) { if ( self::GLOBAL_SELECTOR === $selector ) { // Classes at the global level do not need any CSS prefixed, // and we don't want to increase its specificity. @@ -747,7 +753,7 @@ private static function compute_preset_classes( &$stylesheet, $context, $selecto } foreach ( self::PRESETS_METADATA as $preset ) { - $values = gutenberg_experimental_get( $context, $preset['path'], array() ); + $values = gutenberg_experimental_get( $settings, $preset['path'], array() ); foreach ( $values as $value ) { foreach ( $preset['classes'] as $class ) { $stylesheet .= self::to_ruleset( @@ -765,7 +771,7 @@ private static function compute_preset_classes( &$stylesheet, $context, $selecto } /** - * Given a context, it extracts the CSS Custom Properties + * Given the block settings, it extracts the CSS Custom Properties * for the presets and adds them to the $declarations array * following the format: * @@ -779,11 +785,11 @@ private static function compute_preset_classes( &$stylesheet, $context, $selecto * Note that this modifies the $declarations in place. * * @param array $declarations Holds the existing declarations. - * @param array $context Input context to process. + * @param array $settings Settings to process. */ - private static function compute_preset_vars( &$declarations, $context ) { + private static function compute_preset_vars( &$declarations, $settings ) { foreach ( self::PRESETS_METADATA as $preset ) { - $values = gutenberg_experimental_get( $context, $preset['path'], array() ); + $values = gutenberg_experimental_get( $settings, $preset['path'], array() ); foreach ( $values as $value ) { $declarations[] = array( 'name' => '--wp--preset--' . $preset['css_var_infix'] . '--' . $value['slug'], @@ -794,7 +800,7 @@ private static function compute_preset_vars( &$declarations, $context ) { } /** - * Given a context, it extracts the CSS Custom Properties + * Given an array of settings, it extracts the CSS Custom Properties * for the custom values and adds them to the $declarations * array following the format: * @@ -808,10 +814,10 @@ private static function compute_preset_vars( &$declarations, $context ) { * Note that this modifies the $declarations in place. * * @param array $declarations Holds the existing declarations. - * @param array $context Input context to process. + * @param array $settings Settings to process. */ - private static function compute_theme_vars( &$declarations, $context ) { - $custom_values = gutenberg_experimental_get( $context, array( 'settings', 'custom' ) ); + private static function compute_theme_vars( &$declarations, $settings ) { + $custom_values = gutenberg_experimental_get( $settings, array( 'custom' ) ); $css_vars = self::flatten_tree( $custom_values ); foreach ( $css_vars as $key => $value ) { $declarations[] = array( @@ -861,15 +867,15 @@ function ( $carry, $element ) { } /** - * Converts each context into a list of rulesets + * Converts each styles section into a list of rulesets * to be appended to the stylesheet. * These rulesets contain all the css variables (custom variables and preset variables). * * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax * - * For each context this creates a new ruleset such as: + * For each section this creates a new ruleset such as: * - * context-selector { + * block-selector { * --wp--preset--category--slug: value; * --wp--custom--variable: value; * } @@ -878,16 +884,20 @@ function ( $carry, $element ) { */ private function get_css_variables() { $stylesheet = ''; - $metadata = $this->get_blocks_metadata(); - foreach ( $this->contexts as $context_name => $context ) { - if ( empty( $metadata[ $context_name ]['selector'] ) ) { + if ( ! isset( $this->theme_json['settings'] ) ) { + return $stylesheet; + } + + $metadata = $this->get_blocks_metadata(); + foreach ( $this->theme_json['settings'] as $block_selector => $settings ) { + if ( empty( $metadata[ $block_selector ]['selector'] ) ) { continue; } - $selector = $metadata[ $context_name ]['selector']; + $selector = $metadata[ $block_selector ]['selector']; $declarations = array(); - self::compute_preset_vars( $declarations, $context ); - self::compute_theme_vars( $declarations, $context ); + self::compute_preset_vars( $declarations, $settings ); + self::compute_theme_vars( $declarations, $settings ); // Attach the ruleset for style and custom properties. $stylesheet .= self::to_ruleset( $selector, $declarations ); @@ -896,14 +906,14 @@ private function get_css_variables() { } /** - * Converts each context into a list of rulesets + * Converts each style section into a list of rulesets * containing the block styles to be appended to the stylesheet. * * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax * - * For each context this creates a new ruleset such as: + * For each section this creates a new ruleset such as: * - * context-selector { + * block-selector { * style-property-one: value; * } * @@ -934,28 +944,45 @@ private function get_css_variables() { */ private function get_block_styles() { $stylesheet = ''; - $metadata = $this->get_blocks_metadata(); - foreach ( $this->contexts as $context_name => $context ) { - if ( empty( $metadata[ $context_name ]['selector'] ) || empty( $metadata[ $context_name ]['supports'] ) ) { + if ( ! isset( $this->theme_json['styles'] ) && ! isset( $this->theme_json['settings'] ) ) { + return $stylesheet; + } + + $metadata = $this->get_blocks_metadata(); + foreach ( $metadata as $block_selector => $metadata ) { + if ( empty( $metadata['selector'] ) || empty( $metadata['supports'] ) ) { continue; } - $selector = $metadata[ $context_name ]['selector']; - $supports = $metadata[ $context_name ]['supports']; + + $selector = $metadata['selector']; + $supports = $metadata['supports']; $declarations = array(); - self::compute_style_properties( $declarations, $context, $supports ); + if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) { + self::compute_style_properties( + $declarations, + $this->theme_json['styles'][ $block_selector ], + $supports + ); + } $stylesheet .= self::to_ruleset( $selector, $declarations ); // Attach the rulesets for the classes. - self::compute_preset_classes( $stylesheet, $context, $selector ); + if ( isset( $this->theme_json['settings'][ $block_selector ] ) ) { + self::compute_preset_classes( + $stylesheet, + $this->theme_json['settings'][ $block_selector ], + $selector + ); + } } return $stylesheet; } /** - * Returns the existing settings for each context. + * Returns the existing settings for each block. * * Example: * @@ -972,15 +999,14 @@ private function get_block_styles() { * } * } * - * @return array Settings per context. + * @return array Settings per block. */ public function get_settings() { - return array_filter( - array_map( array( $this, 'extract_settings' ), $this->contexts ), - function ( $element ) { - return null !== $element; - } - ); + if ( ! isset( $this->theme_json['settings'] ) ) { + return array(); + } else { + return $this->theme_json['settings']; + } } /** @@ -1004,37 +1030,41 @@ public function get_stylesheet( $type = 'all' ) { /** * Merge new incoming data. * - * @param WP_Theme_JSON $theme_json Data to merge. + * @param WP_Theme_JSON $incoming Data to merge. */ - public function merge( $theme_json ) { - $incoming_data = $theme_json->get_raw_data(); - - foreach ( array_keys( $incoming_data ) as $context ) { - foreach ( array( 'settings', 'styles' ) as $subtree ) { - if ( ! isset( $incoming_data[ $context ][ $subtree ] ) ) { - continue; - } - - if ( ! isset( $this->contexts[ $context ][ $subtree ] ) ) { - $this->contexts[ $context ][ $subtree ] = $incoming_data[ $context ][ $subtree ]; - continue; - } - - foreach ( array_keys( self::SCHEMA[ $subtree ] ) as $leaf ) { - if ( ! isset( $incoming_data[ $context ][ $subtree ][ $leaf ] ) ) { - continue; - } - - if ( ! isset( $this->contexts[ $context ][ $subtree ][ $leaf ] ) ) { - $this->contexts[ $context ][ $subtree ][ $leaf ] = $incoming_data[ $context ][ $subtree ][ $leaf ]; - continue; - } - - $this->contexts[ $context ][ $subtree ][ $leaf ] = array_merge( - $this->contexts[ $context ][ $subtree ][ $leaf ], - $incoming_data[ $context ][ $subtree ][ $leaf ] - ); - } + public function merge( $incoming ) { + $incoming_data = $incoming->get_raw_data(); + $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); + + // The array_replace_recursive algorithm merges at the leaf level. + // This means that when a leaf value is an array, + // the incoming array won't replace the existing, + // but the numeric indexes are used for replacement. + // + // These are the cases that have array values at the leaf levels. + $block_metadata = self::get_blocks_metadata(); + foreach ( $block_metadata as $block_selector => $meta ) { + // Color presets: palette & gradients. + if ( isset( $incoming_data['settings'][ $block_selector ]['color']['palette'] ) ) { + $this->theme_json['settings'][ $block_selector ]['color']['palette'] = $incoming_data['settings'][ $block_selector ]['color']['palette']; + } + if ( isset( $incoming_data['settings'][ $block_selector ]['color']['gradients'] ) ) { + $this->theme_json['settings'][ $block_selector ]['color']['gradients'] = $incoming_data['settings'][ $block_selector ]['color']['gradients']; + } + // Spacing: units. + if ( isset( $incoming_data['settings'][ $block_selector ]['spacing']['units'] ) ) { + $this->theme_json['settings'][ $block_selector ]['spacing']['units'] = $incoming_data['settings'][ $block_selector ]['spacing']['units']; + } + // Typography presets: fontSizes & fontFamilies. + if ( isset( $incoming_data['settings'][ $block_selector ]['typography']['fontSizes'] ) ) { + $this->theme_json['settings'][ $block_selector ]['typography']['fontSizes'] = $incoming_data['settings'][ $block_selector ]['typography']['fontSizes']; + } + if ( isset( $incoming_data['settings'][ $block_selector ]['typography']['fontFamilies'] ) ) { + $this->theme_json['settings'][ $block_selector ]['typography']['fontFamilies'] = $incoming_data['settings'][ $block_selector ]['typography']['fontFamilies']; + } + // Custom section. + if ( isset( $incoming_data['settings'][ $block_selector ]['custom'] ) ) { + $this->theme_json['settings'][ $block_selector ]['custom'] = $incoming_data['settings'][ $block_selector ]['custom']; } } } @@ -1044,53 +1074,41 @@ public function merge( $theme_json ) { */ public function remove_insecure_properties() { $blocks_metadata = self::get_blocks_metadata(); - foreach ( $this->contexts as $context_name => &$context ) { - // Escape the context key. - if ( empty( $blocks_metadata[ $context_name ] ) ) { - unset( $this->contexts[ $context_name ] ); - continue; - } - - $escaped_settings = null; - $escaped_styles = null; + foreach ( $blocks_metadata as $block_selector => $metadata ) { + $escaped_settings = array(); + $escaped_styles = array(); // Style escaping. - if ( ! empty( $context['styles'] ) ) { - $supports = $blocks_metadata[ $context_name ]['supports']; + if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) { $declarations = array(); - self::compute_style_properties( $declarations, $context, $supports ); + self::compute_style_properties( $declarations, $this->theme_json['styles'][ $block_selector ], $metadata['supports'] ); foreach ( $declarations as $declaration ) { $style_to_validate = $declaration['name'] . ': ' . $declaration['value']; if ( esc_html( safecss_filter_attr( $style_to_validate ) ) === $style_to_validate ) { - if ( null === $escaped_styles ) { - $escaped_styles = array(); - } $property = self::to_property( $declaration['name'] ); $path = self::PROPERTIES_METADATA[ $property ]['value']; if ( self::has_properties( self::PROPERTIES_METADATA[ $property ] ) ) { $declaration_divided = explode( '-', $declaration['name'] ); $path[] = $declaration_divided[1]; - gutenberg_experimental_set( - $escaped_styles, - $path, - gutenberg_experimental_get( $context['styles'], $path ) - ); - } else { - gutenberg_experimental_set( - $escaped_styles, - $path, - gutenberg_experimental_get( $context['styles'], $path ) - ); } + gutenberg_experimental_set( + $escaped_styles, + $path, + gutenberg_experimental_get( $this->theme_json['styles'][ $block_selector ], $path ) + ); } } } // Settings escaping. // For now the ony allowed settings are presets. - if ( ! empty( $context['settings'] ) ) { + if ( isset( $this->theme_json['settings'][ $block_selector ] ) ) { foreach ( self::PRESETS_METADATA as $preset_metadata ) { - $current_preset = gutenberg_experimental_get( $context, $preset_metadata['path'], null ); + $current_preset = gutenberg_experimental_get( + $this->theme_json['settings'][ $block_selector ], + $preset_metadata['path'], + null + ); if ( null !== $current_preset ) { $escaped_preset = array(); foreach ( $current_preset as $single_preset ) { @@ -1120,34 +1138,23 @@ public function remove_insecure_properties() { } } } - if ( count( $escaped_preset ) > 0 ) { - if ( null === $escaped_settings ) { - $escaped_settings = array(); - } + if ( ! empty( $escaped_preset ) ) { gutenberg_experimental_set( $escaped_settings, $preset_metadata['path'], $escaped_preset ); } } } - if ( null !== $escaped_settings ) { - $escaped_settings = $escaped_settings['settings']; - } } - if ( null === $escaped_settings && null === $escaped_styles ) { - unset( $this->contexts[ $context_name ] ); - } elseif ( null !== $escaped_settings && null !== $escaped_styles ) { - $context = array( - 'styles' => $escaped_styles, - 'settings' => $escaped_settings, - ); - } elseif ( null === $escaped_settings ) { - $context = array( - 'styles' => $escaped_styles, - ); + if ( empty( $escaped_settings ) ) { + unset( $this->theme_json['settings'][ $block_selector ] ); } else { - $context = array( - 'settings' => $escaped_settings, - ); + $this->theme_json['settings'][ $block_selector ] = $escaped_settings; + } + + if ( empty( $escaped_styles ) ) { + unset( $this->theme_json['styles'][ $block_selector ] ); + } else { + $this->theme_json['styles'][ $block_selector ] = $escaped_styles; } } } @@ -1158,7 +1165,7 @@ public function remove_insecure_properties() { * @return array Raw data. */ public function get_raw_data() { - return $this->contexts; + return $this->theme_json; } } diff --git a/lib/experimental-default-theme.json b/lib/experimental-default-theme.json index 1ad199567df11..d4c363788d7eb 100644 --- a/lib/experimental-default-theme.json +++ b/lib/experimental-default-theme.json @@ -1,6 +1,6 @@ { - "global": { - "settings": { + "settings": { + "global": { "color": { "palette": [ { diff --git a/lib/experimental-i18n-theme.json b/lib/experimental-i18n-theme.json index 4731a59df0e97..cd8d4f2c76148 100644 --- a/lib/experimental-i18n-theme.json +++ b/lib/experimental-i18n-theme.json @@ -1,32 +1,18 @@ { "settings": { - "typography": { - "fontSizes": [ - "name" - ], - "fontStyles": [ - "name" - ], - "fontWeights": [ - "name" - ], - "fontFamilies": [ - "name" - ], - "textTransforms": [ - "name" - ], - "textDecorations": [ - "name" - ] - }, - "color": { - "palette": [ - "name" - ], - "gradients": [ - "name" - ] + "*": { + "typography": { + "fontSizes": [ "name" ], + "fontStyles": [ "name" ], + "fontWeights": [ "name" ], + "fontFamilies": [ "name" ], + "textTransforms": [ "name" ], + "textDecorations": [ "name" ] + }, + "color": { + "palette": [ "name" ], + "gradients": [ "name" ] + } } } } diff --git a/lib/global-styles.php b/lib/global-styles.php index 5aab9277715e4..0f717938ae078 100644 --- a/lib/global-styles.php +++ b/lib/global-styles.php @@ -23,59 +23,59 @@ function gutenberg_experimental_global_styles_has_theme_json_support() { */ function gutenberg_experimental_global_styles_get_theme_support_settings( $settings ) { $theme_settings = array(); - $theme_settings['global'] = array(); - $theme_settings['global']['settings'] = array(); + $theme_settings['settings'] = array(); + $theme_settings['settings']['global'] = array(); // Deprecated theme supports. if ( isset( $settings['disableCustomColors'] ) ) { - if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { - $theme_settings['global']['settings']['color'] = array(); + if ( ! isset( $theme_settings['settings']['global']['color'] ) ) { + $theme_settings['settings']['global']['color'] = array(); } - $theme_settings['global']['settings']['color']['custom'] = ! $settings['disableCustomColors']; + $theme_settings['settings']['global']['color']['custom'] = ! $settings['disableCustomColors']; } if ( isset( $settings['disableCustomGradients'] ) ) { - if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { - $theme_settings['global']['settings']['color'] = array(); + if ( ! isset( $theme_settings['settings']['global']['color'] ) ) { + $theme_settings['settings']['global']['color'] = array(); } - $theme_settings['global']['settings']['color']['customGradient'] = ! $settings['disableCustomGradients']; + $theme_settings['settings']['global']['color']['customGradient'] = ! $settings['disableCustomGradients']; } if ( isset( $settings['disableCustomFontSizes'] ) ) { - if ( ! isset( $theme_settings['global']['settings']['typography'] ) ) { - $theme_settings['global']['settings']['typography'] = array(); + if ( ! isset( $theme_settings['settings']['global']['typography'] ) ) { + $theme_settings['settings']['global']['typography'] = array(); } - $theme_settings['global']['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes']; + $theme_settings['settings']['global']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes']; } if ( isset( $settings['enableCustomLineHeight'] ) ) { - if ( ! isset( $theme_settings['global']['settings']['typography'] ) ) { - $theme_settings['global']['settings']['typography'] = array(); + if ( ! isset( $theme_settings['settings']['global']['typography'] ) ) { + $theme_settings['settings']['global']['typography'] = array(); } - $theme_settings['global']['settings']['typography']['customLineHeight'] = $settings['enableCustomLineHeight']; + $theme_settings['settings']['global']['typography']['customLineHeight'] = $settings['enableCustomLineHeight']; } if ( isset( $settings['enableCustomUnits'] ) ) { - if ( ! isset( $theme_settings['global']['settings']['spacing'] ) ) { - $theme_settings['global']['settings']['spacing'] = array(); + if ( ! isset( $theme_settings['settings']['global']['spacing'] ) ) { + $theme_settings['settings']['global']['spacing'] = array(); } - $theme_settings['global']['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? + $theme_settings['settings']['global']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? array( 'px', 'em', 'rem', 'vh', 'vw' ) : $settings['enableCustomUnits']; } if ( isset( $settings['colors'] ) ) { - if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { - $theme_settings['global']['settings']['color'] = array(); + if ( ! isset( $theme_settings['settings']['global']['color'] ) ) { + $theme_settings['settings']['global']['color'] = array(); } - $theme_settings['global']['settings']['color']['palette'] = $settings['colors']; + $theme_settings['settings']['global']['color']['palette'] = $settings['colors']; } if ( isset( $settings['gradients'] ) ) { - if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { - $theme_settings['global']['settings']['color'] = array(); + if ( ! isset( $theme_settings['settings']['global']['color'] ) ) { + $theme_settings['settings']['global']['color'] = array(); } - $theme_settings['global']['settings']['color']['gradients'] = $settings['gradients']; + $theme_settings['settings']['global']['color']['gradients'] = $settings['gradients']; } if ( isset( $settings['fontSizes'] ) ) { @@ -86,25 +86,25 @@ function gutenberg_experimental_global_styles_get_theme_support_settings( $setti $font_size['size'] = $font_size['size'] . 'px'; } } - if ( ! isset( $theme_settings['global']['settings']['typography'] ) ) { - $theme_settings['global']['settings']['typography'] = array(); + if ( ! isset( $theme_settings['settings']['global']['typography'] ) ) { + $theme_settings['settings']['global']['typography'] = array(); } - $theme_settings['global']['settings']['typography']['fontSizes'] = $font_sizes; + $theme_settings['settings']['global']['typography']['fontSizes'] = $font_sizes; } // Things that didn't land in core yet, so didn't have a setting assigned. if ( current( (array) get_theme_support( 'custom-spacing' ) ) ) { - if ( ! isset( $theme_settings['global']['settings']['spacing'] ) ) { - $theme_settings['global']['settings']['spacing'] = array(); + if ( ! isset( $theme_settings['settings']['global']['spacing'] ) ) { + $theme_settings['settings']['global']['spacing'] = array(); } - $theme_settings['global']['settings']['spacing']['customPadding'] = true; + $theme_settings['settings']['global']['spacing']['customPadding'] = true; } if ( current( (array) get_theme_support( 'experimental-link-color' ) ) ) { - if ( ! isset( $theme_settings['global']['settings']['color'] ) ) { - $theme_settings['global']['settings']['color'] = array(); + if ( ! isset( $theme_settings['settings']['global']['color'] ) ) { + $theme_settings['settings']['global']['color'] = array(); } - $theme_settings['global']['settings']['color']['link'] = true; + $theme_settings['settings']['global']['color']['link'] = true; } return $theme_settings; diff --git a/packages/edit-site/src/components/editor/global-styles-provider.js b/packages/edit-site/src/components/editor/global-styles-provider.js index c24a08964e6de..7b55d3b5a2505 100644 --- a/packages/edit-site/src/components/editor/global-styles-provider.js +++ b/packages/edit-site/src/components/editor/global-styles-provider.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { set, get, mapValues, mergeWith } from 'lodash'; +import { set, get, mergeWith } from 'lodash'; /** * WordPress dependencies @@ -142,6 +142,9 @@ export default function GlobalStylesProvider( { children, baseStyles } ) { if ( ! newUserStyles.isGlobalStylesUserThemeJSON ) { newUserStyles = EMPTY_CONTENT; } + // TODO: we probably want to check here that the shape is what we want + // This is, settings & styles are top-level keys, or perhaps a version. + // As to avoid merging trees that are different. const newMergedStyles = mergeWith( {}, baseStyles, @@ -159,32 +162,33 @@ export default function GlobalStylesProvider( { children, baseStyles } ) { () => ( { contexts, getSetting: ( context, path ) => - get( userStyles?.[ context ]?.settings, path ), + get( userStyles?.settings?.[ context ], path ), setSetting: ( context, path, newValue ) => { const newContent = { ...userStyles }; - let contextSettings = newContent?.[ context ]?.settings; + let contextSettings = newContent?.settings?.[ context ]; if ( ! contextSettings ) { contextSettings = {}; - set( newContent, [ context, 'settings' ], contextSettings ); + set( newContent, [ 'settings', context ], contextSettings ); } set( contextSettings, path, newValue ); setContent( JSON.stringify( newContent ) ); }, getStyle: ( context, propertyName, origin = 'merged' ) => { - const styles = 'user' === origin ? userStyles : mergedStyles; + const styleOrigin = + 'user' === origin ? userStyles : mergedStyles; const value = get( - styles?.[ context ]?.styles, + styleOrigin?.styles?.[ context ], STYLE_PROPERTY[ propertyName ].value ); return getValueFromVariable( mergedStyles, context, value ); }, setStyle: ( context, propertyName, newValue ) => { const newContent = { ...userStyles }; - let contextStyles = newContent?.[ context ]?.styles; + let contextStyles = newContent?.styles?.[ context ]; if ( ! contextStyles ) { contextStyles = {}; - set( newContent, [ context, 'styles' ], contextStyles ); + set( newContent, [ 'styles', context ], contextStyles ); } set( contextStyles, @@ -228,10 +232,7 @@ export default function GlobalStylesProvider( { children, baseStyles } ) { isGlobalStyles: true, }, ], - __experimentalFeatures: mapValues( - mergedStyles, - ( value ) => value?.settings || {} - ), + __experimentalFeatures: mergedStyles.settings, } ); }, [ contexts, mergedStyles ] ); diff --git a/packages/edit-site/src/components/editor/global-styles-renderer.js b/packages/edit-site/src/components/editor/global-styles-renderer.js index d2f538d9371f1..c00ab558af3af 100644 --- a/packages/edit-site/src/components/editor/global-styles-renderer.js +++ b/packages/edit-site/src/components/editor/global-styles-renderer.js @@ -146,9 +146,11 @@ export default ( blockData, tree, type = 'all' ) => { ( styles, { selector }, context ) => { if ( type === 'all' || type === 'cssVariables' ) { const variableDeclarations = [ - ...getBlockPresetsDeclarations( tree?.[ context ] ), + ...getBlockPresetsDeclarations( + tree?.settings?.[ context ] + ), ...flattenTree( - tree?.[ context ]?.settings?.custom, + tree?.settings?.[ context ]?.custom, '--wp--custom--', '--' ), @@ -165,7 +167,7 @@ export default ( blockData, tree, type = 'all' ) => { if ( type === 'all' || type === 'blockStyles' ) { const blockStyleDeclarations = getBlockStylesDeclarations( blockData[ context ].supports, - tree?.[ context ]?.styles + tree?.styles?.[ context ] ); if ( blockStyleDeclarations.length > 0 ) { @@ -178,7 +180,7 @@ export default ( blockData, tree, type = 'all' ) => { const presetClasses = getBlockPresetClasses( selector, - tree?.[ context ] + tree?.settings?.[ context ] ); if ( presetClasses ) { styles.push( presetClasses ); diff --git a/packages/edit-site/src/components/editor/utils.js b/packages/edit-site/src/components/editor/utils.js index 1bd54dc32d675..68bf1747cec40 100644 --- a/packages/edit-site/src/components/editor/utils.js +++ b/packages/edit-site/src/components/editor/utils.js @@ -26,7 +26,7 @@ export const GLOBAL_CONTEXT_SUPPORTS = [ export const PRESET_METADATA = [ { - path: [ 'settings', 'color', 'palette' ], + path: [ 'color', 'palette' ], valueKey: 'color', cssVarInfix: 'color', classes: [ @@ -38,7 +38,7 @@ export const PRESET_METADATA = [ ], }, { - path: [ 'settings', 'color', 'gradients' ], + path: [ 'color', 'gradients' ], valueKey: 'gradient', cssVarInfix: 'gradient', classes: [ @@ -49,13 +49,13 @@ export const PRESET_METADATA = [ ], }, { - path: [ 'settings', 'typography', 'fontSizes' ], + path: [ 'typography', 'fontSizes' ], valueKey: 'size', cssVarInfix: 'font-size', classes: [ { classSuffix: 'font-size', propertyName: 'font-size' } ], }, { - path: [ 'settings', 'typography', 'fontFamilies' ], + path: [ 'typography', 'fontFamilies' ], valueKey: 'fontFamily', cssVarInfix: 'font-family', classes: [], diff --git a/phpunit/class-wp-theme-json-legacy-settings-test.php b/phpunit/class-wp-theme-json-legacy-settings-test.php index addadeceeccb0..5124ee3004ef0 100644 --- a/phpunit/class-wp-theme-json-legacy-settings-test.php +++ b/phpunit/class-wp-theme-json-legacy-settings-test.php @@ -43,8 +43,8 @@ function get_editor_settings_no_theme_support() { function test_legacy_settings_blank() { $input = array(); $expected = array( - 'global' => array( - 'settings' => array(), + 'settings' => array( + 'global' => array(), ), ); @@ -56,8 +56,8 @@ function test_legacy_settings_blank() { function test_legacy_settings_no_theme_support() { $input = $this->get_editor_settings_no_theme_support(); $expected = array( - 'global' => array( - 'settings' => array( + 'settings' => array( + 'global' => array( 'color' => array( 'custom' => true, 'customGradient' => true, @@ -88,7 +88,7 @@ function test_legacy_settings_custom_units_can_be_disabled() { $actual = gutenberg_experimental_global_styles_get_theme_support_settings( $input ); - $this->assertEqualSetsWithIndex( $expected, $actual['global']['settings']['spacing'] ); + $this->assertEqualSetsWithIndex( $expected, $actual['settings']['global']['spacing'] ); } function test_legacy_settings_custom_units_can_be_enabled() { @@ -101,7 +101,7 @@ function test_legacy_settings_custom_units_can_be_enabled() { $actual = gutenberg_experimental_global_styles_get_theme_support_settings( $input ); - $this->assertEqualSetsWithIndex( $expected, $actual['global']['settings']['spacing'] ); + $this->assertEqualSetsWithIndex( $expected, $actual['settings']['global']['spacing'] ); } function test_legacy_settings_custom_units_can_be_filtered() { @@ -114,7 +114,7 @@ function test_legacy_settings_custom_units_can_be_filtered() { $actual = gutenberg_experimental_global_styles_get_theme_support_settings( $input ); - $this->assertEqualSetsWithIndex( $expected, $actual['global']['settings']['spacing'] ); + $this->assertEqualSetsWithIndex( $expected, $actual['settings']['global']['spacing'] ); } function test_legacy_settings_filled() { @@ -148,8 +148,8 @@ function test_legacy_settings_filled() { ); $expected = array( - 'global' => array( - 'settings' => array( + 'settings' => array( + 'global' => array( 'color' => array( 'custom' => false, 'customGradient' => false, diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 057f340cfca7c..10e567923c4e2 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -8,20 +8,30 @@ class WP_Theme_JSON_Test extends WP_UnitTestCase { - function test_contexts_not_valid_are_skipped() { + function test_schema_validation_subtree_is_removed_if_key_invalid() { $theme_json = new WP_Theme_JSON( array( - 'global' => array( - 'settings' => array( + 'invalid/key' => 'content', + 'styles' => array( + 'invalid/key' => array( 'color' => array( 'custom' => 'false', ), ), - ), - 'core/invalid' => array( - 'settings' => array( - 'color' => array( - 'custom' => 'false', + 'core/group' => array( + 'invalid/key' => array( + 'custom' => false, + 'background' => 'red', + ), + 'color' => array( + 'invalid/key' => true, + 'background' => 'red', + ), + 'spacing' => array( + 'padding' => array( + 'invalid/key' => false, + 'top' => '10px', + ), ), ), ), @@ -30,10 +40,15 @@ function test_contexts_not_valid_are_skipped() { $result = $theme_json->get_raw_data(); $expected = array( - 'global' => array( - 'settings' => array( - 'color' => array( - 'custom' => 'false', + 'styles' => array( + 'core/group' => array( + 'color' => array( + 'background' => 'red', + ), + 'spacing' => array( + 'padding' => array( + 'top' => '10px', + ), ), ), ), @@ -42,44 +57,84 @@ function test_contexts_not_valid_are_skipped() { $this->assertEqualSetsWithIndex( $expected, $result ); } - function test_properties_not_valid_are_skipped() { + function test_schema_validation_subtree_is_removed_if_not_array() { $theme_json = new WP_Theme_JSON( array( - 'global' => array( - 'invalidKey' => 'invalid value', - 'settings' => array( - 'color' => array( - 'custom' => 'false', - 'invalidKey' => 'invalid value', - ), - 'invalidSection' => array( - 'invalidKey' => 'invalid value', - ), + 'settings' => 'invalid/not/array', + 'styles' => array( + 'global' => 'invalid/not/array', + 'core/paragraph' => array( + 'invalid/not/array' => false, ), - 'styles' => array( - 'typography' => array( - 'fontSize' => '12', - 'invalidProperty' => 'invalid value', + 'core/group' => array( + 'invalid/not/array' => false, + 'color' => array( + 'link' => 'pink', ), - 'invalidSection' => array( - 'invalidProperty' => 'invalid value', + 'typography' => array( + 'invalid/key' => false, + ), + 'spacing' => array( + 'padding' => array( + 'invalid/key' => '10px', + ), ), ), ), ) ); - $result = $theme_json->get_raw_data(); + $actual = $theme_json->get_raw_data(); $expected = array( - 'global' => array( - 'settings' => array( + 'styles' => array( + 'core/group' => array( 'color' => array( - 'custom' => 'false', + 'link' => 'pink', + ), + ), + ), + ); + + $this->assertEqualSetsWithIndex( $expected, $actual ); + } + + function test_schema_validation_subtree_is_removed_if_empty() { + $theme_json = new WP_Theme_JSON( + array( + 'settings' => array( + 'invalid/key' => array( + 'color' => array( + 'custom' => false, + ), + ), + 'global' => array( + 'invalid/key' => false, ), ), 'styles' => array( - 'typography' => array( - 'fontSize' => '12', + 'global' => array( + 'color' => array( + 'link' => 'blue', + ), + 'typography' => array( + 'invalid/key' => false, + ), + 'spacing' => array( + 'padding' => array( + 'invalid/key' => '10px', + ), + ), + ), + ), + ) + ); + $result = $theme_json->get_raw_data(); + + $expected = array( + 'styles' => array( + 'global' => array( + 'color' => array( + 'link' => 'blue', ), ), ), @@ -88,44 +143,81 @@ function test_properties_not_valid_are_skipped() { $this->assertEqualSetsWithIndex( $expected, $result ); } + function test_schema_validation_subtree_is_removed_if_style_not_supported_by_block() { + $theme_json = new WP_Theme_JSON( + array( + 'styles' => array( + 'global' => array( + 'color' => array( + 'text' => 'var:preset|color|dark-gray', + ), + 'spacing' => array( + 'padding' => array( + 'top' => '1px', + 'right' => '1px', + 'bottom' => '1px', + 'left' => '1px', + ), + ), + ), + ), + ) + ); + + $actual = $theme_json->get_raw_data(); + $expected = array( + 'styles' => array( + 'global' => array( + 'color' => array( + 'text' => 'var:preset|color|dark-gray', + ), + ), + ), + ); + $this->assertEqualSetsWithIndex( $expected, $actual ); + } + function test_get_settings() { // See schema at WP_Theme_JSON::SCHEMA. $theme_json = new WP_Theme_JSON( array( - 'global' => array( - 'settings' => array( - 'color' => array( - 'link' => 'value', + 'settings' => array( + 'global' => array( + 'color' => array( + 'custom' => false, ), - 'custom' => 'value', - 'typography' => 'value', - 'misc' => 'value', + 'invalid/key' => 'value', ), - 'styles' => array( - 'color' => 'value', - 'misc' => 'value', + ), + 'styles' => array( + 'global' => array( + 'color' => array( + 'link' => 'blue', + ), ), - 'misc' => 'value', ), ) ); $result = $theme_json->get_settings(); - $this->assertArrayHasKey( 'global', $result ); - $this->assertCount( 1, $result ); + $expected = array( + 'global' => array( + 'color' => array( + 'custom' => false, + ), + ), + ); - $this->assertArrayHasKey( 'color', $result['global'] ); - $this->assertArrayHasKey( 'custom', $result['global'] ); - $this->assertCount( 2, $result['global'] ); + $this->assertEqualSetsWithIndex( $expected, $result ); } function test_get_stylesheet() { // See schema at WP_Theme_JSON::SCHEMA. $theme_json = new WP_Theme_JSON( array( - 'global' => array( - 'settings' => array( + 'settings' => array( + 'global' => array( 'color' => array( 'text' => 'value', 'palette' => array( @@ -149,17 +241,26 @@ function test_get_stylesheet() { ), 'misc' => 'value', ), - 'styles' => array( + 'core/group' => array( + 'custom' => array( + 'base-font' => 16, + 'line-height' => array( + 'small' => 1.2, + 'medium' => 1.4, + 'large' => 1.8, + ), + ), + ), + ), + 'styles' => array( + 'global' => array( 'color' => array( 'link' => '#111', 'text' => 'var:preset|color|grey', ), 'misc' => 'value', ), - 'misc' => 'value', - ), - 'core/group' => array( - 'styles' => array( + 'core/group' => array( 'spacing' => array( 'padding' => array( 'top' => '12px', @@ -168,11 +269,12 @@ function test_get_stylesheet() { ), ), ), + 'misc' => 'value', ) ); $this->assertEquals( - ':root{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}:root{--wp--style--color--link: #111;color: var(--wp--preset--color--grey);}.has-grey-color{color: grey;}.has-grey-background-color{background-color: grey;}.wp-block-group{padding-top: 12px;padding-bottom: 24px;}', + ':root{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}:root{--wp--style--color--link: #111;color: var(--wp--preset--color--grey);}.has-grey-color{color: grey;}.has-grey-background-color{background-color: grey;}.wp-block-group{padding-top: 12px;padding-bottom: 24px;}', $theme_json->get_stylesheet() ); $this->assertEquals( @@ -180,57 +282,58 @@ function test_get_stylesheet() { $theme_json->get_stylesheet( 'block_styles' ) ); $this->assertEquals( - ':root{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}', + ':root{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}', $theme_json->get_stylesheet( 'css_variables' ) ); } public function test_merge_incoming_data() { $initial = array( - 'global' => array( - 'settings' => array( + 'settings' => array( + 'global' => array( 'color' => array( - 'custom' => 'false', + 'custom' => false, 'palette' => array( array( 'slug' => 'red', 'color' => 'red', ), array( - 'slug' => 'blue', - 'color' => 'blue', + 'slug' => 'green', + 'color' => 'green', ), ), ), ), - 'styles' => array( - 'typography' => array( - 'fontSize' => '12', + 'core/paragraph' => array( + 'color' => array( + 'custom' => false, ), ), ), - 'core/paragraph' => array( - 'settings' => array( - 'color' => array( - 'custom' => 'false', + 'styles' => array( + 'global' => array( + 'typography' => array( + 'fontSize' => '12', ), ), ), ); - $add_new_context = array( - 'core/list' => array( - 'settings' => array( + $add_new_block = array( + 'settings' => array( + 'core/list' => array( 'color' => array( - 'custom' => 'false', + 'custom' => false, ), ), - 'styles' => array( + ), + 'styles' => array( + 'core/list' => array( 'typography' => array( 'fontSize' => '12', ), 'color' => array( - 'link' => 'pink', 'background' => 'brown', ), ), @@ -238,51 +341,52 @@ public function test_merge_incoming_data() { ); $add_key_in_settings = array( - 'global' => array( - 'settings' => array( + 'settings' => array( + 'global' => array( 'color' => array( - 'customGradient' => 'true', + 'customGradient' => true, ), ), ), ); $update_key_in_settings = array( - 'global' => array( - 'settings' => array( + 'settings' => array( + 'global' => array( 'color' => array( - 'custom' => 'true', + 'custom' => true, ), ), ), ); $add_styles = array( - 'core/paragraph' => array( - 'styles' => array( - 'typography' => array( - 'fontSize' => '12', - ), - 'color' => array( - 'link' => 'pink', + 'styles' => array( + 'core/group' => array( + 'spacing' => array( + 'padding' => array( + 'top' => '12px', + ), ), ), ), ); $add_key_in_styles = array( - 'core/paragraph' => array( - 'styles' => array( - 'typography' => array( - 'lineHeight' => '12', + 'styles' => array( + 'core/group' => array( + 'spacing' => array( + 'padding' => array( + 'bottom' => '12px', + ), ), ), ), ); $add_invalid_context = array( - 'core/para' => array( - 'styles' => array( + 'styles' => array( + 'core/para' => array( 'typography' => array( 'lineHeight' => '12', ), @@ -291,13 +395,13 @@ public function test_merge_incoming_data() { ); $update_presets = array( - 'global' => array( - 'settings' => array( + 'settings' => array( + 'global' => array( 'color' => array( 'palette' => array( array( - 'slug' => 'color', - 'color' => 'color', + 'slug' => 'blue', + 'color' => 'blue', ), ), 'gradients' => array( @@ -326,15 +430,15 @@ public function test_merge_incoming_data() { ); $expected = array( - 'global' => array( - 'settings' => array( + 'settings' => array( + 'global' => array( 'color' => array( - 'custom' => 'true', - 'customGradient' => 'true', + 'custom' => true, + 'customGradient' => true, 'palette' => array( array( - 'slug' => 'color', - 'color' => 'color', + 'slug' => 'blue', + 'color' => 'blue', ), ), 'gradients' => array( @@ -359,40 +463,36 @@ public function test_merge_incoming_data() { ), ), ), - 'styles' => array( - 'typography' => array( - 'fontSize' => '12', + 'core/paragraph' => array( + 'color' => array( + 'custom' => false, ), ), - ), - 'core/paragraph' => array( - 'settings' => array( + 'core/list' => array( 'color' => array( - 'custom' => 'false', + 'custom' => false, ), ), - 'styles' => array( + ), + 'styles' => array( + 'global' => array( 'typography' => array( - 'fontSize' => '12', - 'lineHeight' => '12', - ), - 'color' => array( - 'link' => 'pink', + 'fontSize' => '12', ), ), - ), - 'core/list' => array( - 'settings' => array( - 'color' => array( - 'custom' => 'false', + 'core/group' => array( + 'spacing' => array( + 'padding' => array( + 'top' => '12px', + 'bottom' => '12px', + ), ), ), - 'styles' => array( + 'core/list' => array( 'typography' => array( 'fontSize' => '12', ), 'color' => array( - 'link' => 'pink', 'background' => 'brown', ), ), @@ -400,7 +500,7 @@ public function test_merge_incoming_data() { ); $theme_json = new WP_Theme_JSON( $initial ); - $theme_json->merge( new WP_Theme_JSON( $add_new_context ) ); + $theme_json->merge( new WP_Theme_JSON( $add_new_block ) ); $theme_json->merge( new WP_Theme_JSON( $add_key_in_settings ) ); $theme_json->merge( new WP_Theme_JSON( $update_key_in_settings ) ); $theme_json->merge( new WP_Theme_JSON( $add_styles ) ); @@ -412,86 +512,17 @@ public function test_merge_incoming_data() { $this->assertEqualSetsWithIndex( $expected, $result ); } - function test_remove_insecure_properties_removes_invalid_contexts() { + function test_remove_insecure_properties_removes_unsafe_styles() { $theme_json = new WP_Theme_JSON( array( - 'global' => array( - 'styles' => array( - 'color' => array( - 'background' => 'green', - 'text' => 'var:preset|color|dark-gray', - ), - ), - ), - '.my-class' => array( - 'styles' => array( - 'color' => array( - 'background' => 'green', - 'text' => 'var:preset|color|dark-gray', - ), - ), - ), - ), - true - ); - $theme_json->remove_insecure_properties(); - $result = $theme_json->get_raw_data(); - $expected = array( - 'global' => array( 'styles' => array( - 'color' => array( - 'background' => 'green', - 'text' => 'var:preset|color|dark-gray', - ), - ), - ), - ); - $this->assertEqualSetsWithIndex( $expected, $result ); - } - - function test_remove_insecure_properties_removes_invalid_properties() { - $theme_json = new WP_Theme_JSON( - array( - 'global' => array( - 'styles' => array( - 'color' => array( - 'gradient' => 'linear-gradient(55deg,rgba(6,147,227,1) 0%,rgb(84,177,218) 54%,rgb(155,81,224) 100%)', - 'text' => 'var:preset|color|dark-gray', - ), - ), - 'invalid' => array( - 'background' => 'green', - ), - ), - ), - true - ); - $theme_json->remove_insecure_properties(); - $result = $theme_json->get_raw_data(); - $expected = array( - 'global' => array( - 'styles' => array( - 'color' => array( - 'gradient' => 'linear-gradient(55deg,rgba(6,147,227,1) 0%,rgb(84,177,218) 54%,rgb(155,81,224) 100%)', - 'text' => 'var:preset|color|dark-gray', - ), - ), - ), - ); - $this->assertEqualSetsWithIndex( $expected, $result ); - } - - function test_remove_insecure_properties_removes_unsafe_properties() { - $theme_json = new WP_Theme_JSON( - array( - 'global' => array( - 'styles' => array( + 'core/group' => array( 'color' => array( 'gradient' => 'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+PHNjcmlwdD5hbGVydCgnb2snKTwvc2NyaXB0PjxsaW5lYXJHcmFkaWVudCBpZD0nZ3JhZGllbnQnPjxzdG9wIG9mZnNldD0nMTAlJyBzdG9wLWNvbG9yPScjRjAwJy8+PHN0b3Agb2Zmc2V0PSc5MCUnIHN0b3AtY29sb3I9JyNmY2MnLz4gPC9saW5lYXJHcmFkaWVudD48cmVjdCBmaWxsPSd1cmwoI2dyYWRpZW50KScgeD0nMCcgeT0nMCcgd2lkdGg9JzEwMCUnIGhlaWdodD0nMTAwJScvPjwvc3ZnPg==\')', 'text' => 'var:preset|color|dark-gray', ), ), - 'invalid' => array( + 'invalid/key' => array( 'background' => 'green', ), ), @@ -501,84 +532,22 @@ function test_remove_insecure_properties_removes_unsafe_properties() { $theme_json->remove_insecure_properties(); $result = $theme_json->get_raw_data(); $expected = array( - 'global' => array( - 'styles' => array( - 'color' => array( - 'text' => 'var:preset|color|dark-gray', - ), - ), - ), - ); - $this->assertEqualSetsWithIndex( $expected, $result ); - } - - function test_remove_insecure_properties_removes_properties_when_not_allowed_in_a_context() { - $theme_json = new WP_Theme_JSON( - array( - 'global' => array( - 'styles' => array( - 'color' => array( - 'text' => 'var:preset|color|dark-gray', - ), - 'spacing' => array( - 'padding' => array( - 'top' => '1px', - 'right' => '1px', - 'bottom' => '1px', - 'left' => '1px', - ), - ), - ), - 'invalid' => array( - 'background' => 'green', - ), - ), + 'styles' => array( 'core/group' => array( - 'styles' => array( - 'spacing' => array( - 'padding' => array( - 'top' => '1px', - 'right' => '1px', - 'bottom' => '1px', - 'left' => '1px', - ), - ), - ), - ), - ), - true - ); - $theme_json->remove_insecure_properties(); - $result = $theme_json->get_raw_data(); - $expected = array( - 'global' => array( - 'styles' => array( 'color' => array( 'text' => 'var:preset|color|dark-gray', ), ), ), - 'core/group' => array( - 'styles' => array( - 'spacing' => array( - 'padding' => array( - 'top' => '1px', - 'right' => '1px', - 'bottom' => '1px', - 'left' => '1px', - ), - ), - ), - ), ); $this->assertEqualSetsWithIndex( $expected, $result ); } - function test_remove_insecure_properties_removes_unsafe_sub_properties() { + function test_remove_insecure_properties_removes_unsafe_styles_sub_properties() { $theme_json = new WP_Theme_JSON( array( - 'core/group' => array( - 'styles' => array( + 'styles' => array( + 'core/group' => array( 'spacing' => array( 'padding' => array( 'top' => '1px', @@ -595,8 +564,8 @@ function test_remove_insecure_properties_removes_unsafe_sub_properties() { $theme_json->remove_insecure_properties(); $result = $theme_json->get_raw_data(); $expected = array( - 'core/group' => array( - 'styles' => array( + 'styles' => array( + 'core/group' => array( 'spacing' => array( 'padding' => array( 'top' => '1px', @@ -613,8 +582,8 @@ function test_remove_insecure_properties_removes_unsafe_sub_properties() { function test_remove_insecure_properties_removes_non_preset_settings() { $theme_json = new WP_Theme_JSON( array( - 'global' => array( - 'settings' => array( + 'settings' => array( + 'global' => array( 'color' => array( 'custom' => true, 'palette' => array( @@ -646,8 +615,8 @@ function test_remove_insecure_properties_removes_non_preset_settings() { $theme_json->remove_insecure_properties(); $result = $theme_json->get_raw_data(); $expected = array( - 'global' => array( - 'settings' => array( + 'settings' => array( + 'global' => array( 'color' => array( 'palette' => array( array( @@ -676,8 +645,8 @@ function test_remove_insecure_properties_removes_non_preset_settings() { function test_remove_insecure_properties_removes_unsafe_preset_settings() { $theme_json = new WP_Theme_JSON( array( - 'global' => array( - 'settings' => array( + 'settings' => array( + 'global' => array( 'color' => array( 'palette' => array( array( @@ -734,8 +703,8 @@ function test_remove_insecure_properties_removes_unsafe_preset_settings() { $theme_json->remove_insecure_properties(); $result = $theme_json->get_raw_data(); $expected = array( - 'global' => array( - 'settings' => array( + 'settings' => array( + 'global' => array( 'color' => array( 'palette' => array( array(