在现代Web开发中,CSS是不可或缺的一部分。随着CSS标准的不断更新,对CSS文件的解析和处理变得越来越复杂。本文将介绍一个将Mozilla Firefox的CSS解析器代码移植到C#的项目,这个项目支持所有现代CSS特性,包括媒体查询、弹性盒模型等。
该项目支持所有现代CSS特性,包括媒体查询、弹性盒模型等。同时,也支持Mozilla特有的扩展(以-moz-为前缀的属性)。它支持两种兼容性模式进行解析:FullStandards(严格遵循标准)和Quirks(允许一些不正确的表达,例如可以省略长度单位)。属性值被解析成一个详细的对象模型。例如,简写背景属性background被展开成多个background-*属性。其中,background-image由一个值列表表示(对应CSS3支持的多个背景),每个值可以是一个渐变函数或一个URL;渐变又包含渐变停止和各种属性,如大小和角度。
错误恢复遵循标准的CSS解析规则。如果遇到不支持的特性或无效语法(通常由"CSS hacks"引起),解析将跳过下一个声明并从那里继续。详细的错误日志:当遇到语法错误时,会产生警告消息。这些消息被记录到TraceSource并作为事件触发。
可以使用NuGet包进行安装。调试符号和源代码由SymbolSource包提供。
未实现的功能包括不同的编码、@charset规则(被忽略,用户需要在解析前将字符串转换为Unicode)、修改解析后的样式表并序列化回字符串、CSS对象模型(像所有"标准"DOM模型一样,CSSOM笨重且不符合C#编码标准,因此其必要性值得怀疑)、其他浏览器的特定功能(以-webkit-、-ms-、-o-等为前缀的属性)被忽略。库中包含的单元测试非常少,并且没有经过彻底测试。不幸的是,Firefox的测试是用JS编写的,移植到C#太麻烦了。
实际上需要的是一种方法来提取所有CSS文件中的url()表达式,以便下载它们链接的所有图片。然而,正如看到的,有点失控,移植了很多代码。已经有一个用C#编写的解析CSS文件的库ExCSS。然而,它在解析一些表达式后停止解析,所以不符合需求。它正在被重写以使用ANTLR,所以可能会看到它支持完整的CSS特性集。
在选择要移植到C#的库时,有两个选择:Chrome的代码和Firefox的代码。这两个主要的开源浏览器都支持所有现代CSS特性,并随着CSS标准的演变而定期更新。Chrome依赖于语法生成器,所以移植到C#需要找到一个具有类似功能集的工具,并重写语法和补充代码。另一方面,Firefox使用手动编写的LL(1)解析器。选择了后者,因为这样依赖更少:只有纯C++,没有第三方工具和语法语言。
由于CSS在不断演变,需要找到一种方法来保持移植的代码随着原始代码的新版本发布而更新,而不会太麻烦。而不是手动重写代码,选择将正则表达式应用于T4代码生成模板中的C++代码。由于C++和C#语言有很多共同点,很多代码只需要小的修改。有些代码需要重大修改,如成员指针和返回引用。幸运的是,这种情况非常少。也很幸运,因为Mozilla的编码标准有非常严格的命名规则:参数、字段、常量、静态变量、枚举成员有不同的前缀。类命名规则要么缺失,要么没有严格遵循,但这是一个小问题。
显然,不是所有的代码都可以(或者值得)使用正则表达式进行转换。不太可能经常变化的代码(CSS值类型、CSS规则类型)是手动编写的,以及所有在C#中没有直接对应物的代码,如新的运算符重载以分配额外的内存用于"未命名"字段或使用联合。
这种方法是否能够在未来简化Firefox代码新版本的移植,只有时间能告诉。到目前为止,统计数据相当不错。例如,将超过10,000行的nsCSSParser.cpp转换为CssParser.conv.cs只需要400行的正则表达式(4%)。小文件的统计数据就不那么令人印象深刻了。
解析从创建CssLoader类的实例开始,可选地更改其Compatibility属性,然后调用其主要方法ParseSheet,该方法将表示CSS文件内容的字符串解析为CssStyleSheet对象。
C# CssStyleSheet css = new CssLoader().ParseSheet("h1, h2 { color: #123; }", new Uri("http://example.com/sheet.css"), new Uri("http://example.com/"));
Console.WriteLine(css.SheetUri);
// http://example.com/sheet.css
CssStyleSheet对象包含Rules属性,其中包含不同类型的CSS规则:样式规则、字符集规则、媒体查询规则、关键帧规则等。在这种情况下,对样式规则感兴趣。它可以通过使用OfType<T>() LINQ方法过滤Rules获得,也可以通过StyleRules属性获得(对于每种规则类型都有这样的快捷方式;代码生成的好处之一)。
CssStyleRule包含一个CssDeclaration,其中包含所有属性。Data和ImportantData在CssDeclaration中是属性-值对的列表。CssValue表示一个CSS值,它可以是一个单一的值、一个列表或一个列表对等。它的类型可以通过其Unit属性来区分。要获取特定类型的值,请使用String、Color、List、Uri等属性。
C# CssStyleSheet css = new CssLoader().ParseSheet("h1, h2 { color: #123; }", new Uri("http://example.com/sheet.css"), new Uri("http://example.com/"));
Console.WriteLine(css.SheetUri);
// http://example.com/sheet.css
// 获取颜色属性(等效代码)
Console.WriteLine(css.StyleRules.Single().Declaration.Color.Color.R);
// 17
Console.WriteLine(css.Rules.OfType<CssStyleRule>().Single().Declaration.GetValue(CssProperty.Color).Color.R);
// 17
// 获取h1选择器
Console.WriteLine(css.StyleRules.Single().SelectorGroups.First().Selectors.Single().Tag);
一个更有用的示例,提取所有URL:
C# List<string> uris = new CssLoader().GetUris(source).ToList();
此方法仅依赖于词法分析器CssScanner。它不会检查属性是否正确,它只会找到所有的url()表达式。
或者,在将文件解析为样式表对象后提取URL。这种方法将最接近Web浏览器的行为:它将跳过无效的属性、无效的表达式(如color: url(a) url(b)等)。
C# CssStyleSheet css = new CssLoader().ParseSheet(source, sheetUri, baseUri);
// 获取所有级别的CssStyleRule类型的规则(包括媒体规则中的样式规则)
List<string> uris = css.AllStyleRules
.SelectMany(styleRule => styleRule.Declaration.AllData)
.SelectMany(prop => prop.Value.Unit == CssUnit.List ? prop.Value.List : new[] { prop.Value })
.Where(val => val.Unit == CssUnit.Url)
.Select(val => val.OriginalUri)
.ToList();
类图的文件名在源代码中是Alba.CsCss/Diagrams/CssStyleSheet.cd。
注意:需要.NET 3.5或更高版本来构建项目。需要.NET 4.5或更高版本来构建完整的解决方案(包括测试和补充项目)。
PM> Install-Package Alba.CsCss
可以直接从Visual Studio安装NuGet包(在NuGet.org上查看更详细的说明),或者使用包管理器控制台:
可以从GitHub上的源代码构建。要使用库,只需要在解决方案中包含Alba.CsCss/Alba.CsCss.csproj。