Less.js in WordPress

I found the desire to enqueue .less files into my wordpress theme. I tried a few back end less processors, which worked really well, but their code was a port of less to php, and was a very old version. So I decided to do it by hand. This whole php function in heavily inspired by wp-less. My requirements were

  1. modularity
  2. low server intensity
  3. nill to little impact on everyday users

All you have to do is enqueue the .less as you would a normal css file in your functions.php. The name of the file will then be used to save the compiled css

require get_template_directory() . '/less-js.php';
add_action( 'wp_enqueue_scripts', 'enqueueLess', 1 );
function enqueueLess() {
	wp_enqueue_style( 'less-style', '/style.less', array(), null, 'all');
}

Options

$alwaysCompile

If set to true, the php will never check for, or try to save, cached css, thus it will always compile the .less file. Useful for debugging

$directory

Set the directory of the cached css. The directory written will be placed within your theme folder

$lessLink

Set the link to the less.js file, I’ve only tested this with less.js 3.7.1, but as long as how it places in the inline style in the head, and the naming structures dont change, any version should work.

$minify

Runs an optional minifying script while saving the cached css.

$vars

There are default variables sent to your less.js script. I use these mainly to import different less libraries and mixins:

  • @themeurl
  • @themedirectory
  • @parenturl *
  • @parentdirectory *

*  Useful if you are using a child theme

If you want to send variables to your less.js script, you would do it like:

add_filter( 'less_vars', 'my_less_vars', 10, 2 );
function my_less_vars( $vars, $handle ) {
	$vars[ 'black' ] = "#000";
	return $vars;
}

This is how the php file works

  1. Parses through the enqueued css
  2. returns without altering any files that do not contain “.less”
  3. Checks if a cached version of the compiled less file exist
  4. If a cached version does exist, compares the “Last Modified” time against a WordPress option called “less-js-{id}” containing the cached css’s less’ files “Last Modifed” time
  5. If the .less hasn’t been changed, convert the .less enqueue source to the cached source.
  6. If the .less has been changed, or doesnt exist, add “/less” to rel, and enqueue the less.js file
  7. On page load, the less.js file runs as usual
  8. After the less.js script, another script is ran that listens to the DOM Head, and executes whenever the DOM changes. That script grabs the inline css generated by less.js, and send it via ajax back to WordPress
  9. The wp_ajax & wp_ajax_nopriv sends that style to be saved back in the server, and updates the Wordpress option called “less-js-{id}” with the less’ files “Last Modifed” time.

<?php
/**
 * Plugin Name:  Less.js in Wordpress
 * Plugin URI:   https://github.com/dryane/Less.js-In-Wordpress
 * Description:  Allows you to enqueue <code>.less</code> files and have them automatically compiled whenever a change is detected.
 * Author:       Daniel Joseph Ryan
 * Version:      1.4
 * Author URI:   https://danieljosephryan.com/
 * License:      MIT
 */

// Busted! No direct file access
! defined( 'ABSPATH' ) AND exit;

if ( ! class_exists( 'less_js' ) ) {
	add_action( 'init', array( 'less_js', 'instance' ) );
	class less_js {

		protected static $instance = null;
		/**
		 * Creates a new instance. Called on 'after_setup_theme'.
		 * May be used to access class methods from outside.
		 *
		 * @see    __construct()
		 * @static
		 * @return \less_js
		 */
		public static function instance() {
			null === self:: $instance AND self:: $instance = new self;
			return self:: $instance;
		}

		public $alwaysCompile = false;

		public $directory = "/less-css/";

		public $lessLink = "https://cdnjs.cloudflare.com/ajax/libs/less.js/3.7.1/less.min.js";

		public $minify = true;

		public $enqueuedBefore = false;

		public $vars = array();

		/**
		 * Constructor
		 */
		public function __construct() {
			add_filter( 'style_loader_tag', array( $this,'parse_enqueued_style'), 10, 4 ); 

			add_action( 'wp_ajax_save_less', array( $this,'save_less') );
			add_action( 'wp_ajax_nopriv_save_less', array( $this,'save_less') );

			add_action( 'customize_save_after', array( $this,'delete_less') );
			
		}

		public function parse_enqueued_style( $html, $handle, $href, $media ) {
			if ( !strpos($html, '.less') ) { // If not .less
				return $html;
			}

			$serverCSSFile = $this->get_cache_dir(true) . $handle . ".css";
			
			if ( file_exists( $serverCSSFile ) ) {
				$exists = true;
			} else {
				$exists = false;
			}
			
			$serverLESSFile = $_SERVER['DOCUMENT_ROOT'] . str_replace( get_site_url() , "", $href);
			$modTime = filemtime($serverLESSFile);
			if (get_option("less-js-".$handle) == $modTime) {
				$unchanged = true;
			} else {
				$unchanged = false;
			}
			
			if ($exists && $unchanged && !$this->alwaysCompile){ // is cached
				$html = str_replace($href,$this->get_cache_dir() . $handle . ".css",$html);
				return $html;
			} else {
				$html = str_replace("rel='stylesheet'",'rel="stylesheet/less"',$html);
				$this->enqueue_less_scripts();
				return $html; 	
			} 
		}
		
		public function variables() {
			/**
			 * How to Add More vars
			 * variable names MUST be lowercase letters only
			 * add_filter( 'less_vars', 'my_less_vars', 10, 2 );
			 * function my_less_vars( $vars, $handle ) {
			 * 	$vars[ 'black' ] = "#000";
			 * 	return $vars;
			 * }
			 * 
			 **/
			
			$this->vars['themeurl'] = '"' . get_stylesheet_directory_uri() . '"';
			$this->vars['themedirectory'] = '"' . get_stylesheet_directory() . '"';
			$this->vars['parenturl'] = '"' . get_template_directory_uri() . '"';
			$this->vars['parentdirectory'] = '"' . get_template_directory() . '"';
			$this->vars = apply_filters( 'less_vars', $this->vars, $handle );
			
			$vars;
			$values = $this->vars;
			$keys = array_keys($this->vars);
			
			for ($i = 0; $i < count($keys); $i++) {
				$vars .= '\'' . $keys[$i] . '\':\'' . $values[$keys[$i]] . '\'';
				if ($i + 1 != count($keys)) {
					$vars .= ",";
				}
			}
			return $vars;
			
		}
		public function enqueue_less_scripts() {
			
			if ($this->enqueuedBefore) {
				return;
			}
			$this->enqueuedBefore = true;
						
			wp_enqueue_script( 'less-js', $this->lessLink, array('jquery') , null, false  );

			$vars = $this->variables();
			
			$before_script = "var less = {env:'development',errorReporting:'console',globalVars:{ $vars }};";
			wp_add_inline_script( 'less-js', $before_script, 'before' );

			$after_script = <<<END
function sendLess(e){window.location.pathname.replace(RegExp("%","g"),"-").replace(RegExp("/","g"),"");var t=e.previousSibling.id.replace("-css",""),s=e.previousSibling.href,n=e.innerHTML,a={};a.name=t,a.href=s,a.css=n,jQuery.post(document.location.origin+"/wp-admin/admin-ajax.php",{action:"save_less",data:a})}document.querySelector("html").addEventListener("DOMNodeInserted",function(e){try{e.target.id.startsWith("less:")&&sendLess(e.target)}catch(e){}}),jQuery(document).ready(function(){style=document.querySelectorAll("head style");for(var e=0;e<style.length;e++)style[e].id.startsWith("less:")&&sendLess(style[e])});
END;
			if (!$this->alwaysCompile) {		
				wp_add_inline_script( 'less-js', $after_script, 'after' );	
			}	
		}
		
		public function save_less() {
			$data = $_POST['data'];
			if ($this->minify) {
				$css = $this->minifyCss($data['css']);	
			}

			$my_file = $this->get_cache_dir(true) . $data['name'] . ".css";
			$handle = fopen($my_file, 'w') or die('Cannot open file:  '.$my_file);
			fwrite($handle, stripslashes($css));
			fclose($handle);
			
			$serverLESSFile = $_SERVER['DOCUMENT_ROOT'] . str_replace( get_site_url() , "", $data['href']);
			$modTime = filemtime($serverLESSFile);
			update_option("less-js-".$data['name'], $modTime);
			
			wp_die();

			return;
			
		}
		public function delete_less() {
			$server = $this->get_cache_dir(true);
			if ( file_exists( $server ) ) {
				$files = glob($server . '*');
				foreach($files as $file){
					if(is_file($file)){
						unlink($file);
					}
				}
			}
		}
		/** Helper Functions **/
		public function get_cache_dir($returnServer = false) {

			$server = get_stylesheet_directory() . $this->directory;
			$baseurl = get_stylesheet_directory_uri() . $this->directory;

			if ( ! file_exists( $server ) ) {
				mkdir( $server );
			}
			if ($returnServer) {
				return $server;
			}
			return $baseurl;

		}
		public function minifyCss($css) {
			$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
			preg_match_all('/(\'[^\']*?\'|"[^"]*?")/ims', $css, $hit, PREG_PATTERN_ORDER);
			for ($i=0; $i < count($hit[1]); $i++) {
				$css = str_replace($hit[1][$i], '##########' . $i . '##########', $css);
			}
			$css = preg_replace('/;[\s\r\n\t]*?}[\s\r\n\t]*/ims', "}\r\n", $css);
			$css = preg_replace('/;[\s\r\n\t]*?([\r\n]?[^\s\r\n\t])/ims', ';$1', $css);
			$css = preg_replace('/[\s\r\n\t]*:[\s\r\n\t]*?([^\s\r\n\t])/ims', ':$1', $css);
			$css = preg_replace('/[\s\r\n\t]*,[\s\r\n\t]*?([^\s\r\n\t])/ims', ',$1', $css);
			$css = preg_replace('/[\s\r\n\t]*>[\s\r\n\t]*?([^\s\r\n\t])/ims', '>$1', $css);
			$css = preg_replace('/[\s\r\n\t]*\+[\s\r\n\t]*?([^\s\r\n\t])/ims', '+$1', $css);
			$css = preg_replace('/[\s\r\n\t]*{[\s\r\n\t]*?([^\s\r\n\t])/ims', '{$1', $css);
			$css = preg_replace('/([\d\.]+)[\s\r\n\t]+(px|em|pt|%)/ims', '$1$2', $css);
			$css = preg_replace('/([^\d\.]0)(px|em|pt|%)/ims', '$1', $css);
			$css = preg_replace('/\p{Zs}+/ims',' ', $css);
			$css = str_replace(array("\r\n", "\r", "\n"), '', $css);
			for ($i=0; $i < count($hit[1]); $i++) {
				$css = str_replace('##########' . $i . '##########', $hit[1][$i], $css);
			}
			return $css;
		}
		/** End Helper Functions **/
		
	}
}

do_action( 'customize_save_after', $array );