题
我需要从C#中的样式表中进行内联CS。
像这样的工作方式。
http://www.mailchimp.com/labs/inlinecss.php
CSS很简单,只有类,没有花哨的选择器。
我正在考虑使用正则 (?<rule>(?<selector>[^{}]+){(?<style>[^{}]+)})+
从CSS中剥离规则,然后尝试进行简单的字符串替换了调用类的地方,但是某些HTML元素已经具有样式标签,因此我也必须考虑到这一点。
有更简单的方法吗?还是已经用C#写的东西?
更新 - 2010年9月16日
如果您的HTML也是有效的XML,我就可以提出一个简单的CSS Inliner。它使用正则表达式来获取您的所有样式 <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());
}
解决方案
既然您已经在当前的实施中已经有90%的方式了,为什么不使用现有框架,而是用HTML解析器替换XML解析?最受欢迎的是 HTML敏捷包. 。它支持XPATH查询,甚至具有类似于XML提供的标准.NET接口的LINQ接口,因此它应该是相当简单的替换。
其他提示
我在GitHub上有一个项目,该项目使CSS内联。这很简单,并支持移动风格。在我的博客上阅读更多信息: http://martinnormark.com/move-css-inline-promailer-net
由于此选项在其他答复中不太清楚,因此我认为它值得一个简单的答案。
采用 premailer.net.
您要做的就是:
- 通过Nuget安装Premailer.net。
键入此:
var inlineStyles = PreMailer.Net.PreMailer.MoveCssInline(htmlSource, false); destination = inlineStyles.Html;
你就完成了!
顺便说一句,您可能要添加一个 using
指令缩短该线路。
当然,在上面的链接中进行了更多使用信息。
这是一个主意,你为什么不打电话给 http://www.mailchimp.com/labs/inlinecss.php 使用C#。从使用Firebug的分析中,帖子呼叫需要2个参数 html 和 条 该值(开/关)的结果是在一个称为文本的参数中。
这是有关如何制作的示例 使用C#发布通话
乍得,您一定必须添加CSS内联吗?或者,您可以通过添加一个更好的选择 <style>
阻止你 <head>
?本质上,这将取代对CSS文件的引用的需求,并维护实际的内联规则覆盖标题/引用CSS文件中设置的规则。
(抱歉,忘了添加代码的报价)
我会推荐这样的命令:
private Dictionary<string, Dictionary<string, string>> cssDictionary = new Dictionary<string, Dictionary<string, string>();
我会解析CSS以填补此CSSDictionary。
(添加“样式类型”,“样式 - 毛皮”,“ value”。例如:
Dictionary<string,string> bodyStyleDictionary = new Dictionary<string, string();
bodyStyleDictionary.Add("background", "#000000");
cssDictionary.Add("body", bodyStyleDictionary);
之后,我最好将HTML转换为Xmldocument。
您可以通过孩子的孩子递归遍历文档节点,也可以查找其父母(这甚至可以使您能够使用选择器)。
在每个元素上,您检查元素类型,ID和类。然后,您浏览CSSDICTIONARY,将此元素的任何样式添加到样式属性(授予,如果它们具有重叠的属性,则可能需要按照这些方式将其放置(并添加最后的Inline样式)。
完成后,您将xmldocument发射为字符串,然后删除第一行(<?xml version="1.0"?>
)这应该为您提供带有Inline CSS的有效HTML文档。
当然,这可能一半看起来像是一个黑客,但最终我认为这是一个非常坚实的解决方案,可以确保稳定性,并且可以做到您似乎正在寻找的东西。