Minifying Inline Css, Js and Html using Tag Helpers in ASP.NET Core

 

        Introduction:

 

                I think Tag Helpers is one of coolest feature in ASP.NET Core. Tag helpers allows our server side code to participate in generating final html response using normal (or custom) html tags. In this article, I will show you how we can leverage tag helpers to minify inline css and inline javascript. I will also show you how we can minify the html.           

 

        Description:

 

                   Note that I am using RC1 at the time of writing. First we need the css, javascript and html minifiers. You can use any minifier library which work for you but here I will use the minifiers available in ServiceStack source. Here are the html, css and js minifiers,    

 

 

    public class BasicHtmlMinifier
    {
        static Regex BetweenScriptTagsRegEx = new Regex(@"<script[^>]*>[\w|\t|\r|\W]*?</script>", RegexOptions.Compiled);
        static Regex BetweenTagsRegex = new Regex(@"(?<=[^])\t{2,}|(?<=[>])\s{2,}(?=[<])|(?<=[>])\s{2,11}(?=[<])|(?=[\n])\s{2,}|(?=[\r])\s{2,}", RegexOptions.Compiled);
        static Regex MatchBodyRegEx = new Regex(@"</body>", RegexOptions.Compiled);

        public static string MinifyHtml(string html)
        {
            if (html == null)
                return html;

            var mymatch = BetweenScriptTagsRegEx.Matches(html);
            html = BetweenScriptTagsRegEx.Replace(html, string.Empty);
            html = BetweenTagsRegex.Replace(html, string.Empty);

            var str = string.Empty;
            foreach (Match match in mymatch)
            {
                str += match.ToString();
            }

            html = MatchBodyRegEx.Replace(html, str + "</body>");
            return html;
        }
    }

 

 

    public class JSMinifier
    {
        const int EOF = -1;

        TextReader sr;
        StringBuilder sb;
        int theA;
        int theB;
        int theLookahead = EOF;

        public string Compress(string js)
        {
            using (sr = new StringReader(js))
            {
                sb = new StringBuilder();
                jsmin();
                return sb.ToString(); // return the minified string  
            }
        }

        public static string MinifyJs(string js) //removed the out file path  
        {
            return new JSMinifier().Compress(js);
        }

        /* jsmin -- Copy the input to the output, deleting the characters which are 
                insignificant to JavaScript. Comments will be removed. Tabs will be 
                replaced with spaces. Carriage returns will be replaced with linefeeds. 
                Most spaces and linefeeds will be removed. 
        */
        void jsmin()
        {
            theA = '\n';
            action(3);
            while (theA != EOF)
            {
                switch (theA)
                {
                    case ' ':
                        {
                            if (isAlphanum(theB))
                            {
                                action(1);
                            }
                            else
                            {
                                action(2);
                            }
                            break;
                        }
                    case '\n':
                        {
                            switch (theB)
                            {
                                case '{':
                                case '[':
                                case '(':
                                case '+':
                                case '-':
                                    {
                                        action(1);
                                        break;
                                    }
                                case ' ':
                                    {
                                        action(3);
                                        break;
                                    }
                                default:
                                    {
                                        if (isAlphanum(theB))
                                        {
                                            action(1);
                                        }
                                        else
                                        {
                                            action(2);
                                        }
                                        break;
                                    }
                            }
                            break;
                        }
                    default:
                        {
                            switch (theB)
                            {
                                case ' ':
                                    {
                                        if (isAlphanum(theA))
                                        {
                                            action(1);
                                            break;
                                        }
                                        action(3);
                                        break;
                                    }
                                case '\n':
                                    {
                                        switch (theA)
                                        {
                                            case '}':
                                            case ']':
                                            case ')':
                                            case '+':
                                            case '-':
                                            case '"':
                                            case '\'':
                                                {
                                                    action(1);
                                                    break;
                                                }
                                            default:
                                                {
                                                    if (isAlphanum(theA))
                                                    {
                                                        action(1);
                                                    }
                                                    else
                                                    {
                                                        action(3);
                                                    }
                                                    break;
                                                }
                                        }
                                        break;
                                    }
                                default:
                                    {
                                        action(1);
                                        break;
                                    }
                            }
                            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). 
           action treats a string as a single character. Wow! 
           action recognizes a regular expression if it is preceded by ( or , or =. 
        */
        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 Exception(string.Format("Error: JSMIN 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 Exception(string.Format("Error: JSMIN 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 Exception("Error: JSMIN 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 = sr.Read();
            }
            if (c >= ' ' || c == '\n' || c == EOF)
            {
                return c;
            }
            if (c == '\r')
            {
                return '\n';
            }
            return ' ';
        }

        void put(int c)
        {
            sb.Append((char)c);
        }
        /* isAlphanum -- return true if the character is a letter, digit, underscore, 
                dollar sign, or non-ASCII character. 
        */
        bool isAlphanum(int c)
        {
            return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
                    (c >= 'A' && c <= 'Z') || c == '_' || c == '$' || c == '\\' ||
                    c > 126);
        }
    }

 

 

    public class CssMinifier
    {
        public static string MinifyCss(string css)
        {
            css = Regex.Replace(css, @"[a-zA-Z]+#", "#");
            css = Regex.Replace(css, @"[\n\r]+\s*", String.Empty);
            css = Regex.Replace(css, @"\s+", " ");
            css = Regex.Replace(css, @"\s?([:,;{}])\s?", "$1");
            css = css.Replace(";}", "}");
            css = Regex.Replace(css, @"([\s:]0)(px|pt|%|em)", "$1");

            // Remove comments from CSS
            css = Regex.Replace(css, @"/\[\d\D]?\*/", String.Empty);

            return css;
        }
    }

 

 

                    BasicHtmlMinifier.MinifyHtml will minify the html, JsMinifier.MinifyJs will minify javascript and CssMinifier.MinifyCss will minify css. Now let's add our html, script and style tag helpers,

 

 

    public class HtmlTagHelper : TagHelper
    {
        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (output == null)
            {
                throw new ArgumentNullException(nameof(output));
            }

            var html = await output.GetChildContentAsync();
            var minifiedHtml = BasicHtmlMinifier.MinifyHtml(html.GetContent());
            output.Content.SetHtmlContent(minifiedHtml);
        }
    }

 

 

    public class ScriptTagHelper : TagHelper
    {
        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (output == null)
            {
                throw new ArgumentNullException(nameof(output));
            }

            var js = (await output.GetChildContentAsync()).GetContent();
            if (!string.IsNullOrWhiteSpace(js))
            {
                var minifiedJs = JSMinifier.MinifyJs(js);
                output.Content.SetHtmlContent(minifiedJs);
            }
        }
    }

 

 

    public class StyleTagHelper : TagHelper
    {
        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (output == null)
            {
                throw new ArgumentNullException(nameof(output));
            }

            var css = await output.GetChildContentAsync();
            var minifiedCss = CssMinifier.MinifyCss(css.GetContent());
            output.Content.SetHtmlContent(minifiedCss);
        }
    }

 

                    These tag helpers simply get the inner/child contents of the tag, then minify and set the minified contents. Finally just add these tag helpers inside your razor/cshtml view(s) where you need to minify,

 

 

@addTagHelper "YourNameSpace.HtmlTagHelper, YourProject"
@addTagHelper "YourNameSpace.StyleTagHelper, YourProject"
@addTagHelper "YourNameSpace.ScriptTagHelper, YourProject"

   

                    Now just run your application and see the view source, you will see the html, inline css and inline js are minified.  

 

 

        Summary:

 

                    In this article, I showed you how easily we can minify html, inline css and inline javascript using tag helpers (which makes this task very easy). I have used ServiceStack helpers to minify but you can use any minifier you like.

1 Comment

  • Could you please help me..i have a question related to your previous blog http://weblogs.asp.net/imranbaloch/chart-helper-in-asp-net-mvc-3-0-with-transparent-background. Using chart helper to create chart and render image to the view. I am sending a chart theme.I have added 3 series for example all are of type column and i would want to specify color for each. Is it possible to do it in Chart helper without taking a default color it is showing?

Comments have been disabled for this content.