« Disposing handlers in ASP.NET AJAX: addHandler vs. addHandlers | Main | Resourceful ASP.NET MVC: Multiple approaches to optimizing CSS »

April 11, 2008

Minifying embedded CSS using stream filters

Update [04/12/2008]: I made an assumption that <style> tags, like <script> tags, can have src attributes. In actuality, they will, according to this draft of XHTML 2.0, but today we use <link> tags to reference external styles. Now, we can update our filter described below to pull out the <link> tags and replace their href resources with our handler, but it is better to just bundle this up with the ASP.NET Theme combiner. In other words, a small change to that code which you will in the next post will automatically send external styles to the handler.

As an extension to my previous posts on CSS minification and combining ASP.NET Theme styles at runtime, a further step we need to take to truly optimize our production styles is to apply the same process to CSS styles that do not live in your Theme folder. These arrive either through our own doing, by directly creating CSS styles in <style> tags on our page, or against our will, when a well-meaning third party library injects the styles for us.

We already have a handler in place; the same handler we built to combine our Theme styles can also be used to reference individual files. What's missing is a way to minify the styles that appear in <style> tags, and a way to utilize both of these solutions at runtime.

We can accomplish this using one of my favorite techniques: stream filtering. I always think of stream filters like camera filters: you can add multiple filters to the same raw output stream of html, and they all do their job in sequence, leaving you with the transformed output.

So this is our plan of attack: for every <style> tag that possesses a 'src' attribute, in other words its contents are obtained from an external source, we will re-route that location to our existing style handler. For every <style> tag that does not possess a 'src' attribute, its inline style content will be minified on the spot.

To accomplish this we're going to build a special kind of Stream filter base class that we can reuse for task that requires us to intercept a section of text, and replace some of its contents with new content. In this case we're looking in markup text for <style> tags with and without 'src' attributes, and either replacing the 'src' attribute value with a handler query to the same location (to minify, cache, and compress it for us), or we're replacing the contents of the <style> tag itself with its minified version (and leaving any compression tasks up to whatever method is handling the entire page, if any). So far so good?

namespace Dimebrain.Web.Filters
{
    public abstract class ReplaceFilterStream : Stream
    {
        private readonly Stream _stream;
 
        #region Stream Overrides
 
        protected ReplaceFilterStream(Stream stream)
        {
            _stream = stream;
        }
 
        public override bool CanRead
        {
            get { return _stream.CanRead; }
        }
 
        public override bool CanSeek
        {
            get { return _stream.CanSeek; }
        }
 
        public override bool CanWrite
        {
            get { return _stream.CanWrite; }
        }
 
        public override long Length
        {
            get { return _stream.Length; }
        }
 
        public override long Position
        {
            get { return _stream.Position; }
            set { _stream.Position = value; }
        }
 
        public override void Flush()
        {
            _stream.Flush();
        }
 
        public override int Read(byte[] buffer, int offset, int count)
        {
            return _stream.Read(buffer, offset, count);
        }
 
        public override long Seek(long offset, SeekOrigin origin)
        {
            return _stream.Seek(offset, origin);
        }
 
        public override void SetLength(long value)
        {
            _stream.SetLength(value);
        }
 
        public override void Write(byte[] buffer, int offset, int count)
        {
            var text = Encoding.Default.GetString(buffer);
            text = Find(text);
 
            var bytes = Encoding.Default.GetBytes(text);
            _stream.Write(bytes, 0, bytes.Length);
        } 
        #endregion
 
        #region Private Methods
 
        private string Find(string html)
        {
            return SubjectPattern.Replace(html, new MatchEvaluator(Found));
        }
 
        public string Found(Match m)
        {
            return ReplacePattern.Replace(m.Value, new MatchEvaluator(Replace));
        }
 
        #endregion
 
        public abstract Regex SubjectPattern { get; }
        public abstract Regex ReplacePattern { get; }
        public abstract string Replace(Match m);
    }
}

This abstract class simply applies a pattern of recognition, and you provide the replacement method. What you need to implement are the two Regex patterns that are used for the subject (a subset of a large block of text, such as your entire page) and replacer (a subset of the subject, that you're interested in replacing with your own text), and the method that is called when our custom Stream finds the text you want to replace in its buffer.

Sending external CSS in <style> tags to our handler

Now that we have our base Stream, we'll implement two filters with it for each task. The first is to offload external CSS embedded in <style> tags to our handler. To find our <style> blocks with src attributes, we'll use a simple Regex for the subject that returns the opening <style> tag up to the content of the src attribute. The replacer Regex will return the last quoted section (in other words, just the src attribute content including its initial quote), which is what we'll replace with our handler query. This is what our new filter might look like:

 
namespace Dimebrain.Web.Filters
{
    public class InlineStyleToHandlerStream : ReplaceFilterStream
    {
        // Match a <style> tag with a src attribute, capturing up to that attribute's content and first quote
        private static readonly Regex StyleWithSrc = new Regex(@"<style\s.*src=""[A-Za-z0-9/_\-.\?]+css",
                                                               RegexOptions.Compiled | 
                                                               RegexOptions.IgnoreCase |
                                                               RegexOptions.Singleline |
                                                               RegexOptions.Multiline);
 
        // Match the last occurrence of a quote (from the src attribute) and any characters after
        private static readonly Regex StyleSrc = new Regex(@"""[A-Za-z0-9/_\-.\?]*$",
                                                           RegexOptions.Compiled | 
                                                           RegexOptions.IgnoreCase);
 
        public InlineStyleToHandlerStream(Stream stream) : base(stream)
        {
        }
 
        public override Regex SubjectPattern
        {
            get { return StyleWithSrc; }
        }
 
        public override Regex ReplacePattern
        {
            get { return StyleSrc; }
        }
 
        public override string Replace(Match m)
        {
            if (m.Value.IsNotNullOrEmpty() && !m.Value.Contains(".ashx"))
            {
                var context = HttpContext.Current;
                if (context == null)
                {
                    return m.Value;
                }
 
                // Replace this with your own requirements
                var src = m.Value.Substring(1, m.Value.Length - 1);
                var path = context.Request.Path.Substring(0, context.Request.Path.LastIndexOf("/") + 1);
                var file = "~{0}".Fill(String.Concat(path, src));
                var query = "css.axd?f={0}".Fill(file);
                var value = "\"{0}".Fill(query);
 
                return value;
            }
            return m.Value;
        }
    }
}
While resolving the embedded src attribute to a real application-relative link will likely require a bit more work, this example demonstrates how the ReplaceFilterStream can be leveraged to shuttle all external <style> tags to our handler for caching, compression, and minification. What's important here is when that replacement occurs so that the page makes the handler requests. For that you'll need the HttpModule at the end of this article.

Minifying inline CSS in <style> tags

Our last scenario to round out our CSS optimization arsenal it to minify <style> information that exists directly in the page itself. This might arrive through another library or we may have done this ourselves as a way to marry page-specific CSS to its owner. However it got there, it's likely nice and readable, and therefore needs to be minified. Let's build up another filter to do the job. Our subject Regex is an entire <style> block but only if it does not contain a 'src' attribute, otherwise we'd be wasting our time; our replacer Regex is everything within the <style> block that we want to minify. Here's the code for the next filter:

namespace Dimebrain.Web.Filters
{
    public class InlineStyleMinifyStream : ReplaceFilterStream
    {
        // Match any <style> tag that does not have a src attribute, and include its inner content
        private static readonly Regex StyleWithoutSrc = new Regex(@"<style\s?(?:(?!src).)*/style>",
                                                                  RegexOptions.Compiled |
                                                                  RegexOptions.IgnoreCase |
                                                                  RegexOptions.Singleline |
                                                                  RegexOptions.Multiline);
 
        // Match the content within the matched <style> tag
        private static readonly Regex StyleContent = new Regex(@">[^>]*>(.*)<",
                                                               RegexOptions.Compiled | 
                                                               RegexOptions.IgnoreCase |
                                                               RegexOptions.Singleline | 
                                                               RegexOptions.Multiline);
 
        public InlineStyleMinifyStream(Stream stream) : base(stream)
        {
 
        }
 
        public override Regex ReplacePattern
        {
            get { return StyleContent; }
        }
 
        public override Regex SubjectPattern
        {
            get { return StyleWithoutSrc; }
        }
 
        public override string Replace(Match m)
        {
            if (m.Value.IsNotNullOrEmpty() && !m.Value.Contains(".ashx"))
            {
                var context = HttpContext.Current;
                var inner = m.Value.Substring(1, m.Value.Length - 2);
 
                // Replace the angle brackets and minify the contents
                return context == null ? m.Value : ">{0}<".Fill(inner.CssMinify());
            }
            return m.Value;
        }
    }
}

This stream will now take an innocent block of <style> and minify it on the spot. The caveat to using this filter, is that it is not taking advantage of our handler (it can't), so the overhead necessary to minify the contents will occur on every page load; the most we can do is pre-compile our Regex patterns in advance. Also remember that we leave compression in this case up to the Page the <style> is served on.

Through this three article series you have learned how to optimize CSS styles across your application while retaining your personal taste, comments, and tedium of manually optimizing deployment styles.

Some readers wanted a chance to download this content as a sample project rather than piece together the code themselves (probably because I make liberal use of extension methods), so in my next post you'll see how to implement these strategies in a working, downloadable ASP.NET MVC web application! In the meantime, the following basic HttpModule is what you'll need to lock these streams into your pipeline:

namespace Dimebrain.Web.Modules
{
    public sealed class InlineStyleModule : IHttpModule
    {
        #region IHttpModule Members
 
        void IHttpModule.Dispose()
        {
        }
 
        void IHttpModule.Init(HttpApplication context)
        {
            context.PostReleaseRequestState += OnPostReleaseRequestState;
        }
 
        #endregion
 
        private static void OnPostReleaseRequestState(object sender, EventArgs e)
        {
            var app = sender as HttpApplication;
 
            if (app == null || !(app.Context.CurrentHandler is Page))
            {
                return;
            }
 
            // ASP.NET AJAX doesn't like these filters
            if (app.Context.Request.Headers["X-MicrosoftAjax"].IsNotNull())
            {
                return;
            }
 
            app.Response.Filter = new InlineStyleToHandlerStream(app.Response.Filter);
            app.Response.Filter = new InlineStyleMinifyStream(app.Response.Filter);
        }
    }
}

kick it on DotNetKicks.com

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/t/trackback/2508730/27406538

Listed below are links to weblogs that reference Minifying embedded CSS using stream filters:

Comments

The comments to this entry are closed.


Open Source

The Book

(Coming Soon)

Micro-updates

Get Microsoft Silverlight

Archived Code

Bookshelf