Skip to content

Instantly share code, notes, and snippets.

@Robbertdk
Last active October 17, 2019 04:19
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Robbertdk/dbab8f2a4e38bdac067b2f2105d6977e to your computer and use it in GitHub Desktop.
Save Robbertdk/dbab8f2a4e38bdac067b2f2105d6977e to your computer and use it in GitHub Desktop.
<?php
/**
* Kirki Sass Compiler
*
* Create a CSS file based on a SCSS-file and Kirki variables
* File gets saved in the public folder with a cache buster.
*/
namespace App\Kirki;
use ScssPhp\ScssPhp\Compiler;
class DynamicCSS {
private $css_dir;
private $css_file_name;
private $css_file_url;
public function __construct($settings) {
if (!class_exists( 'Kirki' )) return;
$this->css_dir = $this->get_file_dir($settings['foldername']);
$this->css_file_name = $this->get_file_name($settings['filename']);
$this->css_file_url = $this->get_cached_file_url($settings['foldername']);
// Create an CSS file with the initial values on theme change
add_action('switch_theme', array( $this, 'remove_css_directory' ), 999);
add_action('after_switch_theme', array( $this, 'refresh_css'));
add_action( 'init', [$this, 'init'], 999 );
}
/**
* Hooks up the compiler
*
* @return void
*/
public function init() {
global $wp_customize;
// If whe're in the customizer, add the styles directly into the head
// Thereby we see the changes on partial refresh, before the customize_save_after hook is triggered
if ( $wp_customize ) {
add_action( 'wp_enqueue_scripts', array( $this, 'get_inline_styles' ), 999 );
// Create a new CSS file on customizer settings save
add_action( 'customize_save_after', array( $this, 'refresh_css' ) );
return;
}
// Enqueue the script
add_action( 'wp_enqueue_scripts', array($this, 'enqueue_dynamic_css'), 999);
}
/**
* Compiles CSS and adds it as inline style.
*
* Used during style changes in the Customizer
*
* @return void
*/
public function get_inline_styles() {
$styles = $this->compile_scss();
if ( !empty($styles) ) {
wp_enqueue_style( 'dynamic-css', $this->css_file_url );
wp_add_inline_style( 'dynamic-css', $styles );
}
}
/**
* Returns the path to the css file directory.
*
* @return string
*/
private function get_file_dir($folder = 'dynamic-css') {
$upload_dir = wp_upload_dir();
return $upload_dir['basedir'] . DIRECTORY_SEPARATOR . $folder;
}
/**
* Removes the css folder
*
* @return void
*/
public function remove_css_directory() {
global $wp_filesystem;
if ( empty( $wp_filesystem ) ) {
require_once( ABSPATH . '/wp-admin/includes/file.php' );
WP_Filesystem();
}
return $wp_filesystem->rmdir( $this->css_dir, true);
}
/**
* Returns the file name adjusted for multisite.
*
* @return string
*/
private function get_file_name($basename = 'styles') {
// Add blog ID if on multisite
global $blog_id;
$blog_id = ( is_multisite() && $blog_id > 1 ) ? '_blog-' . $blog_id : null;
return $basename . $blog_id . '.css';
}
/**
* Returns the path to the css file with the cache buster in the filename.
*
* @param string cachebuster
* @return string
*/
private function get_cached_file_path($buster = false) {
// Get cache buster
if (!$buster) {
$buster = $this->get_cache_buster();
}
$file_name = str_replace('.css', '-' .$buster . '.css', $this->css_file_name);
return $this->css_dir . DIRECTORY_SEPARATOR . $file_name;
}
/**
* Returns the url to the css file name, adjusted for multisite.
* @param string foldername
* @return string
*/
private function get_cached_file_url($foldername = 'dynamic-styles'){
$upload_dir = wp_upload_dir();
$cache_buster = $this->get_cache_buster();
$file_name = str_replace('.css', '-' . $cache_buster . '.css', $this->css_file_name);
$css_url = trailingslashit( $upload_dir['baseurl'] ) . $foldername . '/' . $file_name;
// Take care of domain mapping
if ( defined( 'DOMAIN_MAPPING' ) && DOMAIN_MAPPING ) {
if ( function_exists( 'domain_mapping_siteurl' ) && function_exists( 'get_original_url' ) ) {
$mapped_domain = domain_mapping_siteurl( false );
$original_domain = get_original_url( 'siteurl' );
$css_url = str_replace( $original_domain, $mapped_domain, $css_url );
}
}
// Strip protocols
$css_url = str_replace( 'https://', '//', $css_url );
$css_url = str_replace( 'http://', '//', $css_url );
return $css_url;
}
/**
* Returns the cache buster id
* @return String
*/
private function get_cache_buster() {
$filename = $this->css_dir . DIRECTORY_SEPARATOR . 'assets.json';
$buster = file_exists($filename) ? json_decode(file_get_contents($filename), true) : false;
return $buster ? $buster['assets'] : Null;
}
/**
* Creates a json file with the cache buster
*
* @param string Cache buster id
* @return String
*/
private function set_cache_buster($id) {
global $wp_filesystem;
// Initialize the Wordpress filesystem.
if ( empty( $wp_filesystem ) ) {
require_once( ABSPATH . '/wp-admin/includes/file.php' );
WP_Filesystem();
}
$buster = ['assets' => $id];
$buster = json_encode($buster);
if ( ! $wp_filesystem->put_contents( $this->css_dir . DIRECTORY_SEPARATOR . 'assets.json', $buster, FS_CHMOD_FILE ) ) {
error_log('Can\'t create Cache buster. Something went wrong');
return false;
}
}
/**
* Delete all css-files, except the one with the new cache buster
* @return String
*/
private function delete_old_styles($new_file_buster) {
$files = glob( $this->css_dir . DIRECTORY_SEPARATOR . '*.css' );
foreach ( $files as $file ) {
// If the file has the new buster, don't delete it (caus its new)!
if (is_file( $file ) && strpos(basename($file), $new_file_buster) === false) {
unlink( $file );
}
}
}
/**
* Enqueue the dynamic CSS.
*/
public function enqueue_dynamic_css() {
wp_enqueue_style( 'dynamic-css', $this->css_file_url );
}
public function refresh_css() {
global $wp_filesystem;
// Initialize the Wordpress filesystem.
if ( empty( $wp_filesystem ) ) {
require_once( ABSPATH . '/wp-admin/includes/file.php' );
WP_Filesystem();
}
$content = "/********* Compiled - Do not edit *********/\n" . $this->compile_scss();
// Take care of domain mapping
if ( defined( 'DOMAIN_MAPPING' ) && DOMAIN_MAPPING ) {
if ( function_exists( 'domain_mapping_siteurl' ) && function_exists( 'get_original_url' ) ) {
$mapped_domain = domain_mapping_siteurl( false );
$mapped_domain = str_replace( 'https://', '//', $domain_mapping );
$mapped_domain = str_replace( 'http://', '//', $mapped_domain );
$original_domain = get_original_url( 'siteurl' );
$original_domain = str_replace( 'https://', '//', $original_domain );
$original_domain = str_replace( 'http://', '//', $original_domain );
$content = str_replace( $original_domain, $mapped_domain, $content );
}
}
// Strip protocols
$content = str_replace( 'https://', '//', $content );
$content = str_replace( 'http://', '//', $content );
// Create a unique buster
$buster = uniqid();
// Create file
if ($this->can_write()) {
if ( ! $wp_filesystem->put_contents( $this->get_cached_file_path($buster), $content, FS_CHMOD_FILE ) ) {
// Fail!
error_log('Can\'t create CSS. Something went wrong');
return false;
}
// Update the cache buster file
$this->set_cache_buster($buster);
// Delete old files
$this->delete_old_styles($buster);
} else {
error_log('Can\'t create CSS. File does not exist or is not writable');
}
}
/*
* Determines if the CSS file is writable.
*/
public function can_write() {
// Does the folder exist?
if ( file_exists( $this->css_dir ) ) {
// Folder exists, but is the folder writable?
if ( ! is_writable( $this->css_dir ) ) {
// Folder is not writable.
error_log('CSS file could not be written.');
return false;
}
} else {
// Can we create the folder?
// returns true if yes and false if not.
return wp_mkdir_p( $this->css_dir );
}
// all is well!
return true;
}
/**
* Compiles the Kirki Variables and SCSS-file to a CSS-string
*
* @return string
*/
public function compile_scss()
{
$scss = new Compiler();
$scss->setImportPaths( get_stylesheet_directory() . '/assets/styles' );
$scss->setFormatter( 'ScssPhp\ScssPhp\Formatter\Compressed' );
$variables = \Kirki_Util::get_variables();
$vars = '';
foreach ( $variables as $variable => $value ) {
if (is_string($value)) {
$vars .= '$' . $variable . ':' . $value . ';';
}
}
// Create SCSS string from Kirki variables and scss functions we've written
$scss_string = $vars . '
@import "theme"';
// Compile SCSS string to CSS string
$css = $scss->compile( $scss_string );
return $css;
}
}
$compiler = new DynamicCSS([
'filename' => 'styles',
'foldername' => 'dynamic-css',
]);
@ge022
Copy link

ge022 commented Aug 29, 2017

I wanted to do this, but after thinking for awhile I realized that integrating sass into the customizer simply isn't worth my time.

@Robbertdk
Copy link
Author

@ge022 I think that is a good solution when your SCSS-files are quite small. You can easily recreate some SCSS-functions in PHP, via the output property in the field. You can call your recreated scss-functions in the sanitize_callback. For example:

'output' => array(
    array(
        'element'  => '#my-div',
        'property' => 'background-color',
    ),
    array(
        'element'           => '#my-div',
        'property'          => 'border-color',
        'sanitize_callback' => 'darken',
    ),
),
function darken( $value ) {
    // Get RGB values
    $hex = str_replace('#', '', $hex);
    if (strlen($hex) == 3) {
        $hex = str_repeat(substr($hex,0,1), 2).str_repeat(substr($hex,1,1), 2).str_repeat(substr($hex,2,1), 2);
    }

    // Split into three parts: R, G and B
    $color_parts = str_split($hex, 2);
    $return = '#';

    foreach ($color_parts as $color) {
        $color   = hexdec($color); // Convert to decimal
        $color   = max(0,min(255, $color - 20)); // Adjust color
        $return .= str_pad(dechex($color), 2, '0', STR_PAD_LEFT); // Make two char hex code
    }

    return $return;
}

@JeffAspen
Copy link

@Robbertdk

Have you figured out a way for a user to see their changes when controls are selected? I'm still having issues with the user can't see the changes until they've saved the changes.

@Robbertdk
Copy link
Author

Robbertdk commented Sep 13, 2017

Hi @JeffAspen, unfortunately not.
Edit: I found a solution. I add the scss as inline style on pageload, only when we're in the customizer.
Otherwise I add the generated css file. See the updates in this gist.

Based my solution on Kirki's new save to file functionality, https://github.com/aristath/kirki/blob/1b97e9c53182f77d590af8d56b8af89c4065101f/modules/css/class-kirki-modules-css.php
Kirki's solution is much more extensive, but this works in my case.

@perusoa
Copy link

perusoa commented Sep 13, 2017

@Robbertdk

I'm attempting to get this to work so that when I change a color in the customizer it will update the inline styles just as you explained. I am struggling getting this to work properly and when I look at the CSS link being added to the head it is coming up as:

<link rel='stylesheet' id='dynamic-css-css' href='//local.demo.local/wp-content/uploads/dynamic-css/styles-.css?ver=4.7.2' type='text/css' media='all' />

So far I have this code file above added to the root of my theme. I then I added the following lines to my functions.php file:
require_once ( $dir.'/scssphp/scss.inc.php' ); require_once ( $dir.'/kirki-sass-compiler.php' );

Do I have to edit some of the code above in this GIST to get this working for my specific build?

Any help/insight would be greatly appreciated! thanks so much!

@Robbertdk
Copy link
Author

Robbertdk commented Sep 15, 2017

@perusoa

  1. Wordpress tries to enqueue the dynamic stylesheet, but there isn't one created yet so it can't find any. I've updated the gist so a css file with the default options is created on theme activation (and deleted on switch_theme)
  2. It looks like you use an other scss package, I use Leafo\ScssPhp.

@emanueljacob
Copy link

Thanks for your gist, i really appreciate it.

In 269 you should check wether the value is a string or not, because values in the variables array can themselves be from type array, which would throw a warning in php.

@jessekafor
Copy link

jessekafor commented May 18, 2019

@Robbertdk How could we now use the script when Kirki’s output is basic inline css in the header?

@Robbertdk
Copy link
Author

I don't get notifications for these comments, so it suprises me when I see new comments over here. So, sorry for the delay in my answers.

@ecksite good point, thanks! I've added it to the gist.

@jessekafor I don't see the problem. I use the latest version of Kirki and generated css file of this gist is added as an external script and not inlined. Can you eleborate on the problem you encounter?

@Robbertdk
Copy link
Author

Because I can't add commit messages to changes on this script, I just leave them here:

Today I've commited the following changes:

  1. The Leafo\ScssPhp composer package is archived, development of that package will be continued under the package ScssPhp\ScssPhp. So I swapped that.
  2. Remove unneeded error log message
  3. Add check for the Kirki class so it won't fail when the Kirki plugin is not activated.
  4. Add string check for variables added via the customizer as mentioned by @ecksite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment