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 ' ': case '{': case ':': case '\n': case '\r': case '\t': action(2);
break;
default:
action(1);
break;
}
break;
}
case ':':
case ',':
{
switch (theB)
{
case ' ': action(3);
break;
default:
action(1);
break;
}
break;
}
case ';':
{
switch (theB)
{
case ' ': case '\n': case '\r': case '\t': action(3);
break;
case '}': action(2);
break;
default:
action(1);
break;
}
break;
}
case '\t': case '\r': case '\n': action(2);
break;
default:
action(1);
break;
}
}
}
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.