Editor: Add functionality required for theme export in the site editor

This bring across changes to theme export functionality, and related code, and tests. Relates issue in Gutenberg: https://github.com/WordPress/gutenberg/issues/39889.

Props scruffian, timothyblynjacobs, oandregal, ajlende, zieleadam.
See #55505.


Built from https://develop.svn.wordpress.org/trunk@53129


git-svn-id: http://core.svn.wordpress.org/trunk@52718 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
gziolo
2022-04-11 10:38:00 +00:00
parent 5d78798b5c
commit 9e3c5a4215
6 changed files with 377 additions and 69 deletions

View File

@@ -74,41 +74,49 @@ class WP_Theme_JSON {
*
* This contains the necessary metadata to process them:
*
* - path => where to find the preset within the settings section
* - override => whether a theme preset with the same slug as a default preset
* can override it
* - 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.
* - 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' ),
'override' => array( 'color', 'defaultPalette' ),
'prevent_override' => array( 'color', 'defaultPalette' ),
'use_default_names' => false,
'value_key' => 'color',
'css_vars' => '--wp--preset--color--$slug',
@@ -121,7 +129,7 @@ class WP_Theme_JSON {
),
array(
'path' => array( 'color', 'gradients' ),
'override' => array( 'color', 'defaultGradients' ),
'prevent_override' => array( 'color', 'defaultGradients' ),
'use_default_names' => false,
'value_key' => 'gradient',
'css_vars' => '--wp--preset--gradient--$slug',
@@ -130,7 +138,7 @@ class WP_Theme_JSON {
),
array(
'path' => array( 'color', 'duotone' ),
'override' => true,
'prevent_override' => array( 'color', 'defaultDuotone' ),
'use_default_names' => false,
'value_func' => 'wp_get_duotone_filter_property',
'css_vars' => '--wp--preset--duotone--$slug',
@@ -139,7 +147,7 @@ class WP_Theme_JSON {
),
array(
'path' => array( 'typography', 'fontSizes' ),
'override' => true,
'prevent_override' => false,
'use_default_names' => true,
'value_key' => 'size',
'css_vars' => '--wp--preset--font-size--$slug',
@@ -148,7 +156,7 @@ class WP_Theme_JSON {
),
array(
'path' => array( 'typography', 'fontFamilies' ),
'override' => true,
'prevent_override' => false,
'use_default_names' => false,
'value_key' => 'fontFamily',
'css_vars' => '--wp--preset--font-family--$slug',
@@ -229,10 +237,12 @@ class WP_Theme_JSON {
*/
const VALID_TOP_LEVEL_KEYS = array(
'customTemplates',
'patterns',
'settings',
'styles',
'templateParts',
'version',
'title',
);
/**
@@ -242,6 +252,7 @@ class WP_Theme_JSON {
* @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(
@@ -257,6 +268,7 @@ class WP_Theme_JSON {
'custom' => null,
'customDuotone' => null,
'customGradient' => null,
'defaultDuotone' => null,
'defaultGradients' => null,
'defaultPalette' => null,
'duotone' => null,
@@ -347,6 +359,24 @@ class WP_Theme_JSON {
'h6' => 'h6',
);
/**
* 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.
*
@@ -429,19 +459,7 @@ class WP_Theme_JSON {
* @param array $context The context to which the settings belong.
*/
protected static function do_opt_in_into_settings( &$context ) {
$to_opt_in = 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' ),
);
foreach ( $to_opt_in as $path ) {
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' ) ) {
@@ -830,8 +848,8 @@ class WP_Theme_JSON {
$has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
if ( $has_block_gap_support ) {
$block_rules .= '.wp-site-blocks > * { margin-top: 0; margin-bottom: 0; }';
$block_rules .= '.wp-site-blocks > * + * { margin-top: var( --wp--style--block-gap ); }';
$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 ); }';
}
}
}
@@ -1501,6 +1519,45 @@ class WP_Theme_JSON {
return $nodes;
}
/**
* 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.
* @return boolean
*/
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.
*
@@ -1550,7 +1607,7 @@ class WP_Theme_JSON {
// Replace the presets.
foreach ( static::PRESETS_METADATA as $preset ) {
$override_preset = static::should_override_preset( $this->theme_json, $node['path'], $preset['override'] );
$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'] );
@@ -1624,13 +1681,16 @@ class WP_Theme_JSON {
* 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.
* @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;
}
@@ -2013,4 +2073,151 @@ class WP_Theme_JSON {
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;
}
}