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

[WIP] Style engine: register styles for enqueuing and rendering #41424

Closed
wants to merge 7 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
8 changes: 2 additions & 6 deletions lib/block-supports/elements.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,13 @@ function gutenberg_render_elements_support_styles( $pre_render, $block ) {
$link_block_styles = isset( $element_block_styles['link'] ) ? $element_block_styles['link'] : null;

if ( $link_block_styles ) {
$styles = gutenberg_style_engine_generate(
gutenberg_style_engine_enqueue_styles(
aristath marked this conversation as resolved.
Show resolved Hide resolved
$link_block_styles,
array(
'selector' => ".$class_name a",
'css_vars' => true,
'layer' => 'block-supports',
)
);

if ( ! empty( $styles['css'] ) ) {
gutenberg_enqueue_block_support_styles( $styles['css'] );
}
}

return null;
Expand Down
8 changes: 8 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ function gutenberg_is_experiment_enabled( $name ) {
require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-gutenberg.php';
}

if ( file_exists( __DIR__ . '/../build/style-engine/class-wp-style-engine-store-gutenberg.php' ) ) {
require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-store-gutenberg.php';
}

if ( file_exists( __DIR__ . '/../build/style-engine/class-wp-style-engine-renderer-gutenberg.php' ) ) {
require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-renderer-gutenberg.php';
}

// Block supports overrides.
require __DIR__ . '/block-supports/utils.php';
require __DIR__ . '/block-supports/elements.php';
Expand Down
194 changes: 194 additions & 0 deletions packages/style-engine/class-wp-style-engine-renderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<?php
/**
* WP_Style_Engine_Renderer
*
* A library of CSS rule generators and render functions.
*
* @TODO Consider splitting out the rule generators (?).
* Or creating a base interface and then a bunch of separate render classes for each "layer", e.g.,
* one for block supports, one for global styles.
*
* @package Gutenberg
*/

if ( class_exists( 'WP_Style_Engine_Renderer' ) ) {
return;
}

/**
* Renders CSS on the frontend.
*
* @access private
*/
class WP_Style_Engine_Renderer {
/**
* Prints registered styles in the page head or footer.
*
* @TODO this shares code with the styles engine class in generate(). Centralize.
*
* @see $this->enqueue_block_support_styles
*/
public static function render_registered_block_supports_styles() {
$style_engine = WP_Style_Engine::get_instance();
$block_support_styles = $style_engine->get_registered_styles();

if ( empty( $block_support_styles ) ) {
return;
}

$output = '';

foreach ( $block_support_styles as $selector => $css_definitions ) {
$output .= self::generate_css_rule( $selector, $css_definitions, array( 'prettify' => true ) );
}

echo "<style>\n$output</style>\n";
}

/**
* Filters incoming CSS properties against WordPress Core's allowed CSS attributes in wp-includes/kses.php.
*
* @param string $property_declaration A CSS property declaration, e.g., `color: 'pink'`.
*
* @return string A filtered CSS property. Empty if not allowed.
*/
public static function sanitize_property_declaration( $property_declaration ) {
return esc_html( safecss_filter_attr( $property_declaration ) );
}

/**
* Creates a string consisting of CSS property declarations suitable for the value of an HTML element's style attribute.
*
* @param array $css_definitions An collection of CSS definitions `[ [ 'color' => 'red' ] ]`.
Copy link
Member Author

Choose a reason for hiding this comment

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

References to $css_definitions should rather be $css_declarations since they contain property: value pairs

*
* @return string A concatenated string of CSS properties, e.g. `'color: red; font-size:12px'`
*/
public static function generate_inline_property_declarations( $css_definitions ) {
$css_rule_inline = '';

if ( empty( $css_definitions ) ) {
return $css_rule_inline;
}
foreach ( $css_definitions as $definition => $value ) {
$filtered_css = self::sanitize_property_declaration( "{$definition}: {$value}" );
if ( ! empty( $filtered_css ) ) {
$css_rule_inline .= $filtered_css . ';';
}
}
return $css_rule_inline;
}

/**
* Creates a string consisting of a CSS rule.
*
* @param string $selector A CSS selector, e.g., `.some-class-name`.
* @param array $css_definitions An collection of CSS definitions `[ [ 'color' => 'red' ] ]`.
* @param array $options array(
* 'prettify' => (boolean) Whether to add carriage returns and indenting.
* 'indent' => (number) The number of tab indents to apply to the rule. Applies if `prettify` is `true`.
* );.
*
* @return string A CSS rule, e.g. `'.some-selector { color: red; font-size:12px }'`
*/
public static function generate_css_rule( $selector, $css_definitions, $options = array() ) {
$css_rule_block = '';

if ( ! $selector || empty( $css_definitions ) ) {
return $css_rule_block;
}

$defaults = array(
'prettify' => false,
'indent' => 0,
);
$options = wp_parse_args( $options, $defaults );
$indent = str_repeat( "\t", $options['indent'] );
$css_rule_block = $options['prettify'] ? "$indent$selector {\n" : "$selector { ";

foreach ( $css_definitions as $definition => $value ) {
$filtered_css = self::sanitize_property_declaration( "{$definition}: {$value}" );
if ( ! empty( $filtered_css ) ) {
if ( $options['prettify'] ) {
$css_rule_block .= "\t$indent$filtered_css;\n";
} else {
$css_rule_block .= $filtered_css . ';';
}
}
}
$css_rule_block .= $options['prettify'] ? "$indent}\n" : ' }';
return $css_rule_block;
}

// @TODO Using cascade layers should be opt-in.
/**
* Builds layers and styles rules from registered layers and styles for output.
*/
public static function enqueue_cascade_layers() {
Copy link
Member Author

Choose a reason for hiding this comment

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

Just testing outputting cascade layers for now to test the concept.

We really want registered styles to be the focus beforehand.

TODO:

  • Needs to be tested against other styles for specificity
  • How to make this opt-in? (We're instantiating WP_Style_Engine as a singleton, so should it be passed to the constructor somehow)

$style_engine = WP_Style_Engine::get_instance();
$registered_layers = $style_engine->get_registered_styles();

if ( empty( $registered_layers ) ) {
return;
}

$layer_output = array();
$styles_output = '';

foreach ( $style_engine::STYLE_LAYERS as $layer_name ) {
if ( ! isset( $registered_layers[ $layer_name ] ) || empty( $registered_layers[ $layer_name ] ) ) {
continue;
}

$layer_output[] = $layer_name;
$styles_output .= "@layer {$layer_name} {\n";

foreach ( $registered_layers[ $layer_name ] as $selector => $css_definitions ) {
$styles_output .= self::generate_css_rule(
$selector,
$css_definitions,
array(
'prettify' => true,
'indent' => 1,
)
);
}
$styles_output .= '}';
}

if ( ! empty( $styles_output ) ) {
$layer_output = '@layer ' . implode( ', ', $layer_output ) . ";\n";
wp_register_style( 'wp-styles-layers', false, array(), true, true );
wp_add_inline_style( 'wp-styles-layers', $layer_output . $styles_output );
wp_enqueue_style( 'wp-styles-layers' );
}
}

/**
* Taken from gutenberg_enqueue_block_support_styles()
*
* This function takes care of adding inline styles
* in the proper place, depending on the theme in use.
*
* For block themes, it's loaded in the head.
* For classic ones, it's loaded in the body
* because the wp_head action happens before
* the render_block.
*
* @see gutenberg_enqueue_block_support_styles()
*
* @param int $priority To set the priority for the add_action.
*/
public static function enqueue_registered_styles( $priority = 10 ) {
$action_hook_name = 'wp_footer';
if ( wp_is_block_theme() ) {
$action_hook_name = 'wp_head';
}
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_cascade_layers' ) );
add_action(
$action_hook_name,
array( __CLASS__, 'enqueue_cascade_layers' ),
$priority
);
}
}

80 changes: 80 additions & 0 deletions packages/style-engine/class-wp-style-engine-store.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php
/**
* WP_Style_Engine_Store
*
* Registers and stores styles to be processed or rendered on the frontend.
*
* @package Gutenberg
*/

if ( class_exists( 'WP_Style_Engine_Store' ) ) {
return;
}

/**
* Registers and stores styles to be processed or rendered on the frontend.
*
* For each style category we could have a separate object, e.g.,
* $global_style_store = new WP_Style_Engine_Store();
* $block_supports_style_store = new WP_Style_Engine_Store();
*
* @access private
*/
class WP_Style_Engine_Store {
/**
* Registered styles.
*
* @var WP_Style_Engine_Store|null
*/
private $registered_styles = array();

/**
* Gather internals.
*/
public function __construct( $layers = array() ) {
foreach ( $layers as $layer ) {
$this->registered_styles[ $layer ] = array();
}
}

/**
* Register a style
*
* @param string $layer Unique key for a layer.
* @param string $key Unique key for a $style_data object.
* @param array $style_data Associative array of style information.
* @return boolean Whether registration was successful.
*/
public function register( $layer, $key, $style_data ) {
if ( empty( $layer ) || empty( $key ) || empty( $style_data ) ) {
return false;
}

if ( isset( $this->registered_styles[ $layer ][ $key ] ) ) {
$style_data = array_unique( array_merge( $this->registered_styles[ $layer ][ $key ], $style_data ) );
}
$this->registered_styles[ $layer ][ $key ] = $style_data;
return true;
}

/**
* Retrieves style data from the store. If neither $layer nor $key are provided,
* this method will return everything in the store.
*
* @param string $layer Optional unique key for a layer to return all styles for a layer.
* @param string $key Optional unique key for a $style_data object to return a single style object.
*
* @return array Registered styles
*/
public function get( $layer = null, $key = null ) {
if ( isset( $this->registered_styles[ $layer ][ $key ] ) ) {
return $this->registered_styles[ $layer ][ $key ];
}

if ( isset( $this->registered_styles[ $layer ] ) ) {
return $this->registered_styles[ $layer ];
}

return $this->registered_styles;
}
}
Loading