LibCSS 的源码解析
LibCSS 是一个 CSS 解析器和选择引擎,由 C 语言编写,是 NetSurf 网页浏览器项目的一部分且可供其它基于 MIT 许可协议的软件使用。它的主要特性如下:
- 解析 CSS,无论好的还是坏的
- 简单的 C API
- 低内存占用
- 快速的选择引擎
- 可移植
- 共享库
笔者之所以选择研究 LibCSS 源码,是因为在改造 CSS 库 lcui/css 时遇到了瓶颈。虽然参考 MDN 上的 CSS 值定义语法文档设计并实现了 CSS 值定义语法的解析器和匹配器,但在解决取值的问题时一直找不到最优的方案,其难点在于 CSS 属性的值的数量和类型不是固定的,且由于 background
这类简写属性的存在,还得考虑如何复用取值逻辑。因此 LibCSS 作为一个 C 语言编写的且已经历过浏览器考验的项目,很适合作为笔者的研究对象。
用法示例
LibCSS 的示例程序源码在 examples/exmaple1.c
中,它展示了如何用 LibCSS 实现 CSS 样式的加载、选择和读取,我们先从它入手,了解 LibCSS 大致的用法和概念,以便于后续的深入研究。
首先从 main()
函数开始,开头部分的代码初始化了 CSS 字符串、样式表的创建参数。
css_error code;
css_stylesheet *sheet;
size_t size;
const char data[] = "h1 { color: red } "
"h4 { color: #321; } "
"h4, h5 { color: #123456; }";
css_select_ctx *select_ctx;
uint32_t count;
unsigned int hh;
css_stylesheet_params params;
css_media media = {
.type = CSS_MEDIA_SCREEN,
};
UNUSED(argc);
UNUSED(argv);
params.params_version = CSS_STYLESHEET_PARAMS_VERSION_1;
params.level = CSS_LEVEL_21;
params.charset = "UTF-8";
params.url = "foo";
params.title = "foo";
params.allow_quirks = false;
params.inline_style = false;
params.resolve = resolve_url;
params.resolve_pw = NULL;
params.import = NULL;
params.import_pw = NULL;
params.color = NULL;
params.color_pw = NULL;
params.font = NULL;
params.font_pw = NULL;
然后,创建样式表,解析 CSS 字符串并追加进样式表中。
/* create a stylesheet */
code = css_stylesheet_create(¶ms, &sheet);
if (code != CSS_OK)
die("css_stylesheet_create", code);
code = css_stylesheet_size(sheet, &size);
if (code != CSS_OK)
die("css_stylesheet_size", code);
printf("created stylesheet, size %zu\n", size);
/* parse some CSS source */
code = css_stylesheet_append_data(sheet, (const uint8_t *) data,
sizeof data);
if (code != CSS_OK && code != CSS_NEEDDATA)
die("css_stylesheet_append_data", code);
code = css_stylesheet_data_done(sheet);
if (code != CSS_OK)
die("css_stylesheet_data_done", code);
code = css_stylesheet_size(sheet, &size);
if (code != CSS_OK)
die("css_stylesheet_size", code);
printf("appended data, size now %zu\n", size);
从中我们可以看出 在 LibCSS 的设计中样式解析和存储是围绕样式表进行的,css_stylesheet_append_data()
函数能够解析 CSS 代码字符串并将结果存储到样式表中。
接着是创建选择上下文,将上面新建的样式表追加进选择上下文中。
/* prepare a selection context containing the stylesheet */
code = css_select_ctx_create(&select_ctx);
if (code != CSS_OK)
die("css_select_ctx_create", code);
code = css_select_ctx_append_sheet(select_ctx, sheet, CSS_ORIGIN_AUTHOR,
NULL);
if (code != CSS_OK)
die("css_select_ctx_append_sheet", code);
code = css_select_ctx_count_sheets(select_ctx, &count);
if (code != CSS_OK)
die("css_select_ctx_count_sheets", code);
printf("created selection context with %i sheets\n", count);
最后,选择样式,输出选择结果:
/* select style for each of h1 to h6 */
for (hh = 1; hh != 7; hh++) {
css_select_results *style;
char element[20];
lwc_string *element_name;
uint8_t color_type;
css_color color_shade;
/* in this very simple example our "document tree" is just one
* node and is in fact a libwapcaplet string containing the
* element name */
snprintf(element, sizeof element, "h%i", hh);
lwc_intern_string(element, strlen(element), &element_name);
code = css_select_style(select_ctx, element_name,
&media, NULL,
&select_handler, 0,
&style);
if (code != CSS_OK)
die("css_select_style", code);
lwc_string_unref(element_name);
color_type = css_computed_color(
style->styles[CSS_PSEUDO_ELEMENT_NONE],
&color_shade);
if (color_type == CSS_COLOR_INHERIT)
printf("color of h%i is 'inherit'\n", hh);
else
printf("color of h%i is %x\n", hh, color_shade);
code = css_select_results_destroy(style);
if (code != CSS_OK)
die("css_computed_style_destroy", code);
}
css_select_style()
用于根据给定的选择上下文和结点来选择匹配的样式。它接受 7 个参数,其中的 node 参数指定了用于选择样式的结点,handler 参数提供 node 的各种属性的获取方法。由此可见其它 UI 库若要使用 LibCSS 实现对 CSS 的支持的话是比较容易的,只需要准备好控件句柄/指针和控件的各种操作函数合集即可。
css_computed_color()
用于从已计算样式中获取 color 属性的值,传入已计算样式和接受值的指针,返回值是颜色类型。从这个函数的调用代码中我们可以比较容易地推测出其它属性也有对应的以 css_computed_
开头命名的函数来获取值,且用法基本一样。另外,它的第一个参数 style->styles
是个数组且指定了 CSS_PSEUDO_ELEMENT_NONE
下标,这表明样式选择结果中不只有元素本身的样式,还包括其它伪元素的样式。
解析
通过研究上述的示例代码,我们已经知道 css_stylesheet_append_data()
能解析 CSS 字符串,以它为起点进行查找,可以找到包括数据结构、函数、变量在内的相关依赖项:
css_parser
css_lexer
css_language
css_parser_event
css_parser_event_handler
css__parser_parse_chunk()
parser_state
parseFuncs
css__language_create()
language_handle_event()
parseProperty
property_handlers
以这些依赖项为线索继续深入研究代码,我们会发现整个解析功能涉及到语言解析器、解析器前端、解析器、解析器事件、词法分析器这些概念,源码涉及 parse.c、lex.c、langguage.c、properties.c 等文件,接下来本文将逐个讲解它们。
词法分析器
词法分析器(Lexer)的职责是将字符串转换成一个个词法单元(Token),它基于有限状态自动机进行解析,大致的解析过程是根据字符内容更新状态,然后调用与状态对应的方法去解析后续字符。
词法单元的数据结构如下:
typedef struct css_token {
css_token_type type;
struct {
uint8_t *data;
size_t len;
} data;
lwc_string *idata;
uint32_t col;
uint32_t line;
} css_token;
它包含类型、数据、行列,data.data
和 data.len
成员记录了字符串的起始位置和长度,由此可见词法单元本质上就是对字符串中的一段字符串的标记。
至于状态自动机,它的实现代码可以概括为:
css_error 获取词法单元(css_lexer *词法分析器, css_token **词法单元)
{
switch (词法分析器->状态) {
case 初始状态:
return 初始状态的动作(词法分析器, 词法单元);
case 状态1:
return 状态1的动作(词法分析器, 词法单元);
case 状态2:
...
}
}
整个解析过程基于状态驱动,每种状态都有对应的动作,动作执行完后状态会被改为另一个状态,如此往复直到字符串全部解析完为止。这种运转机制,就是有限状态自动机。
解析器
解析器(Parser)的职责是解析词法单元序列表达的语义。与词法分析器相同之处是它也基于有限状态自动机,其大致的解析过程是调用词法分析器获取下个词法单元,然后根据词法单元内容更新状态,之后调用对应的子解析器。而不同之处是它的解析结果是通过事件处理函数传递的,事件枚举在 src/parse/parse.h 文件中有定义:
typedef enum css_parser_event {
CSS_PARSER_START_STYLESHEET,
CSS_PARSER_END_STYLESHEET,
CSS_PARSER_START_RULESET,
CSS_PARSER_END_RULESET,
CSS_PARSER_START_ATRULE,
CSS_PARSER_END_ATRULE,
CSS_PARSER_START_BLOCK,
CSS_PARSER_END_BLOCK,
CSS_PARSER_BLOCK_CONTENT,
CSS_PARSER_END_BLOCK_CONTENT,
CSS_PARSER_DECLARATION
} css_parser_event;
从中我们可以看出事件涉及样式表、规则集、@
规则、块、块内容、声明,在它们开始和结束解析的时候会触发事件。
语言解析器
语言解析器作为解析器的前端,负责对接属性解析器和解析器,将解析结果输出到样式表。
src/parse/language.c 文件中 css__language_create()
函数包含了语言解析器的创建过程,其中有一段是将 language_handle_event
函数设为解析器的事件处理器:
params.event_handler.handler = language_handle_event;
params.event_handler.pw = c;
error = css__parser_setopt(parser, CSS_PARSER_EVENT_HANDLER, ¶ms);
if (error != CSS_OK) {
parserutils_stack_destroy(c->context);
free(c);
return error;
}
language_handle_event
函数的作用就是根据解析器的事件来对后续词法单元做进一步处理,其中包括解析选择器、解析 @
规则、调用属性解析器等。
属性解析器
属性解析器的职责是在解析器解析样式声明时将每条属性转换成一个个连续的字节码存入样式表中。
LibCSS 的属性解析器源码是由生成器自动生成的,Makefile 中有具体的生成方法:
# Sources
AUTOGEN_PARSERS := $(shell $(PERL) -pe'$$_="" unless /^([^\#][^:]+):/;$$_=$$1 . " "' $(DIR)properties.gen)
# Dodgy use of define/eval to bypass DIR changing
define build_gen_parser
$(BUILDDIR)/gen_parser: $(DIR)css_property_parser_gen.c
$$(VQ)$$(ECHO) $$(ECHOFLAGS) " PREPARE: $$@"
$$(Q)$$(BUILD_CC) -o $$@ $$^
endef
$(eval $(build_gen_parser))
define gen_prop_parser
$(DIR)autogenerated_$1.c: $(DIR)properties.gen $(BUILDDIR)/gen_parser
$$(VQ)$$(ECHO) $$(ECHOFLAGS) "GENERATE: $$@"
$$(Q)$$(BUILDDIR)/gen_parser -o $$@ '$(shell $(GREP) "^$1:" $(DIR)properties.gen)'
AUTOGEN_SOURCES := $$(AUTOGEN_SOURCES) autogenerated_$1.c
endef
AUTOGEN_SOURCES :=
$(eval $(foreach PROP,$(AUTOGEN_PARSERS),$(call gen_prop_parser,$(PROP))))
属性解析器源文件命名格式为 autogenerated_${属性名}.c
, 都依赖生成器 gen_parser
,make 时会调用生成器为 properties.gen
文件中定义的每个属性生成解析器源文件。
properties.gen
文件头部有给出属性定义格式示例:
##Common templates
#
#property:CSS_PROP_ENUM IDENT:( INHERIT: IDENT:)
#property:CSS_PROP_ENUM IDENT:INHERIT NUMBER:( false: RANGE: NUMBER:)
#property:CSS_PROP_ENUM IDENT:INHERIT LENGTH_UNIT:( UNIT_HZ:PITCH_FREQUENCY ALLOW: DISALLOW: RANGE:<0 LENGTH_UNIT:)
#property:CSS_PROP_ENUM IDENT:( INHERIT: IDENT:) LENGTH_UNIT:( UNIT_HZ:PITCH_FREQUENCY ALLOW: DISALLOW: RANGE:<0 LENGTH_UNIT:)
#property:CSS_PROP_ENUM WRAP:
在这种设计中,自定义属性的添加方式应该是先在 properties.gen
添加属性定义,然后运行 make 命令生成解析器源文件,之后重新编译 LibCSS。
简写属性解析器
简写属性是可以让你同时设置好几个 CSS 属性值的 CSS 属性。使用简写属性,Web 开发人员可以编写更简洁、更具可读性的样式表,节省时间和精力。
以简写属性 background 为例,它的解析器源文件是 parse/properties/background.c,其中的 css__parse_background()
函数包含了属性的解析过程。
函数开头处定义了几个变量,用于存储非简写属性的值和解析状态(是否需解析)。
bool attachment = true;
bool color = true;
bool image = true;
bool position = true;
bool repeat = true;
css_style * attachment_style;
css_style * color_style;
css_style * image_style;
css_style * position_style;
css_style * repeat_style;
首先是处理继承,如果属性值是 inherit
则为每个属性追加带 inherit
标记的字节码。
/* Firstly, handle inherit */
token = parserutils_vector_peek(vector, *ctx);
if (token == NULL)
return CSS_INVALID;
if (is_css_inherit(c, token)) {
error = css_stylesheet_style_inherit(result, CSS_PROP_BACKGROUND_ATTACHMENT);
if (error != CSS_OK)
return error;
...
}
给每个属性分配内存资源。
/* allocate styles */
error = css__stylesheet_style_create(c->sheet, &attachment_style);
if (error != CSS_OK)
return error;
...
遍历每个词法单元,尝试用每个非简写属性的解析函数解析它,若解析成功则标记该属性为不需要再解析。
/* Attempt to parse the various longhand properties */
do {
prev_ctx = *ctx;
error = CSS_OK;
if (is_css_inherit(c, token)) {
error = CSS_INVALID;
goto css__parse_background_cleanup;
}
/* Try each property parser in turn, but only if we
* haven't already got a value for this property.
*/
if ((attachment) &&
(error = css__parse_background_attachment(c, vector, ctx,
attachment_style)) == CSS_OK) {
attachment = false;
} else if ((color) && ...
} while (*ctx != prev_ctx && token != NULL);
if (attachment) {
error = css__stylesheet_style_appendOPV(attachment_style,
CSS_PROP_BACKGROUND_ATTACHMENT, 0,
BACKGROUND_ATTACHMENT_SCROLL);
if (error != CSS_OK)
goto css__parse_background_cleanup;
}
...
解析完后,给未获得值的属性追加初始值:
if (attachment) {
error = css__stylesheet_style_appendOPV(attachment_style,
CSS_PROP_BACKGROUND_ATTACHMENT, 0,
BACKGROUND_ATTACHMENT_SCROLL);
if (error != CSS_OK)
goto css__parse_background_cleanup;
}
if (color) {
...
之后,将每个非简写属性值合并进结果中。
error = css__stylesheet_merge_style(result, attachment_style);
if (error != CSS_OK)
goto css__parse_background_cleanup;
...
最后,销毁相关数据。
css__stylesheet_style_destroy(attachment_style);
...
return error;
以上就是 background 属性的解析过程,从中我们可以看出简写属性自身并不占用样式存储空间,它会在解析阶段被分解成若干个非简写属性进行解析和存储,分解的过程就是遍历每个值然后逐个检测是否与哪个非简写属性匹配。
计算
css_select_style()
函数实现了样式选择和计算功能,它所解决的问题可分为六类:指示、选择、层叠、内联、初始值、绝对值。值得注意的是,该函数的注释中有提到它生成的已计算样式还不能立即使用,需要调用 css_computed_style_compose()
获取完整的已计算样式,这种两步式样式计算方法旨在允许客户端存储部分已计算样式,并在布局变动时高效地更新节点的完整已计算样式。
指示
指示(Hinting)用于让元素对样式做自定义的控制。css_select_style()
函数在初始化选择状态后会调用 node_presentational_hint
回调函数获取元素的展现指示,这些指示的作用相当于元素内部自定义了部分属性的初始值,典型的例子就是 canvas 元素,它给 width 和 height 属性指示的值分别是 300 和 150。
选择
选择(Selection)是 CSS 引擎的核心功能,其作用是从样式表中查找与元素匹配的规则集。由于选择引擎并不在本文的研究范围内,本文不会继续对它做进一步的讲解,如需了解更多,请自行查阅 LibCSS 代码库中的 src/select/select.c 文件。
层叠
层叠(Cascade)的作用是为了从匹配到的规则集中选取权重最高的规则。css_select_style()
在查找到匹配的规则后会调用 cascade_style()
函数层叠样式,其实现代码如下:
css_error cascade_style(const css_style *style, css_select_state *state)
{
css_style s = *style;
while (s.used > 0) {
opcode_t op;
css_error error;
css_code_t opv = *s.bytecode;
advance_bytecode(&s, sizeof(opv));
op = getOpcode(opv);
error = prop_dispatch[op].cascade(opv, &s, state);
if (error != CSS_OK)
return error;
}
return CSS_OK;
}
从这段简短的代码中我们可以看出,层叠的过程就是遍历字节码然后调用属性调度表中注册的 cascade 函数。以 width 属性为例,它的层叠操作由 select/properties/width.c 中的 css__cascade_width()
函数实现,该函数内部调用了 css__cascade_length_auto()
函数:
css_error css__cascade_length_auto(uint32_t opv, css_style *style,
css_select_state *state,
css_error (*fun)(css_computed_style *, uint8_t, css_fixed,
css_unit))
{
uint16_t value = CSS_BOTTOM_INHERIT;
css_fixed length = 0;
uint32_t unit = UNIT_PX;
if (isInherit(opv) == false) {
switch (getValue(opv)) {
case BOTTOM_SET:
value = CSS_BOTTOM_SET;
length = *((css_fixed *) style->bytecode);
advance_bytecode(style, sizeof(length));
unit = *((uint32_t *) style->bytecode);
advance_bytecode(style, sizeof(unit));
break;
case BOTTOM_AUTO:
value = CSS_BOTTOM_AUTO;
break;
}
}
unit = css__to_css_unit(unit);
if (css__outranks_existing(getOpcode(opv), isImportant(opv), state,
isInherit(opv))) {
return fun(state->computed, value, length, unit);
}
return CSS_OK;
}
当属性值未指定为继承时,对字节码中表达的值进行转换,若当前属性值的权重在已有属性值之上则调用 fun
指向的回调函数来设置值,这个 fun
指针在 css__cascade_width()
中被赋值为 set_width
:
static inline css_error set_width(css_computed_style *style, uint8_t type,
css_fixed length, css_unit unit)
{
uint32_t *bits;
bits = &style->i.bits[WIDTH_INDEX];
/* 7bits: uuuuutt : unit | type */
*bits = (*bits & ~WIDTH_MASK) | ((((uint32_t)type & 0x3) | (unit << 2))
<< WIDTH_SHIFT);
style->i.width = length;
return CSS_OK;
}
内联
内联样式(Inline style)的优先 级高于选择的样式,它层叠在选择的样式之上,css_select_style()
对它的处理同样是调用 cascade_style()
函数。
初始值
当样式层叠完后,未设置值的属性会被设置初始值。
绝对值
绝对值是可以直接使用的值,除此之外还有相对值。CSS 的部分属性支持使用相对单位的值,例如:font-size: smaller
、color: currentColor
、line-height: 1.6
、padding-left: 1em
,这些值并不能直接使用,需要在它们依赖的值都是绝对值时才能确定,因此样式计算过程中会有一个将相对值转换为绝对值的计算过程。
除了 css_select_style()
会在元素为根元素时为其计算绝对值外,我们也可以手动调用 css_computed_style_compose()
函数来完成这一计算。
已计算样式
src/select/computed.c 文件的内容可划分为属性访问器、计算函数、内部函数这三个部分。
属性访问器
属性访问器是一系列名称以 css_computed_
开头的函数,用于从已计算样式结构体中获取特定属性的值。它们的返 回值类型都为 uint8_t
,表示属性值的类型,传入的参数用于接收值,例如:
uint8_t css_computed_width(const css_computed_style *style,
css_fixed *length, css_unit *unit)
{
return get_width(style, length, unit);
}
对于 left 这种在特定情况下依赖 position 和 right 属性才能计算的属性,它的访问器代码是这样的:
uint8_t css_computed_left(const css_computed_style *style,
css_fixed *length, css_unit *unit)
{
uint8_t position = css_computed_position(style);
uint8_t left = get_left(style, length, unit);
/* Fix up, based on computed position */
if (position == CSS_POSITION_STATIC) {
/* Static -> auto */
left = CSS_LEFT_AUTO;
} else if (position == CSS_POSITION_RELATIVE) {
/* Relative -> follow $9.4.3 */
uint8_t right = get_right_bits(style);
if (left == CSS_LEFT_AUTO && (right & 0x3) == CSS_RIGHT_AUTO) {
/* Both auto => 0px */
*length = 0;
*unit = CSS_UNIT_PX;
} else if (left == CSS_LEFT_AUTO) {
/* Left is auto => -right */
*length = -style->i.right;
*unit = (css_unit) (right >> 2);
} else {
/** \todo Consider containing block's direction
* if overconstrained */
}
left = CSS_LEFT_SET;
}
return left;
}
计算函数
计算函数是一系名称以 compute_
开头的函数,它们主要被css__compute_absolute_values()
函数调用,其中大部分用于计算某类属性的绝对值。
存储
LibCSS 的低内存占用主要体现在样式存储上,CSS 字符串中的样式属性声明经过解析后会转换成内存空间利用率高的字节码,而已计算样式虽然因包含所有属性而占用较大内存,但也做了一些优化,它的头部集中存储了所有属性值的类型数据。
字节码的存储格式
字节码是 unit32_t
类型,表达了 CSS 属性的代码、值、是否重要、是否继承,bytecode.h 中的 buildOPV()
函数定义代码展示了它们是如何组成字节码的:
static inline css_code_t buildOPV(opcode_t opcode, uint8_t flags, uint16_t value)
{
return (opcode & 0x3ff) | (flags << 10) | ((value & 0x3fff) << 18);
}
从中我们可以看出字节码在内存中的布局:
| opcode | flags | value |
| 1 ~ 11bit | 12 ~ 13bit | 14 ~ 32bit |
对于有多种类型值的属性,它们的 value 字段会被用来表达数据类型,而实际值则存储在追加的数据段中,每个值占用一个字节码的空间,格式如下:
| CSS_PROP_XXXX | flags | XXXXX_SET | 实际值1 | 实际值2 | ... |
| 32bits | 32bits | 32bits | |
以 width 属性为例,width: 10px
的字节码格式是:
| CSS_PROP_WIDTH | flags | BOTTOM_SET | FIXED (10) | UNIT (px) |
| 32bits | 32bits | 32bits |
而 width: auto
的字节码格式是:
| CSS_PROP_WIDTH | flags | BOTTOM_AUTO |
| 32bits |
test/dump.h 文件展示了如何理解字节码并将其输出为 CSS 代码字符串,我们可以通过查阅它来了解更多的字节码操作方法。