diff --git a/Documentation/CSSGeneratedFiles.md b/Documentation/CSSGeneratedFiles.md index 474e6efb2f..c655f213f5 100644 --- a/Documentation/CSSGeneratedFiles.md +++ b/Documentation/CSSGeneratedFiles.md @@ -319,7 +319,7 @@ This is a single JSON object, describing each [CSS environment variable](https:/ with the keys being the environment variable names, and the values being objects describing the variable's properties. This generates `EnvironmentVariable.h` and `EnvironmentVariable.cpp`. -Each entry has 3 properties, all taken from the +Each entry has 3 properties, all taken from the spec: | Field | Description | |--------------|---------------------------------------------------------------------| @@ -333,3 +333,27 @@ The generated code provides: - `StringView to_string(EnvironmentVariable)` to convert the `EnvironmentVariable` back to a string - `ValueType environment_variable_type(EnvironmentVariable)` to get the variable's value type - `u32 environment_variable_dimension_count(EnvironmentVariable)` to get its dimension count + +## Units.json + +This is a JSON object with the keys being dimension type names, and the values being objects. Those objects' keys are +unit names, and their values are data about each unit. +It generates `Units.h` and `Units.cpp`. + +Each unit has the following properties: + +| Field | Description | +|----------------------------|------------------------------------------------------------------------------------------------------------------------------------| +| `is-canonical-unit` | Boolean, default `false`. Each dimension has one canonical unit. | +| `number-of-canonical-unit` | Number. How many of the canonical units 1 of this is equivalent to. Ignore this for relative units, and the canonical unit itself. | +| `is-relative-to` | String. Some length units are relative to the font or viewport. Set this to `"font"` or `"viewport"` for those. | + +The generated code provides: +- A `DimensionType` enum, listing each type of dimension that has units defined. +- `Optional dimension_for_unit(StringView)` for querying which dimension a unit applies to, if any. +- A `FooUnit` enum for each dimension "foo", which lists all the units of that dimension. +- For each of those... + - `Optional string_to_foo_unit(StringView)` for parsing a unit from a string. + - `StringView to_string(FooUnit)` for serializing those units. + - `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. diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index f1ffd12d44..be51d4b9d9 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -1082,6 +1082,7 @@ set(GENERATED_SOURCES CSS/PseudoElement.cpp CSS/QuirksModeStyleSheetSource.cpp CSS/TransformFunctions.cpp + CSS/Units.cpp MathML/MathMLStyleSheetSource.cpp SVG/SVGStyleSheetSource.cpp Worker/WebWorkerClientEndpoint.h diff --git a/Libraries/LibWeb/CSS/Angle.h b/Libraries/LibWeb/CSS/Angle.h index a3e4cc55d1..339558287d 100644 --- a/Libraries/LibWeb/CSS/Angle.h +++ b/Libraries/LibWeb/CSS/Angle.h @@ -8,6 +8,7 @@ #include #include +#include #include namespace Web::CSS { diff --git a/Libraries/LibWeb/CSS/Units.json b/Libraries/LibWeb/CSS/Units.json new file mode 100644 index 0000000000..bd8579822f --- /dev/null +++ b/Libraries/LibWeb/CSS/Units.json @@ -0,0 +1,182 @@ +{ + "angle": { + "deg": { + "is-canonical-unit": true + }, + "grad": { + "number-of-canonical-unit": 0.9 + }, + "rad": { + "number-of-canonical-unit": 57.2957795130823208767981548141051703324054724665643215491602438612028471483 + }, + "turn": { + "number-of-canonical-unit": 360 + } + }, + "flex": { + "fr": { + "is-canonical-unit": true + } + }, + "frequency": { + "Hz": { + "is-canonical-unit": true + }, + "kHz": { + "number-of-canonical-unit": 1000 + } + }, + "length": { + "cap": { + "relative-to": "font" + }, + "ch": { + "relative-to": "font" + }, + "cm": { + "number-of-canonical-unit": 37.7952755905511811023622047244094488188976377952755905511811023622047244094 + }, + "dvb": { + "relative-to": "viewport" + }, + "dvh": { + "relative-to": "viewport" + }, + "dvi": { + "relative-to": "viewport" + }, + "dvmax": { + "relative-to": "viewport" + }, + "dvmin": { + "relative-to": "viewport" + }, + "dvw": { + "relative-to": "viewport" + }, + "em": { + "relative-to": "font" + }, + "ex": { + "relative-to": "font" + }, + "ic": { + "relative-to": "font" + }, + "in": { + "number-of-canonical-unit": 96 + }, + "lh": { + "relative-to": "font" + }, + "lvb": { + "relative-to": "viewport" + }, + "lvh": { + "relative-to": "viewport" + }, + "lvi": { + "relative-to": "viewport" + }, + "lvmax": { + "relative-to": "viewport" + }, + "lvmin": { + "relative-to": "viewport" + }, + "lvw": { + "relative-to": "viewport" + }, + "mm": { + "number-of-canonical-unit": 3.77952755905511811023622047244094488188976377952755905511811023622047244094 + }, + "pc": { + "number-of-canonical-unit": 16 + }, + "pt": { + "number-of-canonical-unit": 1.33333333333333333333333333333333333333333333333333333333333333333333333333 + }, + "px": { + "is-canonical-unit": true + }, + "Q": { + "number-of-canonical-unit": 0.94488188976377952755905511811023622047244094488188976377952755905511811023 + }, + "rcap": { + "relative-to": "font" + }, + "rch": { + "relative-to": "font" + }, + "rem": { + "relative-to": "font" + }, + "rex": { + "relative-to": "font" + }, + "ric": { + "relative-to": "font" + }, + "rlh": { + "relative-to": "font" + }, + "svb": { + "relative-to": "viewport" + }, + "svh": { + "relative-to": "viewport" + }, + "svi": { + "relative-to": "viewport" + }, + "svmax": { + "relative-to": "viewport" + }, + "svmin": { + "relative-to": "viewport" + }, + "svw": { + "relative-to": "viewport" + }, + "vb": { + "relative-to": "viewport" + }, + "vh": { + "relative-to": "viewport" + }, + "vi": { + "relative-to": "viewport" + }, + "vmax": { + "relative-to": "viewport" + }, + "vmin": { + "relative-to": "viewport" + }, + "vw": { + "relative-to": "viewport" + } + }, + "resolution": { + "dpcm": { + "number-of-canonical-unit": 0.02645833333333333333333333333333333333333333333333333333333333333333333333 + }, + "dpi": { + "number-of-canonical-unit": 0.01041666666666666666666666666666666666666666666666666666666666666666666666 + }, + "dppx": { + "is-canonical-unit": true + }, + "x": { + "number-of-canonical-unit": 1 + } + }, + "time": { + "ms": { + "number-of-canonical-unit": 0.001 + }, + "s": { + "is-canonical-unit": true + } + } +} diff --git a/Meta/CMake/libweb_generators.cmake b/Meta/CMake/libweb_generators.cmake index e4fec80a82..cca96d2970 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_cpp_generator( + "Units.cpp" + Lagom::GenerateCSSUnits + "${LIBWEB_INPUT_FOLDER}/CSS/Units.json" + "CSS/Units.h" + "CSS/Units.cpp" + arguments -j "${LIBWEB_INPUT_FOLDER}/CSS/Units.json" + ) + invoke_cpp_generator( "Keyword.cpp" Lagom::GenerateCSSKeyword diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt b/Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt index 0c48becd42..481b5f9bdc 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt @@ -11,6 +11,7 @@ lagom_tool(GenerateCSSPseudoClass SOURCES GenerateCSSPseudoClass.cpp L lagom_tool(GenerateCSSPseudoElement SOURCES GenerateCSSPseudoElement.cpp LIBS LibMain) lagom_tool(GenerateCSSStyleProperties SOURCES GenerateCSSStyleProperties.cpp LIBS LibMain) lagom_tool(GenerateCSSTransformFunctions SOURCES GenerateCSSTransformFunctions.cpp LIBS LibMain) +lagom_tool(GenerateCSSUnits SOURCES GenerateCSSUnits.cpp LIBS LibMain) lagom_tool(GenerateWindowOrWorkerInterfaces SOURCES GenerateWindowOrWorkerInterfaces.cpp LIBS LibMain LibIDL) lagom_tool(GenerateAriaRoles SOURCES GenerateAriaRoles.cpp LIBS LibMain) lagom_tool(GenerateNamedCharacterReferences SOURCES GenerateNamedCharacterReferences.cpp LIBS LibMain) diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSUnits.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSUnits.cpp new file mode 100644 index 0000000000..d8896bc90c --- /dev/null +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSUnits.cpp @@ -0,0 +1,371 @@ +/* + * 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 + +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&) { + 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"~~~( +}; +Optional<@dimension_name:titlecase@Unit> string_to_@dimension_name:snakecase@_unit(StringView); +StringView to_string(@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 +#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 {}; +} + +StringView 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@"sv;)~~~"); + }); + + dimension_generator.append(R"~~~( + default: + VERIFY_NOT_REACHED(); + } +} + +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 {}; +}