Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

CSS and JavaScript minify and combine in MVC3

0.00/5 (No votes)
20 Jul 2013 1  
CSS and JavaScript minify and combine in MVC3.

Have you ever wondered how to minify and combine your CSS and JavaScript and switch from something like this:

<link href='http://www.codeproject.com/Content/default.css' rel='stylesheet' media='all'/>
<link href='http://www.codeproject.com/Content/themes/base/jquery.ui.base.css' rel='stylesheet' media='all'/>
<link href='http://www.codeproject.com/Content/themes/base/jquery.ui.theme.css' rel='stylesheet' media='all'/>
<link href='http://www.codeproject.com/Content/farbtastic.css' rel='stylesheet' media='all'/>
.....

... to something like this:

<link href='http://www.codeproject.com/MinifiedAndCombined.css' rel='stylesheet' media='all'/>

... without any manual configuration, post-build, and with URL automatically changed every time you build new version?

OK, this is how I do it in my MVC3 projects - just using simple HTML helper:

@Html.StyleCollection(
        "/Content/default.css",
        "/Content/themes/base/jquery.ui.base.css",
        "/Content/themes/base/jquery.ui.theme.css",
        "/Content/farbtastic.css",
        "/Content/fileuploader.css",
        ....
    )

The helper itself creates unique file name for the collection (MD5 hash) and saves combined and minified code into that file:

public static MvcHtmlString StyleCollection(this HtmlHelper helper, params string[] partialFiles)
{
string hash = CollectionHash(partialFiles);

try
{
var path = HttpContext.Current.Server.MapPath("/Content/" + hash + ".css");

if (!File.Exists(path))
{
#region invoke CssMin

using (var outFile = File.OpenWrite(path))
{
using (var outWriter = new StreamWriter(outFile))
{
foreach (var f in partialFiles)
{
using (var inFile = File.OpenRead(HttpContext.Current.Server.MapPath(f)))
{
using (var inReader = new StreamReader(inFile))
{
new CssMin().Minify(inReader, outWriter);
}
}
}
}
}

#endregion
}

return new MvcHtmlString("<link href='http://www.codeproject.com/Content/" + 
  hash + ".css' rel='stylesheet' media='all'/>");
}
catch
{
return new MvcHtmlString(string.Join("\r\n", 
  partialFiles.Select(f => "<link href='" + f + "' rel='stylesheet' media='all'/>")));
}
}

This is how I calculate hash. Please note that assembly hash is included so every time you rebuild the project it will generate different file names:

private static string CollectionHash(string[] partialFiles)
{
var sb = new StringBuilder();

foreach (var f in partialFiles)
sb.Append(f);

sb.Append(Assembly.GetExecutingAssembly().GetHashCode());

using (var md5 = MD5.Create())
{
byte[] inputBytes = Encoding.ASCII.GetBytes(sb.ToString());
byte[] h = md5.ComputeHash(inputBytes);

var sb2 = new StringBuilder();
for (int i = 0; i < h.Length; i++)
sb2.Append(h[i].ToString("X2"));

return sb2.ToString();
}
}

And finally the CSS minifier code itslef:

public sealed class CssMin
{
const int EOF = -1;

TextReader tr;
StreamWriter sb;
int theA;
int theB;
int theLookahead = EOF;


public string Minify(TextReader reader, StreamWriter writer)
{
sb = writer;
tr = reader;
theA = '\n';
theB = 0;
theLookahead = EOF;
cssmin();
return sb.ToString();
}

void cssmin()
{
action(3);
while (theA != EOF)
{
switch (theA)
{
case ' ':
{
switch (theB)
{
case ' ': //body.Replace(" ", String.Empty);
case '{': //body = body.Replace(" {", "{");
case ':': //body = body.Replace(" {", "{");
case '\n': //body = body.Replace(" \n", "\n");
case '\r': //body = body.Replace(" \r", "\r");
case '\t': //body = body.Replace(" \t", "\t");
action(2);
break;
default:
action(1);
break;
}
break;
}
case ':':
case ',':
{
switch (theB)
{
case ' ': //body.Replace(": ", ":"); body.Replace(", ", ","); 
action(3);
break;
default:
action(1);
break;
}
break;
}
case ';':
{
switch (theB)
{
case ' ': //body.Replace("; ", ";"); 
case '\n': //body = body.Replace(";\n", ";");
case '\r': //body = body.Replace(";\r", ";");
case '\t': //body = body.Replace(";\t", ";");
action(3);
break;
case '}': //body.Replace(";}", "}");
action(2);
break;
default:
action(1);
break;
}
break;
}
case '\t': //body = body.Replace("\t", "");
case '\r': //body = body.Replace("\r", "");
case '\n': //body = body.Replace("\n", "");
action(2);
break;
default:
action(1);
break;
}
}
}
/* action -- do something! What you do is determined by the argument:
1 Output A. Copy B to A. Get the next B.
2 Copy B to A. Get the next B. (Delete A).
3 Get the next B. (Delete B).
*/
void action(int d)
{
if (d <= 1)
{
put(theA);
}
if (d <= 2)
{
theA = theB;
if (theA == '\'' || theA == '"')
{
for (; ; )
{
put(theA);
theA = get();
if (theA == theB)
{
break;
}
if (theA <= '\n')
{
throw new FormatException(string.Format("Error: unterminated string literal: {0}\n", theA));
}
if (theA == '\\')
{
put(theA);
theA = get();
}
}
}
}
if (d <= 3)
{
theB = next();
if (theB == '/' && (theA == '(' || theA == ',' || theA == '=' ||
theA == '[' || theA == '!' || theA == ':' ||
theA == '&' || theA == '|' || theA == '?' ||
theA == '{' || theA == '}' || theA == ';' ||
theA == '\n'))
{
put(theA);
put(theB);
for (; ; )
{
theA = get();
if (theA == '/')
{
break;
}
else if (theA == '\\')
{
put(theA);
theA = get();
}
else if (theA <= '\n')
{
throw new FormatException(string.Format(
  "Error: unterminated Regular Expression literal : {0}.\n", theA));
}
put(theA);
}
theB = next();
}
}
}
/* next -- get the next character, excluding comments. peek() is used to see
if a '/' is followed by a '/' or '*'.
*/
int next()
{
int c = get();
if (c == '/')
{
switch (peek())
{
case '/':
{
for (; ; )
{
c = get();
if (c <= '\n')
{
return c;
}
}
}
case '*':
{
get();
for (; ; )
{
switch (get())
{
case '*':
{
if (peek() == '/')
{
get();
return ' ';
}
break;
}
case EOF:
{
throw new FormatException("Error: Unterminated comment.\n");
}
}
}
}
default:
{
return c;
}
}
}
return c;
}
/* peek -- get the next character without getting it.
*/
int peek()
{
theLookahead = get();
return theLookahead;
}
/* get -- return the next character from stdin. Watch out for lookahead. If
the character is a control character, translate it to a space or
linefeed.
*/
int get()
{
int c = theLookahead;
theLookahead = EOF;
if (c == EOF)
{
c = tr.Read();
}
if (c >= ' ' || c == '\n' || c == EOF)
{
return c;
}
if (c == '\r')
{
return '\n';
}
return ' ';
}
void put(int c)
{
sb.Write((char)c);
}
}

In a similar way you can create ScriptCollection helper, the only difference is that you should use different minifier, for example JsMin C# version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here