diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 59632f3e2bd169..cfb5fbe36c5f8e 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -26,100 +26,310 @@ function gutenberg_register_layout_support( $block_type ) { } /** - * Generates the CSS corresponding to the provided layout. + * Generates styles for the default flow layout type. * - * @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 array $layout Layout object. * @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 string $gap_value The block gap value to apply. * - * @return string CSS style. + * @return array An array of class names corresponding to the generated layout CSS. */ -function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support = false, $gap_value = null ) { - $layout_type = isset( $layout['type'] ) ? $layout['type'] : 'default'; - - $style = ''; - if ( 'default' === $layout_type ) { - $content_size = isset( $layout['contentSize'] ) ? $layout['contentSize'] : ''; - $wide_size = isset( $layout['wideSize'] ) ? $layout['wideSize'] : ''; - - $all_max_width_value = $content_size ? $content_size : $wide_size; - $wide_max_width_value = $wide_size ? $wide_size : $content_size; - - // Make sure there is a single CSS rule, and all tags are stripped for security. - // TODO: Use `safecss_filter_attr` instead - once https://core.trac.wordpress.org/ticket/46197 is patched. - $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] ); - - 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; }"; - } +function gutenberg_generate_layout_style_flow( $layout, $has_block_gap_support = false, $gap_value = null ) { + $style_engine = WP_Style_Engine_Gutenberg::get_instance(); + $class_names = array(); - $style .= "$selector .alignleft { float: left; margin-right: 2em; margin-left: 0; }"; - $style .= "$selector .alignright { float: right; margin-left: 2em; margin-right: 0; }"; - 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; }"; - } - } elseif ( 'flex' === $layout_type ) { - $layout_orientation = isset( $layout['orientation'] ) ? $layout['orientation'] : 'horizontal'; + $content_size = isset( $layout['contentSize'] ) ? $layout['contentSize'] : ''; + $wide_size = isset( $layout['wideSize'] ) ? $layout['wideSize'] : ''; + + $all_max_width_value = $content_size ? $content_size : $wide_size; + $wide_max_width_value = $wide_size ? $wide_size : $content_size; + + // Make sure there is a single CSS rule, and all tags are stripped for security. + // TODO: Use `safecss_filter_attr` instead - once https://core.trac.wordpress.org/ticket/46197 is patched. + $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] ); + + // Add universal styles for all default layouts. + // Add left align rule. + $class_names[] = $style_engine->add_style( + 'wp-layout-flow', + array( + 'selector' => '.alignleft', + 'rules' => array( + 'float' => 'left', + 'margin-right' => '2em', + 'margin-left' => '0', + ), + ) + ); - $justify_content_options = array( - 'left' => 'flex-start', - 'right' => 'flex-end', - 'center' => 'center', + // Add right align rule. + $class_names[] = $style_engine->add_style( + 'wp-layout-flow', + array( + 'selector' => '.alignright', + 'rules' => array( + 'float' => 'right', + 'margin-left' => '2em', + 'margin-right' => '0', + ), + ) + ); + + if ( $content_size || $wide_size ) { + // Add value specific content size. + $class_names[] = $style_engine->add_style( + 'wp-layout-flow-content-size', + array( + 'suffix' => $all_max_width_value, + 'selector' => '> :where(:not(.alignleft):not(.alignright))', + 'rules' => array( + 'max-width' => esc_html( $all_max_width_value ), + 'margin-left' => 'auto !important', + 'margin-right' => 'auto !important', + ), + ) + ); + + // Add value specific wide size. + $class_names[] = $style_engine->add_style( + 'wp-layout-flow-wide-size', + array( + 'suffix' => $wide_max_width_value, + 'selector' => '> .alignwide', + 'rules' => array( + 'max-width' => esc_html( $wide_max_width_value ), + ), + ) ); - if ( 'horizontal' === $layout_orientation ) { - $justify_content_options += array( 'space-between' => 'space-between' ); + // Add universal full width. + $class_names[] = $style_engine->add_style( + 'wp-layout-flow', + array( + 'selector' => '> .alignfull', + 'rules' => array( + 'max-width' => 'none', + ), + ) + ); + } + + if ( $has_block_gap_support ) { + if ( ! $gap_value ) { + $class_names[] = $style_engine->add_style( + 'wp-layout-flow-global-gap', + array( + 'selector' => '> *', + 'rules' => array( + 'margin-top' => '0', + 'margin-bottom' => '0', + ), + ) + ); + $class_names[] = $style_engine->add_style( + 'wp-layout-flow-global-gap', + array( + 'selector' => '> * + *', + 'rules' => array( + 'margin-top' => 'var( --wp--style--block-gap )', + 'margin-bottom' => '0', + ), + ) + ); + } else { + $class_names[] = $style_engine->add_style( + 'wp-layout-flow-custom-gap', + array( + 'suffix' => $gap_value, + 'selector' => '> *', + 'rules' => array( + 'margin-top' => '0', + 'margin-bottom' => '0', + ), + ) + ); + $class_names[] = $style_engine->add_style( + 'wp-layout-flow-custom-gap', + array( + 'suffix' => $gap_value, + 'selector' => '> * + *', + 'rules' => array( + 'margin-top' => $gap_value, + 'margin-bottom' => '0', + ), + ) + ); } + } + + return $class_names; +} + +/** + * Generates styles for the flex layout type. + * + * @param array $layout Layout object. + * @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 array An array of class names corresponding to the generated layout CSS. + */ +function gutenberg_generate_layout_style_flex( $layout, $has_block_gap_support = false, $gap_value = null ) { + $style_engine = WP_Style_Engine_Gutenberg::get_instance(); + $class_names = array(); + $layout_orientation = isset( $layout['orientation'] ) ? $layout['orientation'] : 'horizontal'; + + $justify_content_options = array( + 'left' => 'flex-start', + 'right' => 'flex-end', + 'center' => 'center', + ); - $flex_wrap_options = array( 'wrap', 'nowrap' ); - $flex_wrap = ! empty( $layout['flexWrap'] ) && in_array( $layout['flexWrap'], $flex_wrap_options, true ) ? - $layout['flexWrap'] : - 'wrap'; + if ( 'horizontal' === $layout_orientation ) { + $justify_content_options += array( 'space-between' => 'space-between' ); + } - $style = "$selector {"; - $style .= 'display: flex;'; - if ( $has_block_gap_support ) { - $gap_style = $gap_value ? $gap_value : 'var( --wp--style--block-gap, 0.5em )'; - $style .= "gap: $gap_style;"; + $flex_wrap_options = array( 'wrap', 'nowrap' ); + $flex_wrap = ! empty( $layout['flexWrap'] ) && in_array( $layout['flexWrap'], $flex_wrap_options, true ) ? + $layout['flexWrap'] : + 'wrap'; + + $class_names[] = $style_engine->add_style( + 'wp-layout-flex', + array( + 'rules' => array( + 'display' => 'flex', + 'gap' => '0.5em', + ), + ) + ); + + $class_names[] = $style_engine->add_style( + 'wp-layout-flex', + array( + 'selector' => '> *', + 'rules' => array( + 'margin' => '0', + ), + ) + ); + + $class_names[] = $style_engine->add_style( + 'wp-layout-flex', + array( + 'suffix' => $flex_wrap, + 'rules' => array( + 'flex-wrap' => $flex_wrap, + ), + ) + ); + + if ( $has_block_gap_support ) { + if ( ! $gap_value ) { + $class_names[] = $style_engine->add_style( + 'wp-layout-flex-global-gap', + array( + 'rules' => array( + 'gap' => 'var( --wp--style--block-gap, 0.5em )', + ), + ) + ); } else { - $style .= 'gap: 0.5em;'; + $class_names[] = $style_engine->add_style( + 'wp-layout-flex-custom-gap', + array( + 'suffix' => $gap_value, + 'rules' => array( + 'gap' => $gap_value, + ), + ) + ); + } + } + + if ( 'horizontal' === $layout_orientation ) { + $class_names[] = $style_engine->add_style( + 'wp-layout-flex-orientation-horizontal', + array( + 'rules' => 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 ) ) { + $class_names[] = $style_engine->add_style( + 'wp-layout-flex-orientation-horizontal', + array( + 'suffix' => $justify_content_options[ $layout['justifyContent'] ], + 'rules' => array( + 'justify-content' => $justify_content_options[ $layout['justifyContent'] ], + ), + ) + ); } - $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 { + $class_names[] = $style_engine->add_style( + 'wp-layout-flex-orientation-vertical', + array( + 'rules' => array( + 'flex-direction' => 'column', + ), + ) + ); + + if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) { + $class_names[] = $style_engine->add_style( + 'wp-layout-flex-orientation-vertical', + array( + 'suffix' => $justify_content_options[ $layout['justifyContent'] ], + 'rules' => array( + 'align-items' => $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;'; - } + $class_names[] = $style_engine->add_style( + 'wp-layout-flex-orientation-vertical', + array( + 'suffix' => 'flex-start', + 'rules' => array( + 'align-items' => 'flex-start', + ), + ) + ); } - $style .= '}'; + } - $style .= "$selector > * { margin: 0; }"; + return $class_names; +} + +/** + * Generates the CSS corresponding to the provided layout. + * + * @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 array An array of class names corresponding to the generated layout CSS. + */ +function gutenberg_get_layout_style( $layout, $has_block_gap_support = false, $gap_value = null ) { + $layout_type = isset( $layout['type'] ) ? $layout['type'] : 'flow'; + $class_names = array(); + + if ( 'flow' === $layout_type ) { + $class_names = gutenberg_generate_layout_style_flow( $layout, $has_block_gap_support, $gap_value ); + } elseif ( 'flex' === $layout_type ) { + $class_names = gutenberg_generate_layout_style_flex( $layout, $has_block_gap_support, $gap_value ); } - return $style; + return array_unique( $class_names ); } /** @@ -149,24 +359,21 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { $used_layout = $default_layout; } - $class_name = wp_unique_id( 'wp-container-' ); - $gap_value = _wp_array_get( $block, array( 'attrs', 'style', 'spacing', 'blockGap' ) ); + $gap_value = _wp_array_get( $block, array( 'attrs', 'style', 'spacing', 'blockGap' ) ); // Skip if gap value contains unsupported characters. // 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 ); + $gap_value = preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value; + $class_names = gutenberg_get_layout_style( $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( ' ', $class_names ) ) . ' ', $block_content, 1 ); - gutenberg_enqueue_block_support_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 00000000000000..d970727943112f --- /dev/null +++ b/lib/class-wp-style-engine-gutenberg.php @@ -0,0 +1,106 @@ +registered_styles[ $prefix . $class . $selector ] = $rules; + + return $class; + } + + /** + * Render registered styles as key { ...rules } for final output. + */ + public function output_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"; + } + echo "\n"; + } +} diff --git a/lib/load.php b/lib/load.php index b008f233700714..1697629b9a7120 100644 --- a/lib/load.php +++ b/lib/load.php @@ -120,6 +120,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';