Repository URL to install this package:
<?php
/**
* CSS Optimization: Optimizes the CSS rendered by our blocks in the frontend in
* a safe manner.
*
* Main logic: when a post is saved, gather all the CSS generated by our blocks
* and optimize them. Cache the optimized CSS in the post meta - this will be
* used to load the CSS in the frontend head tag.
*
* This is done when a post is saved in order to pre-calculate all the CSS,
* making it fast since we just need to load the saved CSS in the frontend
* without any added processing.
*
* The styles (before being optimized) are saved as well, so we can make sure
* first that the block's styles have been optimized before stripping them out
* `render_block`.
*
* This is backward compatible, if no optimized CSS is found, then the original
* blocks/content will just be used.
*
* Important notes:
* - The optimized CSS is saved when the post is saved.
* - The actual post content / blocks aren't modified.
* - This works well with caching solutions like caching plugins
* - No additional block rendering / processing is done (aside from stripping
* the CSS in `render_block`)
* - When previewing posts, no optimization is done.
*
* @since 3.3.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'Stackable_CSS_Optimize' ) ) {
/**
* Stackable Optimize CSS.
*/
class Stackable_CSS_Optimize {
/**
* This holds the optimized CSS for the current post, this will be
* loaded in the head.
*
* @var String
*/
public $optimized_css = '';
/**
* This holds the raw CSS values of $this->optimized_css above, this is
* used for cross checking whether a block's CSS was optimized and if
* it's safe to strip out.
*
* @var array
*/
public $css_raw = array();
/**
* This should be the order of the media queries to prevent wrong overrides.
*/
const MEDIA_QUERY_ORDER = array(
'', // All screens,
'@media screen and (min-width:1024px)', // Desktop only.
'@media screen and (min-width:768px)', // Desktop & tablet.
'@media screen and (min-width:768px) and (max-width:1023px)', // Tablet.
'@media screen and (max-width:1023px)', // Tablet & mobile.
'@media screen and (max-width:767px)', // Mobile.
);
/**
* Initialize
*/
function __construct() {
// When a post is saved, generate an optimized version of the styles used.
add_action( 'save_post', array( $this, 'generate_optimied_css_for_post' ), 10, 3 );
// Only do this when inline style optimization is enabled.
// If stackable_optimize_inline_css === false (or option isn't
// present), that's the default value (true) for the option.
if ( get_option( 'stackable_optimize_inline_css' ) !== '' ) {
// Load the optimized CSS in the head of posts.
add_action( 'wp', array( $this, 'load_cached_css_for_post' ) );
// If the optimized CSS was loaded, then strip out the styles which were in the CSS.
add_filter( 'render_block', array( $this, 'strip_optimized_block_styles' ), 10, 2 );
}
}
/**
* When editing a post, we need to generate the optimized CSS for all
* the blocks used in the post.
*
* @param Int $post_id
* @param WP_Post $post
* @param Boolean $update
*
* @return void
*/
public function generate_optimied_css_for_post( $post_id, $post, $update ) {
if ( $post->post_type === 'attachment' ||
$post->post_type === 'revision' || // Don't do this when previewing a post.
$post->post_type === 'nav_menu_item' ||
$post->post_type === 'wp_template' || // DEV NOTE: This should work for FSE as well, but disallow this for now while we don't support it yet.
$post->post_type === 'wp_template_part' || // DEV NOTE: This should work for FSE as well, but disallow this for now while we don't support it yet.
$post->post_type === 'stackable_temp_post' ) { // Temporary post type used by Stackable for editing default blocks or UI Kits.
return;
}
// Convert content to blocks.
$blocks = parse_blocks( $post->post_content );
// Go through and gather all the styles.
$styles = array(); // Holds unique ids and styles from blocks.
$this->parse_blocks( $blocks, $styles );
// Generate the optimized CSS.
$styles_only = array();
foreach ( $styles as $block_styles ) {
foreach ( $block_styles as $block_style ) {
$styles_only[] = $block_style[1];
}
}
$optimized_css = $this->generate_css( $styles_only );
// Save the optimized CSS to the post.
update_post_meta( $post_id, 'stackable_optimized_css', $optimized_css );
update_post_meta( $post_id, 'stackable_optimized_css_raw', $styles );
}
/**
* Goes through all the given blocks and gathers all the styles.
*
* @param Array $blocks
* @param Array $style_arr Mutated array that will contain all the styles
*
* @return void
*/
public function parse_blocks( $blocks, &$style_arr ) {
foreach ( $blocks as $block ) {
if ( stripos( $block['blockName'], 'stackable/' ) !== false ) {
$this->parse_block_style( $block, $style_arr );
}
$this->parse_blocks( $block['innerBlocks'], $style_arr );
}
}
/**
* Parses a block and gathers all the styles.
*
* @param Array $block
* @param Array $style_arr
*
* @return void
*/
public function parse_block_style( $block, &$style_arr ) {
$block_content = $block['innerHTML'];
if ( stripos( $block_content, '<style' ) !== false ) {
// We need the unique id for tracking.
if ( is_array( $block['attrs'] ) && array_key_exists( 'uniqueId', $block['attrs'] ) ) {
// Gather all the styles.
preg_match_all( '#<style[^>]*>(.*?)</style>#', $block_content, $styles );
// $style contains:
// 0 = whole style tag
// 1 = css inside the style tag
$all_block_styles = array();
foreach ( $styles[0] as $i => $style_tag ) {
// Add the styles to our list of styles to optimize.
$all_block_styles[] = array(
$styles[0][ $i ],
$styles[1][ $i ],
);
}
$unique_id = $block['attrs']['uniqueId'];
$style_arr[ $unique_id ] = $all_block_styles;
}
}
}
/**
* Gets the optimized CSS very early on for the current post, and
* trigger the printing of the styles in the head.
*
* @return void
*/
public function load_cached_css_for_post() {
// DEV NOTE: If we'll also do this for wp_template and
// wp_template_part then we might need to use the actions:
// render_block_core_template_part_post and
// render_block_core_template_part_file
if ( is_singular() && ! is_preview() && ! is_attachment() ) {
$post_id = get_the_ID();
$this->optimized_css = get_post_meta( $post_id, 'stackable_optimized_css', true );
$this->css_raw = get_post_meta( $post_id, 'stackable_optimized_css_raw', true );
if ( ! empty( $this->optimized_css ) ) {
add_action( 'wp_head', array( $this, 'print_optimized_styles' ) );
}
}
}
/**
* Prints the optimized CSS in the head.
*
* @return void
*/
public function print_optimized_styles() {
if ( ! empty( $this->optimized_css ) ) {
echo "\n";
echo '<style class="stk-block-styles">';
echo apply_filters( 'stackable_frontend_css', $this->optimized_css );
echo '</style>';
}
}
/**
* HOVER STYLES HACK (1/4): The CSS generated by hover states in
* src/util/styles/style-object.js get reordered because of
* optimization. This stops the hover states from working. To fix it,
* we add a placeholder to the styles for the hover states so we can
* group them together. We move those with the placeholders to the end
* to ensure that our hover styles work.
*
* @param string $a Selector
* @param string $b Selector
*
* @return int -1, 0, 1 to reorder the array
*/
public function selector_sort( $a, $b ) {
if ( stripos( $a, '/* */' ) !== false && stripos( $b, '/* */' ) !== false ) {
return 0;
} else if ( stripos( $a, '/* */' ) !== false ) {
return 1;
} else if ( stripos( $b, '/* */' ) !== false ) {
return -1;
}
return 0;
}
/**
* Strips out the styles of a rendered block if the block's CSS has been
* optimized in the head of the page.
*
* @param String $block_content
* @param Array $block
*
* @return String The modified $block_content
*/
public function strip_optimized_block_styles( $block_content, $block ) {
if ( ! is_singular() || is_preview() ) {
return $block_content;
}
if ( empty( $this->css_raw ) || empty( $this->optimized_css ) ) {
return $block_content;
}
// Only do this to our blocks.
if ( ! empty( $block ) && is_array( $block ) && stripos( $block['blockName'], 'stackable/' ) === 0 ) {
if ( stripos( $block_content, '<style' ) !== false ) {
// We need the unique id for tracking.
if ( is_array( $block['attrs'] ) && array_key_exists( 'uniqueId', $block['attrs'] ) ) {
$unique_id = $block['attrs']['uniqueId'];
if ( array_key_exists( $unique_id, $this->css_raw ) ) {
$css_to_strip = $this->css_raw[ $unique_id ];
foreach ( $css_to_strip as $style ) {
// $style[0] - contains the whole style tag.
if ( stripos( $block_content, $style[0] ) !== false ) {
$block_content = str_replace( $style[0], '', $block_content );
}
}
}
}
}
}
return $block_content;
}
/**
* Combines similar class selectors in a single :is()
*
* @param Array $selectors
* @return Array Combined selectors
*/
public function combine_selectors( $selectors ) {
$new_selectors = array();
$classes_to_combine = array();
foreach( $selectors as $selector ) {
$selector = trim( $selector );
// Find all the unique id classes of the form ".stk-123bcd"
preg_match( '/(.stk-[a-f0-9-]{7})(?=\s|$)/', $selector, $matches );
// If it doesn't have a block selector that we can combine, just add it.
if ( ! count( $matches ) ) {
$new_selectors[] = $selector;
continue;
}
$match = $matches[1];
// Don't do this if the selector is only the unique id.
if ( $selector === $match ) {
$new_selectors[] = $match;
continue;
}
// Collect all the selectors we can combine.
$selector = preg_replace( "#" . $match . "(?!-)#", '%s', $selector, 1 ); // Don't replace partial classname matches and only do it once.
if ( ! array_key_exists( $selector, $classes_to_combine ) ) {
$classes_to_combine[ $selector ] = array();
}
$classes_to_combine[ $selector ][] = $match;
}
// Combine the selectors into a single :is() selector.
foreach ( $classes_to_combine as $selector => $classes ) {
if ( count( $classes ) === 1 ) {
$new_selectors[] = sprintf( $selector, $classes[0] );
} else {
$new_selectors[] = sprintf( $selector, ':is(' . implode( ', ', $classes ) . ')' );
}
}
return $new_selectors;
}
/**
* Generates an optimized version of an array of CSS strings.
*
* @param Array An array of CSS strings.
*
* @return String The optimized CSS.
*/
public function generate_css( $styles ) {
// This contains styles as keys and selectors as values for easy
// lookups.
Loading ...