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: server side rendering of CSS #39086

Closed
wants to merge 2 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
109 changes: 73 additions & 36 deletions lib/block-supports/layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ function gutenberg_register_layout_support( $block_type ) {
/**
* Generates the CSS 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 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.
* @param array $gap_value The block gap value to apply.
*
* @return string CSS style.
* @return string CSS style.
*/
function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support = false, $gap_value = null ) {
$layout_type = isset( $layout['type'] ) ? $layout['type'] : 'default';

$style = '';
$styles = array();
if ( 'default' === $layout_type ) {
$content_size = isset( $layout['contentSize'] ) ? $layout['contentSize'] : '';
$wide_size = isset( $layout['wideSize'] ) ? $layout['wideSize'] : '';
Expand All @@ -51,24 +51,44 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support
$all_max_width_value = wp_strip_all_tags( explode( ';', $all_max_width_value )[0] );
$wide_max_width_value = wp_strip_all_tags( explode( ';', $wide_max_width_value )[0] );

$style = '';
if ( $content_size || $wide_size ) {
$style = "$selector > :where(:not(.alignleft):not(.alignright)) {";
$style .= 'max-width: ' . esc_html( $all_max_width_value ) . ';';
$style .= 'margin-left: auto !important;';
$style .= 'margin-right: auto !important;';
$style .= '}';

$style .= "$selector > .alignwide { max-width: " . esc_html( $wide_max_width_value ) . ';}';
$style .= "$selector .alignfull { max-width: none; }";
$styles[ "$selector > :where(:not(.alignleft):not(.alignright))" ] = array(
'max-width' => esc_html( $all_max_width_value ),
'margin-left' => 'auto !important',
'margin-right' => 'auto !important',
);

$styles[ "$selector > .alignwide" ] = array(
'max-width' => esc_html( $wide_max_width_value ),
);

$styles[ "$selector > .alignfull" ] = array(
'max-width' => 'none',
);
}

$style .= "$selector .alignleft { float: left; margin-right: 2em; margin-left: 0; }";
$style .= "$selector .alignright { float: right; margin-left: 2em; margin-right: 0; }";
$styles[ "$selector .alignleft" ] = array(
'float' => 'left',
'margin-right' => '2em',
'margin-left' => '0',
);

$styles[ "$selector .alignright" ] = array(
'float' => 'right',
'margin-right' => '0',
'margin-left' => '2em',
);

if ( $has_block_gap_support ) {
$gap_style = $gap_value ? $gap_value : 'var( --wp--style--block-gap )';
$style .= "$selector > * { margin-top: 0; margin-bottom: 0; }";
$style .= "$selector > * + * { margin-top: $gap_style; margin-bottom: 0; }";
$gap_style = $gap_value ? $gap_value : 'var( --wp--style--block-gap )';
$styles[ "$selector > *" ] = array(
'margin-top' => '0',
'margin-bottom' => '0',
);
$styles[ "$selector > * + *" ] = array(
'margin-top' => $gap_style,
'margin-bottom' => '0',
);
}
} elseif ( 'flex' === $layout_type ) {
$layout_orientation = isset( $layout['orientation'] ) ? $layout['orientation'] : 'horizontal';
Expand All @@ -88,39 +108,57 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support
$layout['flexWrap'] :
'wrap';

$style = "$selector {";
$style .= 'display: flex;';
$styles[ "$selector" ] = array(
'display' => 'flex',
'flex-wrap' => $flex_wrap,
);

if ( $has_block_gap_support ) {
$gap_style = $gap_value ? $gap_value : 'var( --wp--style--block-gap, 0.5em )';
$style .= "gap: $gap_style;";
$gap_style = $gap_value ? $gap_value : 'var( --wp--style--block-gap, 0.5em )';
$styles[ "$selector" ] = array(
'gap' => $gap_style,
);
} else {
$style .= 'gap: 0.5em;';
$styles[ "$selector" ] = array(
'gap' => '0.5em',
);
}
$style .= "flex-wrap: $flex_wrap;";

if ( 'horizontal' === $layout_orientation ) {
$style .= 'align-items: center;';
$styles[ "$selector" ] = array(
'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'] ]};";
$styles[ "$selector" ] = array(
'justify-content' => $justify_content_options[ $layout['justifyContent'] ],
);
}
} else {
$style .= 'flex-direction: column;';
$styles[ "$selector" ] = array(
'flex-direction' => 'column',
);
if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) {
$style .= "align-items: {$justify_content_options[ $layout['justifyContent'] ]};";
$styles[ "$selector" ] = array(
'align-items' => $justify_content_options[ $layout['justifyContent'] ],
);
} else {
$style .= 'align-items: flex-start;';
$styles[ "$selector" ] = array(
'align-items' => 'flex-start',
);
}
}
$style .= '}';

$style .= "$selector > * { margin: 0; }";
$styles[ "$selector > *" ] = array(
'margin' => '0',
);
}

return $style;
return $styles;
}

/**
Expand Down Expand Up @@ -156,7 +194,8 @@ 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 );
WP_Style_Engine_Gutenberg::get_instance()->add_styles( $styles );
// 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(
Expand All @@ -166,8 +205,6 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
1
);

gutenberg_enqueue_block_support_styles( $style );

return $content;
}

Expand Down
112 changes: 112 additions & 0 deletions lib/class-wp-style-engine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php
/**
* WP_Style_Engine class
*
* @package Gutenberg
*/

/**
* Singleton class representing the style engine.
*
* Consolidates rendering block styles to reduce duplication and streamline
* CSS styles generation.
*
* @since 6.0.0
*/
class WP_Style_Engine_Gutenberg {
/**
* Registered CSS styles.
*
* @since 5.5.0
* @var array
*/
private $registered_styles = array();

/**
* Container for the main instance of the class.
*
* @since 5.5.0
* @var WP_Style_Engine_Gutenberg|null
*/
private static $instance = null;

public function __construct() {
// Borrows the logic from `gutenberg_enqueue_block_support_styles`.
$action_hook_name = 'wp_footer';
if ( wp_is_block_theme() ) {
$action_hook_name = 'wp_enqueue_scripts';
}
add_action(
$action_hook_name,
array( $this, 'output_styles' )
);
}

/**
* Utility method to retrieve the main instance of the class.
*
* The instance will be created if it does not exist yet.
*
* @return WP_Style_Engine_Gutenberg The main instance.
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}

return self::$instance;
}

public function add_styles( $styles ) {
if ( ! is_array( $styles ) ) {
return array();
}

foreach ( $styles as $key => $value ) {
$value = is_array( $value ) ? $value : array();
if ( isset( $this->registered_styles[ $key ] ) ) {
$this->registered_styles[ $key ] = array_merge( $this->registered_styles[ $key ], $value );
} else {
$this->registered_styles[ $key ] = $value;
}
}
}

protected function deduplicate_styles() {
$result = array();
$unique_styles = array();
array_walk(
$this->registered_styles,
function( $value, $key ) use ( &$result, &$unique_styles ) {
$stringified_value = json_encode( $value );
if ( array_key_exists( $stringified_value, $unique_styles ) ) {
$new_key = $unique_styles[ $stringified_value ] . ",\n" . $key;
unset( $result[ $unique_styles[ $stringified_value ] ] );
$unique_styles[ $stringified_value ] = $new_key;
$result[ $new_key ] = $value;
} else {
$unique_styles[ $stringified_value ] = $key;
$result[ $key ] = $value;
}
}
);
return $result;
}

public function output_styles() {
$deduped_styles = $this->deduplicate_styles();
$callback = function( $css_selector, $css_ruleset ) {
$style = "{$css_selector} {\n";
foreach ( $css_ruleset as $css_property => $css_value ) {
$style .= " {$css_property}: {$css_value};\n";
}
$style .= "}\n";
return $style;
};

$output = array_map( $callback, array_keys( $deduped_styles ), array_values( $deduped_styles ) );
$output = implode( "\n", $output );

echo "<style>\n$output</style>\n";
}
}
5 changes: 5 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,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.php';

require __DIR__ . '/block-supports/elements.php';
require __DIR__ . '/block-supports/colors.php';
require __DIR__ . '/block-supports/typography.php';
Expand Down