我需要从c#中的样式表内联css。
就像这是如何工作的。
http://www.mailchimp.com/labs/inlinecss.php
css很简单,只是类,没有花哨的选择器。
我正在考虑使用正则表达式(?<rule>(?<selector>[^{}]+){(?<style>[^{}]+)})+
从css中删除规则,然后尝试在调用类的地方执行简单的字符串替换,但是某些html元素已经有样式标记,所以我我也必须考虑到这一点。
有更简单的方法吗?或者已经用c#写的东西了?
如果您的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文件,我不会发布所有这些!
但是我应该发布Normalize扩展方法!
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());
}
答案 0 :(得分:16)
我在Github上有一个使CSS内联的项目。它非常简单,并支持移动样式。阅读我的博客:http://martinnormark.com/move-css-inline-premailer-net
答案 1 :(得分:9)
由于您已经有90%的方式使用当前的实现,为什么不使用现有的框架,而是用HTML解析器替换XML解析?其中一个比较受欢迎的是HTML Agility Pack。它支持XPath查询,甚至有一个类似于为XML提供的标准.NET接口的LINQ接口,因此它应该是一个相当简单的替代品。
答案 2 :(得分:4)
很棒的问题。
我不知道是否有.NET解决方案,但我找到了一个名为 Premailer 的Ruby程序,声称内联CSS。如果你想使用它,你有几个选择:
答案 3 :(得分:3)
我建议使用实际的CSS解析器而不是Regexes。您不需要解析完整的语言,因为您主要对复制感兴趣,但无论如何这种解析器都可用(对于.NET也是如此)。例如,看一下antlr的list of grammars,特别是CSS 2.1 grammar或CSS3语法。如果您不介意其中内联样式可能包含重复定义的次优结果,您可以剥离两个语法的大部分内容,但为了做到这一点,您需要内部CSS逻辑的一些概念能够解决速记属性。
然而,从长远来看,这肯定会比一系列特殊的正则表达式修复工作少很多。
答案 4 :(得分:2)
由于此选项在其他回复中不是很清楚,我认为这应该是一个直截了当的答案。
使用PreMailer.Net 。
您所要做的就是:
输入:
var inlineStyles = PreMailer.Net.PreMailer.MoveCssInline(htmlSource, false);
destination = inlineStyles.Html;
你完成了!
BTW,您可能需要添加using
指令来缩短该行。
当然,在上面的链接中有更多使用信息。
答案 5 :(得分:1)
这是一个想法,为什么不用c#给http://www.mailchimp.com/labs/inlinecss.php打个电话。从使用firebug的分析看来,post调用需要2个参数 html 和 strip ,它取值(开/关),结果在一个名为text的参数中。
以下是有关如何制作post call using c#
的示例答案 6 :(得分:1)
Chad,您是否必须添加CSS内联?或者通过向<style>
添加<head>
块可能会更好吗?这实际上将取代对CSS文件的引用的需要,并且保持实际内联规则覆盖在头/引用的css文件中设置的规则。
(抱歉,忘了为代码添加引号)
答案 7 :(得分:1)
我会推荐像这样的dictonary:
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文档。
当然,它可能看起来像是一个黑客,但最后我认为这是一个非常可靠的解决方案,可以确保稳定性并完全符合您的需求。