Click here to Skip to main content
15,868,141 members
Articles / Web Development / CSS

CSS and JavaScript minify and combine in MVC3

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
20 Jul 2013CPOL 9K   3   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:

XML
<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:

XML
<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:

C#
@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:

C#
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:

C#
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:

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionOr you could use the built-in BundleConfig functionality Pin
drzaus1312-Nov-13 6:52
drzaus1312-Nov-13 6:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.