Ladybird 浏览器中新增与修改 CSS 属性的完整开发指南:从 Properties.json 数据定义到 ComputedValues 消费
本文基于 Ladybird 浏览器官方文档 CSSProperties.md 展开,完整讲解在 LibWeb 引擎中添加或修改一个 CSS 属性需要改动的全部位置:数据定义、解析、计算样式、JavaScript 接口四个环节。阅读并结合仓库源码后,你将能够独立完成诸如"新增一个长手属性"或"为某个简写属性实现自定义解析/序列化"这类引擎级贡献,并理解每个环节的底层调用链与数据流向。
总体流程:Ladybird 处理一个 CSS 属性的四个阶段
Ladybird 处理 CSS 属性的顺序是:数据定义 → 解析 → 计算样式 → 在渲染/JS 层被消费。官方文档明确说明,新增或修改属性需要在多处做改动,"以下按 Ladybird 处理它们的顺序列出,从解析开始,到被使用为止"。各阶段对应的关键文件如下:
| 阶段 | 关键文件 | 职责 |
|---|---|---|
| 数据定义 | Properties.json、Keywords.json、Enums.json、LogicalPropertyGroups.json | 声明属性的取值、初始值、继承性、动画类型等元数据,驱动代码生成 |
| 解析 | Parser.h、PropertyParsing.cpp | 把 CSS 值字符串解析为 StyleValue 对象;简单属性自动解析,复杂语法需手写 |
| 计算样式 | ComputedProperties、ComputedValues.h | 长手属性以 StyleValue 指针存储,再转换/拷贝为紧凑的扁平结构供布局与绘制读取 |
| JavaScript | CSSStyleProperties.cpp | 处理 getComputedStyle() 等 JS 场景下的特殊取值规则 |
第一步:数据定义(CSS/Properties.json)
Properties.json:每个属性的"身份证"
第一个要改动的地方是 Properties.json。该文件包含每个 CSS 属性的定义,用于生成 PropertyID 枚举及一批函数。你可能还需要修改 Keywords.json、Enums.json 和 LogicalPropertyGroups.json。这些 JSON 文件由 Meta/Generators 目录下的生成器在构建时自动编译为 C++ 代码(产出 PropertyID.h/.cpp、GeneratedCSSStyleProperties.* 等),生成机制的完整细节见配套文档 CSSGeneratedFiles.md。
从仓库实际内容看,文件组织为一个 JSON 对象,key 是属性名,value 是该属性的元数据。以 opacity 为例,其完整定义体现了文档中提到的核心字段:
"opacity": {
"style-group": "EffectsValues",
"affects-accumulated-visual-contexts": true,
"animation-type": "by-computed-value",
"affects-layout": false,
"affects-stacking-context": true,
"inherited": false,
"initial": "1",
"requires-computation": "cascaded-value",
"valid-types": [
"opacity-value"
]
}
对照 CSSGeneratedFiles.md 中的字段表,可以逐字段理解这份定义的含义:
inherited(必填):opacity不继承,会进入ComputedValues的m_noninherited结构;而继承属性(如下面的visibility)进入m_inherited。initial(必填):未指定时的初始值,这里为1。animation-type(必填):按 Web Animations 规范声明动画类型,opacity是按计算值插值(by-computed-value)。affects-layout: false:改变该属性不会使元素布局失效——这正是opacity动画不触发重排的数据来源。requires-computation: "cascaded-value":只有当指定值来自级联时才需要跑计算过程,优化了继承/初始路径。
再看一个继承属性的示例,visibility 的定义:
"visibility": {
"style-group": "InheritedBoxValues",
"animation-type": "custom",
"inherited": true,
"initial": "visible",
"requires-computation": "never",
"valid-types": [
"visibility"
]
}
其中 valid-types 里引用的 visibility 是一个枚举类型名——它定义在 Enums.json 中(生成 Visibility 枚举及 Keyword 互转函数)。这就是文档所说"你可能还需要修改 Enums.json"的典型场景:当一组关键字在规范中有名称时(如 border-*-style 共用的 line-style),应定义枚举而非用 valid-identifiers 罗列关键字。
简写属性与位置值列表
简写(shorthand)属性通过 longhands 字段声明其展开目标。例如 border-radius:
"border-radius": {
"affects-layout": false,
"initial": "0",
"longhands": [
"border-top-left-radius",
"border-top-right-radius",
"border-bottom-left-radius",
"border-bottom-right-radius"
],
"valid-types": [
"length [0,∞]",
"percentage [0,∞]"
],
"percentages-resolve-to": "length"
}
而 margin 这类"位置值列表简写"还需要额外打上 positional-value-list-shorthand: true 标记:
"margin": {
"initial": "0",
"positional-value-list-shorthand": true,
"longhands": [
"margin-top",
"margin-right",
"margin-bottom",
"margin-left"
],
"valid-types": [
"length [-∞,∞]",
"percentage [-∞,∞]"
],
"valid-identifiers": ["auto"],
"percentages-resolve-to": "length",
"quirks": ["unitless-length"],
"needs-layout-for-getcomputedstyle": true
}
margin 的 1~4 个值如何映射到 4 个长手并非一对一,因此需要该标记让解析与序列化走通用逻辑。此外 quirks 声明了单位无关长度的怪癖模式行为,needs-layout-for-getcomputedstyle 声明了 getComputedStyle() 查询前必须先完成最新布局——这类字段让"属性元数据"直接驱动运行时行为。
逻辑属性与遗留别名
如果新增的是逻辑属性(如 margin-block-start),需要在属性条目中声明 logical-alias-for(含 group 与 mapping 两个字段),并在 LogicalPropertyGroups.json 中维护对应的物理属性映射,运行时根据书写方向映射到 margin-top 等物理属性。仓库中的 border-radius 组即为实例(border-top-left-radius 等条目里带有 "group": "border-radius")。而规范更名但语法不变的属性(如 font-stretch 更名为 font-width)则用 legacy-alias-for 声明:
"font-stretch": {
"legacy-alias-for": "font-width"
}
设置了 legacy-alias-for 或 logical-alias-for 的条目可以省略其余必填字段。
第二步:解析(CSS/Parser)
多数属性无需手写解析代码
文档明确指出:对于接受单个值的属性,或其长手属性列表形式的简写,Properties.json 中的数据就足以完成自动解析。只有语法更复杂的属性才需要自定义解析。
从当前仓库的源码结构可以进一步观察到,通用解析路径已经相当深:Parser.cpp 中的 Parser::parse_css_value_from_source() 会经由 RustValueParsing.cpp 进入 Rust 侧的值解析器(value_parser.rs),基于 JSON 元数据完成 token 到 StyleValue 的转换。也就是说,valid-types/valid-identifiers 写得越规范,引擎能"零代码"覆盖的属性就越多。
复杂语法:在 parse_css_value() 的 switch 中挂接
对于需要自定义解析的属性,代码放在 PropertyParsing.cpp 与 Parser.h。调用入口是 Parser::parse_css_value(),其内部有一个按特定属性分发的 switch,新属性的解析方法从这里调用,并应返回一个指向 StyleValue 或其子类的 RefPtr。Parser.h 中可以看到该入口的对外声明:
RefPtr<CSS::StyleValue const> parse_css_value(CSS::Parser::ParsingParams const&, StringView, CSS::PropertyID);
RefPtr<CSS::StyleValue const> parse_css_value(CSS::Parser::ParsingParams const&, Utf16View, CSS::PropertyID);
简写属性:ShorthandStyleValue 与自定义序列化
简写属性通常应使用 ShorthandStyleValue,它会负责自动展开(expand)到长手属性值。如果你的简写有特殊的序列化规则,则可能需要修改 ShorthandStyleValue 的 to_string。文档给出的实例是 border-radius:它用 / 分隔水平与垂直两个半径分量(border-radius: 4px / 8px),这种"斜杠列表"无法用通用序列化表达,必须定制。
如果属性的值无法用现有的 StyleValue 子类型表示,可能需要新增一个 style value 类。文档在此处留下了一个幽默的注脚:"如果你需要这样做,去催 @AtkinsSJ 直到他把文档写完为止 ;^)"——可见这部分文档化仍在进行中,动手前建议参照 StyleValues 目录下现有子类的实现模式。
第三步:计算样式(ComputedProperties 到 ComputedValues)
长手存储:ComputedProperties
解析和样式计算完成后,长手属性以 StyleValue 指针的形式存储在 ComputedProperties 中。所有简写此时已被展开,因此不需要直接存储。
随后,这些长手值需要被转换成更易用的紧凑形式。做法是:在 ComputedProperties 中添加一个与属性同名的 getter,返回一个以紧凑形式持有值的类型。这一步的意义在于避免每次布局/绘制都从 StyleValue 对象树中解引用取值。
ComputedValues:扁平化的最终形态
ComputedValues.h 中有三个相关的类:
ComputedValues:以扁平格式持有每个属性的计算值。根据属性是否继承,字段需要加到m_inherited或m_noninherited结构体中,并配一个对应的 getter;MutableComputedValues:还需要为属性加一个 setter,供样式计算写入;InitialValues:为属性的默认值提供 getter。并非总是需要——例如当默认计算值就是空的Optional或Vector时。
从当前仓库源码看,ComputedValues.h 中每个 getter 都带有静态初始值兜底(如 static Visibility visibility() { return Visibility::Visible; }、static float opacity() { return 1.0f; }),与实例级 getter 并存;实例 getter 则从对应的 m_inherited/m_noninherited 结构字段中读出。
拷贝与消费:apply_style() 与 getter
样式从 ComputedProperties 拷贝到 ComputedValues 发生在 NodeWithStyle::apply_style() 中——每个属性是逐个拷贝的。当前实现位于 Layout/Node.cpp:apply_style 发布样式记录视图后,各消费方即可读取紧凑值。
文档给出的消费端示例是读取 visibility 与 opacity 的判断可见性代码(当前仓库中对应逻辑已演进为 BoxViews.cpp 中的 layout_node_is_visible / is_visible,判据不变):
bool Paintable::is_visible() const
{
auto const& computed_values = this->computed_values();
return computed_values.visibility() == CSS::Visibility::Visible && computed_values.opacity() != 0;
}
这个例子恰好串起了前几步的全部成果:visibility 的枚举来自 Enums.json 的生成代码,opacity 的 float 紧凑类型来自 ComputedValues 的转换层,最终在绘制阶段被 O(1) 地消费。
第四步:JavaScript 层的特殊取值规则
部分属性在从 JS 读取计算值时有特殊规则(典型如 getComputedStyle() 的已解析值语义)。这些需要在 CSSStyleProperties.cpp 的 CSSStyleProperties::style_value_for_computed_property() 中登记处理;源码中该函数的注释也印证了这一点——它"枚举了需要此类处理的属性"。而以不寻常方式序列化的简写,则需要在 CSSShorthandStyleValue::to_string() 内部处理(与解析阶段的序列化定制相呼应,例如 border-radius 的 / 分隔符)。
实战清单:新增一个属性的完整改动路径
综合全文,新增一个长手属性按 Ladybird 的处理顺序需要:
- Properties.json:添加条目,填全必填字段(
animation-type、inherited、initial、requires-computation),按需声明valid-types(引用 Enums.json 中的枚举名或length [0,∞]这类区间类型)、valid-identifiers、affects-layout、percentages-resolve-to等;新关键字同步登记到 Keywords.json。 - PropertyParsing.cpp / Parser.h:仅当语法复杂时在
parse_css_value()的 switch 中挂接自定义解析,返回StyleValue子类的RefPtr;简写用ShorthandStyleValue,特殊序列化改to_string。 - ComputedProperties:添加同名 getter,返回紧凑类型。
- ComputedValues.h:字段按继承性加入
m_inherited/m_noninherited+ getter;MutableComputedValues加 setter;InitialValues按需加默认值 getter。 - CSSStyleProperties.cpp:若 JS 读取计算值有特殊规则,登记到
style_value_for_computed_property()。
验证时,可参考 Tests/LibWeb 下与 CSS 相关的测试用例(如 text 类型的解析测试与 html 类型的布局/渲染测试)为新属性补充覆盖。整条链路的元数据驱动设计是 Ladybird 引擎的一个显著特点:JSON 定义既是文档也是代码,构建期生成保证"数据—枚举—函数"三者永不失同步。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00