Got more questions? Find advice on: ASP | SQL | XML | Windows
in Search
Welcome to RegexAdvice Sign in | Join | Help

Michael Ash's Regex Blog

Regex Musings

Follow up to Additional CSS minifying regex patterns

OK, there regexes were discussed in the previous post this is mostly just their application.

This is a C# 2.0 enhancement of a C# port of YUI Compressor's  CSS minification code

 Since I was doing this is C# I took full advantage of it's regex engine, namely using lookbehinds and delegates for some replaces.

Almost all the regexes after the "New Test" comment are the new or modified regexes from the ported version. There is also one new and two modified expressions before that comment. One of those modification is just a change in writing style, the other modifications are replacing some code but (hopefully) not functionality with a regex replace. The new regex replacements of course are the new compression enhancements.

There are also a couple of new regexes not mentioned in the previous post that match and replace some of the color values with an equivalent but a more concisely written value. The replace the color "red" is a straight replace but the other colors require some code evaluation and are using delegates.

I've done some very limited testing but as I mentioned in the previous post most of the CSS I've written doesn't have some of the new things I was searching for. I could add them for a test (which I did) but that won't catch any problems they my cause to the actual CSS application since I wasn't really using the test values. So the source code is now available for beta testing.  Test early and often before committing to use it.  I'm willing to fix any minor bugs for things I may have overlook but if a particular replace is problematic it's easy enough to comment out the offender and use the rest.

And as was mentioned in the comments of the previous post any generated content that looks like CSS may get stepped on so be aware of that.

And also that all licenses for previous versions still apply.

UPDATE 2008-04-27

After a little more testing I discovered one of the replaces I was doing can alter how the CSS is processed.  So I have just crossed out the  functions and function call
I've come up with a safer, though less likely to occur replacement.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
 namespace CSSMinify
{
 class CSSMinify
 {
   public static Hashtable shortColorNames = new Hashtable();
   public static Hashtable shortHexColors = new Hashtable();
   public static string Minify(string css)
   {
     return Minify(css, 0);
   }
   public static string Minify(string css, int columnWidth)
   {
   // BSD License http://developer.yahoo.net/yui/license.txt
   // New css tests and regexes by Michael Ash
     createHashTable();
     MatchEvaluator rgbDelegate = new MatchEvaluator(RGBMatchHandler);
     MatchEvaluator shortColorNameDelegate = new     MatchEvaluator(ShortColorNameMatchHandler);
     MatchEvaluator shortColorHexDelegate = new MatchEvaluator(ShortColorHexMatchHandler);
     css = RemoveCommentBlocks(css);
     css = Regex.Replace(css, @"\s+", " "); //Normalize whitespace
     css = Regex.Replace(css, @"\x22\x5C\x22}\x5C\x22\x22", "___PSEUDOCLASSBMH___"); //hide Box model hack
     /* Remove the spaces before the things that should not have spaces before           them.
         But, be careful not to turn "p :link {...}" into "p:link{...}"
     */
     css = Regex.Replace(css, @"(?#no preceding space needed)\s+((?:[!{};>+()\],])|(?<={[^{}]*):(?=[^}]*}))", "$1");
     css = Regex.Replace(css, @"([!{}:;>+([,])\s+", "$1"); // Remove the spaces after the things that should not have spaces after them.
     css = Regex.Replace(css, @"([^;}])}", "$1;}"); // Add the semicolon where it's missing.
     css = Regex.Replace(css, @"(\d+)\.0+(p(?:[xct])|(?:[cem])m|%|in|ex)\b", "$1$2"); // Remove .0 from size units x.0em becomes xem
     css = Regex.Replace(css, @"([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)\b", "$1$2"); // Remove unit from zero
     //New test
     css = ShortHandProperty(css);
     //css = Regex.Replace(css, @":(\s*0){2,4}\s*;", ":0;"); // if all parameters zero just use 1 parameter
     // if all 4 parameters the same unit make 1 parameter
     css = Regex.Replace(css, @":\s*(0|(?:(?:\d*\.?\d+(?:p(?:[xct])|(?:[cem])m|%|in|ex))))(\s+\1){1,3};", ":$1;", RegexOptions.IgnoreCase);
     // if has 4 parameters and top unit = bottom unit and right unit = left unit make 2 parameters
     css = Regex.Replace(css, @":\s*((0|(?:(?:\d?\.?\d(?:p(?:[xct])|(?:[cem])m|%|in|ex))))\s+(0|(?:(?:\d?\.?\d(?:p(?:[xct])|(?:[cem])m|%|in|ex)))))\s+\2\s+\3;", ":$1;", RegexOptions.IgnoreCase);
     // if has 4 parameters and top unit != bottom unit and right unit = left unit make 3 parameters
     css = Regex.Replace(css, @":\s*((?:(?:0|(?:(?:\d?\.?\d(?:p(?:[xct])|(?:[cem])m|%|in|ex))))\s+)?(0|(?:(?:\d?\.?\d(?:p(?:[xct])|(?:[cem])m|%|in|ex))))\s+(?:0|(?:(?:\d?\.?\d(?:p(?:[xct])|(?:[cem])m|%|in|ex)))))\s+\2;", ":$1;", RegexOptions.IgnoreCase);
     //// if has 3 parameters and top unit = bottom unit make 2 parameters
     //css = Regex.Replace(css, @":\s*((0|(?:(?:\d?\.?\d(?:p(?:[xct])|    (?:[cem])m|%|in|ex))))\s+(?:0|(?:(?:\d?\.?\d(?:p(?:[xct])|(?:[cem])m|%|in|ex)))))\s+\2;", ":$1;", RegexOptions.IgnoreCase);
     css = Regex.Replace(css,"background-position:0;", "background-position:0 0;");
     css = Regex.Replace(css,@"(:|\s)0+\.(\d+)", "$1.$2");
   // Outline-styles and Border-sytles parameter reduction
     css = Regex.Replace(css, @"(outline|border)-style\s*:\s*(none|hidden|d(?:otted|ashed|ouble)|solid|groove|ridge|inset|outset)(?:\s+\2){1,3};", "$1-style:$2;", RegexOptions.IgnoreCase);
     css = Regex.Replace(css, @"(outline|border)-style\s*:\s*((none|hidden|d(?:otted|ashed|ouble)|solid|groove|ridge|inset|outset)\s+(none|hidden|d(?:otted|ashed|ouble)|solid|groove|ridge|inset|outset ))(?:\s+\3)(?:\s+\4);", "$1-style:$2;", RegexOptions.IgnoreCase);
     css = Regex.Replace(css, @"(outline|border)-style\s*:\s*((?:(?:none|hidden|d(?:otted|ashed|ouble)|solid|groove|ridge|inset|outset)\s+)?(none|hidden|d(?:otted|ashed|ouble)|solid|groove|ridge|inset|outset )\s+(?:none|hidden|d(?:otted|ashed|ouble)|solid|groove|ridge|inset|outset ))(?:\s+\3);", "$1-style:$2;", RegexOptions.IgnoreCase);
     css = Regex.Replace(css, @"(outline|border)-style\s*:\s*((none|hidden|d(?:otted|ashed|ouble)|solid|groove|ridge|inset|outset)\s+(?:none|hidden|d(?:otted|ashed|ouble)|solid|groove|ridge|inset|outset ))(?:\s+\3);", "$1-style:$2;", RegexOptions.IgnoreCase);
     // Outline-color and Border-color parameter reduction
     css = Regex.Replace(css, @"(outline|border)-color\s*:\s*((?:\#(?:[0-9A-F]{3}){1,2})|\S+)(?:\s+\2){1,3};", "$1-color:$2;", RegexOptions.IgnoreCase);
     css = Regex.Replace(css, @"(outline|border)-color\s*:\s*(((?:\#(?:[0-9A-F]{3}){1,2})|\S+)\s+((?:\#(?:[0-9A-F]{3}){1,2})|\S+))(?:\s+\3)(?:\s+\4);", "$1-color:$2;", RegexOptions.IgnoreCase);
     css = Regex.Replace(css, @"(outline|border)-color\s*:\s*((?:(?:(?:\#(?:[0-9A-F]{3}){1,2})|\S+)\s+)?((?:\#(?:[0-9A-F]{3}){1,2})|\S+)\s+(?:(?:\#(?:[0-9A-F]{3}){1,2})|\S+))(?:\s+\3);", "$1-color:$2;", RegexOptions.IgnoreCase);
 // Shorten colors from rgb(51,102,153) to #336699
 // This makes it more likely that it'll get further compressed in the next step.
     css = Regex.Replace(css,@"rgb\s*\x28((?:25[0-5])|(?:2[0-4]\d)|(?:[01]?\d?\d))\s*,\s*((?:25[0-5])|(?:2[0-4]\d)|(?:[01]?\d?\d))\s*,\s*((?:25[0-5])|(?:2[0-4]\d)|(?:[01]?\d?\d))\s*\x29", rgbDelegate);
     css = Regex.Replace(css, @"(?<![\x22\x27=]\s*)\#(?:([0-9A-F])\1)(?:([0-9A-F])\2)(?:([0-9A-F])\3)", "#$1$2$3", RegexOptions.IgnoreCase);
 // Replace hex color code with named value is shorter
     css = Regex.Replace(css, @"(?<=color\s*:\s*.*)\#(?<hex>f00)\b", "red",RegexOptions.IgnoreCase);
     css = Regex.Replace(css, @"(?<=color\s*:\s*.*)\#(?<hex>[0-9a-f]{6})", shortColorNameDelegate, RegexOptions.IgnoreCase);
     css = Regex.Replace(css, @"(?<=color\s*:\s*)\b(Black|Fuchsia|LightSlateGr[ae]y|Magenta|White|Yellow)\b", shortColorHexDelegate,RegexOptions.IgnoreCase);
     // Remove empty rules.
     css = Regex.Replace(css,@"[^}]+{;}", "");
     //Remove semicolon of last property
     css = Regex.Replace(css, ";(})", "$1");
     if (columnWidth > 0)
     {
       css = BreakLines(css, columnWidth);
     }
     return css;
 }
 private static string RemoveCommentBlocks(string input)
 {
   int startIndex = 0;
   int endIndex = 0;
   bool iemac = false;
   startIndex = input.IndexOf(@"/*", startIndex);
   while (startIndex >= 0)
   {
     endIndex = input.IndexOf(@"*/", startIndex + 2);
     if (endIndex >= startIndex + 2)
     {
       if (input[endIndex - 1] == '\\')
       {
         startIndex = endIndex + 2;
         iemac = true;
       }
       else if (iemac)
       {
         startIndex = endIndex + 2;
         iemac = false;
        }
       else
       {
        input = input.Remove(startIndex, endIndex + 2 - startIndex);
        }
     }
     startIndex = input.IndexOf(@"/*", startIndex);
   }
 return input;
}
 private static String RGBMatchHandler(Match m)
 {
   int val = 0;
   StringBuilder hexcolor = new StringBuilder("#");
   for(int index=1; index <= 3; index += 1)
   {
     val = Int32.Parse(m.Groups[index].Value);
     hexcolor.Append(val.ToString("x2"));
   }
   return hexcolor.ToString();
 }
 private static string BreakLines(string css, int columnWidth)
{
   int i = 0;
   int start = 0;
   StringBuilder sb = new StringBuilder(css);
   while (i < sb.Length)
   {
     char c = sb[i++];
     if (c == '}' && i - start > columnWidth)
     {
       sb.Insert(i, '\n');
       start = i;
     }
   }
 return sb.ToString();
 }
 private static string ShortHandProperty(string css)
 {
 /*
  * This function searchs for properties specifying at least 2 of the top, right, bottom or left box model
  * positions and reduces it to a single property use shorthand notation
  */
   Regex reCSSBlock = new Regex("{[^{}]*}");
   Regex reTRBL1 = new Regex(@"(?<fullProperty>(?:(?<property>padding)-(?<position>top|right|bottom|left)))\s*:\s*(?<unit>[\w.]+);?", RegexOptions.IgnoreCase);
   Regex reTRBL2 = new Regex(@"(?<fullProperty>(?:(?<property>margin)-(?<position>top|right|bottom|left)))\s*:\s*(?<unit>[\w.]+);?", RegexOptions.IgnoreCase);
   Regex reTRBL3 = new Regex(@"(?<fullProperty>(?<property>border)-(?<position>top|right|bottom|left)(?<property2>-(?:color)))\s*:\s*(?<unit>[#\w.]+);?", RegexOptions.IgnoreCase);
   Regex reTRBL4 = new Regex(@"(?<fullProperty>(?<property>border)-(?<position>top|right|bottom|left)(?<property2>-(?:style)))\s*:\s*(?<unit>none|hidden|d(?:otted|ashed|ouble)|solid|groove|ridge|inset|outset);?",  RegexOptions.IgnoreCase);
   Regex reTRBL5 = new Regex(@"(?<fullProperty>(?<property>border)-(?<position>top|right|bottom|left)(?<property2>-(?:width)))\s*:\s*(?<unit>[\w.]+);?", RegexOptions.IgnoreCase);
   MatchCollection mcBlocks = reCSSBlock.Matches(css);
   foreach (Match mBlock in mcBlocks)
   {
     string strBlock= mBlock.Value;
     MatchCollection mcProperySet = reTRBL1.Matches(strBlock);
     if (mcProperySet.Count > 1)
     {
       strBlock = ShortHandReplace(mcProperySet, reTRBL1, strBlock);
 
     }
     mcProperySet = reTRBL2.Matches(strBlock);
     if (mcProperySet.Count > 1)
     {
       strBlock = ShortHandReplace(mcProperySet, reTRBL2, strBlock);
     }
     mcProperySet = reTRBL3.Matches(strBlock);
     if (mcProperySet.Count > 1)
     {
       strBlock = ShortHandReplace(mcProperySet, reTRBL3, strBlock);
     }
     mcProperySet = reTRBL4.Matches(strBlock);
     if (mcProperySet.Count > 1)
     {
       strBlock = ShortHandReplace(mcProperySet, reTRBL4, strBlock);
     }
     mcProperySet = reTRBL5.Matches(strBlock);
     if (mcProperySet.Count > 1)
     {
       strBlock = ShortHandReplace(mcProperySet, reTRBL5, strBlock);
     }
     css = css.Replace(mBlock.Value, strBlock);
   }
   return css;
 }
 private static string ShortHandReplace(MatchCollection mcProperySet, Regex reTRBL1, string InputText)
 {
 // Replace method for regexes used in ShortHand property method.
   string strTop, strRight, strBottom, strLeft;
   strTop = string.Empty;
   strRight = string.Empty;
   strBottom = string.Empty;
   strLeft = string.Empty;
   string strProperty;
   string strDefaultValue;
 
   strProperty = string.Format("{0}{1}", mcProperySet[0].Groups["property"].Value, mcProperySet[0].Groups["property2"].Value);
   switch (strProperty){
     case "border-color":
       strDefaultValue = "inherit";
       break;
     case "border-style":
       strDefaultValue = "none";
       break;
     default:
       strDefaultValue = "0";
       break;
   }
   foreach (Match mProperty in mcProperySet)
   {
     if (mProperty.Groups["position"].Value == "top")
     {
       if (strTop == string.Empty)
       {
         strTop = mProperty.Groups["unit"].Value;
       }
       else
       {
         break;
       }
     }
     if (mProperty.Groups["position"].Value == "right")
     {
       if (strRight == string.Empty)
       {
         strRight = mProperty.Groups["unit"].Value;
       }
       else
       {
         break;
       }
      }
     if (mProperty.Groups["position"].Value == "bottom")
     {
       if (strBottom == string.Empty)
       {
         strBottom = mProperty.Groups["unit"].Value;
       }
       else
       {
         break;
       }
     }
     if (mProperty.Groups["position"].Value == "left")
     {
       if (strLeft == string.Empty)
       {
         strLeft = mProperty.Groups["unit"].Value;
       }
       else
       {
         break;
       }
     }
   }
   if (strTop == string.Empty)
   {
     strTop = strDefaultValue;
   }
   if (strRight == string.Empty)
   {
     strRight = strDefaultValue;
   }
   if (strBottom == string.Empty)
   {
     strBottom = strDefaultValue;
   }
   if (strLeft == string.Empty)
   {
     strLeft = strDefaultValue;
   }
   string strShortcut = string.Format("{0}:{1} {2} {3} {4};", strProperty, strTop, strRight, strBottom, strLeft);
   string strNewBlock = reTRBL1.Replace(InputText, "");
   strNewBlock = strNewBlock.Insert(1, strShortcut);
   return strNewBlock;
 }

 private static string ShortColorNameMatchHandler(Match m)
 {
 // This function replace hex color values named colors if the name is shorter than the hex code
   string returnValue = m.Value;
   if (shortColorNames.ContainsKey(m.Groups["hex"].Value))
   {
     returnValue = shortColorNames[m.Groups["hex"].Value].ToString();
   }
   return returnValue;
 }
 private static string ShortColorHexMatchHandler(Match m)
 {
   return shortHexColors[m.Value.ToString().ToLower()].ToString();
 }
 private static void createHashTable()
 {
 //Color names shorter than hex notation. Except for red.
   shortColorNames.Add("F0FFFF".ToLower(), "Azure".ToLower());
   shortColorNames.Add("F5F5DC".ToLower(), "Beige".ToLower());
   shortColorNames.Add("FFE4C4".ToLower(), "Bisque".ToLower());
   shortColorNames.Add("A52A2A".ToLower(), "Brown".ToLower());
   shortColorNames.Add("FF7F50".ToLower(), "Coral".ToLower());
   shortColorNames.Add("FFD700".ToLower(), "Gold".ToLower());
   shortColorNames.Add("808080".ToLower(), "Grey".ToLower());
   shortColorNames.Add("008000".ToLower(), "Green".ToLower());
   shortColorNames.Add("4B0082".ToLower(), "Indigo".ToLower());
   shortColorNames.Add("FFFFF0".ToLower(), "Ivory".ToLower());
   shortColorNames.Add("F0E68C".ToLower(), "Khaki".ToLower());
   shortColorNames.Add("FAF0E6".ToLower(), "Linen".ToLower());
   shortColorNames.Add("800000".ToLower(), "Maroon".ToLower());
   shortColorNames.Add("000080".ToLower(), "Navy".ToLower());
   shortColorNames.Add("808000".ToLower(), "Olive".ToLower());
   shortColorNames.Add("FFA500".ToLower(), "Orange".ToLower());
   shortColorNames.Add("DA70D6".ToLower(), "Orchid".ToLower());
   shortColorNames.Add("CD853F".ToLower(), "Peru".ToLower());
   shortColorNames.Add("FFC0CB".ToLower(), "Pink".ToLower());
   shortColorNames.Add("DDA0DD".ToLower(), "Plum".ToLower());
   shortColorNames.Add("800080".ToLower(), "Purple".ToLower());
   shortColorNames.Add("FA8072".ToLower(), "Salmon".ToLower());
   shortColorNames.Add("A0522D".ToLower(), "Sienna".ToLower());
   shortColorNames.Add("C0C0C0".ToLower(), "Silver".ToLower());
   shortColorNames.Add("FFFAFA".ToLower(), "Snow".ToLower());
   shortColorNames.Add("D2B48C".ToLower(), "Tan".ToLower());
   shortColorNames.Add("008080".ToLower(), "Teal".ToLower());
   shortColorNames.Add("FF6347".ToLower(), "Tomato".ToLower());
   shortColorNames.Add("EE82EE".ToLower(), "Violet".ToLower());
   shortColorNames.Add("F5DEB3".ToLower(), "Wheat".ToLower());
   // Hex notation shorter than named value
   shortHexColors.Add("black", "#000");
   shortHexColors.Add("fuchsia", "#f0f");
   shortHexColors.Add("lightSlategray", "#789");
   shortHexColors.Add("lightSlategrey", "#789");
   shortHexColors.Add("magenta", "#f0f");
   shortHexColors.Add("white", "#fff");
   shortHexColors.Add("yellow", "#ff0");
   }
 }
}
 

Published Friday, April 18, 2008 3:04 PM by mash

Comments

No Comments
Anonymous comments are disabled