diff --git a/Documentation/CSSGeneratedFiles.md b/Documentation/CSSGeneratedFiles.md index ee5557aaca..bfffa71a21 100644 --- a/Documentation/CSSGeneratedFiles.md +++ b/Documentation/CSSGeneratedFiles.md @@ -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. diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index a02a2091dd..c0099336b4 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -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 diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index e4ae5884e1..a92755fe74 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -565,6 +566,11 @@ private: RefPtr parse_white_space_trim_value(TokenStream&); RefPtr parse_will_change_value(TokenStream&); +#define __ENUMERATE_GENERATED_CSS_VALUE_TYPE(value_type_name) \ + RefPtr parse_##value_type_name##_value(TokenStream& tokens); + ENUMERATE_GENERATED_CSS_VALUE_TYPES +#undef __ENUMERATE_GENERATED_CSS_VALUE_TYPE + RefPtr convert_to_calculation_node(CalcParsing::Node const&, CalculationContext const&); RefPtr parse_a_calculation(TokenStream&, CalculationContext const&); diff --git a/Libraries/LibWeb/CSS/ValueTypes.json b/Libraries/LibWeb/CSS/ValueTypes.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Libraries/LibWeb/CSS/ValueTypes.json @@ -0,0 +1 @@ +{} diff --git a/Meta/CMake/libweb_generators.cmake b/Meta/CMake/libweb_generators.cmake index 875ae45c7e..d96201e312 100644 --- a/Meta/CMake/libweb_generators.cmake +++ b/Meta/CMake/libweb_generators.cmake @@ -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" diff --git a/Meta/Generators/generate_libweb_css_value_types_parsing.py b/Meta/Generators/generate_libweb_css_value_types_parsing.py new file mode 100644 index 0000000000..265def60b6 --- /dev/null +++ b/Meta/Generators/generate_libweb_css_value_types_parsing.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2026, Callum Law +# +# 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 + +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 Parser::parse_{name_snake_case}_value(TokenStream& 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())