Compress CSS with @import statements

I own and run a digital agency called Red Wolf Digital, for the most part my work involves building websites. We use an in-house content management system we call Mercury to build most of the sites we produce for clients. One of the things I’ve been trying to find for some time was a way to automatically minify the CSS used to style these sites.

There are loads of CSS and JavaScript minifiers out there, but I was struggling to find one that would handle @import statements. I’m a great believer in object orientated coding principles and the benefits afforded to you by breaking large, monolithic entities into smaller more manageable chunks. Something that was kept in mind whilst designing and developing Mercury, which of course, makes full use of CSS @import rules. The lack of a minifier that will handle this has prompted me to write a small script to do it for me, I include it here should anyone happen across it and get some use from it.

Firstly, to run through exactly what we’ll be doing. We will:

At this point it’s worth mentioning that there are many more things that could be done to compress the resulting CSS, such as removing the whitespace within the decelerations block. I’m not bothering to do so here, but a few extra regular expressions will likely allow you to do that.

/**
 * Function that takes a string containing CSS and removes all the comments,
 * leaving the CSS unmangled.
 * 
 * Taken from the example at: http://stackoverflow.com/a/1581063
 * 
 * @param	String	$css	Sting containing the CSS to be de-commentified
 * @return	String			The comment free CSS
 */
function removeComments( $css ) {
	$regex = array(
		"`^([\t\s]+)`ism"=>'',
		"`^\/\*(.+?)\*\/`ism"=>"",
		"`([\n\A;]+)\/\*(.+?)\*\/`ism"=>"$1",
		"`([\n\A;\s]+)//(.+?)[\n\r]`ism"=>"$1\n",
		"`(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+`ism"=>"\n"
	);
	return preg_replace( array_keys( $regex ), $regex, $css );
}

/**
 * Parses a string containing CSS and creates an array of all the import rules.
 * 
 * @param	String	$css	String containing the CSS to parse
 * @return	Array			Array containing the CSS matched, a regex to replace
 * 							the CSS and the filename of the imported CSS file
 *							for each of the import matches in the given string.
 */
function getImports( $css ) {
	// Match all the import statements
	preg_match_all( "/@import url\( \"(.*)\" \);/", $css, $matches );

	$includes	= array();
	foreach( $matches[0] as $key => $match ) {
		$import		= "@import url\( \"{$matches[1][ $key ]}\" \);";
		$includes[]	= array(
			"css"		=> $match,
			"regex"		=> "/".str_replace( "/", "\/", $import )."/",
			"file"		=> $matches[1][ $key ]
		);
	}
	return $includes;
}

/* GET THE ROOT DIRECTORY AND BASE CSS FILE FROM THE URL **********************/
$rootDirectory	= $_GET["rootDirectory"];
$rootFile		= $_GET["rootFile"];

/* GET THE FILE CONTENTS, REMOVE COMMENTS *************************************/
$css			= file_get_contents( "{$rootDirectory}{$rootFile}" );
$css			= removeComments( $css );

/* GET IMPORTED FILES, LOOP THROUGH THEM REPLACING WITH CONTENTS **************/
foreach( getImports( $css ) as $import ) {
	$fileName		= "{$rootDirectory}{$import["file"]}";
	// Add a @ in case the file doesn't exist and thows a warning
	$importedCSS	= @file_get_contents( $fileName );
	$importedCSS	= removeComments( $importedCSS );
	$css			= preg_replace( $import["regex"], $importedCSS, $css );
}

/* CONDENSE ANY EXTRA WHITESPACE INTO JUST A SINGLE SPACE CHARACTER ***********/
$css			= preg_replace( "/\s+/", " ", $css );

// Fix any malformed relative URLs
$css			= preg_replace( "/(\.\.\/)+/", "../", $css );

header( "Content-Type: text/css" );
echo $css;

The above code is expanded for readability, there are a range of places you could drop some variables and nest function calls.

The two $_GET parameters are used to pull in the relevant CSS file. rootDirectory is the directory where the CSS file lives and where all import rules are relative to. rootFile is the CSS file that you wish to compress.

We remove comments each time a file is pulled in, which is probably not the most efficient way to handle them, however we have large header comments in each of our source files that we’d rather have stripped out earlier – rather than have a massive bloat as we concatenate all the CSS files.

We correct for malformed relative URLs from the imported CSS files, changing them all to “../” as we serve the CSS from a css/ directory at the site root, whereas all our included files live in subdirectories of that. You should be able to change or remove this based on how you’re referencing resources in your CSS (or alternatively, write CSS that takes this into account).

Only one level of @import statements are checked for, since that’s all we tend to use at Red Wolf Digital. It’s easy enough to set up a recursive function call to check each of the imported files’ contents as you pull them in. Likewise only @import: url( "[stylesheet]" ); formatted imports are catered for, feel free to extend upon it to add support for the various other styles accepted for importing stylesheets.

We are compressing and serving the CSS every time in this example, a better solution is of course to cache a pre-compressed version to disk so you can serve it to the browser if nothing has changed. You can set that up on your own!