diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 3b4f2b5a9bd4b..6d6027fb84e96 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -25,19 +25,92 @@ function gutenberg_register_layout_support( $block_type ) { } } +function gutenberg_get_layout_preset_styles( $preset_metadata, $presets ) { + $style_engine = WP_Style_Engine_Gutenberg::get_instance(); + $style_engine->reset(); + + foreach ( $presets as $key => $preset ) { + if ( ! empty( $preset['type'] ) && ! empty( $preset['styles'] ) ) { + $slug = ! empty( $preset['slug'] ) ? $preset['slug'] : $preset['type']; + $base_class = 'wp-layout-' . sanitize_title( $slug ); + + if ( ! empty( $preset['styles'] ) ) { + foreach ( $preset['styles'] as $style ) { + if ( ! empty( $style['rules'] ) && is_array( $style['rules'] ) ) { + $args = array( + 'selector' => ! empty( $style['selector'] ) ? $style['selector'] : null, + 'suffix' => ! empty( $style['suffix'] ) ? sanitize_title( $style['suffix'] ) : null, + 'rules' => $style['rules'], + ); + $style_engine->add_style( $base_class, $args ); + } + } + } + + if ( ! empty( $preset['controlledSets'] ) ) { + foreach ( $preset['controlledSets'] as $controlled_set ) { + $property = ! empty( $controlled_set['property'] ) ? sanitize_title( $controlled_set['property'] ) : null; + $suffix = ! empty( $controlled_set['suffix'] ) ? sanitize_title( $controlled_set['suffix'] ) : null; + + if ( $property && $suffix && ! empty( $controlled_set['options'] ) ) { + foreach( $controlled_set['options'] as $option_setting => $option_value ) { + $args = array( + 'suffix' => array( $suffix, sanitize_title( $option_setting ) ), + 'rules' => array( $property => $option_value ), + ); + $style_engine->add_style( $base_class, $args ); + } + } + } + } + } + } + + return $style_engine->get_generated_styles(); +} + /** - * Generates the CSS corresponding to the provided layout. + * Generates the CSS and classes corresponding to the provided layout. * * @param string $selector CSS selector. * @param array $layout Layout object. The one that is passed has already checked the existence of default block layout. * @param boolean $has_block_gap_support Whether the theme has support for the block gap. * @param string $gap_value The block gap value to apply. * - * @return string CSS style. + * @return array CSS style and CSS classes. */ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support = false, $gap_value = null ) { $layout_type = isset( $layout['type'] ) ? $layout['type'] : 'default'; + // Generate classes for preset styles. + $classes = array(); + if ( 'default' === $layout_type ) { + $classes[] = 'wp-layout-flow'; + if ( $has_block_gap_support ) { + $classes[] = 'wp-layout-flow--global-gap'; + } + } else if ( 'flex' === $layout_type ) { + $classes[] = 'wp-layout-flex'; + + $layout_orientation = isset( $layout['orientation'] ) ? $layout['orientation'] : 'horizontal'; + $orientation_key = 'horizontal' === $layout_orientation ? 'h-justify' : 'v-justify'; + + $classes[] = "wp-layout-flex--$orientation_key"; + + if ( ! empty( $layout['flexWrap'] ) ) { + $classes[] = 'wp-layout-flex--wrap--' . sanitize_key( $layout['flexWrap'] ); + } + + if ( ! empty( $layout['justifyContent'] ) ) { + $classes[] = sprintf( + 'wp-layout-flex--%s--%s', + $orientation_key, + sanitize_key( $layout['justifyContent'] ) + ); + } + } + + // Generate one-off style values. $style = ''; if ( 'default' === $layout_type ) { $content_size = isset( $layout['contentSize'] ) ? $layout['contentSize'] : ''; @@ -59,68 +132,25 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support $style .= '}'; $style .= "$selector > .alignwide { max-width: " . esc_html( $wide_max_width_value ) . ';}'; - $style .= "$selector .alignfull { max-width: none; }"; } - $style .= "$selector > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }"; - $style .= "$selector > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }"; - $style .= "$selector > .aligncenter { margin-left: auto !important; margin-right: auto !important; }"; if ( $has_block_gap_support ) { $gap_style = $gap_value ? $gap_value : 'var( --wp--style--block-gap )'; - $style .= "$selector > * { margin-block-start: 0; margin-block-end: 0; }"; $style .= "$selector > * + * { margin-block-start: $gap_style; margin-block-end: 0; }"; } } elseif ( 'flex' === $layout_type ) { - $layout_orientation = isset( $layout['orientation'] ) ? $layout['orientation'] : 'horizontal'; - - $justify_content_options = array( - 'left' => 'flex-start', - 'right' => 'flex-end', - 'center' => 'center', - ); - - if ( 'horizontal' === $layout_orientation ) { - $justify_content_options += array( 'space-between' => 'space-between' ); - } - - $flex_wrap_options = array( 'wrap', 'nowrap' ); - $flex_wrap = ! empty( $layout['flexWrap'] ) && in_array( $layout['flexWrap'], $flex_wrap_options, true ) ? - $layout['flexWrap'] : - 'wrap'; - - $style = "$selector {"; - $style .= 'display: flex;'; if ( $has_block_gap_support ) { + $style = "$selector {"; $gap_style = $gap_value ? $gap_value : 'var( --wp--style--block-gap, 0.5em )'; $style .= "gap: $gap_style;"; - } else { - $style .= 'gap: 0.5em;'; - } - $style .= "flex-wrap: $flex_wrap;"; - if ( 'horizontal' === $layout_orientation ) { - $style .= 'align-items: center;'; - /** - * Add this style only if is not empty for backwards compatibility, - * since we intend to convert blocks that had flex layout implemented - * by custom css. - */ - if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) { - $style .= "justify-content: {$justify_content_options[ $layout['justifyContent'] ]};"; - } - } else { - $style .= 'flex-direction: column;'; - if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) { - $style .= "align-items: {$justify_content_options[ $layout['justifyContent'] ]};"; - } else { - $style .= 'align-items: flex-start;'; - } + $style .= '}'; } - $style .= '}'; - - $style .= "$selector > * { margin: 0; }"; } - return $style; + return array( + 'style' => $style, + 'classes' => $classes + ); } /** @@ -156,17 +186,17 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { // Regex for CSS value borrowed from `safecss_filter_attr`, and used here // because we only want to match against the value, not the CSS attribute. $gap_value = preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value; - $style = gutenberg_get_layout_style( ".$class_name", $used_layout, $has_block_gap_support, $gap_value ); + $styles = gutenberg_get_layout_style( ".$class_name", $used_layout, $has_block_gap_support, $gap_value ); // This assumes the hook only applies to blocks with a single wrapper. // I think this is a reasonable limitation for that particular hook. $content = preg_replace( '/' . preg_quote( 'class="', '/' ) . '/', - 'class="' . esc_attr( $class_name ) . ' ', + 'class="' . esc_attr( implode( ' ', array_merge( array( $class_name ), $styles['classes'] ) ) ) . ' ', $block_content, 1 ); - gutenberg_enqueue_block_support_styles( $style ); + gutenberg_enqueue_block_support_styles( $styles['style'] ); return $content; } diff --git a/lib/class-wp-style-engine-gutenberg.php b/lib/class-wp-style-engine-gutenberg.php new file mode 100644 index 0000000000000..bde156028a8e6 --- /dev/null +++ b/lib/class-wp-style-engine-gutenberg.php @@ -0,0 +1,116 @@ +registered_styles = array(); + } + + public function get_generated_styles() { + $output = ''; + foreach ( $this->registered_styles as $selector => $rules ) { + $output .= "{$selector} {\n"; + + if ( is_string( $rules ) ) { + $output .= ' '; + $output .= $rules; + } else { + foreach ( $rules as $rule => $value ) { + $output .= " {$rule}: {$value};\n"; + } + } + $output .= "}\n"; + } + return $output; + } + + /** + * Stores style rules for a given CSS selector (the key) and returns an associated classname. + * + * @param string $key A class name used to construct a key. + * @param array $options An array of options, rules, and selector for constructing the rules. + * + * @return string The class name for the added style. + */ + public function add_style( $key, $options ) { + $suffix = ! empty( $options['suffix'] ) && is_array( $options['suffix'] ) ? implode( '--', $options['suffix'] ) : $options['suffix']; + $class = ! empty( $suffix ) ? $key . '--' . sanitize_key( $suffix ) : $key; + $selector = ! empty( $options['selector'] ) ? ' ' . trim( $options['selector'] ) : ''; + $rules = ! empty( $options['rules'] ) ? $options['rules'] : array(); + $prefix = ! empty( $options['prefix'] ) ? $options['prefix'] : '.'; + + if ( ! $class ) { + return; + } + + $this->registered_styles[ $prefix . $class . $selector ] = $rules; + + return $class; + } + + /** + * Render registered styles as key { ...rules } for final output. + */ + public function output_styles() { + $output = $this->get_generated_styles(); + echo "\n"; + } +} diff --git a/lib/compat/wordpress-5.9/class-wp-theme-json-5-9.php b/lib/compat/wordpress-5.9/class-wp-theme-json-5-9.php index 5ad476b7d13bb..e004058afc1a3 100644 --- a/lib/compat/wordpress-5.9/class-wp-theme-json-5-9.php +++ b/lib/compat/wordpress-5.9/class-wp-theme-json-5-9.php @@ -948,6 +948,20 @@ protected static function compute_preset_classes( $settings, $selector, $origins $stylesheet = ''; foreach ( static::PRESETS_METADATA as $preset_metadata ) { + if ( ! empty( $preset_metadata['path'] ) && 'layout' === $preset_metadata['path'][0] ) { + + $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); + + if ( ! empty( $preset_per_origin ) ) { + if ( isset( $preset_metadata['value_func'] ) && + is_callable( $preset_metadata['value_func'] ) + ) { + $value_func = $preset_metadata['value_func']; + $stylesheet .= call_user_func( $value_func, $preset_metadata, $preset_per_origin ); + continue; + } + } + } $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins ); foreach ( $preset_metadata['classes'] as $class => $property ) { foreach ( $slugs as $slug ) { @@ -1135,6 +1149,9 @@ protected static function replace_slug_in_string( $input, $slug ) { protected static function compute_preset_vars( $settings, $origins ) { $declarations = array(); foreach ( static::PRESETS_METADATA as $preset_metadata ) { + if ( isset( $preset_metadata['type'] ) && 'layout' === $preset_metadata['type'] ) { + continue; + } $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins ); foreach ( $values_by_slug as $slug => $value ) { $declarations[] = array( diff --git a/lib/compat/wordpress-5.9/theme.json b/lib/compat/wordpress-5.9/theme.json index 7691aa4a64e6a..c683e226ff458 100644 --- a/lib/compat/wordpress-5.9/theme.json +++ b/lib/compat/wordpress-5.9/theme.json @@ -185,6 +185,121 @@ ], "text": true }, + "layout": { + "types": { + "flow": { + "slug": "flow", + "type": "flow", + "styles": [ + { + "selector": "> .alignleft", + "rules": { + "float": "left", + "margin-inline-end": "2em", + "margin-inline-start": "0" + } + }, + { + "selector": "> .alignright", + "rules": { + "float": "right", + "margin-inline-start": "2em", + "margin-inline-end": "0" + } + }, + { + "selector": "> .aligncenter", + "rules": { + "margin-left": "auto !important", + "margin-right": "auto !important" + } + }, + { + "selector": "> .alignfull", + "rules": { + "max-width": "none" + } + }, + { + "suffix": "global-gap", + "selector": "> *", + "rules": { + "margin-top": "0", + "margin-bottom": "0" + } + }, + { + "suffix": "global-gap", + "selector": "> * + *", + "rules": { + "margin-top": "var( --wp--style--block-gap )", + "margin-bottom": 0 + } + } + ] + }, + "flex": { + "slug": "flex", + "type": "flex", + "styles": [ + { + "rules": { + "display": "flex", + "gap": "0.5em" + } + }, + { + "selector": "> *", + "rules": { + "margin": "0" + } + }, + { + "suffix": "h-justify", + "rules": { + "align-items": "center" + } + }, + { + "suffix": "v-justify", + "rules": { + "align-items": "flex-start", + "flex-direction": "column" + } + } + ], + "controlledSets": { + "flexWrap": { + "suffix": "wrap", + "property": "flex-wrap", + "options": { + "wrap": "wrap", + "nowrap": "nowrap" + } + }, + "horizontalJustification": { + "suffix": "h-justify", + "property": "justify-content", + "options": { + "left": "flex-start", + "right": "flex-end", + "center": "center", + "space-between": "space-between" + } + }, + "verticalJustification": { + "suffix": "v-justify", + "property": "align-items", + "options": { + "left": "flex-start", + "right": "flex-end", + "center": "center" + } + } + } + } + } + }, "spacing": { "blockGap": null, "margin": false, diff --git a/lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php b/lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php index 567dfb0cf4027..13ececb0f585f 100644 --- a/lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php +++ b/lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php @@ -15,6 +15,174 @@ * @access private */ class WP_Theme_JSON_Gutenberg extends WP_Theme_JSON_5_9 { + /** + * Presets are a set of values that serve + * to bootstrap some styles: colors, font sizes, etc. + * + * They are a unkeyed array of values such as: + * + * ```php + * array( + * array( + * 'slug' => 'unique-name-within-the-set', + * 'name' => 'Name for the UI', + * => 'value' + * ), + * ) + * ``` + * + * This contains the necessary metadata to process them: + * + * - path => Where to find the preset within the settings section. + * - prevent_override => Whether a theme preset with the same slug as a default preset + * should not override it or the path to a setting for the same + * When defaults. + * 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. + * - 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: " 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. + */ + const PRESETS_METADATA = array( + array( + 'path' => array( 'color', 'palette' ), + 'prevent_override' => array( 'color', 'defaultPalette' ), + 'use_default_presets' => 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_presets' => 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', 'duotone' ), + 'prevent_override' => array( 'color', 'defaultDuotone' ), + 'use_default_presets' => array( 'color', 'defaultDuotone' ), + 'use_default_names' => false, + 'value_func' => 'gutenberg_get_duotone_filter_property', + 'css_vars' => '--wp--preset--duotone--$slug', + 'classes' => array(), + 'properties' => array( 'filter' ), + ), + array( + 'path' => array( 'typography', 'fontSizes' ), + 'prevent_override' => false, + 'use_default_presets' => true, + '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_presets' => true, + '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' ), + ), + array( + 'path' => array( 'layout', 'types' ), + 'prevent_override' => false, + 'use_default_presets' => true, + 'use_default_names' => false, + 'value_func' => 'gutenberg_get_layout_preset_styles', + 'classes' => array(), + ), + ); + + /** + * The valid properties under the settings key. + * + * @var array + */ + const VALID_SETTINGS = array( + 'appearanceTools' => null, + 'border' => array( + 'color' => null, + 'radius' => null, + 'style' => null, + 'width' => null, + ), + '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, + ), + 'custom' => null, + 'layout' => array( + 'contentSize' => null, + 'wideSize' => null, + 'types' => null, + ), + 'spacing' => array( + 'blockGap' => null, + 'margin' => null, + 'padding' => null, + 'units' => null, + ), + 'typography' => array( + 'customFontSize' => null, + 'dropCap' => null, + 'fontFamilies' => null, + 'fontSizes' => null, + 'fontStyle' => null, + 'fontWeight' => null, + 'letterSpacing' => null, + 'lineHeight' => null, + 'textDecoration' => null, + 'textTransform' => null, + ), + ); /** * The top-level keys a theme.json can have. diff --git a/lib/load.php b/lib/load.php index c6619708df3df..d4db8634b993f 100644 --- a/lib/load.php +++ b/lib/load.php @@ -121,6 +121,11 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/global-styles.php'; require __DIR__ . '/pwa.php'; +// TODO: Before this PR merges, move this to be a part of the style engine package. +// Part of the build process should be to copy the PHP file to the correct location, +// similar to the loading behaviour in `blocks.php`. +require __DIR__ . '/class-wp-style-engine-gutenberg.php'; + require __DIR__ . '/block-supports/elements.php'; require __DIR__ . '/block-supports/colors.php'; require __DIR__ . '/block-supports/typography.php';