Note that there are some explanatory texts on larger screens.

plurals
  1. POInlining CSS in C#
    primarykey
    data
    text
    <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>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload