Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inlining CSS in C#

Tags:

c#

css

inlining

I need to inline css from a stylesheet in c#.

Like how this works.

http://www.mailchimp.com/labs/inlinecss.php

The css is simple, just classes, no fancy selectors.

I was contemplating using a regex (?<rule>(?<selector>[^{}]+){(?<style>[^{}]+)})+ 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.

Is there a simpler approach? Or something already written in c#?

UPDATE - Sep 16, 2010

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 <style /> element. Then converts the css selectors to xpath expressions, and adds the style inline to the matching elements, before any pre-existing inline style.

Note, that the CssToXpath is not fully implemented, there are some things it just can't do... yet.

CssInliner.cs

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*(?<rule>(?<selector>[^{}]+){(?<style>[^{}]+)})",                                                 RegexOptions.IgnoreCase                                                 | RegexOptions.CultureInvariant                                                 | RegexOptions.IgnorePatternWhitespace                                                 | RegexOptions.Compiled                                             );          public List<Match> 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 && !string.IsNullOrEmpty(inlineStyle.Value))                     {                         newInlineStyle += inlineStyle.Value;                     }                      element.SetAttributeValue("style", newInlineStyle.Trim().NormalizeCharacter(';').NormalizeSpace());                 }             }              XhtmlDocument.Descendants("style").Remove();             InlinedXhtml = XhtmlDocument.ToString();         }          private List<Match> GetStyleMatches()         {             var styles = new List<Match>();              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);         }     } } 

CssToXpath.cs

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 = @"|"                                           },                                           // , + ~ >                                           new RegexReplace {                                               Regex = new Regex(@"\s*(\+|~|>)\s*", RegexOptions.Multiline),                                               Replace = @"$1"                                           },                                           //* ~ + >                                           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_\-\*])>([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(@"([\>\+\|\~\,\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;     } } 

And some tests

    [TestMethod]     public void TestCssToXpathRules()     {         var translations = new Dictionary<string, string>                                {                                    { "*", "//*" },                                     { "p", "//p" },                                     { "p > *", "//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(@"<html>                                         <head>                                             <title>Hello, World Page!</title>                                             <style>                                                 .redClass {                                                      background: red;                                                      color: purple;                                                  }                                             </style>                                         </head>                                         <body>                                             <div class=""redClass"">Hello, World!</div>                                         </body>                                     </html>").ToString();         #endregion          #region const string expected ...         var expected = XElement.Parse(@"<html>                                             <head>                                                 <title>Hello, World Page!</title>                                             </head>                                             <body>                                                 <div class=""redClass"" style=""background: red; color: purple;"">Hello, World!</div>                                             </body>                                         </html>").ToString();         #endregion          var result = new CssInliner.CssInliner(html);          Assert.AreEqual(expected, result.InlinedXhtml);     } 

There are more tests, but, they import html files for the input and expected output and I'm not posting all that!

But I should post the Normalize extension methods!

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()); } 
like image 436
CaffGeek Avatar asked Sep 09 '10 17:09

CaffGeek


People also ask

What is inlining CSS?

CSS inlining is a form of email preprocessing that moves styles in a CSS style sheet into the body of an HTML email. The term “inlining” refers to the fact that styles are applied “inline” to individual HTML elements.

What is inline CSS with example?

An inline CSS is used to apply a unique style to a single HTML element. An inline CSS uses the style attribute of an HTML element.

What is inlining critical CSS?

The Tools and Technique By including the critical CSS inline, we can dramatically reduce the perceived load time. Eliminating the step of loading separate CSS files allows the browser to parse the CSS simultaneously with the HTML, rendering the page and making it interactive not long after the HTML file is returned.


2 Answers

I have a project on Github that makes CSS inline. It's very simple, and support mobile styles. Read more on my blog: http://martinnormark.com/move-css-inline-premailer-net

like image 102
MartinHN Avatar answered Sep 20 '22 11:09

MartinHN


Since you're already 90% of the way there with your current implementation, why don't you use your existing framework but replace the XML parsing with an HTML parser instead? One of the more popular ones out there is the HTML Agility Pack. It supports XPath queries and even has a LINQ interface similar to the standard .NET interface provided for XML so it should be a fairly straightforward replacement.

like image 45
Richard Cook Avatar answered Sep 22 '22 11:09

Richard Cook