Skip to content

Commit 772be9d

Browse files
Lubrsinico
authored andcommitted
LibWeb: Generate IDL attributes for all supported CSS properties
The CSSOM spec tells us to potentially add up to three different IDL attributes to CSSStyleDeclaration for every CSS property we support: - A camelCased attribute, where a dash indicates the next character should be uppercase - A camelCased attribute for every -webkit- prefixed property, with the first letter always being lowercase - A dashed-attribute for every property with a dash in it. Additionally, every attribute must have the CEReactions and LegacyNullToEmptyString extended attributes specified on it. Since we specify every property we support with Properties.json, we can use that file to generate the IDL file and it's implementation. We import it from the Build directory with the help of multiple import base paths. Then, we add it to CSSStyleDeclaration via the mixin functionality and inheriting the generated class in CSSStyleDeclaration. (cherry picked from commit aacf9b08ed8c4286c84145b52fa70f399ed0bdff; amended to fix tiny conflict in libweb_generators.cmake, and to change the default value of all gap-related properties from 'auto' to 'normal' since serenity does not yet have LadybirdBrowser/ladybird#2253 and cherry-picking that has a pretty long dependency tree. It's easy to update the test once that is cherry-picked)
1 parent 3ce2605 commit 772be9d

12 files changed

+909
-60
lines changed

Documentation/Browser/CSSGeneratedFiles.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ They are run automatically as part of the build, and most of the time you can ig
1111
## Properties.json
1212

1313
Each CSS property has an entry here, which describes what values it accepts, whether it's inherited, and similar data.
14-
This generates `PropertyID.h` and `PropertyID.cpp`.
14+
This generates `PropertyID.h`, `PropertyID.cpp`, `GeneratedCSSStyleProperties.h`, `GeneratedCSSStyleProperties.cpp` and `GeneratedCSSStyleProperties.idl`.
1515
Most of this data is found in the information box for that property in the relevant CSS spec.
1616

1717
The file is organized as a single JSON object, with keys being property names, and the values being the data for that property.

Meta/CMake/libweb_generators.cmake

+13-2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ function (generate_css_implementation)
6464
arguments -j "${LIBWEB_INPUT_FOLDER}/CSS/Keywords.json"
6565
)
6666

67+
invoke_idl_generator(
68+
"GeneratedCSSStyleProperties.cpp"
69+
Lagom::GenerateCSSStyleProperties
70+
"${LIBWEB_INPUT_FOLDER}/CSS/Properties.json"
71+
"CSS/GeneratedCSSStyleProperties.h"
72+
"CSS/GeneratedCSSStyleProperties.cpp"
73+
"CSS/GeneratedCSSStyleProperties.idl"
74+
arguments -j "${LIBWEB_INPUT_FOLDER}/CSS/Properties.json"
75+
)
76+
6777
embed_as_string(
6878
"DefaultStyleSheetSource.cpp"
6979
"${LIBWEB_INPUT_FOLDER}/CSS/Default.css"
@@ -98,6 +108,7 @@ function (generate_css_implementation)
98108

99109
set(CSS_GENERATED_TO_INSTALL
100110
"CSS/Enums.h"
111+
"CSS/GeneratedCSSStyleProperties.h"
101112
"CSS/Keyword.h"
102113
"CSS/MathFunctions.h"
103114
"CSS/MediaFeatureID.h"
@@ -161,7 +172,7 @@ function (generate_js_bindings target)
161172
add_custom_command(
162173
OUTPUT ${BINDINGS_SOURCES}
163174
COMMAND "$<TARGET_FILE:Lagom::BindingsGenerator>" -o "Bindings" --depfile "Bindings/${basename}.d"
164-
${depfile_prefix_arg} "${LIBWEB_INPUT_FOLDER}/${class}.idl" "${LIBWEB_INPUT_FOLDER}"
175+
${depfile_prefix_arg} "${LIBWEB_INPUT_FOLDER}/${class}.idl" "${LIBWEB_INPUT_FOLDER}" "${CMAKE_CURRENT_BINARY_DIR}"
165176
VERBATIM
166177
COMMENT "Generating Bindings for ${class}"
167178
DEPENDS Lagom::BindingsGenerator
@@ -191,7 +202,7 @@ function (generate_js_bindings target)
191202
add_custom_command(
192203
OUTPUT ${exposed_interface_sources}
193204
COMMAND "${CMAKE_COMMAND}" -E make_directory "tmp"
194-
COMMAND $<TARGET_FILE:Lagom::GenerateWindowOrWorkerInterfaces> -o "${CMAKE_CURRENT_BINARY_DIR}/tmp" -b "${LIBWEB_INPUT_FOLDER}" ${LIBWEB_ALL_IDL_FILES}
205+
COMMAND $<TARGET_FILE:Lagom::GenerateWindowOrWorkerInterfaces> -o "${CMAKE_CURRENT_BINARY_DIR}/tmp" -b "${LIBWEB_INPUT_FOLDER}" -b "${CMAKE_CURRENT_BINARY_DIR}" ${LIBWEB_ALL_IDL_FILES}
195206
COMMAND "${CMAKE_COMMAND}" -E copy_if_different tmp/IntrinsicDefinitions.cpp "Bindings/IntrinsicDefinitions.cpp"
196207
COMMAND "${CMAKE_COMMAND}" -E copy_if_different tmp/DedicatedWorkerExposedInterfaces.h "Bindings/DedicatedWorkerExposedInterfaces.h"
197208
COMMAND "${CMAKE_COMMAND}" -E copy_if_different tmp/DedicatedWorkerExposedInterfaces.cpp "Bindings/DedicatedWorkerExposedInterfaces.cpp"

Meta/CMake/utils.cmake

+20
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,26 @@ function(invoke_generator name generator primary_source header implementation)
231231
set(CURRENT_LIB_GENERATED ${CURRENT_LIB_GENERATED} PARENT_SCOPE)
232232
endfunction()
233233

234+
function(invoke_idl_generator name generator primary_source header implementation idl)
235+
cmake_parse_arguments(invoke_idl_generator "" "" "arguments;dependencies" ${ARGN})
236+
237+
add_custom_command(
238+
OUTPUT "${header}" "${implementation}" "${idl}"
239+
COMMAND $<TARGET_FILE:${generator}> -h "${header}.tmp" -c "${implementation}.tmp" -i "${idl}.tmp" ${invoke_idl_generator_arguments}
240+
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${header}.tmp" "${header}"
241+
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${implementation}.tmp" "${implementation}"
242+
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${idl}.tmp" "${idl}"
243+
COMMAND "${CMAKE_COMMAND}" -E remove "${header}.tmp" "${implementation}.tmp" "${idl}.tmp"
244+
VERBATIM
245+
DEPENDS ${generator} ${invoke_idl_generator_dependencies} "${primary_source}"
246+
)
247+
248+
add_custom_target("generate_${name}" DEPENDS "${header}" "${implementation}" "${idl}")
249+
add_dependencies(all_generated "generate_${name}")
250+
list(APPEND CURRENT_LIB_GENERATED "${name}")
251+
set(CURRENT_LIB_GENERATED ${CURRENT_LIB_GENERATED} PARENT_SCOPE)
252+
endfunction()
253+
234254
function(download_file_multisource urls path)
235255
cmake_parse_arguments(DOWNLOAD "" "SHA256;VERSION;VERSION_FILE;CACHE_PATH" "" ${ARGN})
236256

Meta/Lagom/Tools/CodeGenerators/LibWeb/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ lagom_tool(GenerateCSSMathFunctions SOURCES GenerateCSSMathFunctions.cpp
66
lagom_tool(GenerateCSSMediaFeatureID SOURCES GenerateCSSMediaFeatureID.cpp LIBS LibMain)
77
lagom_tool(GenerateCSSPropertyID SOURCES GenerateCSSPropertyID.cpp LIBS LibMain)
88
lagom_tool(GenerateCSSPseudoClass SOURCES GenerateCSSPseudoClass.cpp LIBS LibMain)
9+
lagom_tool(GenerateCSSStyleProperties SOURCES GenerateCSSStyleProperties.cpp LIBS LibMain)
910
lagom_tool(GenerateCSSTransformFunctions SOURCES GenerateCSSTransformFunctions.cpp LIBS LibMain)
1011
lagom_tool(GenerateWindowOrWorkerInterfaces SOURCES GenerateWindowOrWorkerInterfaces.cpp LIBS LibMain LibIDL)
1112
lagom_tool(GenerateAriaRoles SOURCES GenerateAriaRoles.cpp LIBS LibMain)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*
2+
* Copyright (c) 2024, Luke Wilde <[email protected]>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#include "GeneratorUtil.h"
8+
#include <AK/SourceGenerator.h>
9+
#include <AK/StringBuilder.h>
10+
#include <LibCore/ArgsParser.h>
11+
#include <LibMain/Main.h>
12+
13+
ErrorOr<void> generate_header_file(JsonObject& properties, Core::File& file);
14+
ErrorOr<void> generate_implementation_file(JsonObject& properties, Core::File& file);
15+
ErrorOr<void> generate_idl_file(JsonObject& properties, Core::File& file);
16+
17+
ErrorOr<int> serenity_main(Main::Arguments arguments)
18+
{
19+
StringView generated_header_path;
20+
StringView generated_implementation_path;
21+
StringView generated_idl_path;
22+
StringView properties_json_path;
23+
24+
Core::ArgsParser args_parser;
25+
args_parser.add_option(generated_header_path, "Path to the CSSStyleProperties header file to generate", "generated-header-path", 'h', "generated-header-path");
26+
args_parser.add_option(generated_implementation_path, "Path to the CSSStyleProperties implementation file to generate", "generated-implementation-path", 'c', "generated-implementation-path");
27+
args_parser.add_option(generated_idl_path, "Path to the CSSStyleProperties IDL file to generate", "generated-idl-path", 'i', "generated-idl-path");
28+
args_parser.add_option(properties_json_path, "Path to the JSON file to read from", "json-path", 'j', "json-path");
29+
args_parser.parse(arguments);
30+
31+
auto json = TRY(read_entire_file_as_json(properties_json_path));
32+
VERIFY(json.is_object());
33+
auto properties = json.as_object();
34+
35+
auto generated_header_file = TRY(Core::File::open(generated_header_path, Core::File::OpenMode::Write));
36+
auto generated_implementation_file = TRY(Core::File::open(generated_implementation_path, Core::File::OpenMode::Write));
37+
auto generated_idl_file = TRY(Core::File::open(generated_idl_path, Core::File::OpenMode::Write));
38+
39+
TRY(generate_header_file(properties, *generated_header_file));
40+
TRY(generate_implementation_file(properties, *generated_implementation_file));
41+
TRY(generate_idl_file(properties, *generated_idl_file));
42+
43+
return 0;
44+
}
45+
46+
static String get_snake_case_function_name_for_css_property_name(ByteString const& name)
47+
{
48+
auto snake_case_name = snake_casify(name);
49+
if (snake_case_name.starts_with('_'))
50+
return MUST(snake_case_name.substring_from_byte_offset(1));
51+
52+
return snake_case_name;
53+
}
54+
55+
static String make_name_acceptable_cpp(String const& name)
56+
{
57+
if (name.is_one_of("float")) {
58+
StringBuilder builder;
59+
builder.append(name);
60+
builder.append('_');
61+
return MUST(builder.to_string());
62+
}
63+
64+
return name;
65+
}
66+
67+
ErrorOr<void> generate_header_file(JsonObject& properties, Core::File& file)
68+
{
69+
StringBuilder builder;
70+
SourceGenerator generator { builder };
71+
72+
generator.append(R"~~~(
73+
#pragma once
74+
75+
#include <AK/String.h>
76+
#include <LibWeb/Forward.h>
77+
78+
namespace Web::Bindings {
79+
80+
class GeneratedCSSStyleProperties {
81+
public:
82+
)~~~");
83+
84+
properties.for_each_member([&](auto& name, auto&) {
85+
auto declaration_generator = generator.fork();
86+
auto snake_case_name = get_snake_case_function_name_for_css_property_name(name);
87+
declaration_generator.set("name:acceptable_cpp", make_name_acceptable_cpp(snake_case_name));
88+
89+
declaration_generator.append(R"~~~(
90+
WebIDL::ExceptionOr<void> set_@name:acceptable_cpp@(StringView value);
91+
String @name:acceptable_cpp@() const;
92+
)~~~");
93+
});
94+
95+
generator.append(R"~~~(
96+
protected:
97+
GeneratedCSSStyleProperties() = default;
98+
virtual ~GeneratedCSSStyleProperties() = default;
99+
100+
virtual CSS::CSSStyleDeclaration& generated_style_properties_to_css_style_declaration() = 0;
101+
CSS::CSSStyleDeclaration const& generated_style_properties_to_css_style_declaration() const { return const_cast<GeneratedCSSStyleProperties&>(*this).generated_style_properties_to_css_style_declaration(); }
102+
}; // class GeneratedCSSStyleProperties
103+
104+
} // namespace Web::Bindings
105+
)~~~");
106+
107+
TRY(file.write_until_depleted(generator.as_string_view().bytes()));
108+
return {};
109+
}
110+
111+
ErrorOr<void> generate_implementation_file(JsonObject& properties, Core::File& file)
112+
{
113+
StringBuilder builder;
114+
SourceGenerator generator { builder };
115+
116+
generator.append(R"~~~(
117+
#include <LibWeb/CSS/CSSStyleDeclaration.h>
118+
#include <LibWeb/CSS/GeneratedCSSStyleProperties.h>
119+
#include <LibWeb/WebIDL/ExceptionOr.h>
120+
121+
namespace Web::Bindings {
122+
)~~~");
123+
124+
properties.for_each_member([&](auto& name, auto&) {
125+
auto definition_generator = generator.fork();
126+
definition_generator.set("name", name);
127+
128+
auto snake_case_name = get_snake_case_function_name_for_css_property_name(name);
129+
definition_generator.set("name:acceptable_cpp", make_name_acceptable_cpp(snake_case_name));
130+
131+
definition_generator.append(R"~~~(
132+
WebIDL::ExceptionOr<void> GeneratedCSSStyleProperties::set_@name:acceptable_cpp@(StringView value)
133+
{
134+
return generated_style_properties_to_css_style_declaration().set_property("@name@"sv, value, ""sv);
135+
}
136+
137+
String GeneratedCSSStyleProperties::@name:acceptable_cpp@() const
138+
{
139+
return generated_style_properties_to_css_style_declaration().get_property_value("@name@"sv);
140+
}
141+
)~~~");
142+
});
143+
144+
generator.append(R"~~~(
145+
} // namespace Web::Bindings
146+
)~~~");
147+
148+
TRY(file.write_until_depleted(generator.as_string_view().bytes()));
149+
return {};
150+
}
151+
152+
ErrorOr<void> generate_idl_file(JsonObject& properties, Core::File& file)
153+
{
154+
StringBuilder builder;
155+
SourceGenerator generator { builder };
156+
157+
generator.append(R"~~~(
158+
interface mixin GeneratedCSSStyleProperties {
159+
)~~~");
160+
161+
properties.for_each_member([&](auto& name, auto&) {
162+
auto member_generator = generator.fork();
163+
164+
member_generator.set("name", name);
165+
166+
auto snake_case_name = get_snake_case_function_name_for_css_property_name(name);
167+
member_generator.set("name:snakecase", snake_case_name);
168+
member_generator.set("name:acceptable_cpp", make_name_acceptable_cpp(snake_case_name));
169+
170+
// https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-camel-cased-attribute
171+
// For each CSS property property that is a supported CSS property, the following partial interface applies
172+
// where camel-cased attribute is obtained by running the CSS property to IDL attribute algorithm for property.
173+
// partial interface CSSStyleProperties {
174+
// [CEReactions] attribute [LegacyNullToEmptyString] CSSOMString _camel_cased_attribute;
175+
// };
176+
member_generator.set("name:camelcase", css_property_to_idl_attribute(name));
177+
178+
member_generator.append(R"~~~(
179+
[CEReactions, LegacyNullToEmptyString, AttributeCallbackName=@name:snakecase@_regular, ImplementedAs=@name:acceptable_cpp@] attribute CSSOMString @name:camelcase@;
180+
)~~~");
181+
182+
// For each CSS property property that is a supported CSS property and that begins with the string -webkit-,
183+
// the following partial interface applies where webkit-cased attribute is obtained by running the CSS property
184+
// to IDL attribute algorithm for property, with the lowercase first flag set.
185+
if (name.starts_with("-webkit-"sv)) {
186+
member_generator.set("name:webkit", css_property_to_idl_attribute(name, /* lowercase_first= */ true));
187+
member_generator.append(R"~~~(
188+
[CEReactions, LegacyNullToEmptyString, AttributeCallbackName=@name:snakecase@_webkit, ImplementedAs=@name:acceptable_cpp@] attribute CSSOMString @name:webkit@;
189+
)~~~");
190+
}
191+
192+
// For each CSS property property that is a supported CSS property, except for properties that have no
193+
// "-" (U+002D) in the property name, the following partial interface applies where dashed attribute is
194+
// property.
195+
// partial interface CSSStyleProperties {
196+
// [CEReactions] attribute [LegacyNullToEmptyString] CSSOMString _dashed_attribute;
197+
// };
198+
if (name.contains('-')) {
199+
member_generator.append(R"~~~(
200+
[CEReactions, LegacyNullToEmptyString, AttributeCallbackName=@name:snakecase@_dashed, ImplementedAs=@name:acceptable_cpp@] attribute CSSOMString @name@;
201+
)~~~");
202+
}
203+
});
204+
205+
generator.append(R"~~~(
206+
};
207+
)~~~");
208+
209+
TRY(file.write_until_depleted(generator.as_string_view().bytes()));
210+
return {};
211+
}

Meta/Lagom/Tools/CodeGenerators/LibWeb/GeneratorUtil.h

+39
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,42 @@ inline ErrorOr<JsonValue> read_entire_file_as_json(StringView filename)
6464
TRY(file->read_until_filled(json_data.bytes()));
6565
return JsonValue::from_string(json_data);
6666
}
67+
68+
// https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
69+
inline String css_property_to_idl_attribute(StringView property_name, bool lowercase_first = false)
70+
{
71+
// The CSS property to IDL attribute algorithm for property, optionally with a lowercase first flag set, is as follows:
72+
// 1. Let output be the empty string.
73+
StringBuilder output;
74+
75+
// 2. Let uppercase next be unset.
76+
bool uppercase_next = false;
77+
78+
// 3. If the lowercase first flag is set, remove the first character from property.
79+
StringView actual_property_name;
80+
if (lowercase_first) {
81+
actual_property_name = property_name.substring_view(1);
82+
} else {
83+
actual_property_name = property_name;
84+
}
85+
86+
// 4. For each character c in property:
87+
for (auto c : actual_property_name) {
88+
// 1. If c is "-" (U+002D), let uppercase next be set.
89+
if (c == '-') {
90+
uppercase_next = true;
91+
}
92+
// 2. Otherwise, if uppercase next is set, let uppercase next be unset and append c converted to ASCII uppercase to output.
93+
else if (uppercase_next) {
94+
uppercase_next = false;
95+
output.append(to_ascii_uppercase(c));
96+
}
97+
// 3. Otherwise, append c to output.
98+
else {
99+
output.append(c);
100+
}
101+
}
102+
103+
// 5. Return output.
104+
return MUST(output.to_string());
105+
}

0 commit comments

Comments
 (0)