Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Style Engine: add typography to backend #40082

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/block-supports/spacing.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function gutenberg_apply_spacing_support( $block_type, $block_attributes ) {
return $attributes;
}

$style_engine = WP_Style_Engine_Gutenberg::get_instance();
$style_engine = gutenberg_get_style_engine();
$skip_padding = gutenberg_should_skip_block_supports_serialization( $block_type, 'spacing', 'padding' );
$skip_margin = gutenberg_should_skip_block_supports_serialization( $block_type, 'spacing', 'margin' );
$spacing_block_styles = array();
Expand Down
138 changes: 79 additions & 59 deletions lib/block-supports/typography.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,117 +92,139 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) {
$has_text_decoration_support = _wp_array_get( $typography_supports, array( '__experimentalTextDecoration' ), false );
$has_text_transform_support = _wp_array_get( $typography_supports, array( '__experimentalTextTransform' ), false );

if ( $has_font_size_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontSize' ) ) {
// Whether to skip individual block support features.
$should_skip_font_size = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontSize' );
$should_skip_font_family = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontFamily' );
$should_skip_font_style = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontStyle' );
$should_skip_font_weight = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontWeight' );
$should_skip_line_height = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'lineHeight' );
$should_skip_text_decoration = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'textDecoration' );
$should_skip_text_transform = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'textTransform' );
$should_skip_letter_spacing = gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'letterSpacing' );

if ( $has_font_size_support && ! $should_skip_font_size ) {
$has_named_font_size = array_key_exists( 'fontSize', $block_attributes );
$has_custom_font_size = isset( $block_attributes['style']['typography']['fontSize'] );

if ( $has_named_font_size ) {
$classes[] = sprintf( 'has-%s-font-size', _wp_to_kebab_case( $block_attributes['fontSize'] ) );
$classes['fontSize'] = $block_attributes['fontSize'];
} elseif ( $has_custom_font_size ) {
$styles[] = sprintf( 'font-size: %s;', $block_attributes['style']['typography']['fontSize'] );
$styles['fontSize'] = $block_attributes['style']['typography']['fontSize'];
}
}

if ( $has_font_family_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontFamily' ) ) {
if ( $has_font_family_support && ! $should_skip_font_family ) {
$has_named_font_family = array_key_exists( 'fontFamily', $block_attributes );
$has_custom_font_family = isset( $block_attributes['style']['typography']['fontFamily'] );

if ( $has_named_font_family ) {
$classes[] = sprintf( 'has-%s-font-family', _wp_to_kebab_case( $block_attributes['fontFamily'] ) );
$classes['fontFamily'] = $block_attributes['fontFamily'];
} elseif ( $has_custom_font_family ) {
// Before using classes, the value was serialized as a CSS Custom Property.
// We don't need this code path when it lands in core.
$font_family_custom = $block_attributes['style']['typography']['fontFamily'];
if ( strpos( $font_family_custom, 'var:preset|font-family' ) !== false ) {
$index_to_splice = strrpos( $font_family_custom, '|' ) + 1;
$font_family_slug = _wp_to_kebab_case( substr( $font_family_custom, $index_to_splice ) );
$font_family_custom = sprintf( 'var(--wp--preset--font-family--%s)', $font_family_slug );
}
$styles[] = sprintf( 'font-family: %s;', $font_family_custom );
// Before using classes, the value was serialized as a CSS Custom Property.
// We don't need to check for a preset when it lands in core.
Copy link
Member Author

@ramonjd ramonjd Apr 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to check for a preset when it lands in core

TODO: check what this means

cc @ockham Do you know if this comment is still valid? Grabbed your name from 1e17b5a#diff-2c54d3c8305590cf826f83511a9fd85d5b405470addd62edc3183ff9118177fbR115

Or would we need the code permanently for backwards compatibility?

$font_family_value = gutenberg_typography_get_preset_inline_style_value( $font_family_custom, 'font-family' );
$styles['fontFamily'] = $font_family_value;
}
}

if ( $has_font_style_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontStyle' ) ) {
$font_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'fontStyle', 'font-style' );
if ( $font_style ) {
$styles[] = $font_style;
if ( $has_font_style_support && ! $should_skip_font_style ) {
$font_style = _wp_array_get( $block_attributes, array( 'style', 'typography', 'fontStyle' ), null );
$font_style_value = gutenberg_typography_get_preset_inline_style_value( $font_style, 'font-style' );
if ( $font_style_value ) {
$styles['fontStyle'] = $font_style_value;
}
}

if ( $has_font_weight_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'fontWeight' ) ) {
$font_weight = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'fontWeight', 'font-weight' );
if ( $font_weight ) {
$styles[] = $font_weight;
if ( $has_font_weight_support && ! $should_skip_font_weight ) {
$font_weight = _wp_array_get( $block_attributes, array( 'style', 'typography', 'fontWeight' ), null );
$font_weight_value = gutenberg_typography_get_preset_inline_style_value( $font_weight, 'font-weight' );
if ( $font_weight_value ) {
$styles['fontWeight'] = $font_weight_value;
}
}

if ( $has_line_height_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'lineHeight' ) ) {
if ( $has_line_height_support && ! $should_skip_line_height ) {
$has_line_height = isset( $block_attributes['style']['typography']['lineHeight'] );
if ( $has_line_height ) {
$styles[] = sprintf( 'line-height: %s;', $block_attributes['style']['typography']['lineHeight'] );
$styles['lineHeight'] = $block_attributes['style']['typography']['lineHeight'];
}
}

if ( $has_text_decoration_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'textDecoration' ) ) {
$text_decoration_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'textDecoration', 'text-decoration' );
if ( $text_decoration_style ) {
$styles[] = $text_decoration_style;
if ( $has_text_decoration_support && ! $should_skip_text_decoration ) {
$text_decoration = _wp_array_get( $block_attributes, array( 'style', 'typography', 'textDecoration' ), null );
$text_decoration_value = gutenberg_typography_get_preset_inline_style_value( $text_decoration, 'text-decoration' );
if ( $text_decoration_value ) {
$styles['textDecoration'] = $text_decoration_value;
}
}

if ( $has_text_transform_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'textTransform' ) ) {
$text_transform_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'textTransform', 'text-transform' );
if ( $text_transform_style ) {
$styles[] = $text_transform_style;
if ( $has_text_transform_support && ! $should_skip_text_transform ) {
$text_transform = _wp_array_get( $block_attributes, array( 'style', 'typography', 'textTransform' ), null );
$text_transform_value = gutenberg_typography_get_preset_inline_style_value( $text_transform, 'text-transform' );
if ( $text_transform_value ) {
$styles['textTransform'] = $text_transform_value;
}
}

if ( $has_letter_spacing_support && ! gutenberg_should_skip_block_supports_serialization( $block_type, 'typography', 'letterSpacing' ) ) {
$letter_spacing_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'letterSpacing', 'letter-spacing' );
if ( $letter_spacing_style ) {
$styles[] = $letter_spacing_style;
if ( $has_letter_spacing_support && ! $should_skip_letter_spacing ) {
$letter_spacing = _wp_array_get( $block_attributes, array( 'style', 'typography', 'letterSpacing' ), null );
$letter_spacing_value = gutenberg_typography_get_preset_inline_style_value( $letter_spacing, 'letter-spacing' );
if ( $letter_spacing_value ) {
$styles['letterSpacing'] = $letter_spacing_value;
}
}

if ( ! empty( $classes ) ) {
$attributes['class'] = implode( ' ', $classes );
$style_engine = gutenberg_get_style_engine();
$inline_styles = $style_engine->generate(
array( 'typography' => $styles ),
array(
'inline' => true,
)
);

$classnames = $style_engine->get_classnames(
array( 'typography' => $classes )
);

if ( ! empty( $classnames ) ) {
$attributes['class'] = $classnames;
}
if ( ! empty( $styles ) ) {
$attributes['style'] = implode( ' ', $styles );

if ( ! empty( $inline_styles ) ) {
$attributes['style'] = $inline_styles;
}

return $attributes;
}

/**
* Generates an inline style for a typography feature e.g. text decoration,
* Generates an inline style value for a typography feature e.g. text decoration,
* text transform, and font style.
*
* @param array $attributes Block's attributes.
* @param string $feature Key for the feature within the typography styles.
* @param string $css_property Slug for the CSS property the inline style sets.
* @param string $style_value A raw style value for a single typography feature from a block's style attribute.
* @param string $css_property Slug for the CSS property the inline style sets.
*
* @return string CSS inline style.
* @return string? A CSS inline style value.
*/
function gutenberg_typography_get_css_variable_inline_style( $attributes, $feature, $css_property ) {
// Retrieve current attribute value or skip if not found.
$style_value = _wp_array_get( $attributes, array( 'style', 'typography', $feature ), false );
if ( ! $style_value ) {
return;
}

// If we don't have a preset CSS variable, we'll assume it's a regular CSS value.
if ( strpos( $style_value, "var:preset|{$css_property}|" ) === false ) {
return sprintf( '%s:%s;', $css_property, $style_value );
function gutenberg_typography_get_preset_inline_style_value( $style_value, $css_property ) {
// If the style value is not a preset CSS variable go no further.
if ( empty( $style_value ) || strpos( $style_value, "var:preset|{$css_property}|" ) === false ) {
return $style_value;
}

// For backwards compatibility.
// Presets were removed in https://github.com/WordPress/gutenberg/pull/27555.
// We have a preset CSS variable as the style.
// Get the style value from the string and return CSS style.
$index_to_splice = strrpos( $style_value, '|' ) + 1;
$slug = substr( $style_value, $index_to_splice );
// @TODO
// Font family requires the slugs to be converted to kebab case. Should this be optional in this method?
// Let's test with some older blocks.
$slug = _wp_to_kebab_case( substr( $style_value, $index_to_splice ) );

// Return the actual CSS inline style e.g. `text-decoration:var(--wp--preset--text-decoration--underline);`.
return sprintf( '%s:var(--wp--preset--%s--%s);', $css_property, $css_property, $slug );
// Return the actual CSS inline style value e.g. `var(--wp--preset--text-decoration--underline);`.
return sprintf( 'var(--wp--preset--%s--%s);', $css_property, $slug );
}

// Register the block support.
Expand All @@ -213,5 +235,3 @@ function gutenberg_typography_get_css_variable_inline_style( $attributes, $featu
'apply' => 'gutenberg_apply_typography_support',
)
);


115 changes: 91 additions & 24 deletions packages/style-engine/class-wp-style-engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,50 @@ class WP_Style_Engine {
* For example, `'padding' => 'array( 'top' => '1em' )` will return `array( 'padding-top' => '1em' )`
*/
const BLOCK_STYLE_DEFINITIONS_METADATA = array(
'spacing' => array(
'spacing' => array(
'padding' => array(
'property_key' => 'padding',
'path' => array( 'spacing', 'padding' ),
'value_func' => 'static::get_css_box_rules',
),
'margin' => array(
'property_key' => 'margin',
'path' => array( 'spacing', 'margin' ),
'value_func' => 'static::get_css_box_rules',
),
),
'typography' => array(
'fontSize' => array(
'property_key' => 'font-size',
'path' => array( 'typography', 'fontSize' ),
'classname_schema' => 'has-%s-font-size',
),
'fontFamily' => array(
'property_key' => 'font-family',
'path' => array( 'typography', 'fontFamily' ),
'classname_schema' => 'has-%s-font-family',
),
'fontStyle' => array(
'property_key' => 'font-style',
'path' => array( 'typography', 'fontStyle' ),
),
'fontWeight' => array(
'property_key' => 'font-weight',
'path' => array( 'typography', 'fontWeight' ),
),
'lineHeight' => array(
'property_key' => 'line-height',
'path' => array( 'typography', 'lineHeight' ),
),
'textDecoration' => array(
'property_key' => 'text-decoration',
'path' => array( 'typography', 'textDecoration' ),
),
'textTransform' => array(
'property_key' => 'text-transform',
'path' => array( 'typography', 'textTransform' ),
),
'letterSpacing' => array(
'property_key' => 'letter-spacing',
'path' => array( 'typography', 'letterSpacing' ),
),
),
);
Expand Down Expand Up @@ -77,17 +111,57 @@ protected function get_block_style_css_rules( $style_value, $path ) {
$style_definition = _wp_array_get( static::BLOCK_STYLE_DEFINITIONS_METADATA, $path, null );

if ( ! empty( $style_definition ) ) {
// Style definitions can define a function to generate custom CSS rules.
if (
isset( $style_definition['value_func'] ) &&
is_callable( $style_definition['value_func'] )
) {
return call_user_func( $style_definition['value_func'], $style_value, $style_definition['property_key'] );
} else {
return static::get_css_rules( $style_value, $style_definition['property_key'] );
}
}

return array();
}

/**
* Returns a classname built using a provided schema.
*
* @param array $block_styles An array of styles from a block's attributes.
* Some values may contain slugs that need to be parsed using a schema.
*
* @return string A CSS classname.
*/
public function get_classnames( $block_styles ) {
$output = '';

if ( empty( $block_styles ) ) {
return $output;
}

$classnames = array();
foreach ( self::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group ) {
foreach ( $definition_group as $style_definition ) {
$classname_value = _wp_array_get( $block_styles, $style_definition['path'], null );

if ( empty( $classname_value ) ) {
continue;
}

$classname_value = _wp_to_kebab_case( $classname_value );
$style_definition = _wp_array_get( static::BLOCK_STYLE_DEFINITIONS_METADATA, $style_definition['path'], null );
// Right now we expect a classname pattern to be stored in BLOCK_STYLE_DEFINITIONS_METADATA.
// One day, if there are no stored schemata, we could allow custom patterns or
// generate classnames based on other properties
// such as a path or a value or a prefix passed in options.
$classnames[] = isset( $style_definition['classname_schema'] ) ? sprintf( $style_definition['classname_schema'], $classname_value ) : $classname_value;
}
}

return implode( ' ', $classnames );
}

/**
* Returns an CSS ruleset.
* Styles are bundled based on the instructions in BLOCK_STYLE_DEFINITIONS_METADATA.
Expand All @@ -109,56 +183,49 @@ public function generate( $block_styles, $options = array() ) {

$rules = array();

// If a path to a specific block style is defined, only return rules for that style.
if ( isset( $options['path'] ) && is_array( $options['path'] ) ) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't actually use these feature, so I'm removing it.

$style_value = _wp_array_get( $block_styles, $options['path'], null );
if ( empty( $style_value ) ) {
return $output;
}
$rules = array_merge( $rules, $this->get_block_style_css_rules( $style_value, $options['path'] ) );
} else {
// Otherwise build them all.
foreach ( self::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group ) {
foreach ( $definition_group as $style_definition ) {
$style_value = _wp_array_get( $block_styles, $style_definition['path'], null );
if ( empty( $style_value ) ) {
continue;
}
$rules = array_merge( $rules, $this->get_block_style_css_rules( $style_value, $style_definition['path'] ) );
foreach ( self::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group ) {
foreach ( $definition_group as $style_definition ) {
$style_value = _wp_array_get( $block_styles, $style_definition['path'], null );
if ( empty( $style_value ) ) {
continue;
}
$rules = array_merge( $rules, $this->get_block_style_css_rules( $style_value, $style_definition['path'] ) );
}
}

if ( ! empty( $rules ) ) {
// Generate inline style rules.
if ( isset( $options['inline'] ) && true === $options['inline'] ) {
foreach ( $rules as $rule => $value ) {
$filtered_css = esc_html( safecss_filter_attr( "{$rule}:{$value}" ) );
$filtered_css = esc_html( safecss_filter_attr( "{$rule}: {$value}" ) );
if ( ! empty( $filtered_css ) ) {
$output .= $filtered_css . ';';
$output .= $filtered_css . '; ';
}
}
}
}

return $output;
return trim( $output );
}

/**
* Returns a CSS ruleset for box model styles such as margins, padding, and borders.
* Default style value parser that returns a CSS ruleset.
* If the input contains an array, it will treated like a box model
* for styles such as margins, padding, and borders
*
* @param string|array $style_value A single raw Gutenberg style attributes value for a CSS property.
* @param string $style_property The CSS property for which we're creating a rule.
*
* @return array The class name for the added style.
*/
public static function get_css_box_rules( $style_value, $style_property ) {
public static function get_css_rules( $style_value, $style_property ) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To match the clientside naming.

$rules = array();

if ( ! $style_value ) {
return $rules;
}

// We assume box model-like properties.
if ( is_array( $style_value ) ) {
foreach ( $style_value as $key => $value ) {
$rules[ "$style_property-$key" ] = $value;
Expand Down
Loading