28

我需要从 c# 中的样式表中内联 css。

就像这是如何工作的。

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

CSS 很简单,只有类,没有花哨的选择器。

我正在考虑使用正则表达式(?<rule>(?<selector>[^{}]+){(?<style>[^{}]+)})+从 css 中删除规则,然后尝试在调用类的位置进行简单的字符串替换,但是一些 html 元素已经有一个样式标签,所以我必须考虑到这一点好。

有没有更简单的方法?或者已经用c#编写的东西?

更新 - 2010 年 9 月 16 日

只要您的 html 也是有效的 xml,我就能够想出一个简单的 CSS 内联器。它使用正则表达式来获取<style />元素中的所有样式。然后将 css 选择器转换为 xpath 表达式,并在任何预先存在的内联样式之前将样式内联添加到匹配元素。

请注意,CssToXpath 没有完全实现,有些事情它还不能做......

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;
    }
}

还有一些测试

    [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);
    }

还有更多测试,但是,它们为输入和预期输出导入 html 文件,我没有发布所有这些!

但我应该发布规范化扩展方法!

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());
}
4

8 回答 8

17

我在 Github 上有一个使 CSS 内联的项目。它非常简单,并且支持移动样式。在我的博客上阅读更多信息:http ://martinnormark.com/move-css-inline-premailer-net

于 2011-06-10T11:06:05.423 回答
9

既然您当前的实现已经完成了 90% 的工作,为什么不使用现有的框架,而是用 HTML 解析器替换 XML 解析呢?HTML Agility Pack是最流行的一种。它支持 XPath 查询,甚至具有类似于为 XML 提供的标准 .NET 接口的 LINQ 接口,因此它应该是一个相当简单的替代品。

于 2010-09-21T02:50:19.503 回答
5

由于这个选项在其他回复中不是很清楚,我认为它应该得到一个直截了当的答案。

使用PreMailer.Net

你所要做的就是:

  1. 通过 nuget 安装 PreMailer.NET。
  2. 输入这个:

    var inlineStyles = PreMailer.Net.PreMailer.MoveCssInline(htmlSource, false);
    destination = inlineStyles.Html;
    

你完成了!

顺便说一句,您可能需要添加一个using指令来缩短该行。

当然,上面链接中的更多使用信息。

于 2017-05-05T22:32:50.660 回答
4

很好的问题。

我不知道是否有 .NET 解决方案,但我发现了一个名为Premailer的 Ruby 程序,它声称内联 CSS。如果你想使用它,你有几个选择:

  1. 用 C#(或任何你熟悉的 .NET 语言)重写 Premailer
  2. 使用IronRuby在 .NET 中运行 Ruby
于 2010-09-09T18:28:04.310 回答
3

我建议使用实际的 CSS 解析器而不是正则表达式。您不需要解析完整的语言,因为您主要对复制感兴趣,但在任何情况下,这样的解析器都是可用的(对于 .NET 也是如此)。例如,查看 antlr 的语法列表,特别是CSS 2.1 语法CSS3语法。如果您不介意内联样式可能包含重复定义的次优结果,您可以剥离这两种语法的大部分内容,但要做到这一点,您需要了解内部 CSS 逻辑才能解析速记属性.

然而,从长远来看,这肯定会比一系列无休止的即席正则表达式修复工作少得多

于 2010-09-22T09:10:33.503 回答
1

这是一个想法,为什么不使用 c#对http://www.mailchimp.com/labs/inlinecss.php进行后调用。从使用 firebug 的分析来看,post 调用需要 2 个参数htmlstrip,它采用值(开/关),结果是一个名为 text 的参数。

这是一个关于如何使用 c# 进行 post call的示例

于 2010-09-16T07:48:44.697 回答
1

乍得,你一定要添加 CSS 内联吗?或者你可以通过在<style>你的 中添加一个块来更好<head>吗?这实质上将取代对 CSS 文件的引用的需要,并维护实际内联规则覆盖在标头/引用的 css 文件中设置的规则。

(对不起,忘记为代码添加引号)

于 2010-09-16T12:48:07.307 回答
1

我会推荐这样的字典:

private Dictionary<string, Dictionary<string, string>> cssDictionary = new Dictionary<string, Dictionary<string, string>();

我会解析 css 来填充这个 cssDictionary。

(添加'style-type','style-property','value'。例如:

Dictionary<string,string> bodyStyleDictionary = new Dictionary<string, string();
    bodyStyleDictionary.Add("background", "#000000");
    cssDictionary.Add("body", bodyStyleDictionary);

之后,我最好将 HTML 转换为 XmlDocument。

您可以通过它的子节点递归地运行文档节点并查找它的父节点(这甚至可以使您能够使用选择器)。

在每个元素上检查元素类型、id 和类。然后浏览 cssDictionary 以将此元素的任何样式添加到样式属性(当然,如果它们具有重叠的属性,您可能希望按出现顺序放置它们(最后添加现有的内联样式)。

完成后,您将 xmlDocument 作为字符串发出并删除第一行 ( <?xml version="1.0"?>) 这应该会为您留下一个带有内联 css 的有效 html 文档。

当然,它可能有一半看起来像黑客,但最后我认为这是一个非常可靠的解决方案,可以确保稳定性并且完全符合您的要求。

于 2010-09-16T14:05:27.603 回答