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_parsercss_lexercss_languagecss_parser_eventcss_parser_event_handlercss__parser_parse_chunk()parser_stateparseFuncscss__language_create()language_handle_event()parsePropertyproperty_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 属性的解析过程,从中我们可以看出简写属性自身并不占用样式存储空间,它会在解析阶段被分解成若干个非简写属性进行解析和存储,分解的过程就是遍历每个值然后逐个检测是否与哪个非简写属性匹配。