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,22 +1172,96 @@ 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;
// 5. Let value be the result of invoking serialize a CSS value of declaration.
auto value = declaration.value->to_string(Web::CSS::SerializationMode::Normal);
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);
}
// 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.
auto serialized_declaration = serialize_a_css_declaration(string_from_property_id(property), move(value), declaration.important);
// 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;
// 7. Append serialized declaration to list.
list.append(move(serialized_declaration));
// 3. Let current longhands be an empty array.
Vector<StyleProperty> current_longhands;
// 8. Append property to already serialized.
already_serialized.set(property);
// 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 = 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.
auto serialized_declaration = serialize_a_css_declaration(string_from_property_id(property), move(value), declaration.important);
// 7. Append serialized declaration to list.
list.append(move(serialized_declaration));
// 8. Append property to already serialized.
already_serialized.set(property);
}
}
// 4. Return list joined with " " (U+0020).
@ -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);