Before I join the language debate I’d like to share something entirely different. My take on optimizing your css files for production.
Making css files production ready is surprisingly simple. I use the following setup in my applications.
The css compressor (just removes whitespace in this configuration).
public class CssMinifier
{
static readonly Regex whitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ECMAScript);
// You can uncomment the following line if you're not using css hacks that rely on comment notation.
//static readonly Regex commentsRegex = new Regex(@"\/\*(.*?)\*\/ ", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ECMAScript);
static readonly Regex addLineBreaksRegex = new Regex(@"\} ", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ECMAScript);
static readonly Regex removeLastBreakRegex = new Regex(@"\n$", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ECMAScript);
static readonly Regex trimInsideLeftBracketsRegex = new Regex(@" \{ ", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ECMAScript);
static readonly Regex trimInsideRightBracketsRegex = new Regex(@"; \}", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ECMAScript);
public static string Compress(string source)
{
source = whitespaceRegex.Replace(source, " ");
// You can uncomment the following line if you're not using css hacks that rely on comment notation.
//source = commentsRegex.Replace(source, string.Empty);
source = addLineBreaksRegex.Replace(source, "}\r\n");
source = removeLastBreakRegex.Replace(source, string.Empty);
source = trimInsideLeftBracketsRegex.Replace(source, " {");
source = trimInsideRightBracketsRegex.Replace(source, "}");
return source;
}
}
(\cf4=”" <\cf1="" list\cf0="" new\cf0="" csslib="\par" >=”" .mappath(folder);\par=”" fileoperations\cf0=”" path=”\cf4″ folder)\par=”" target,=”" stringbuilder\cf0=”" readfolder(\cf4=”" void\cf0=”" static\cf0=”" private\cf0=”" );\par=”" ?\\r\\n?\cf0=”" target.append(\cf5=”" target.append(readfile(fpath));\par=”" fpath);\par=”" \\r\\n\\r\\n?\cf0=”" *=”" \{0\}=”" ?=”" target.appendformat(\cf5=”" fpath)\par=”" readpath(\cf4=”" ??\cf0=”" #endif\par=”" ??\cf1=”" .readalltext(path);\par=”" file\cf0=”" system.io.\cf4=”" #else\par=”" fileoperations.readfile(path);\par=”" return=”" ??\cf7=”" mode.\par=”" production=”" in=”" is=”" app=”" the=”" when=”" disk=”" from=”" read=”" file=”" cache=”" \cf6=”" !debug=”" #if\cf0=”" .mappath(relativepath);\par=”" relativepath)\par=”" readfile(\cf1=”" context.response.write(sb.tostring());\par=”" context.response.write(cssminifier.compress(sb.tostring()));\par=”" context.response.cache.setvaliduntilexpires(true);\par=”" context.response.cache.setexpires(datetime.now.adddays(1));\par=”" context.response.cache.setcacheability(httpcacheability.public);\par=”" mode\par=”" release=”" handler=”" this=”" of=”" results=”" caching=”" start=”" ;\par=”" css?\cf0=”" ?text=”" context.response.contenttype=”\cf5″ cssbase);\par=”" readfolder(sb,=”" ))\par=”" ?application?\cf0=”" (context.request.url.absoluteuri.contains(\cf5=”" ();\par=”" sb=”\cf1″ ];\par=”" ?windows?\cf0=”" windowstheme=”context.Request.QueryString[\cf5" context.response.clear();\par="" context)\par="" httpcontext\cf0="" processrequest(\cf4="" common="" ~="">I will typically have a FileOperations helper class that takes care of common file system things.
public class StylesHandler :IHttpHandler
{
private static string windowsTheme;
private const string CSSBASE = "~/common/css";
public void ProcessRequest(HttpContext context)
{
context.Response.Clear();
windowsTheme = context.Request.QueryString["windows"];
StringBuilder sb = new StringBuilder();
if (context.Request.Url.AbsoluteUri.Contains("application"))
ReadFolder(sb, CSSBASE);
context.Response.ContentType = "text/css";
#if !DEBUG //start caching the results of this handler when in release mode
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetExpires(DateTime.Now.AddDays(1));
context.Response.Cache.SetValidUntilExpires(true);
context.Response.Write(CssMinifier.Compress(sb.ToString()));
#else
context.Response.Write(sb.ToString());
#endif
}
private static string ReadFile(string relativePath)
{
string path = FileOperations.MapPath(relativePath);
#if !DEBUG //Cache the file read from disk when the app is in production mode.
return FileOperations.ReadFile(path);
#else
return System.IO.File.ReadAllText(path);
#endif
}
private static void ReadPath(StringBuilder target, string fPath)
{
target.AppendFormat("/* {0} */\r\n\r\n", fPath);
target.Append(ReadFile(fPath));
target.Append("\r\n");
}
private static void ReadFolder(StringBuilder target, string folder)
{
string path = FileOperations.MapPath(folder);
List<string> csslib =
new List<string>(Directory.GetFiles(path, "*.css", SearchOption.TopDirectoryOnly));
foreach (string s in csslib)
{
ReadPath(target, s);
}
if(!string.IsNullOrEmpty(windowsTheme))
{
ReadPath(target, string.Format("{0}/window_themes/{1}.css", CSSBASE, windowsTheme));
}
}
public bool IsReusable
{
get { return true; }
}
}
July 4, 2007 at 10:37
Nice.
I’d prefer processing it when I deploy, rather an at request time (even if it is cached).
This would give you greater control over the delivered control, as you can test locally far easier.
What I really want to do sometime (if I ever have a free weekend) is write something that will refactor the rules – so join disparate styles, sort the properties (color, background, font, ..etc), and minimise (probably onto one line rules for deployments sake – but no more, gzip will can do that while keeping the code readable)
July 4, 2007 at 11:00
I agree that there is more that can be done.
I chose for this approach of doing it at runtime with caching because it allows for the css person to create separate files keeping his stuff organized without having too much of a deployment hassle.
I try to maintain xcopy deployment which means that I don’t want any extra steps except just copying the files to the webserver and probably changing the connection string and application runtime mode.
If I would have to start scripting a complete deployment script that’s when deployment has the potential to evolve into a nightmare.
Look at ruby deployment which is easily qualified as a less than optimal experience.
One of the strong points of .net is that there is a web.config file in which you can quickly change the behaviour of your application.
I could for example add a config property that disables the minifying if some errors occur.
That’s mostly why I chose for an @ run-time version.