CSS 值定义语法
- 开始日期:2023-04-09
- 目标主要版本:3.x
- 参考问题:无
- 实现 PR:#287
概括
添加新的 CSS 属性注册函数,支持使用 CSS 值定义语法来定义 CSS 属性的有效值。
基本示例
注册自定义属性:
css_register_property(
"custom-prop",
"normal | none", "normal",
css_cascade_custom_prop
);
内置的 width 属性的注册方式:
css_register_property_with_key(
css_key_width,
"width",
"auto | <length> | <percentage>", "auto",
css_cascade_width
);
内置的 background 简写属性的定义方式:
// 先为一些复杂值定义注册别名
css_register_valdef_alias("image", "<url>");
css_register_valdef_alias("bg-image", "none | <image>");
css_register_valdef_alias(
"bg-position",
"["
" [ left | center | right | top | bottom | <length-percentage> ] |"
" [ left | center | right | <length-percentage> ]"
" [ top | center | bottom | <length-percentage> ] |"
" [ center | [ left | right ] <length-percentage>? ] &&"
" [ center | [ top | bottom ] <length-percentage>? ]"
"]");
css_register_valdef_alias(
"bg-size",
"[ <length> | <percentage> | auto ]{1,2} | cover | contain");
css_register_valdef_alias("repeat-style",
"repeat-x | repeat-x | repeat | no-repeat");
// 注册简写属性
css_register_shorthand_property(css_key_background, "background",
"<bg-image> || <bg-position> || <bg-size> || <repeat-style> || "
"<color>");
动机
2.x 版本的 CSS 库添加自定义属性并不方便,主要问题如下:
- 值解析函数不可复用: 值解析函数
SplitValues()
是内部函数,应用层代码添加的自定义属性解析函数无法复用它,这意味着要么在 CSS 库内添加自定义属性,要么在应用层重新实现一遍值解析。 - 解析函数参数比较复杂: 属性解析函数第一个参数是解析器上下文,虽然它包含了各种数据,但大多数情况下函数内部只是通过它往样式声明中写入属性值,这增加了函数复杂度和参数理解成本。
- 各个 CSS 属性的有效值集不明确: 为了节约开发成本,CSS库在设计之初仅支持部分属性和值。然而,现有文档并未详细说明每个属性的有效值集,因此需要逐个测试才能确定可用的属性和值。
详细设计
参考CSS属性值定义语法 ,设计 CSS 值定义解析器和匹配器。
数据结构
先挑几个典型的 CSS 值定义例子:
- color:
<color>
- font-size:
<absolute-size> | <relative-size> | <length-percentage>
- position:
static | relative | absolute | sticky | fixed
- border:
<line-width> || <line-style> || <color>
- box-shadow:
none | <shadow>
- width:
auto | <length> | <percentage> | min-content | max-content | fit-content | fit-content(<length-percentage>)
像 position 这种由多个关键字组成的定义,最简单的方式是解析成数组然后遍历数组逐个判断,但对于 box-shadow 这种有自定义数据类型的就不好做了,box-shadow 值的完整定义是这样的:
none | <shadow>#
where
<shadow> = inset? && <length>{2,4} && <color>?
如果把 box-shadow 值的匹配过程看成一颗树,每个可选值按存在与否分出一个分支,那么这颗树就是这样的:
- none
- <shadow>
- inset?
- inset && <length>{2,4} && <color>?
- inset && <length>{2,4} && <color>
- inset && <length>{2,4}
- <length>{2,4} && <color>?
- <length>{2,4} && <color>
- <length>{2,4}
由此可见,CSS 值的类型定义数据适合存储在树形结构中,值的匹配过程就是树的遍历过程。
为了完整表达值定义,节点应包含关键字(inset
)、数据类型(<length>
)、组合符号(&&
||
)、数量符号({2,4}
),那么节点的数据结构可以设计成这样:
struct css_valdef_t {
css_valdef_sign_t sign;
unsigned min_count;
unsigned max_count;
const css_valdef_t *source;
union {
css_keyword_value_t ident;
/** list_t<css_valdef_t> */
list_t children;
const css_value_type_record_t *type;
};
};
- sign 标识使用哪个组合符号。
min_count
、max_count
记录了数量符号{2,4}
中的数量范围。- source 记录源类型,用于实现类型别名。
- ident 标识关键字的编号,当 sign 值为
NONE
时生效,针对值为normal
这种关键字的情况。 - children 用于记录子节点,当 sign 值为组合符号时生效,针对值为
none || auto
这种包含一组值的情况。 - type 指针指向类型记录,当 sigin 值为 CSS_VALDEF_SIGN_ANGLE_BRACKET 时生效,针对值为
<data-type>
的情况。
解析器
基于有限状态自动机实现解析器,状态由以下枚举表达:
typedef enum css_valdef_parser_target_t {
CSS_VALDEF_PARSER_TARGET_NONE,
CSS_VALDEF_PARSER_TARGET_ERROR,
CSS_VALDEF_PARSER_TARGET_KEYWORD,
CSS_VALDEF_PARSER_TARGET_DATA_TYPE,
CSS_VALDEF_PARSER_TARGET_CURLY_BRACES,
CSS_VALDEF_PARSER_TARGET_BRACKETS,
CSS_VALDEF_PARSER_TARGET_QUESTION_MARK,
CSS_VALDEF_PARSER_TARGET_SIGN
} css_valdef_parser_target_t;
状态流转的实现比较简单,主要的复杂度和难点都集中在值的处理上。
解析器初始创建一个根结点,类型为 Juxtaposition。
对于相同组合符号的结点,解析后将它们存放在同一个数组中,例如:
left | center | right
解析结果是:
SingleBarCombinator(["left", "center", "right"])
当解析到其它组合器符号时或是解析结束时,会涉及到父节点和子节点的修改。例如:
- 当解析到
left && right |
末尾的|
时,需要将当前值right
追加到&&
组合符号的数组值中,然后将父值的符号改为|
,使数据结构变为[left && right] |
。 - 当解析到
left | right &&
末尾的&&
时,需要追加新的&&
符号的数组值,然后将当前值right
追加到该数组中,使数据结构变为left | [right && ]
。
在添加支持方括号之前,解析的都是 none | auto
<length> || <line-style> || <line-width>
这种线性且类型单一的定义,对解析结果的操作类似于对数组操作,但有了方括号后,数据结构变成了树形,需要操作父子结点,这似乎变得复杂了一点,为此我们不得不重新思考现有的设计是否符合解析方括号的需求。
方括号包住的是值定义,因此可以采用递归的方式对方括号内的值定义进行解析,不过与常规的以 \0
为终止符的解析方式不同,方括号的终止符是 ]
,为此我们需要让解析器支持自定义终止符。
由此我们可以得出方括号的解析流程是在遇到 [
时创建子解析器,设置其结束符为 ]
,在子解析器解析完后将它的结果合并进当前的结果中。