SerenityOS LibWeb CSS 代码生成体系:从 JSON 定义到 C++ 实现
2026/9/10 17:10:30 网站建设 项目流程

SerenityOS LibWeb CSS 代码生成体系:从 JSON 定义到 C++ 实现

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

导读

SerenityOS 的 LibWeb 引擎在构建时会从一组 JSON 文件中批量生成大量 CSS 相关 C++ 代码。这些文件定义了每个 CSS 属性的取值、继承性、初始值、动画类型,以及关键字、枚举、伪类、媒体特性、数学函数与变换函数等元数据。本文以 CSSGeneratedFiles.md 为主线,完整梳理 7 个 JSON 输入文件的结构与字段语义,并结合 LibWeb/CSS 源码目录与 LibWeb 代码生成器 中的实现细节,讲解如何为浏览器引擎新增或修改一个 CSS 属性及其取值。读完本文,你将掌握 LibWeb 中 CSS 元数据的组织方式、生成器的产出清单,以及参与 CSS 规范实现时的标准工作流。

一、整体架构:JSON 定义、生成器与构建集成

LibWeb 的 CSS 实现采用"数据驱动 + 构建期代码生成"的模式:

  • 输入:一个或多个.json文件,位于 Userland/Libraries/LibWeb/CSS,目前包含Properties.jsonKeywords.jsonEnums.jsonPseudoClasses.jsonMediaFeatures.jsonMathFunctions.jsonTransformFunctions.json(另外仓库中还存在EasingFunctions.json)。
  • 生成器:位于 Meta/Lagom/Tools/CodeGenerators/LibWeb,包含GenerateCSSPropertyID.cppGenerateCSSKeyword.cppGenerateCSSEnums.cppGenerateCSSPseudoClass.cppGenerateCSSMediaFeatureID.cppGenerateCSSMathFunctions.cppGenerateCSSTransformFunctions.cppGenerateCSSStyleProperties.cpp等。
  • 输出:生成结果落在构建目录Build/<build-preset>/Lagom/Userland/Libraries/LibWeb/CSS/下(如PropertyID.h/cppKeyword.h/cpp等)。

这些生成器会在构建过程中自动运行,通常开发者无需手动干预。但当你需要新增或修改一个 CSS 属性及其取值时,就必然要与这些 JSON 文件打交道。生成器内部通过AK::SourceGenerator输出 C++ 代码,并借助 GeneratorUtil.h 等公共工具完成头文件守卫、write_if_changed之类的落盘逻辑(参见 GenerateCSSPropertyID.cpp)。

二、Properties.json:CSS 属性的"注册中心"

Properties.json(约 2800 行)为每个 CSS 属性维护一条记录,描述它接受哪些值、是否继承、初始值等元数据。它会生成以下文件:

  • PropertyID.h/PropertyID.cpp:属性 ID 枚举及各种查询函数
  • GeneratedCSSStyleProperties.h/GeneratedCSSStyleProperties.cpp:绑定到 WebIDL 的属性访问器
  • GeneratedCSSStyleProperties.idl

文件结构是一个 JSON 对象,键为属性名,值为该属性的数据。大多数元数据都来自对应 CSS 规范中该属性的"信息框"(information box)。每个属性会带有下表的部分字段(注意:带legacy-alias-forlogical-alias-for的属性不要求必填字段):

字段必填默认值描述生成的函数
affects-layouttrue布尔值。修改该属性是否会令元素的布局失效bool property_affects_layout(PropertyID)
affects-stacking-contextfalse布尔值。该属性是否会让元素产生新的层叠上下文bool property_affects_stacking_context(PropertyID)
animation-type字符串。规范定义的属性动画方式,见下文AnimationType animation_type_from_longhand_property(PropertyID)
inherited布尔值。属性是否被子元素继承bool is_inherited_property(PropertyID)
initial字符串。未指定时属性的初始值NonnullRefPtr<CSSStyleValue> property_initial_value(JS::Realm&, PropertyID)
legacy-alias-for字符串。该属性所指向的旧名别名属性,见下文
logical-alias-for字符串数组。该属性所指向的逻辑别名属性,见下文
longhands[]字符串数组。若是简写属性(shorthand),列出其展开的子属性Vector<PropertyID> longhands_for_shorthand(PropertyID)
max-values1整数。该属性最多可解析多少个值,例如margin最多 4 个size_t property_maximum_value_count(PropertyID)
percentages-resolve-to字符串。百分比解析成什么类型,例如width的百分比解析为lengthOptional<ValueType> property_resolves_percentages_relative_to(PropertyID)
quirks[]字符串数组。属性在 quirks 模式下的特殊行为,见下文bool property_has_quirk(PropertyID, Quirk)
valid-identifiers[]字符串数组。属性接受哪些关键字。更推荐定义枚举并把枚举名写进valid-typesbool property_accepts_keyword(PropertyID, Keyword)
valid-types[]字符串数组。属性接受哪些值类型,见下文bool property_accepts_type(PropertyID, ValueType)

从源码看,生成器会逐个遍历该 JSON 对象:先判断属性是否设置了legacy-alias-for(GenerateCSSPropertyID.cpp),处理逻辑别名,再检查longhands(GenerateCSSPropertyID.cpp)与valid-types数组(GenerateCSSPropertyID.cpp),最终为每个属性产出对应的枚举成员、初始值函数与类型判定函数。

仓库中的真实示例可以直观印证字段用法。例如兼容性前缀属性的定义非常精简:

"-webkit-align-content": { "legacy-alias-for": "align-content" }

而一个完整属性则同时携带动画类型、继承性与初始值,例如color一类:

"animation-type": "by-computed-value", "inherited": true, "initial": "currentColor", "valid-types": [ ... ]

(对应 Properties.json 附近的color定义;animation简写属性则使用"initial": "none 0s ease 1 normal running 0s none"这样的复合初始值,见 Properties.json。)

2.1animation-type:属性如何被动画化

该字段的合法取值由 Web Animations 规范定义,JSON 值与规范术语的对应关系如下:

规范术语JSON 值
not animatablenone
discretediscrete
by computed valueby-computed-value
repeatable listrepeatable-list
(见规范正文)custom

从仓库数据看,colorby-computed-value(按计算值平滑过渡),align-content等布局相关属性是discrete(离散跳变),animation-duration则是none(不可动画)。

2.2legacy-alias-forlogical-alias-for:两个名字相似但概念不同的别名

  • 旧名别名(legacy name alias):属性在规范中的名字发生了变化,但语法没有变,因此设置旧名等同于直接设置新名。例如font-stretch被重命名为font-width,于是font-stretch成为font-width的旧名别名。仓库中大量-webkit-*属性就是这类别名的典型:-webkit-align-content-webkit-animation-webkit-border-radius等全部通过legacy-alias-for指回标准属性名(见 Properties.json)。
  • 逻辑别名(logical alias):例如margin-block-start,它会根据应用到的元素,把值赋给margin-topmargin-bottommargin-leftmargin-right中的某一个。因此需要在logical-alias-for中列出所有可能被其指向的属性。

2.3quirks:Quirks 模式下的特殊行为

Quirks 规范定义了以下两种特殊行为:

规范术语JSON 值
The hashless hex color quirkhashless-hex-color
The unitless length quirkunitless-length

例如在 quirks 模式下,允许background-color: f00这种省略#的十六进制颜色写法,或width: 10这种省略单位的长度写法。是否启用这些宽松解析,正是由该字段驱动。

2.4valid-types:值类型与带括号区间记法

valid-types数组列出的是 CSS Values and Units 规范中定义的值类型名(去掉尖括号后的名字)。对数值类型,项目使用带括号区间记法(bracketed range notation):例如width可以接受任意非负长度,因此其valid-types数组中含有"length [0,∞]"。这种写法让生成器可以直接生成带范围约束的解析与校验逻辑,将规范约束落到类型系统层面。

三、Keywords.json:全局关键字注册表

Keywords.json(共 430 行)是一个纯字符串数组,每个元素是一个 CSS 关键字,例如autononemediumcurrentcolor。它会生成Keyword.hKeyword.cpp。任何属性或媒体特性用到的关键字都必须在这里登记。

除了标准关键字,仓库中还登记了一批内部关键字,例如-libweb-center-libweb-left-libweb-link,以及一系列-libweb-palette-*关键字(如-libweb-palette-base-libweb-palette-selection),它们用于把 SerenityOS 系统调色板暴露给 Web 内容(见 Keywords.json)。这说明了该文件的扩展边界:不仅服务标准 CSS,也承载浏览器自身的私有扩展。

生成的代码提供:

  • Keyword枚举,供CSSKeywordValue使用
  • Optional<Keyword> keyword_from_string(StringView):尝试把字符串转成Keyword
  • StringView string_from_keyword(Keyword):把Keyword转回字符串
  • bool is_css_wide_keyword(StringView):判断字符串是否为特殊的 "CSS-wide keywords"(如inheritinitialunsetrevert

四、Enums.json:一键生成"关键字集合"枚举

Enums.json(共 521 行)是一个 JSON 对象,键是枚举名,值是关键字名数组。它生成Enums.hEnums.cpp

很多属性需要接受一组固定的关键字,逐个重复书写valid-identifiers容易出错且冗长。Enums.json允许自动生成这类枚举,以及枚举与Keyword、字符串之间的互转函数。生成的枚举还可以直接通过枚举名出现在Properties.jsonvalid-types数组中,从而在属性定义中被复用。典型的例子是border-*-style系列属性接受同一组关键字,因此被实现为line-style枚举(见 Enums.json)。仓库数据还显示align-contentalign-itemsalign-self等各自的取值集合也都以枚举形式集中定义(见 Enums.json)。

以枚举 "foo" 为例,每个枚举生成的代码包括:

  • 枚举类型Foo
  • Optional<Foo> keyword_to_foo(Keyword):把Keyword转换为Foo
  • Keyword to_keyword(Foo):把Foo转回Keyword
  • StringView to_string(Foo):直接把Foo转成字符串

五、PseudoClasses.json:伪类元数据

PseudoClasses.json(共 146 行)是一个 JSON 对象,键为选择器伪类名,值为描述该伪类的对象。它生成PseudoClass.hPseudoClass.cpp

每个条目只有一个必填字段argument,它是伪类函数参数的语法(grammar)字符串;对标识符式伪类(如:hover:active)则为空字符串。语法直接取自规范。仓库中的实例:

"active": { "argument": "" }, "dir": { "argument": "<ident>" }, "has": { "argument": "<forgiving-relative-selector-list>" }, "host": { "argument": "<compound-selector>?" }, "is": { "argument": "<forgiving-selector-list>" }, "lang": { "argument": "<language-ranges>" }

(分别见 PseudoClasses.json、PseudoClasses.json、PseudoClasses.json、PseudoClasses.json、PseudoClasses.json、PseudoClasses.json。)

从中可以看到:has():is()这类接受"宽松选择器列表"的新伪类,与:hover这类无参伪类的差别——argument直接承载了后续解析函数所需的关键信息。

生成的代码提供:

  • PseudoClass枚举,列出所有伪类名
  • Optional<PseudoClass> pseudo_class_from_string(StringView):把字符串解析为PseudoClass
  • StringView pseudo_class_name(PseudoClass):把PseudoClass转回字符串
  • PseudoClassMetadata结构体,保存 JSON 文件中的数据
  • PseudoClassMetadata pseudo_class_metadata(PseudoClass):获取该元数据

六、MediaFeatures.json:@media可查询的媒体特性

MediaFeatures.json(共 261 行)是一个 JSON 对象,键为媒体特性名,值为描述该特性的对象。它生成MediaFeatureID.hMediaFeatureID.cpp

<media-feature>是媒体查询可以检查的取值,列在最新 Media Queries 规范的@media描述符表中。这里的定义可以看作Properties.json定义的简化版本:

字段描述
type字符串。媒体特性的求值方式:discrete(离散)或range(范围)
values字符串数组。直接取自规范:关键字原样保留,类型名带<>。类型可以是<boolean><integer><length><ratio><resolution>

仓库中的真实定义示例:

"any-hover": { "type": "discrete", "values": ["none", "hover"] }, "color": { "type": "range", "values": ["<integer>"] }, "color-gamut": { "type": "discrete", "values": ["srgb", "p3", "rec2020"] }, "aspect-ratio":{ "type": "range", "values": ["<ratio>"] }

(分别见 MediaFeatures.json、MediaFeatures.json、MediaFeatures.json、MediaFeatures.json。)color使用range类型表示"颜色位深为 n",any-hover使用discrete表示设备是否支持悬停,二者求值逻辑截然不同。

生成的代码提供:

  • MediaFeatureValueType枚举,列出可能的取值类型
  • MediaFeatureID枚举,列出每个媒体特性
  • Optional<MediaFeatureID> media_feature_id_from_string(StringView):字符串转MediaFeatureID
  • StringView string_from_media_feature_id(MediaFeatureID)MediaFeatureID转回字符串
  • bool media_feature_type_is_range(MediaFeatureID):判断是否为range类型(区别于discrete
  • bool media_feature_accepts_type(MediaFeatureID, MediaFeatureValueType):是否接受该值类型
  • bool media_feature_accepts_keyword(MediaFeatureID, Keyword):是否接受该关键字

七、MathFunctions.json:CSS 数学函数

MathFunctions.json(共 232 行)是一个 JSON 对象,描述每个 CSS 数学函数,键为函数名,值为描述函数属性的对象。它生成MathFunctions.hMathFunctions.cpp

每个条目目前只有一个属性parameters,即参数定义对象数组。参数定义具有以下字段:

字段描述
name字符串。参数名,与规范一致
type字符串。参数可接受类型,单个字符串,用\|分隔
required布尔值。该参数是否必填

仓库中的示例:

"abs": { "parameters": [ { "name": "value", "type": "<number>|<dimension>|<percentage>", "required": true } ] }, "atan2": { "parameters": [ { "name": "y", "type": "<number>|<dimension>|<percentage>", "required": true }, { "name": "x", "type": "<number>|<dimension>|<percentage>", "required": true } ] }, "clamp": { "parameters": [ { "name": "min", ... }, { "name": "central", ... }, ... ] }

(见 MathFunctions.json、MathFunctions.json、MathFunctions.json。)atan2(y, x)的两个参数都是必填的数值类参数,abs(value)接受数值、维度或百分比。

生成的代码提供:

  • MathFunction枚举,列出全部数学函数
  • CSS Parser 的parse_math_function()方法的实现

也就是说,MathFunctions.json不止生成数据,还会直接生成解析器的函数体,把"支持哪些数学函数、每个函数接受什么参数"编译进解析流程。

八、TransformFunctions.json:CSS 变换函数

TransformFunctions.json(共 290 行)是一个 JSON 对象,描述每个 CSS 变换函数,键为函数名,值为描述函数属性的对象。它生成TransformFunctions.hTransformFunctions.cpp

每个条目目前只有一个属性parameters(参数定义对象数组),参数定义字段如下:

字段描述
type字符串。参数可接受类型
required布尔值。该参数是否必填

与数学函数不同,变换函数的参数定义不携带name字段。仓库中的示例——matrix()有 6 个必填<number>参数,matrix3d()则有 16 个:

"matrix": { "parameters": [ { "type": "<number>", "required": true }, { "type": "<number>", "required": true }, ... 共 6 个 ... ] }, "matrix3d": { "parameters": [ ... 共 16 个 ... ] }

(见 TransformFunctions.json、TransformFunctions.json。)

生成的代码提供:

  • TransformFunction枚举,列出全部变换函数
  • Optional<TransformFunction> transform_function_from_string(StringView):把字符串解析为TransformFunction
  • StringView to_string(TransformFunction):把TransformFunction转回字符串
  • TransformFunctionMetadata transform_function_metadata(TransformFunction):获取函数元数据(如参数列表)

九、生成器实现要点

代码生成器统一位于 Meta/Lagom/Tools/CodeGenerators/LibWeb,每个 JSON 文件对应一个GenerateCSS*工具:

JSON 输入生成器主要输出
Properties.jsonGenerateCSSPropertyID.cppPropertyID.h/cppGeneratedCSSStyleProperties.h/cpp/idl
Keywords.jsonGenerateCSSKeyword.cppKeyword.h/cpp
Enums.jsonGenerateCSSEnums.cppEnums.h/cpp
PseudoClasses.jsonGenerateCSSPseudoClass.cppPseudoClass.h/cpp
MediaFeatures.jsonGenerateCSSMediaFeatureID.cppMediaFeatureID.h/cpp
MathFunctions.jsonGenerateCSSMathFunctions.cppMathFunctions.h/cpp
TransformFunctions.jsonGenerateCSSTransformFunctions.cppTransformFunctions.h/cpp

从实现细节看(以 GenerateCSSPropertyID.cpp 为例):

  • 生成器以LibMain/Main.h为入口,使用LibCore::ArgsParser解析命令行参数,通过AK::SourceGenerator组织输出文本(见 GenerateCSSPropertyID.cpp)。
  • 输出代码中注入必要的头文件(如AK/NonnullRefPtr.hLibJS/Forward.hLibWeb/Forward.h),保证生成的.h/.cpp可以独立编译(见 GenerateCSSPropertyID.cpp)。
  • 生成的.cpp还会#include <LibWeb/CSS/Enums.h>,把Enums.json生成的枚举直接嵌入属性类型判定逻辑(见 GenerateCSSPropertyID.cpp)——这印证了Enums.jsonProperties.json之间的联动关系。
  • 生成器会自动跳过只含legacy-alias-for的别名属性,避免为别名生成重复的完整定义。

由于生成是构建期自动完成的,修改 JSON 后重新构建即可看到新生成的代码出现在Build/<build-preset>/Lagom/Userland/Libraries/LibWeb/CSS/中。日常开发中基本无需手工触碰生成产物。

十、实战工作流:如何新增一个 CSS 属性或关键字

综合以上内容,在 SerenityOS/LibWeb 中新增一个 CSS 能力通常遵循以下步骤:

  1. 登记关键字:如果新属性用到的新关键字尚未收录,先在 Keywords.json 的字符串数组中追加(所有属性、媒体特性共用的关键字池)。
  2. 定义枚举(可选):若属性接受一组固定关键字(如新的border-*-style同类属性),在 Enums.json 中添加枚举定义,便于在多个属性间复用。
  3. 注册属性:在 Properties.json 中为属性新增条目,按需填写必填字段animation-typeinheritedinitial,以及valid-types/valid-identifierslonghandsmax-valuespercentages-resolve-toquirks等可选字段。
  4. 新增伪类/媒体特性/函数(视情况):在对应的 PseudoClasses.json(填argument语法)、MediaFeatures.json(填typevalues)、MathFunctions.json 或 TransformFunctions.json(填parameters)中登记。
  5. 重新构建:构建系统会自动运行对应生成器,产出新的枚举、ID 与解析函数;此后即可在解析器、样式计算与布局代码中消费这些生成接口。
  6. 查阅生成产物:如需确认生成结果,查看Build/<build-preset>/Lagom/Userland/Libraries/LibWeb/CSS/下的新文件。

这套流程的每个环节都有源码级支撑:关键字注册表、枚举复用、属性元数据、伪类语法、媒体特性类型、数学/变换函数参数,全部由统一的 JSON 数据驱动,最终在构建期被Meta/Lagom/Tools/CodeGenerators/LibWeb下对应的生成器转换为可编译、可调用的 C++ 代码。这也是 LibWeb 能够以较低维护成本跟上 CSS 规范演进的关键机制之一。

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询