Meta+LibWeb: Initial scaffolding for CSS value type parsing code gen

In the future we should switch to using a better file format for this,
i.e. one that supports directly pasting CSS grammar production blocks
(https://drafts.csswg.org/css-values-4/#css-grammar-production-block)
and has support for inline comments, but we use JSON for now for
simplicity's sake.
This commit is contained in:
Callum Law 2026-04-24 21:32:08 +12:00 committed by Sam Atkins
parent 324ed5de0d
commit 8849435d6f
6 changed files with 194 additions and 0 deletions

View file

@ -415,3 +415,24 @@ The generated code provides:
- `bool units_are_compatible(FooUnit, FooUnit)` which returns whether these are compatible - basically whether you can convert from one to the other.
- `double ratio_between_units(FooUnit, FooUnit)` to get a multiplier for converting the first unit into the second.
- `bool is_absolute(LengthUnit)`, `bool is_font_relative(LengthUnit)`, `bool is_viewport_relative(LengthUnit)`, and `bool is_relative(LengthUnit)` for checking the category of length units.
## ValueTypes.json
This is a JSON object with the keys being value type names, and the values being the definition of the value type.
It generates Parser/GeneratedValueTypesParsing.h and Parser/GeneratedValueTypesParsing.cpp
NOTE: The generated parsing code is limited to the information given by the CSS value definition grammar, if there are
additional requirements not representable in this grammar (e.g. bespoke resultant StyleValue types, default value
handling, etc) parsing will need to be implemented manually.
Each value type has the following properties:
| Field | Required | Description |
|-------------|----------|-------------------------------------------------------------------|
| `spec` | Yes | A link to the CSS specification where this value type is defined. |
| `grammar` | Yes | The grammar of the CSS value type, as defined in the spec. |
| `__comment` | No | Strings, for when you want to leave a note. |
The generated code provides:
- A `GenerateValueTypes` enum, listing each of the generated value types.
- For each of those...
- A `CSS::Parser::Parser::parse_foo_value` method to parse the value type.

View file

@ -1214,6 +1214,7 @@ set(GENERATED_SOURCES
CSS/Keyword.cpp
CSS/MathFunctions.cpp
CSS/MediaFeatureID.cpp
CSS/Parser/GeneratedValueTypesParsing.cpp
CSS/PropertyID.cpp
CSS/PseudoClass.cpp
CSS/PseudoElement.cpp

View file

@ -21,6 +21,7 @@
#include <LibWeb/CSS/PageSelector.h>
#include <LibWeb/CSS/ParsedFontFace.h>
#include <LibWeb/CSS/Parser/ComponentValue.h>
#include <LibWeb/CSS/Parser/GeneratedValueTypesParsing.h>
#include <LibWeb/CSS/Parser/RuleContext.h>
#include <LibWeb/CSS/Parser/TokenStream.h>
#include <LibWeb/CSS/Parser/Tokenizer.h>
@ -565,6 +566,11 @@ private:
RefPtr<StyleValue const> parse_white_space_trim_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_will_change_value(TokenStream<ComponentValue>&);
#define __ENUMERATE_GENERATED_CSS_VALUE_TYPE(value_type_name) \
RefPtr<StyleValue const> parse_##value_type_name##_value(TokenStream<ComponentValue>& tokens);
ENUMERATE_GENERATED_CSS_VALUE_TYPES
#undef __ENUMERATE_GENERATED_CSS_VALUE_TYPE
RefPtr<CalculationNode const> convert_to_calculation_node(CalcParsing::Node const&, CalculationContext const&);
RefPtr<CalculationNode const> parse_a_calculation(TokenStream<ComponentValue>&, CalculationContext const&);

View file

@ -0,0 +1 @@
{}

View file

@ -86,6 +86,15 @@ function (generate_css_implementation)
arguments -j "${LIBWEB_INPUT_FOLDER}/CSS/TransformFunctions.json"
)
invoke_py_generator(
"GeneratedValueTypesParsing.cpp"
"generate_libweb_css_value_types_parsing.py"
"${LIBWEB_INPUT_FOLDER}/CSS/ValueTypes.json"
"CSS/Parser/GeneratedValueTypesParsing.h"
"CSS/Parser/GeneratedValueTypesParsing.cpp"
arguments -j "${LIBWEB_INPUT_FOLDER}/CSS/ValueTypes.json"
)
invoke_py_generator(
"Units.cpp"
"generate_libweb_css_units.py"

View file

@ -0,0 +1,156 @@
#!/usr/bin/env python3
# Copyright (c) 2026, Callum Law <callumlaw1709@outlook.com>
#
# SPDX-License-Identifier: BSD-2-Clause
import argparse
import json
import sys
from pathlib import Path
from typing import Any
from typing import TextIO
sys.path.append(str(Path(__file__).resolve().parent.parent))
from Utils.utils import snake_casify
def json_is_valid(value_type_data: dict[str, Any], json_path: str) -> bool:
is_valid = True
most_recent_value_type_name = ""
for value_type_name, value_type_definition in value_type_data.items():
if value_type_name.lower() < most_recent_value_type_name.lower():
print(
f"{json_path}: Value type `{value_type_name}` is in the wrong position. Please keep this list alphabetical!",
file=sys.stderr,
)
is_valid = False
most_recent_value_type_name = value_type_name
if not isinstance(value_type_definition, dict):
print(f"{json_path}: Value type `{value_type_name}` is not an object", file=sys.stderr)
is_valid = False
continue
if "spec" not in value_type_definition:
print(f"{json_path}: Value type `{value_type_name}` is missing a spec link", file=sys.stderr)
is_valid = False
elif not isinstance(value_type_definition["spec"], str):
print(f"{json_path}: Value type `{value_type_name}` has a spec field that is not a string", file=sys.stderr)
is_valid = False
if "grammar" not in value_type_definition:
print(f"{json_path}: Value type `{value_type_name}` is missing a grammar", file=sys.stderr)
is_valid = False
elif not isinstance(value_type_definition["grammar"], str):
print(
f"{json_path}: Value type `{value_type_name}` has a grammar field that is not a string",
file=sys.stderr,
)
is_valid = False
for field_name in value_type_definition:
if field_name in ("spec", "grammar", "__comment"):
continue
print(
f"{json_path}: Value type `{value_type_name}` has an unexpected field `{field_name}`",
file=sys.stderr,
)
is_valid = False
return is_valid
def value_type_name_to_snake_case(value_type_name: str) -> str:
return snake_casify(value_type_name[1:-1])
def generate_header_file(out: TextIO, value_type_data: dict[str, Any]) -> None:
out.write("""// This file is generated by generate_libweb_css_value_types_parsing.py
#pragma once
namespace Web::CSS {
#define ENUMERATE_GENERATED_CSS_VALUE_TYPES \\
""")
for value_type_name in value_type_data:
out.write(f" __ENUMERATE_GENERATED_CSS_VALUE_TYPE({value_type_name_to_snake_case(value_type_name)}) \\\n")
out.write("\n")
out.write("}")
def generate_implementation_file(out: TextIO, value_type_data: dict[str, Any]) -> None:
out.write("""// This file is generated by generate_libweb_css_value_types_parsing.py
#include <LibWeb/CSS/Parser/Parser.h>
namespace Web::CSS::Parser {
""")
for value_type_name, value_type_definition in value_type_data.items():
spec_link = value_type_definition["spec"]
grammar = value_type_definition["grammar"]
name_snake_case = value_type_name_to_snake_case(value_type_name)
out.write(f"""
// {spec_link}
RefPtr<StyleValue const> Parser::parse_{name_snake_case}_value(TokenStream<ComponentValue>& tokens)
{{
// {value_type_name} = {grammar}
(void)tokens;
// FIXME: Generate parser code for this value type.
return nullptr;
}}
""")
out.write("}\n")
def main() -> int:
parser = argparse.ArgumentParser(description="Generate CSS value types parsing methods", add_help=False)
parser.add_argument("--help", action="help", help="Show this help message and exit")
parser.add_argument(
"-h",
"--header",
required=True,
help="Path to the GeneratedValueTypesParsing header file to generate",
)
parser.add_argument(
"-c",
"--implementation",
required=True,
help="Path to the GeneratedValueTypesParsing implementation file to generate",
)
parser.add_argument("-j", "--json", required=True, help="Path to the JSON file to read from")
args = parser.parse_args()
with open(args.json, "r", encoding="utf-8") as json_file:
value_type_data = json.load(json_file)
if not isinstance(value_type_data, dict):
raise RuntimeError(f"{args.json}: expected a JSON object")
if not json_is_valid(value_type_data, args.json):
sys.exit(1)
with (
open(args.header, "w", encoding="utf-8") as header_file,
open(args.implementation, "w", encoding="utf-8") as implementation_file,
):
generate_header_file(header_file, value_type_data)
generate_implementation_file(implementation_file, value_type_data)
return 0
if __name__ == "__main__":
sys.exit(main())