title: Inlining CSS in C#
body: <p>I need to inline css from a stylesheet in c#.</p>
  
  <p>Like how this works. </p>
  
  <p><a href="http://www.mailchimp.com/labs/inlinecss.php" rel="noreferrer">http://www.mailchimp.com/labs/inlinecss.php</a></p>
  
  <p>The css is simple, just classes, no fancy selectors.</p>
  
  <p>I was contemplating using a regex <code>(?&lt;rule&gt;(?&lt;selector&gt;[^{}]+){(?&lt;style&gt;[^{}]+)})+</code> to strip the rules from the css, and then attempting to do simple string replaces where the classes are called, but some of the html elements already have a style tag, so I'd have to account  for that as well.</p>
  
  <p>Is there a simpler approach?  Or something already written in c#?</p>
  
  <h2>UPDATE - Sep 16, 2010</h2>
  
  <p>I've been able to come up with a simple CSS inliner provided your html is also valid xml.  It uses a regex to get all the styles in your <code>&lt;style /&gt;</code> element.  Then converts the css selectors to xpath expressions, and adds the style inline to the matching elements, before any pre-existing inline style.</p>
  
  <p>Note, that the CssToXpath is not fully implemented, there are some things it just can't do... yet.</p>
  
  <p>CssInliner.cs</p>
  
  <pre><code>using System.Collections.Generic;
  using System.Text.RegularExpressions;
  using System.Xml.Linq;
  using System.Xml.XPath;
  
  namespace CssInliner
  {
      public class CssInliner
      {
          private static Regex _matchStyles = new Regex("\\s*(?&lt;rule&gt;(?&lt;selector&gt;[^{}]+){(?&lt;style&gt;[^{}]+)})",
                                                  RegexOptions.IgnoreCase
                                                  | RegexOptions.CultureInvariant
                                                  | RegexOptions.IgnorePatternWhitespace
                                                  | RegexOptions.Compiled
                                              );
  
          public List&lt;Match&gt; Styles { get; private set; }
          public string InlinedXhtml { get; private set; }
  
          private XElement XhtmlDocument { get; set; }
  
          public CssInliner(string xhtml)
          {
              XhtmlDocument = ParseXhtml(xhtml);
              Styles = GetStyleMatches();
  
              foreach (var style in Styles)
              {
                  if (!style.Success)
                      return;
  
                  var cssSelector = style.Groups["selector"].Value.Trim();
                  var xpathSelector = CssToXpath.Transform(cssSelector);
                  var cssStyle = style.Groups["style"].Value.Trim();
  
                  foreach (var element in XhtmlDocument.XPathSelectElements(xpathSelector))
                  {
                      var inlineStyle = element.Attribute("style");
  
                      var newInlineStyle = cssStyle + ";";
                      if (inlineStyle != null &amp;&amp; !string.IsNullOrEmpty(inlineStyle.Value))
                      {
                          newInlineStyle += inlineStyle.Value;
                      }
  
                      element.SetAttributeValue("style", newInlineStyle.Trim().NormalizeCharacter(';').NormalizeSpace());
                  }
              }
  
              XhtmlDocument.Descendants("style").Remove();
              InlinedXhtml = XhtmlDocument.ToString();
          }
  
          private List&lt;Match&gt; GetStyleMatches()
          {
              var styles = new List&lt;Match&gt;();
  
              var styleElements = XhtmlDocument.Descendants("style");
              foreach (var styleElement in styleElements)
              {
                  var matches = _matchStyles.Matches(styleElement.Value);
  
                  foreach (Match match in matches)
                  {
                      styles.Add(match);
                  }
              }
  
              return styles;
          }
  
          private static XElement ParseXhtml(string xhtml)
          {
              return XElement.Parse(xhtml);
          }
      }
  }
  </code></pre>
  
  <p>CssToXpath.cs</p>
  
  <pre><code>using System.Text.RegularExpressions;
  
  namespace CssInliner
  {
      public static class CssToXpath
      {
          public static string Transform(string css)
          {
              #region Translation Rules
              // References:  http://ejohn.org/blog/xpath-css-selectors/
              //              http://code.google.com/p/css2xpath/source/browse/trunk/src/css2xpath.js
              var regexReplaces = new[] {
                                            // add @ for attribs
                                            new RegexReplace {
                                                Regex = new Regex(@"\[([^\]~\$\*\^\|\!]+)(=[^\]]+)?\]", RegexOptions.Multiline),
                                                Replace = @"[@$1$2]"
                                            },
                                            //  multiple queries
                                            new RegexReplace {
                                                Regex = new Regex(@"\s*,\s*", RegexOptions.Multiline),
                                                Replace = @"|"
                                            },
                                            // , + ~ &gt;
                                            new RegexReplace {
                                                Regex = new Regex(@"\s*(\+|~|&gt;)\s*", RegexOptions.Multiline),
                                                Replace = @"$1"
                                            },
                                            //* ~ + &gt;
                                            new RegexReplace {
                                                Regex = new Regex(@"([a-zA-Z0-9_\-\*])~([a-zA-Z0-9_\-\*])", RegexOptions.Multiline),
                                                Replace = @"$1/following-sibling::$2"
                                            },
                                            new RegexReplace {
                                                Regex = new Regex(@"([a-zA-Z0-9_\-\*])\+([a-zA-Z0-9_\-\*])", RegexOptions.Multiline),
                                                Replace = @"$1/following-sibling::*[1]/self::$2"
                                            },
                                            new RegexReplace {
                                                Regex = new Regex(@"([a-zA-Z0-9_\-\*])&gt;([a-zA-Z0-9_\-\*])", RegexOptions.Multiline),
                                                Replace = @"$1/$2"
                                            },
                                            // all unescaped stuff escaped
                                            new RegexReplace {
                                                Regex = new Regex(@"\[([^=]+)=([^'|""][^\]]*)\]", RegexOptions.Multiline),
                                                Replace = @"[$1='$2']"
                                            },
                                            // all descendant or self to //
                                            new RegexReplace {
                                                Regex = new Regex(@"(^|[^a-zA-Z0-9_\-\*])(#|\.)([a-zA-Z0-9_\-]+)", RegexOptions.Multiline),
                                                Replace = @"$1*$2$3"
                                            },
                                            new RegexReplace {
                                                Regex = new Regex(@"([\&gt;\+\|\~\,\s])([a-zA-Z\*]+)", RegexOptions.Multiline),
                                                Replace = @"$1//$2"
                                            },
                                            new RegexReplace {
                                                Regex = new Regex(@"\s+\/\/", RegexOptions.Multiline),
                                                Replace = @"//"
                                            },
                                            // :first-child
                                            new RegexReplace {
                                                Regex = new Regex(@"([a-zA-Z0-9_\-\*]+):first-child", RegexOptions.Multiline),
                                                Replace = @"*[1]/self::$1"
                                            },
                                            // :last-child
                                            new RegexReplace {
                                                Regex = new Regex(@"([a-zA-Z0-9_\-\*]+):last-child", RegexOptions.Multiline),
                                                Replace = @"$1[not(following-sibling::*)]"
                                            },
                                            // :only-child
                                            new RegexReplace {
                                                Regex = new Regex(@"([a-zA-Z0-9_\-\*]+):only-child", RegexOptions.Multiline),
                                                Replace = @"*[last()=1]/self::$1"
                                            },
                                            // :empty
                                            new RegexReplace {
                                                Regex = new Regex(@"([a-zA-Z0-9_\-\*]+):empty", RegexOptions.Multiline),
                                                Replace = @"$1[not(*) and not(normalize-space())]"
                                            },
                                            // |= attrib
                                            new RegexReplace {
                                                Regex = new Regex(@"\[([a-zA-Z0-9_\-]+)\|=([^\]]+)\]", RegexOptions.Multiline),
                                                Replace = @"[@$1=$2 or starts-with(@$1,concat($2,'-'))]"
                                            },
                                            // *= attrib
                                            new RegexReplace {
                                                Regex = new Regex(@"\[([a-zA-Z0-9_\-]+)\*=([^\]]+)\]", RegexOptions.Multiline),
                                                Replace = @"[contains(@$1,$2)]"
                                            },
                                            // ~= attrib
                                            new RegexReplace {
                                                Regex = new Regex(@"\[([a-zA-Z0-9_\-]+)~=([^\]]+)\]", RegexOptions.Multiline),
                                                Replace = @"[contains(concat(' ',normalize-space(@$1),' '),concat(' ',$2,' '))]"
                                            },
                                            // ^= attrib
                                            new RegexReplace {
                                                Regex = new Regex(@"\[([a-zA-Z0-9_\-]+)\^=([^\]]+)\]", RegexOptions.Multiline),
                                                Replace = @"[starts-with(@$1,$2)]"
                                            },
                                            // != attrib
                                            new RegexReplace {
                                                Regex = new Regex(@"\[([a-zA-Z0-9_\-]+)\!=([^\]]+)\]", RegexOptions.Multiline),
                                                Replace = @"[not(@$1) or @$1!=$2]"
                                            },
                                            // ids
                                            new RegexReplace {
                                                Regex = new Regex(@"#([a-zA-Z0-9_\-]+)", RegexOptions.Multiline),
                                                Replace = @"[@id='$1']"
                                            },
                                            // classes
                                            new RegexReplace {
                                                Regex = new Regex(@"\.([a-zA-Z0-9_\-]+)", RegexOptions.Multiline),
                                                Replace = @"[contains(concat(' ',normalize-space(@class),' '),' $1 ')]"
                                            },
                                            // normalize multiple filters
                                            new RegexReplace {
                                                Regex = new Regex(@"\]\[([^\]]+)", RegexOptions.Multiline),
                                                Replace = @" and ($1)"
                                            },
  
                                        };
              #endregion
  
              foreach (var regexReplace in regexReplaces)
              {
                  css = regexReplace.Regex.Replace(css, regexReplace.Replace);
              }
  
              return "//" + css;
          }
      }
  
      struct RegexReplace
      {
          public Regex Regex;
          public string Replace;
      }
  }
  </code></pre>
  
  <p>And some tests</p>
  
  <pre><code>    [TestMethod]
      public void TestCssToXpathRules()
      {
          var translations = new Dictionary&lt;string, string&gt;
                                 {
                                     { "*", "//*" }, 
                                     { "p", "//p" }, 
                                     { "p &gt; *", "//p/*" }, 
                                     { "#foo", "//*[@id='foo']" }, 
                                     { "*[title]", "//*[@title]" }, 
                                     { ".bar", "//*[contains(concat(' ',normalize-space(@class),' '),' bar ')]" }, 
                                     { "div#test .note span:first-child", "//div[@id='test']//*[contains(concat(' ',normalize-space(@class),' '),' note ')]//*[1]/self::span" }
                                 };
  
          foreach (var translation in translations)
          {
              var expected = translation.Value;
              var result = CssInliner.CssToXpath.Transform(translation.Key);
  
              Assert.AreEqual(expected, result);
          }
      }
  
      [TestMethod]
      public void HtmlWithMultiLineClassStyleReturnsInline()
      {
          #region var html = ...
          var html = XElement.Parse(@"&lt;html&gt;
                                          &lt;head&gt;
                                              &lt;title&gt;Hello, World Page!&lt;/title&gt;
                                              &lt;style&gt;
                                                  .redClass { 
                                                      background: red; 
                                                      color: purple; 
                                                  }
                                              &lt;/style&gt;
                                          &lt;/head&gt;
                                          &lt;body&gt;
                                              &lt;div class=""redClass""&gt;Hello, World!&lt;/div&gt;
                                          &lt;/body&gt;
                                      &lt;/html&gt;").ToString();
          #endregion
  
          #region const string expected ...
          var expected = XElement.Parse(@"&lt;html&gt;
                                              &lt;head&gt;
                                                  &lt;title&gt;Hello, World Page!&lt;/title&gt;
                                              &lt;/head&gt;
                                              &lt;body&gt;
                                                  &lt;div class=""redClass"" style=""background: red; color: purple;""&gt;Hello, World!&lt;/div&gt;
                                              &lt;/body&gt;
                                          &lt;/html&gt;").ToString();
          #endregion
  
          var result = new CssInliner.CssInliner(html);
  
          Assert.AreEqual(expected, result.InlinedXhtml);
      }
  </code></pre>
  
  <p>There are more tests, but, they import html files for the input and expected output and I'm not posting all that!</p>
  
  <p>But I should post the Normalize extension methods!</p>
  
  <pre><code>private static readonly Regex NormalizeSpaceRegex = new Regex(@"\s{2,}", RegexOptions.None);
  public static string NormalizeSpace(this string data)
  {
      return NormalizeSpaceRegex.Replace(data, @" ");
  }
  
  public static string NormalizeCharacter(this string data, char character)
  {
      var normalizeCharacterRegex = new Regex(character + "{2,}", RegexOptions.None);
      return normalizeCharacterRegex.Replace(data, character.ToString());
  }
  </code></pre>