LibWeb: Serialize CSS declarations as shorthands where applicable
Some checks are pending
CI / Lagom (arm64, Sanitizer_CI, false, macOS, macos-15, Clang) (push) Waiting to run
CI / Lagom (x86_64, Fuzzers_CI, false, Linux, blacksmith-16vcpu-ubuntu-2404, Clang) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, false, Linux, blacksmith-16vcpu-ubuntu-2404, GNU) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, true, Linux, blacksmith-16vcpu-ubuntu-2404, Clang) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (arm64, macOS, macOS-universal2, macos-15) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (x86_64, Linux, Linux-x86_64, blacksmith-8vcpu-ubuntu-2404) (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

When serializing CSS declarations we now support combining multiple
properties into a single shorthand property in some cases.

This comes with a healthy dose of FIXMEs, including work to be done
around supporting:
 - Nested shorthands (e.g. background, border, etc)
 - Shorthands which aren't represented by the ShorthandStyleValue type
 - Subproperties pending substitution

This gains us a bunch of new test passes, both for WPT and in-tree
This commit is contained in:
Callum Law 2025-05-27 20:28:34 +12:00 committed by Jelle Raaijmakers
commit ed65d5b342
Notes: github-actions[bot] 2025-05-29 10:05:38 +00:00
9 changed files with 249 additions and 36 deletions

View file

@ -1172,12 +1172,85 @@ String CSSStyleProperties::serialized() const
if (already_serialized.contains(property))
continue;
// FIXME: 3. If property maps to one or more shorthand properties, let shorthands be an array of those shorthand properties, in preferred order.
// 3. If property maps to one or more shorthand properties, let shorthands be an array of those shorthand properties, in preferred order.
// FIXME: We don't properly support nested shorthands (e.g. background)
if (property_maps_to_shorthand(property)) {
// FIXME: Sort in the preferred order. https://www.w3.org/TR/cssom/#concept-shorthands-preferred-order
auto shorthands = shorthands_for_longhand(property);
// FIXME: 4. Shorthand loop: For each shorthand in shorthands, follow these substeps: ...
// 4. Shorthand loop: For each shorthand in shorthands, follow these substeps:
for (auto shorthand : shorthands) {
// 1. Let longhands be an array consisting of all CSS declarations in declaration blocks declarations
// that that are not in already serialized and have a property name that maps to one of the shorthand
// properties in shorthands.
Vector<StyleProperty> longhands;
for (auto const& longhand_declaration : m_properties) {
// FIXME: Some of the ad-hoc ShorthandStyleValue::to_string cases don't account for the possibility
// of subproperty values pending substitution, to avoid crashing we don't include those here
if (!already_serialized.contains(longhand_declaration.property_id) && shorthands_for_longhand(longhand_declaration.property_id).contains_slow(shorthand) && !longhand_declaration.value->is_pending_substitution())
longhands.append(longhand_declaration);
}
// 2. If all properties that map to shorthand are not present in longhands, continue with the steps labeled shorthand loop.
if (longhands.is_empty())
continue;
// 3. Let current longhands be an empty array.
Vector<StyleProperty> current_longhands;
// 4. Append all CSS declarations in longhands that have a property name that maps to shorthand to current longhands.
for (auto const& longhand : longhands) {
if (shorthands_for_longhand(longhand.property_id).contains_slow(shorthand))
current_longhands.append(longhand);
}
// 5. If there is one or more CSS declarations in current longhands have their important flag set and
// one or more with it unset, continue with the steps labeled shorthand loop.
auto all_declarations_have_same_important_flag = true;
for (size_t i = 1; i < current_longhands.size(); ++i) {
if (current_longhands[i].important != current_longhands[0].important) {
all_declarations_have_same_important_flag = false;
break;
}
}
if (!all_declarations_have_same_important_flag)
continue;
// FIXME: 6. If theres any declaration in declaration block in between the first and the last longhand
// in current longhands which belongs to the same logical property group, but has a different
// mapping logic as any of the longhands in current longhands, and is not in current
// longhands, continue with the steps labeled shorthand loop.
// 7. Let value be the result of invoking serialize a CSS value of current longhands.
auto value = serialize_a_css_value(current_longhands);
// 8. If value is the empty string, continue with the steps labeled shorthand loop.
if (value.is_empty())
continue;
// 9. Let serialized declaration be the result of invoking serialize a CSS declaration with property
// name shorthand, value value, and the important flag set if the CSS declarations in current
// longhands have their important flag set.
auto serialized_declaration = serialize_a_css_declaration(string_from_property_id(shorthand), move(value), current_longhands.first().important);
// 10. Append serialized declaration to list.
list.append(move(serialized_declaration));
// 11. Append the property names of all items of current longhands to already serialized.
for (auto const& longhand : current_longhands)
already_serialized.set(longhand.property_id);
// 12. Continue with the steps labeled declaration loop.
}
}
// FIXME: File spec issue that this should only be run if we haven't serialized this declaration in the above shorthand loop.
if (!already_serialized.contains(declaration.property_id)) {
// 5. Let value be the result of invoking serialize a CSS value of declaration.
auto value = declaration.value->to_string(Web::CSS::SerializationMode::Normal);
auto value = serialize_a_css_value(declaration);
// 6. Let serialized declaration be the result of invoking serialize a CSS declaration with property name property, value value,
// and the important flag set if declaration has its important flag set.
@ -1189,6 +1262,7 @@ String CSSStyleProperties::serialized() const
// 8. Append property to already serialized.
already_serialized.set(property);
}
}
// 4. Return list joined with " " (U+0020).
StringBuilder builder;
@ -1196,6 +1270,71 @@ String CSSStyleProperties::serialized() const
return MUST(builder.to_string());
}
// https://www.w3.org/TR/cssom/#serialize-a-css-value
String CSSStyleProperties::serialize_a_css_value(StyleProperty const& declaration) const
{
// 1. If If this algorithm is invoked with a list list:
// NOTE: This is handled in other other overload of this method
// 2. Represent the value of the declaration as a list of CSS component values components that, when parsed
// according to the propertys grammar, would represent that value. Additionally:
// - If certain component values can appear in any order without changing the meaning of the value (a pattern
// typically represented by a double bar || in the value syntax), reorder the component values to use the
// canonical order of component values as given in the property definition table.
// - If component values can be omitted or replaced with a shorter representation without changing the meaning
// of the value, omit/replace them.
// - If either of the above syntactic translations would be less backwards-compatible, do not perform them.
// Spec Note: The rules described here outlines the general principles of serialization. For legacy reasons, some
// properties serialize in a different manner, which is intentionally undefined here due to lack of
// resources. Please consult your local reverse-engineer for details.
// 3. Remove any <whitespace-token>s from components.
// 4. Replace each component value in components with the result of invoking serialize a CSS component value.
// 5. Join the items of components into a single string, inserting " " (U+0020 SPACE) between each pair of items
// unless the second item is a "," (U+002C COMMA) Return the result.
// AD-HOC: As the spec is vague we don't follow it exactly here.
return declaration.value->to_string(Web::CSS::SerializationMode::Normal);
}
// https://www.w3.org/TR/cssom/#serialize-a-css-value
String CSSStyleProperties::serialize_a_css_value(Vector<StyleProperty> list) const
{
if (list.is_empty())
return String {};
// 1. Let shorthand be the first shorthand property, in preferred order, that exactly maps to all of the longhand properties in list.
// FIXME: Sort in the preferred order. https://www.w3.org/TR/cssom/#concept-shorthands-preferred-order
Optional<PropertyID> shorthand = shorthands_for_longhand(list.first().property_id).first_matching([&](PropertyID shorthand) {
auto longhands_for_potential_shorthand = longhands_for_shorthand(shorthand);
// The potential shorthand exactly maps to all of the longhand properties in list if:
// a. The number of longhand properties in the list is equal to the number of longhand properties that the potential shorthand maps to.
if (longhands_for_potential_shorthand.size() != list.size())
return false;
// b. All longhand properties in the list are contained in the list of longhands for the potential shorthand.
return all_of(longhands_for_potential_shorthand, [&](auto longhand) { return any_of(list, [&](auto const& declaration) { return declaration.property_id == longhand; }); });
});
// 2. If there is no such shorthand or shorthand cannot exactly represent the values of all the properties in list, return the empty string.
if (!shorthand.has_value())
return String {};
// 3. Otherwise, serialize a CSS value from a hypothetical declaration of the property shorthand with its value representing the combined values of the declarations in list.
// FIXME: Not all shorthands are represented by ShorthandStyleValue, we still need to add support for those that don't.
Vector<PropertyID> longhand_ids;
Vector<ValueComparingNonnullRefPtr<CSSStyleValue const>> longhand_values;
for (auto const& longhand : list) {
longhand_ids.append(longhand.property_id);
longhand_values.append(longhand.value);
}
return ShorthandStyleValue::create(shorthand.value(), longhand_ids, longhand_values)->to_string(SerializationMode::Normal);
}
// https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-csstext
WebIDL::ExceptionOr<void> CSSStyleProperties::set_css_text(StringView css_text)
{

View file

@ -52,6 +52,8 @@ public:
WebIDL::ExceptionOr<void> set_css_float(StringView);
virtual String serialized() const final override;
String serialize_a_css_value(StyleProperty const&) const;
String serialize_a_css_value(Vector<StyleProperty>) const;
virtual WebIDL::ExceptionOr<void> set_css_text(StringView) override;
void set_declarations_from_text(StringView);

View file

@ -268,6 +268,8 @@ bool property_accepts_time(PropertyID, Time const&);
bool property_is_shorthand(PropertyID);
Vector<PropertyID> longhands_for_shorthand(PropertyID);
bool property_maps_to_shorthand(PropertyID);
Vector<PropertyID> shorthands_for_longhand(PropertyID);
size_t property_maximum_value_count(PropertyID);
@ -1094,6 +1096,78 @@ Vector<PropertyID> longhands_for_shorthand(PropertyID property_id)
return { };
}
}
)~~~");
HashMap<String, Vector<String>> shorthands_for_longhand_map;
properties.for_each_member([&](auto& name, auto& value) {
if (is_legacy_alias(value.as_object()))
return;
if (value.as_object().has("longhands"sv)) {
auto longhands = value.as_object().get("longhands"sv);
VERIFY(longhands.has_value() && longhands->is_array());
auto longhand_values = longhands->as_array();
for (auto& longhand : longhand_values.values()) {
VERIFY(longhand.is_string());
auto& longhand_name = longhand.as_string();
shorthands_for_longhand_map.ensure(longhand_name).append(name);
}
}
});
generator.append(R"~~~(
bool property_maps_to_shorthand(PropertyID property_id)
{
switch (property_id) {
)~~~");
for (auto const& longhand : shorthands_for_longhand_map.keys()) {
auto property_generator = generator.fork();
property_generator.set("name:titlecase", title_casify(longhand));
property_generator.append(R"~~~(
case PropertyID::@name:titlecase@:
)~~~");
}
generator.append(R"~~~(
return true;
default:
return false;
}
}
)~~~");
generator.append(R"~~~(
Vector<PropertyID> shorthands_for_longhand(PropertyID property_id)
{
switch (property_id) {
)~~~");
for (auto const& longhand : shorthands_for_longhand_map.keys()) {
auto property_generator = generator.fork();
property_generator.set("name:titlecase", title_casify(longhand));
auto& shorthands = shorthands_for_longhand_map.get(longhand).value();
StringBuilder builder;
bool first = true;
for (auto& shorthand : shorthands) {
if (first)
first = false;
else
builder.append(", "sv);
builder.appendff("PropertyID::{}", title_casify(shorthand));
}
property_generator.set("shorthands", builder.to_byte_string());
property_generator.append(R"~~~(
case PropertyID::@name:titlecase@:
return { @shorthands@ };
)~~~");
}
generator.append(R"~~~(
default:
return { };
}
}
)~~~");
generator.append(R"~~~(

View file

@ -1,2 +1,2 @@
Before: foo<div style="white-space: pre">bar</div>
After: foo<span style="white-space-collapse: preserve; text-wrap-mode: nowrap; white-space-trim: none;">bar</span>
After: foo<span style="white-space: pre;">bar</span>

View file

@ -1,4 +1,4 @@
style.cssText = background-color: yellow; background-image: none; background-position-x: 0%; background-position-y: 0%; background-size: auto auto; background-repeat: repeat; background-attachment: scroll; background-origin: padding-box; background-clip: border-box;
style.cssText = background-color: yellow; background-image: none; background-position: 0% 0%; background-size: auto auto; background-repeat: repeat; background-attachment: scroll; background-origin: padding-box; background-clip: border-box;
style.length = 9
style[] =
1. background-color

View file

@ -2,8 +2,7 @@ Harness status: OK
Found 16 tests
12 Pass
4 Fail
16 Pass
Pass e.style['gap'] = "normal" should set the property value
Pass e.style['gap'] = "10px" should set the property value
Pass e.style['gap'] = "normal normal" should set the property value
@ -12,11 +11,11 @@ Pass e.style['column-gap'] = "normal" should set the property value
Pass e.style['column-gap'] = "10px" should set the property value
Pass e.style['row-gap'] = "normal" should set the property value
Pass e.style['row-gap'] = "10px" should set the property value
Fail 'row-gap: normal; column-gap: normal;' is serialized to 'gap: normal;'
Pass 'row-gap: normal; column-gap: normal;' is serialized to 'gap: normal;'
Pass getPropertyValue for 'row-gap: normal; column-gap: normal;' returns 'normal'
Fail 'row-gap: 10px; column-gap: 10px;' is serialized to 'gap: 10px;'
Pass 'row-gap: 10px; column-gap: 10px;' is serialized to 'gap: 10px;'
Pass getPropertyValue for 'row-gap: 10px; column-gap: 10px;' returns '10px'
Fail 'row-gap: 10px; column-gap: normal;' is serialized to 'gap: 10px normal;'
Pass 'row-gap: 10px; column-gap: normal;' is serialized to 'gap: 10px normal;'
Pass getPropertyValue for 'row-gap: 10px; column-gap: normal;' returns '10px normal'
Fail 'column-gap: normal; row-gap: 10px;' is serialized to 'gap: 10px normal;'
Pass 'column-gap: normal; row-gap: 10px;' is serialized to 'gap: 10px normal;'
Pass getPropertyValue for 'column-gap: normal; row-gap: 10px;' returns '10px normal'

View file

@ -2,13 +2,12 @@ Harness status: OK
Found 10 tests
7 Pass
3 Fail
10 Pass
Pass Single value overflow with CSS-wide keyword should serialize correctly.
Pass Single value overflow with non-CSS-wide keyword should serialize correctly.
Fail Overflow-x/y longhands with same CSS-wide keyword should serialize correctly.
Fail Overflow-x/y longhands with same non-CSS-wide keyword should serialize correctly.
Fail Overflow-x/y longhands with different keywords should serialize correctly.
Pass Overflow-x/y longhands with same CSS-wide keyword should serialize correctly.
Pass Overflow-x/y longhands with same non-CSS-wide keyword should serialize correctly.
Pass Overflow-x/y longhands with different keywords should serialize correctly.
Pass Single value overflow on element with CSS-wide keyword should serialize correctly.
Pass Single value overflow on element with non-CSS-wide keyword should serialize correctly.
Pass Overflow-x/y longhands on element with same CSS-wide keyword should serialize correctly.

View file

@ -2,12 +2,12 @@ Harness status: OK
Found 7 tests
3 Pass
4 Fail
6 Pass
1 Fail
Pass Shorthand serialization with shorthand and longhands mixed.
Fail Shorthand serialization with just longhands.
Pass Shorthand serialization with just longhands.
Fail Shorthand serialization with variable and variable from other shorthand.
Fail Shorthand serialization after setting
Fail Shorthand serialization with 'initial' value.
Pass Shorthand serialization after setting
Pass Shorthand serialization with 'initial' value.
Pass Shorthand serialization with 'initial' value, one longhand with important flag.
Pass Shorthand serialization with 'initial' value, longhands set individually, one with important flag.

View file

@ -2,8 +2,8 @@ Harness status: OK
Found 20 tests
2 Pass
18 Fail
7 Pass
13 Fail
Fail The serialization of border: 1px; border-top: 1px; should be canonical.
Fail The serialization of border: 1px solid red; should be canonical.
Fail The serialization of border: 1px red; should be canonical.
@ -15,12 +15,12 @@ Fail The serialization of border: 1px; border-top: 2px; should be canonical.
Fail The serialization of border: 1px; border-top: 1px !important; should be canonical.
Fail The serialization of border: 1px; border-top-color: red; should be canonical.
Fail The serialization of border: solid; border-style: dotted should be canonical.
Fail The serialization of border-width: 1px; should be canonical.
Fail The serialization of overflow-x: scroll; overflow-y: hidden; should be canonical.
Fail The serialization of overflow-x: scroll; overflow-y: scroll; should be canonical.
Pass The serialization of border-width: 1px; should be canonical.
Pass The serialization of overflow-x: scroll; overflow-y: hidden; should be canonical.
Pass The serialization of overflow-x: scroll; overflow-y: scroll; should be canonical.
Fail The serialization of outline-width: 2px; outline-style: dotted; outline-color: blue; should be canonical.
Fail The serialization of margin-top: 1px; margin-right: 2px; margin-bottom: 3px; margin-left: 4px; should be canonical.
Pass The serialization of margin-top: 1px; margin-right: 2px; margin-bottom: 3px; margin-left: 4px; should be canonical.
Fail The serialization of list-style-type: circle; list-style-position: inside; list-style-image: none; should be canonical.
Pass The serialization of list-style-type: lower-alpha; should be canonical.
Pass The serialization of font-family: sans-serif; line-height: 2em; font-size: 3em; font-style: italic; font-weight: bold; should be canonical.
Fail The serialization of padding-top: 1px; padding-right: 2px; padding-bottom: 3px; padding-left: 4px; should be canonical.
Pass The serialization of padding-top: 1px; padding-right: 2px; padding-bottom: 3px; padding-left: 4px; should be canonical.