/* * Copyright (c) 2025, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ #include "GeneratorUtil.h" #include #include #include #include #include ErrorOr generate_header_file(JsonObject& dimensions_data, Core::File& file); ErrorOr generate_implementation_file(JsonObject& dimensions_data, Core::File& file); bool json_is_valid(JsonObject& dimensions_data, StringView json_path); ErrorOr ladybird_main(Main::Arguments arguments) { StringView generated_header_path; StringView generated_implementation_path; StringView json_path; Core::ArgsParser args_parser; args_parser.add_option(generated_header_path, "Path to the Units header file to generate", "generated-header-path", 'h', "generated-header-path"); args_parser.add_option(generated_implementation_path, "Path to the Units implementation file to generate", "generated-implementation-path", 'c', "generated-implementation-path"); args_parser.add_option(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(json_path)); VERIFY(json.is_object()); auto dimensions_data = json.as_object(); if (!json_is_valid(dimensions_data, json_path)) return 1; 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(dimensions_data, *generated_header_file)); TRY(generate_implementation_file(dimensions_data, *generated_implementation_file)); return 0; } bool json_is_valid(JsonObject& dimensions_data, StringView json_path) { bool is_valid = true; String most_recent_dimension_name; dimensions_data.for_each_member([&](auto& dimension_name, JsonValue const& value) { // Dimensions should be in alphabetical order if (dimension_name.to_ascii_lowercase() < most_recent_dimension_name.to_ascii_lowercase()) { warnln("{}: Dimension `{}` is in the wrong position. Please keep this list alphabetical!", json_path, dimension_name); is_valid = false; } most_recent_dimension_name = dimension_name; String most_recent_unit_name; Optional canonical_unit; value.as_object().for_each_member([&](auto& unit_name, JsonValue const& unit_value) { auto& unit = unit_value.as_object(); // Units should be in alphabetical order if (unit_name.to_ascii_lowercase() < most_recent_unit_name.to_ascii_lowercase()) { warnln("{}: {} unit `{}` is in the wrong position. Please keep this list alphabetical!", json_path, dimension_name, unit_name); is_valid = false; } most_recent_unit_name = unit_name; // A unit must have exactly 1 of: // - is-canonical-unit: true // - number-of-canonical-unit // - relative-to bool is_canonical_unit = unit.get_bool("is-canonical-unit"sv) == true; auto number_of_canonical_unit = unit.get_double_with_precision_loss("number-of-canonical-unit"sv); auto relative_to = unit.get_string("relative-to"sv); auto provided_count = (is_canonical_unit ? 1 : 0) + (number_of_canonical_unit.has_value() ? 1 : 0) + (relative_to.has_value() ? 1 : 0); if (provided_count != 1) { warnln("{}: {} unit `{}` must have exactly 1 of `is-canonical-unit: true`, `number-of-canonical-unit`, or `relative-to` provided.", json_path, dimension_name, unit_name); is_valid = false; } // Exactly 1 canonical unit is allowed. if (is_canonical_unit) { if (canonical_unit.has_value()) { warnln("{}: {} unit `{}` marked canonical, but `{}` was already. Must have exactly 1.", json_path, dimension_name, unit_name, canonical_unit.value()); is_valid = false; } else { canonical_unit = unit_name; } } // Also, relative-to has fixed values and is only permitted for length units, at least for now. if (relative_to.has_value()) { if (dimension_name == "length"sv) { if (!first_is_one_of(relative_to.value(), "font"sv, "viewport"sv)) { warnln("{}: {} unit `{}` is marked as relative to `{}`, which is unsupported.", json_path, dimension_name, unit_name, relative_to.value()); is_valid = false; } } else { warnln("{}: {} unit `{}` is marked as relative, but only relative length units are currently supported.", json_path, dimension_name, unit_name); is_valid = false; } } }); // Must have a canonical unit. if (!canonical_unit.has_value()) { warnln("{}: {} has no unit marked as canonical. Must have exactly 1.", json_path, dimension_name); is_valid = false; } }); return is_valid; } ErrorOr generate_header_file(JsonObject& dimensions_data, Core::File& file) { StringBuilder builder; SourceGenerator generator { builder }; generator.append(R"~~~( #pragma once #include #include namespace Web::CSS { )~~~"); generator.set("enum_type", underlying_type_for_enum(dimensions_data.size())); generator.appendln("enum class DimensionType : @enum_type@ {"); dimensions_data.for_each_member([&](auto& name, auto&) { auto dimension_generator = generator.fork(); dimension_generator.set("dimension_name:titlecase", title_casify(name)); dimension_generator.appendln(" @dimension_name:titlecase@,"); }); generator.append(R"~~~( }; Optional dimension_for_unit(StringView); )~~~"); dimensions_data.for_each_member([&](auto& dimension_name, auto& value) { auto& units = value.as_object(); auto enum_generator = generator.fork(); enum_generator.set("dimension_name:titlecase", title_casify(dimension_name)); enum_generator.set("dimension_name:snakecase", snake_casify(dimension_name)); enum_generator.set("enum_type", underlying_type_for_enum(units.size())); enum_generator.append(R"~~~( enum class @dimension_name:titlecase@Unit : @enum_type@ { )~~~"); units.for_each_member([&](auto& unit_name, auto& unit_value) { auto& unit = unit_value.as_object(); if (unit.get_bool("is-canonical-unit"sv) == true) enum_generator.set("canonical_unit:titlecase", title_casify(unit_name)); auto unit_generator = enum_generator.fork(); unit_generator.set("unit_name:titlecase", title_casify(unit_name)); unit_generator.appendln(" @unit_name:titlecase@,"); }); enum_generator.append(R"~~~( }; constexpr @dimension_name:titlecase@Unit canonical_@dimension_name:snakecase@_unit() { return @dimension_name:titlecase@Unit::@canonical_unit:titlecase@; } Optional<@dimension_name:titlecase@Unit> string_to_@dimension_name:snakecase@_unit(StringView); FlyString to_string(@dimension_name:titlecase@Unit); bool units_are_compatible(@dimension_name:titlecase@Unit, @dimension_name:titlecase@Unit); double ratio_between_units(@dimension_name:titlecase@Unit, @dimension_name:titlecase@Unit); )~~~"); }); generator.append(R"~~~( bool is_absolute(LengthUnit); bool is_font_relative(LengthUnit); bool is_viewport_relative(LengthUnit); inline bool is_relative(LengthUnit unit) { return !is_absolute(unit); } } )~~~"); TRY(file.write_until_depleted(generator.as_string_view().bytes())); return {}; } ErrorOr generate_implementation_file(JsonObject& dimensions_data, Core::File& file) { StringBuilder builder; SourceGenerator generator { builder }; generator.append(R"~~~( #include namespace Web::CSS { Optional dimension_for_unit(StringView unit_name) { )~~~"); dimensions_data.for_each_member([&](String const& dimension_name, JsonValue const& units) { auto dimension_generator = generator.fork(); dimension_generator.set("dimension_name:titlecase", title_casify(dimension_name)); dimension_generator.append(" if ("); bool first = true; units.as_object().for_each_member([&](String const& unit_name, auto const&) { auto unit_generator = dimension_generator.fork(); unit_generator.set("unit_name", unit_name); if (first) first = false; else unit_generator.append("\n || "); unit_generator.append("unit_name.equals_ignoring_ascii_case(\"@unit_name@\"sv)"); }); dimension_generator.append(R"~~~() return DimensionType::@dimension_name:titlecase@; )~~~"); }); generator.append(R"~~~( return {}; } )~~~"); dimensions_data.for_each_member([&](String const& dimension_name, JsonValue const& dimension_data) { auto& units = dimension_data.as_object(); String canonical_unit; units.for_each_member([&](String const& unit_name, JsonValue const& unit_value) { if (unit_value.as_object().get_bool("is-canonical-unit"sv) == true) canonical_unit = unit_name; }); auto dimension_generator = generator.fork(); dimension_generator.set("dimension_name:titlecase", title_casify(dimension_name)); dimension_generator.set("dimension_name:snakecase", snake_casify(dimension_name)); dimension_generator.set("canonical_unit:titlecase", title_casify(canonical_unit)); dimension_generator.append(R"~~~( Optional<@dimension_name:titlecase@Unit> string_to_@dimension_name:snakecase@_unit(StringView unit_name) { )~~~"); units.for_each_member([&](String const& unit_name, JsonValue const&) { auto unit_generator = dimension_generator.fork(); unit_generator.set("unit_name:lowercase", unit_name); unit_generator.set("unit_name:titlecase", title_casify(unit_name)); unit_generator.append(R"~~~( if (unit_name.equals_ignoring_ascii_case("@unit_name:lowercase@"sv)) return @dimension_name:titlecase@Unit::@unit_name:titlecase@;)~~~"); }); dimension_generator.append(R"~~~( return {}; } FlyString to_string(@dimension_name:titlecase@Unit value) { switch (value) {)~~~"); units.for_each_member([&](String const& unit_name, JsonValue const&) { auto unit_generator = dimension_generator.fork(); unit_generator.set("unit_name:lowercase", unit_name); unit_generator.set("unit_name:titlecase", title_casify(unit_name)); unit_generator.append(R"~~~( case @dimension_name:titlecase@Unit::@unit_name:titlecase@: return "@unit_name:lowercase@"_fly_string;)~~~"); }); dimension_generator.append(R"~~~( default: VERIFY_NOT_REACHED(); } } bool units_are_compatible(@dimension_name:titlecase@Unit a, @dimension_name:titlecase@Unit b) { auto is_absolute = [](@dimension_name:titlecase@Unit unit) -> bool { switch (unit) { )~~~"); // https://drafts.csswg.org/css-values-4/#compatible-units // NB: The spec describes two ways units can be compatible. Absolute ones always are, but it also lists em/px // as compatible at computed value time. We should already have absolutized the units by then, but perhaps // there is some case where we need to handle that here instead. units.for_each_member([&](String const& unit_name, JsonValue const& unit_value) { auto const& unit = unit_value.as_object(); if (unit.has("relative-to"sv)) return; auto unit_generator = dimension_generator.fork(); unit_generator.set("unit_name:titlecase", title_casify(unit_name)); unit_generator.appendln(" case @dimension_name:titlecase@Unit::@unit_name:titlecase@:"); }); dimension_generator.append(R"~~~( return true; default: return false; } }; return is_absolute(a) && is_absolute(b); } double ratio_between_units(@dimension_name:titlecase@Unit from, @dimension_name:titlecase@Unit to) { if (from == to) return 1; auto ratio_to_canonical_unit = [](@dimension_name:titlecase@Unit unit) -> double { switch (unit) { )~~~"); units.for_each_member([&](String const& unit_name, JsonValue const& unit_value) { auto const& unit = unit_value.as_object(); if (unit.has("relative-to"sv)) return; auto unit_generator = dimension_generator.fork(); unit_generator.set("unit_name:titlecase", title_casify(unit_name)); if (auto ratio = unit.get_double_with_precision_loss("number-of-canonical-unit"sv); ratio.has_value()) { unit_generator.set("unit_ratio", String::number(ratio.value())); } else { // This must be the canonical unit, so the ratio is 1. unit_generator.set("unit_ratio", "1"); } unit_generator.append(R"~~~( case @dimension_name:titlecase@Unit::@unit_name:titlecase@: return @unit_ratio@; )~~~"); }); dimension_generator.append(R"~~~( default: // `from` is a relative unit, so this isn't valid. VERIFY_NOT_REACHED(); } }; if (to == @dimension_name:titlecase@Unit::@canonical_unit:titlecase@) return ratio_to_canonical_unit(from); return ratio_to_canonical_unit(from) / ratio_to_canonical_unit(to); } )~~~"); }); // And now some length-specific functions. auto& length_units = dimensions_data.get_object("length"sv).value(); generator.append(R"~~~( bool is_absolute(LengthUnit unit) { switch (unit) { )~~~"); length_units.for_each_member([&](String const& unit_name, JsonValue const& unit_value) { auto& unit = unit_value.as_object(); if (unit.has("relative-to"sv)) return; auto unit_generator = generator.fork(); unit_generator.set("unit_name:titlecase", title_casify(unit_name)); unit_generator.appendln(" case LengthUnit::@unit_name:titlecase@:"); }); generator.append(R"~~~( return true; default: return false; } } bool is_font_relative(LengthUnit unit) { switch (unit) { )~~~"); length_units.for_each_member([&](String const& unit_name, JsonValue const& unit_value) { auto& unit = unit_value.as_object(); if (unit.get_string("relative-to"sv) != "font"sv) return; auto unit_generator = generator.fork(); unit_generator.set("unit_name:titlecase", title_casify(unit_name)); unit_generator.appendln(" case LengthUnit::@unit_name:titlecase@:"); }); generator.append(R"~~~( return true; default: return false; } } bool is_viewport_relative(LengthUnit unit) { switch (unit) { )~~~"); length_units.for_each_member([&](String const& unit_name, JsonValue const& unit_value) { auto& unit = unit_value.as_object(); if (unit.get_string("relative-to"sv) != "viewport"sv) return; auto unit_generator = generator.fork(); unit_generator.set("unit_name:titlecase", title_casify(unit_name)); unit_generator.appendln(" case LengthUnit::@unit_name:titlecase@:"); }); generator.append(R"~~~( return true; default: return false; } } } )~~~"); TRY(file.write_until_depleted(generator.as_string_view().bytes())); return {}; }