diff --git a/Meta/CMake/libweb_generators.cmake b/Meta/CMake/libweb_generators.cmake index 4f5a35cec5f..f0eb76e81f1 100644 --- a/Meta/CMake/libweb_generators.cmake +++ b/Meta/CMake/libweb_generators.cmake @@ -38,9 +38,9 @@ function (generate_css_implementation) arguments -j "${LIBWEB_INPUT_FOLDER}/CSS/MathFunctions.json" ) - invoke_cpp_generator( + invoke_py_generator( "MediaFeatureID.cpp" - Lagom::GenerateCSSMediaFeatureID + "generate_libweb_css_media_feature_id.py" "${LIBWEB_INPUT_FOLDER}/CSS/MediaFeatures.json" "CSS/MediaFeatureID.h" "CSS/MediaFeatureID.cpp" diff --git a/Meta/Generators/generate_libweb_css_media_feature_id.py b/Meta/Generators/generate_libweb_css_media_feature_id.py new file mode 100644 index 00000000000..cc37f0dd09f --- /dev/null +++ b/Meta/Generators/generate_libweb_css_media_feature_id.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2022-2026, Sam Atkins +# Copyright (c) 2026-present, the Ladybird developers. +# +# SPDX-License-Identifier: BSD-2-Clause + +import argparse +import json +import sys + +from pathlib import Path +from typing import TextIO + +sys.path.append(str(Path(__file__).resolve().parent.parent)) + +from Utils.utils import title_casify +from Utils.utils import underlying_type_for_enum + +VALUE_TYPE_NAMES = { + "": "Boolean", + "": "Integer", + "": "Length", + "": "Ratio", + "": "Resolution", +} + + +def write_header_file(out: TextIO, media_feature_data: dict) -> None: + underlying_type = underlying_type_for_enum(len(media_feature_data)) + out.write(f"""#pragma once + +#include +#include +#include + +namespace Web::CSS {{ + +enum class MediaFeatureValueType {{ + Boolean, + Integer, + Length, + Ratio, + Resolution, +}}; + +enum class MediaFeatureID : {underlying_type} {{""") + + for name in media_feature_data: + out.write(f""" + {title_casify(name)},""") + + out.write(""" +}; + +Optional media_feature_id_from_string(StringView); +StringView string_from_media_feature_id(MediaFeatureID); + +bool media_feature_type_is_range(MediaFeatureID); +bool media_feature_accepts_type(MediaFeatureID, MediaFeatureValueType); +bool media_feature_accepts_keyword(MediaFeatureID, Keyword); + +bool media_feature_keyword_is_falsey(MediaFeatureID, Keyword); + +} +""") + + +def write_implementation_file(out: TextIO, media_feature_data: dict) -> None: + out.write(""" +#include +#include + +namespace Web::CSS { + +Optional media_feature_id_from_string(StringView string) +{""") + + for name in media_feature_data: + out.write(f""" + if (string.equals_ignoring_ascii_case("{name}"sv)) + return MediaFeatureID::{title_casify(name)}; +""") + + out.write(""" + return {}; +} + +StringView string_from_media_feature_id(MediaFeatureID media_feature_id) +{ + switch (media_feature_id) {""") + + for name in media_feature_data: + out.write(f""" + case MediaFeatureID::{title_casify(name)}: + return "{name}"sv;""") + + out.write(""" + } + VERIFY_NOT_REACHED(); +} + +bool media_feature_type_is_range(MediaFeatureID media_feature_id) +{ + switch (media_feature_id) {""") + + for name, feature in media_feature_data.items(): + is_range = "true" if feature["type"] == "range" else "false" + out.write(f""" + case MediaFeatureID::{title_casify(name)}: + return {is_range};""") + + out.write(""" + } + VERIFY_NOT_REACHED(); +} + +bool media_feature_accepts_type(MediaFeatureID media_feature_id, MediaFeatureValueType value_type) +{ + switch (media_feature_id) {""") + + for name, feature in media_feature_data.items(): + out.write(f""" + case MediaFeatureID::{title_casify(name)}:""") + + have_output_value_type_switch = False + if "values" in feature: + for type_name in feature["values"]: + # Skip keywords. + if not type_name.startswith("<"): + continue + if type_name not in VALUE_TYPE_NAMES: + print(f"Unrecognized media-feature value type: `{type_name}`", file=sys.stderr) + sys.exit(1) + if not have_output_value_type_switch: + out.write(""" + switch (value_type) {""") + have_output_value_type_switch = True + value_type = VALUE_TYPE_NAMES[type_name] + out.write(f""" + case MediaFeatureValueType::{value_type}: + return true;""") + + if have_output_value_type_switch: + out.write(""" + default: + return false; + }""") + else: + out.write(""" + return false;""") + + out.write(""" + } + VERIFY_NOT_REACHED(); +} + +bool media_feature_accepts_keyword(MediaFeatureID media_feature_id, Keyword keyword) +{ + switch (media_feature_id) {""") + + for name, feature in media_feature_data.items(): + out.write(f""" + case MediaFeatureID::{title_casify(name)}:""") + + have_output_keyword_switch = False + if "values" in feature: + for keyword_name in feature["values"]: + # Skip types. + if keyword_name.startswith("<"): + continue + if not have_output_keyword_switch: + out.write(""" + switch (keyword) {""") + have_output_keyword_switch = True + out.write(f""" + case Keyword::{title_casify(keyword_name)}: + return true;""") + + if have_output_keyword_switch: + out.write(""" + default: + return false; + }""") + else: + out.write(""" + return false;""") + + out.write(""" + } + VERIFY_NOT_REACHED(); +} + +bool media_feature_keyword_is_falsey(MediaFeatureID media_feature_id, Keyword keyword) +{ + switch (media_feature_id) {""") + + for name, feature in media_feature_data.items(): + false_keywords = feature.get("false-keywords") + if not false_keywords: + continue + out.write(f""" + case MediaFeatureID::{title_casify(name)}: + switch (keyword) {{""") + for false_keyword in false_keywords: + out.write(f""" + case Keyword::{title_casify(false_keyword)}:""") + out.write(""" + return true; + default: + return false; + }""") + + out.write(""" + default: + return false; + } +} + +} +""") + + +def main(): + parser = argparse.ArgumentParser(description="Generate CSS MediaFeatureID", 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 MediaFeatureID header file to generate") + parser.add_argument( + "-c", "--implementation", required=True, help="Path to the MediaFeatureID 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 input_file: + media_feature_data = json.load(input_file) + + with open(args.header, "w", encoding="utf-8") as output_file: + write_header_file(output_file, media_feature_data) + + with open(args.implementation, "w", encoding="utf-8") as output_file: + write_implementation_file(output_file, media_feature_data) + + +if __name__ == "__main__": + main() diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt b/Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt index 8e9fc87ad6b..ea2dec93702 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt @@ -2,7 +2,6 @@ set(SOURCES "") # avoid pulling SOURCES from parent scope lagom_tool(GenerateCSSDescriptors SOURCES GenerateCSSDescriptors.cpp LIBS LibMain) lagom_tool(GenerateCSSMathFunctions SOURCES GenerateCSSMathFunctions.cpp LIBS LibMain) -lagom_tool(GenerateCSSMediaFeatureID SOURCES GenerateCSSMediaFeatureID.cpp LIBS LibMain) lagom_tool(GenerateCSSNumericFactoryMethods SOURCES GenerateCSSNumericFactoryMethods.cpp LIBS LibMain) lagom_tool(GenerateCSSPropertyID SOURCES GenerateCSSPropertyID.cpp LIBS LibMain) lagom_tool(GenerateCSSPseudoElement SOURCES GenerateCSSPseudoElement.cpp LIBS LibMain) diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSMediaFeatureID.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSMediaFeatureID.cpp deleted file mode 100644 index a5acea85105..00000000000 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSMediaFeatureID.cpp +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright (c) 2022-2023, Sam Atkins - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include "GeneratorUtil.h" -#include -#include -#include -#include - -ErrorOr generate_header_file(JsonObject& media_feature_data, Core::File& file); -ErrorOr generate_implementation_file(JsonObject& media_feature_data, Core::File& file); - -ErrorOr ladybird_main(Main::Arguments arguments) -{ - StringView generated_header_path; - StringView generated_implementation_path; - StringView media_features_json_path; - - Core::ArgsParser args_parser; - args_parser.add_option(generated_header_path, "Path to the MediaFeatureID header file to generate", "generated-header-path", 'h', "generated-header-path"); - args_parser.add_option(generated_implementation_path, "Path to the MediaFeatureID implementation file to generate", "generated-implementation-path", 'c', "generated-implementation-path"); - args_parser.add_option(media_features_json_path, "Path to the JSON file to read from", "json-path", 'j', "json-path"); - args_parser.parse(arguments); - - auto json = TRY(read_entire_file_as_json(media_features_json_path)); - VERIFY(json.is_object()); - auto media_feature_data = json.as_object(); - - auto generated_header_file = TRY(Core::File::open(generated_header_path, Core::File::OpenMode::Write)); - auto generated_implementation_file = TRY(Core::File::open(generated_implementation_path, Core::File::OpenMode::Write)); - - TRY(generate_header_file(media_feature_data, *generated_header_file)); - TRY(generate_implementation_file(media_feature_data, *generated_implementation_file)); - - return 0; -} - -ErrorOr generate_header_file(JsonObject& media_feature_data, Core::File& file) -{ - StringBuilder builder; - SourceGenerator generator { builder }; - - generator.set("media_feature_id_underlying_type", underlying_type_for_enum(media_feature_data.size())); - - generator.append(R"~~~(#pragma once - -#include -#include -#include - -namespace Web::CSS { - -enum class MediaFeatureValueType { - Boolean, - Integer, - Length, - Ratio, - Resolution, -}; - -enum class MediaFeatureID : @media_feature_id_underlying_type@ {)~~~"); - - media_feature_data.for_each_member([&](auto& name, auto&) { - auto member_generator = generator.fork(); - member_generator.set("name:titlecase", title_casify(name)); - member_generator.append(R"~~~( - @name:titlecase@,)~~~"); - }); - - generator.append(R"~~~( -}; - -Optional media_feature_id_from_string(StringView); -StringView string_from_media_feature_id(MediaFeatureID); - -bool media_feature_type_is_range(MediaFeatureID); -bool media_feature_accepts_type(MediaFeatureID, MediaFeatureValueType); -bool media_feature_accepts_keyword(MediaFeatureID, Keyword); - -bool media_feature_keyword_is_falsey(MediaFeatureID, Keyword); - -} -)~~~"); - - TRY(file.write_until_depleted(generator.as_string_view().bytes())); - return {}; -} - -ErrorOr generate_implementation_file(JsonObject& media_feature_data, Core::File& file) -{ - StringBuilder builder; - SourceGenerator generator { builder }; - generator.append(R"~~~( -#include -#include - -namespace Web::CSS { - -Optional media_feature_id_from_string(StringView string) -{)~~~"); - - media_feature_data.for_each_member([&](auto& name, auto&) { - auto member_generator = generator.fork(); - member_generator.set("name", name); - member_generator.set("name:titlecase", title_casify(name)); - member_generator.append(R"~~~( - if (string.equals_ignoring_ascii_case("@name@"sv)) - return MediaFeatureID::@name:titlecase@; -)~~~"); - }); - - generator.append(R"~~~( - return {}; -} - -StringView string_from_media_feature_id(MediaFeatureID media_feature_id) -{ - switch (media_feature_id) {)~~~"); - - media_feature_data.for_each_member([&](auto& name, auto&) { - auto member_generator = generator.fork(); - member_generator.set("name", name); - member_generator.set("name:titlecase", title_casify(name)); - member_generator.append(R"~~~( - case MediaFeatureID::@name:titlecase@: - return "@name@"sv;)~~~"); - }); - - generator.append(R"~~~( - } - VERIFY_NOT_REACHED(); -} - -bool media_feature_type_is_range(MediaFeatureID media_feature_id) -{ - switch (media_feature_id) {)~~~"); - - media_feature_data.for_each_member([&](auto& name, auto& value) { - VERIFY(value.is_object()); - auto& feature = value.as_object(); - - auto member_generator = generator.fork(); - member_generator.set("name:titlecase", title_casify(name)); - VERIFY(feature.has("type"sv)); - auto feature_type = feature.get_string("type"sv); - VERIFY(feature_type.has_value()); - member_generator.set("is_range", feature_type.value() == "range"sv ? "true"_string : "false"_string); - member_generator.append(R"~~~( - case MediaFeatureID::@name:titlecase@: - return @is_range@;)~~~"); - }); - - generator.append(R"~~~( - } - VERIFY_NOT_REACHED(); -} - -bool media_feature_accepts_type(MediaFeatureID media_feature_id, MediaFeatureValueType value_type) -{ - switch (media_feature_id) {)~~~"); - - media_feature_data.for_each_member([&](auto& name, auto& member) { - VERIFY(member.is_object()); - auto& feature = member.as_object(); - - auto member_generator = generator.fork(); - member_generator.set("name:titlecase", title_casify(name)); - member_generator.append(R"~~~( - case MediaFeatureID::@name:titlecase@:)~~~"); - - bool have_output_value_type_switch = false; - if (feature.has("values"sv)) { - auto append_value_type_switch_if_needed = [&] { - if (!have_output_value_type_switch) { - member_generator.append(R"~~~( - switch (value_type) {)~~~"); - } - have_output_value_type_switch = true; - }; - auto values = feature.get_array("values"sv); - VERIFY(values.has_value()); - auto& values_array = values.value(); - for (auto& type : values_array.values()) { - VERIFY(type.is_string()); - auto type_name = type.as_string(); - // Skip keywords. - if (!type_name.starts_with('<')) - continue; - if (type_name == "") { - append_value_type_switch_if_needed(); - member_generator.append(R"~~~( - case MediaFeatureValueType::Boolean: - return true;)~~~"); - } else if (type_name == "") { - append_value_type_switch_if_needed(); - member_generator.append(R"~~~( - case MediaFeatureValueType::Integer: - return true;)~~~"); - } else if (type_name == "") { - append_value_type_switch_if_needed(); - member_generator.append(R"~~~( - case MediaFeatureValueType::Length: - return true;)~~~"); - } else if (type_name == "") { - append_value_type_switch_if_needed(); - member_generator.append(R"~~~( - case MediaFeatureValueType::Ratio: - return true;)~~~"); - } else if (type_name == "") { - append_value_type_switch_if_needed(); - member_generator.append(R"~~~( - case MediaFeatureValueType::Resolution: - return true;)~~~"); - } else { - warnln("Unrecognized media-feature value type: `{}`", type_name); - VERIFY_NOT_REACHED(); - } - } - } - if (have_output_value_type_switch) { - member_generator.append(R"~~~( - default: - return false; - })~~~"); - } else { - member_generator.append(R"~~~( - return false;)~~~"); - } - }); - - generator.append(R"~~~( - } - VERIFY_NOT_REACHED(); -} - -bool media_feature_accepts_keyword(MediaFeatureID media_feature_id, Keyword keyword) -{ - switch (media_feature_id) {)~~~"); - - media_feature_data.for_each_member([&](auto& name, auto& member) { - VERIFY(member.is_object()); - auto& feature = member.as_object(); - - auto member_generator = generator.fork(); - member_generator.set("name:titlecase", title_casify(name)); - member_generator.append(R"~~~( - case MediaFeatureID::@name:titlecase@:)~~~"); - - bool have_output_keyword_switch = false; - if (feature.has("values"sv)) { - auto append_keyword_switch_if_needed = [&] { - if (!have_output_keyword_switch) { - member_generator.append(R"~~~( - switch (keyword) {)~~~"); - } - have_output_keyword_switch = true; - }; - auto values = feature.get_array("values"sv); - VERIFY(values.has_value()); - auto& values_array = values.value(); - for (auto& keyword : values_array.values()) { - VERIFY(keyword.is_string()); - auto const& keyword_name = keyword.as_string(); - // Skip types. - if (keyword_name.starts_with('<')) - continue; - append_keyword_switch_if_needed(); - - auto keyword_generator = member_generator.fork(); - keyword_generator.set("keyword:titlecase", title_casify(keyword_name)); - keyword_generator.append(R"~~~( - case Keyword::@keyword:titlecase@: - return true;)~~~"); - } - } - if (have_output_keyword_switch) { - member_generator.append(R"~~~( - default: - return false; - })~~~"); - } else { - member_generator.append(R"~~~( - return false;)~~~"); - } - }); - - generator.append(R"~~~( - } - VERIFY_NOT_REACHED(); -} - -bool media_feature_keyword_is_falsey(MediaFeatureID media_feature_id, Keyword keyword) -{ - switch (media_feature_id) {)~~~"); - media_feature_data.for_each_member([&](auto& name, JsonValue const& feature_value) { - VERIFY(feature_value.is_object()); - auto& feature = feature_value.as_object(); - auto false_keywords = feature.get_array("false-keywords"sv); - if (!false_keywords.has_value() || false_keywords->is_empty()) - return; - - auto member_generator = generator.fork(); - member_generator.set("name:titlecase", title_casify(name)); - member_generator.append(R"~~~( - case MediaFeatureID::@name:titlecase@: - switch (keyword) {)~~~"); - - false_keywords.value().for_each([&](JsonValue const& value) { - auto value_generator = member_generator.fork(); - member_generator.set("false_keyword:titlecase", title_casify(value.as_string())); - member_generator.append(R"~~~( - case Keyword::@false_keyword:titlecase@:)~~~"); - }); - member_generator.append(R"~~~( - return true; - default: - return false; - })~~~"); - }); - - generator.append(R"~~~( - default: - return false; - } -} - -} -)~~~"); - - TRY(file.write_until_depleted(generator.as_string_view().bytes())); - return {}; -}