From 6c4483fe0ed107700d29abafab0ee0125ddfc577 Mon Sep 17 00:00:00 2001 From: InvalidUsernameException Date: Fri, 18 Jul 2025 20:36:15 +0200 Subject: [PATCH] LibWeb/CSS: Serialize mask shorthand-property properly --- .../CSS/StyleValues/ShorthandStyleValue.cpp | 110 ++++++++++++++++++ ...upported-properties-and-default-values.txt | 8 +- .../css/css-masking/parsing/mask-computed.txt | 33 +++--- .../css-masking/parsing/mask-valid.sub.txt | 34 +++--- 4 files changed, 147 insertions(+), 38 deletions(-) diff --git a/Libraries/LibWeb/CSS/StyleValues/ShorthandStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/ShorthandStyleValue.cpp index aaff4999584..37f904d642a 100644 --- a/Libraries/LibWeb/CSS/StyleValues/ShorthandStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/ShorthandStyleValue.cpp @@ -489,6 +489,116 @@ String ShorthandStyleValue::to_string(SerializationMode mode) const return start->to_string(mode); return MUST(String::formatted("{} / {}", start->to_string(mode), end->to_string(mode))); } + case PropertyID::Mask: { + StringBuilder builder; + + auto serialize_layer = [mode, &builder](String image_value_string, String position_value_string, String size_value_string, String repeat_value_string, String origin_value_string, String clip_value_string, String composite_value_string, String mode_value_string) { + PropertyID canonical_property_order[] = { + PropertyID::MaskImage, + PropertyID::MaskPosition, + // Intentionally skipping MaskSize here, it is handled together with MaskPosition. + PropertyID::MaskRepeat, + PropertyID::MaskOrigin, + PropertyID::MaskClip, + PropertyID::MaskComposite, + PropertyID::MaskMode, + }; + + auto property_value = [&](PropertyID property) -> String const& { + switch (property) { + case PropertyID::MaskImage: + return image_value_string; + case PropertyID::MaskPosition: + return position_value_string; + case PropertyID::MaskSize: + return size_value_string; + case PropertyID::MaskRepeat: + return repeat_value_string; + case PropertyID::MaskOrigin: + return origin_value_string; + case PropertyID::MaskClip: + return clip_value_string; + case PropertyID::MaskComposite: + return composite_value_string; + case PropertyID::MaskMode: + return mode_value_string; + default: + VERIFY_NOT_REACHED(); + } + }; + + auto is_initial_value = [mode, property_value](PropertyID property) -> bool { + return property_value(property) == property_initial_value(property)->to_string(mode); + }; + + auto can_skip_serializing_initial_value = [is_initial_value, property_value](PropertyID property) -> bool { + switch (property) { + case PropertyID::MaskPosition: + return is_initial_value(PropertyID::MaskSize); + case PropertyID::MaskOrigin: + return is_initial_value(PropertyID::MaskClip) || property_value(PropertyID::MaskClip) == string_from_keyword(Keyword::NoClip); + default: + return true; + } + }; + + bool layer_is_empty = true; + for (size_t i = 0; i < array_size(canonical_property_order); i++) { + auto property = canonical_property_order[i]; + auto const& value = property_value(property); + + if (is_initial_value(property) && can_skip_serializing_initial_value(property)) + continue; + if (property == PropertyID::MaskClip && value == property_value(PropertyID::MaskOrigin)) + continue; + + if (!layer_is_empty) + builder.append(" "sv); + builder.append(value); + if (property == PropertyID::MaskPosition && !is_initial_value(PropertyID::MaskSize)) { + builder.append(" / "sv); + builder.append(property_value(PropertyID::MaskSize)); + } + layer_is_empty = false; + } + + if (layer_is_empty) + builder.append("none"sv); + }; + + auto get_layer_count = [](auto const& style_value) -> size_t { + return style_value->is_value_list() ? style_value->as_value_list().size() : 1; + }; + + auto mask_image = longhand(PropertyID::MaskImage); + auto mask_position = longhand(PropertyID::MaskPosition); + auto mask_size = longhand(PropertyID::MaskSize); + auto mask_repeat = longhand(PropertyID::MaskRepeat); + auto mask_origin = longhand(PropertyID::MaskOrigin); + auto mask_clip = longhand(PropertyID::MaskClip); + auto mask_composite = longhand(PropertyID::MaskComposite); + auto mask_mode = longhand(PropertyID::MaskMode); + + auto layer_count = max(get_layer_count(mask_image), max(get_layer_count(mask_position), max(get_layer_count(mask_size), max(get_layer_count(mask_repeat), max(get_layer_count(mask_origin), max(get_layer_count(mask_clip), max(get_layer_count(mask_composite), get_layer_count(mask_mode)))))))); + + if (layer_count == 1) { + serialize_layer(mask_image->to_string(mode), mask_position->to_string(mode), mask_size->to_string(mode), mask_repeat->to_string(mode), mask_origin->to_string(mode), mask_clip->to_string(mode), mask_composite->to_string(mode), mask_mode->to_string(mode)); + } else { + auto get_layer_value_string = [mode](ValueComparingRefPtr const& style_value, size_t index) { + if (style_value->is_value_list()) + return style_value->as_value_list().value_at(index, true)->to_string(mode); + return style_value->to_string(mode); + }; + + for (size_t i = 0; i < layer_count; i++) { + if (i) + builder.append(", "sv); + + serialize_layer(get_layer_value_string(mask_image, i), get_layer_value_string(mask_position, i), get_layer_value_string(mask_size, i), get_layer_value_string(mask_repeat, i), get_layer_value_string(mask_origin, i), get_layer_value_string(mask_clip, i), get_layer_value_string(mask_composite, i), get_layer_value_string(mask_mode, i)); + } + } + return builder.to_string_without_validation(); + } case PropertyID::PlaceContent: case PropertyID::PlaceItems: case PropertyID::PlaceSelf: diff --git a/Tests/LibWeb/Text/expected/css/CSSStyleProperties-all-supported-properties-and-default-values.txt b/Tests/LibWeb/Text/expected/css/CSSStyleProperties-all-supported-properties-and-default-values.txt index b889bcad21f..61aae1df5d8 100644 --- a/Tests/LibWeb/Text/expected/css/CSSStyleProperties-all-supported-properties-and-default-values.txt +++ b/Tests/LibWeb/Text/expected/css/CSSStyleProperties-all-supported-properties-and-default-values.txt @@ -111,9 +111,9 @@ All supported properties and their default values exposed from CSSStylePropertie 'WebkitJustifyContent': 'normal' 'webkitJustifyContent': 'normal' '-webkit-justify-content': 'normal' -'WebkitMask': 'border-box' -'webkitMask': 'border-box' -'-webkit-mask': 'border-box' +'WebkitMask': 'none' +'webkitMask': 'none' +'-webkit-mask': 'none' 'WebkitMaskImage': 'none' 'webkitMaskImage': 'none' '-webkit-mask-image': 'none' @@ -524,7 +524,7 @@ All supported properties and their default values exposed from CSSStylePropertie 'margin-right': '8px' 'marginTop': '8px' 'margin-top': '8px' -'mask': 'border-box' +'mask': 'none' 'maskClip': 'border-box' 'mask-clip': 'border-box' 'maskComposite': 'add' diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-masking/parsing/mask-computed.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-masking/parsing/mask-computed.txt index a26a7f2176b..5ee8dd714b9 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-masking/parsing/mask-computed.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-masking/parsing/mask-computed.txt @@ -2,32 +2,31 @@ Harness status: OK Found 27 tests -12 Pass -15 Fail -Fail Property mask value 'none' +27 Pass +Pass Property mask value 'none' Pass Property mask value 'linear-gradient(to left bottom, red, blue)' Pass Property mask value 'linear-gradient(to left bottom, red, blue) luminance' Pass Property mask value 'url("https://example.com/")' Pass Property mask value 'linear-gradient(to left bottom, red, blue) 1px 2px' -Fail Property mask value 'url("https://example.com/") 1px 2px / contain' +Pass Property mask value 'url("https://example.com/") 1px 2px / contain' Pass Property mask value 'repeat-y' -Fail Property mask value 'border-box' -Fail Property mask value 'linear-gradient(to left bottom, red, blue) padding-box' -Fail Property mask value 'content-box' -Fail Property mask value 'url("https://example.com/") fill-box' -Fail Property mask value 'linear-gradient(to left bottom, red, blue) stroke-box' -Fail Property mask value 'view-box' +Pass Property mask value 'border-box' +Pass Property mask value 'linear-gradient(to left bottom, red, blue) padding-box' +Pass Property mask value 'content-box' +Pass Property mask value 'url("https://example.com/") fill-box' +Pass Property mask value 'linear-gradient(to left bottom, red, blue) stroke-box' +Pass Property mask value 'view-box' Pass Property mask value 'no-clip' Pass Property mask value 'url("https://example.com/") add' Pass Property mask value 'subtract' -Fail Property mask value 'url("https://example.com/") intersect' -Fail Property mask value 'linear-gradient(to left bottom, red, blue) exclude' +Pass Property mask value 'url("https://example.com/") intersect' +Pass Property mask value 'linear-gradient(to left bottom, red, blue) exclude' Pass Property mask value 'alpha' Pass Property mask value 'url("https://example.com/") alpha' -Fail Property mask value 'border-box border-box' -Fail Property mask value 'content-box content-box' -Fail Property mask value 'border-box content-box' +Pass Property mask value 'border-box border-box' +Pass Property mask value 'content-box content-box' +Pass Property mask value 'border-box content-box' Pass Property mask value 'border-box no-clip' -Fail Property mask value 'intersect no-clip space round 1px 2px / contain stroke-box linear-gradient(to left bottom, red, blue) luminance' -Fail Property mask value 'intersect no-clip space round 1px 2px / contain view-box, stroke-box linear-gradient(to left bottom, red, blue) luminance' +Pass Property mask value 'intersect no-clip space round 1px 2px / contain stroke-box linear-gradient(to left bottom, red, blue) luminance' +Pass Property mask value 'intersect no-clip space round 1px 2px / contain view-box, stroke-box linear-gradient(to left bottom, red, blue) luminance' Pass Property mask value 'none alpha' \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-masking/parsing/mask-valid.sub.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-masking/parsing/mask-valid.sub.txt index ab9f9bf0561..92728c8c551 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-masking/parsing/mask-valid.sub.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-masking/parsing/mask-valid.sub.txt @@ -2,34 +2,34 @@ Harness status: OK Found 55 tests -25 Pass -30 Fail -Fail e.style['mask'] = "none" should set the property value +40 Pass +15 Fail +Pass e.style['mask'] = "none" should set the property value Pass e.style['mask'] = "linear-gradient(to left bottom, red, blue)" should set the property value Pass e.style['mask'] = "linear-gradient(to left bottom, red, blue) luminance" should set the property value Pass e.style['mask'] = "url(\"https://wpt.live/\")" should set the property value Pass e.style['mask'] = "linear-gradient(to left bottom, red, blue) 1px 2px" should set the property value -Fail e.style['mask'] = "url(\"https://wpt.live/\") 1px 2px / contain" should set the property value +Pass e.style['mask'] = "url(\"https://wpt.live/\") 1px 2px / contain" should set the property value Pass e.style['mask'] = "repeat-y" should set the property value -Fail e.style['mask'] = "border-box" should set the property value -Fail e.style['mask'] = "linear-gradient(to left bottom, red, blue) padding-box" should set the property value -Fail e.style['mask'] = "content-box" should set the property value -Fail e.style['mask'] = "url(\"https://wpt.live/\") fill-box" should set the property value -Fail e.style['mask'] = "linear-gradient(to left bottom, red, blue) stroke-box" should set the property value -Fail e.style['mask'] = "view-box" should set the property value +Pass e.style['mask'] = "border-box" should set the property value +Pass e.style['mask'] = "linear-gradient(to left bottom, red, blue) padding-box" should set the property value +Pass e.style['mask'] = "content-box" should set the property value +Pass e.style['mask'] = "url(\"https://wpt.live/\") fill-box" should set the property value +Pass e.style['mask'] = "linear-gradient(to left bottom, red, blue) stroke-box" should set the property value +Pass e.style['mask'] = "view-box" should set the property value Pass e.style['mask'] = "no-clip" should set the property value Pass e.style['mask'] = "url(\"https://wpt.live/\") add" should set the property value Pass e.style['mask'] = "subtract" should set the property value -Fail e.style['mask'] = "url(\"https://wpt.live/\") intersect" should set the property value -Fail e.style['mask'] = "linear-gradient(to left bottom, red, blue) exclude" should set the property value +Pass e.style['mask'] = "url(\"https://wpt.live/\") intersect" should set the property value +Pass e.style['mask'] = "linear-gradient(to left bottom, red, blue) exclude" should set the property value Pass e.style['mask'] = "alpha" should set the property value Pass e.style['mask'] = "url(\"https://wpt.live/\") alpha" should set the property value -Fail e.style['mask'] = "border-box border-box" should set the property value -Fail e.style['mask'] = "content-box content-box" should set the property value -Fail e.style['mask'] = "border-box content-box" should set the property value +Pass e.style['mask'] = "border-box border-box" should set the property value +Pass e.style['mask'] = "content-box content-box" should set the property value +Pass e.style['mask'] = "border-box content-box" should set the property value Pass e.style['mask'] = "border-box no-clip" should set the property value -Fail e.style['mask'] = "intersect no-clip space round 1px 2px / contain stroke-box linear-gradient(to left bottom, red, blue) luminance" should set the property value -Fail e.style['mask'] = "intersect no-clip space round 1px 2px / contain view-box, stroke-box linear-gradient(to left bottom, red, blue) luminance" should set the property value +Pass e.style['mask'] = "intersect no-clip space round 1px 2px / contain stroke-box linear-gradient(to left bottom, red, blue) luminance" should set the property value +Pass e.style['mask'] = "intersect no-clip space round 1px 2px / contain view-box, stroke-box linear-gradient(to left bottom, red, blue) luminance" should set the property value Pass e.style['mask'] = "none alpha" should set the property value Fail e.style['mask'] = "none" should set mask-border-outset Fail e.style['mask'] = "none" should set mask-border-repeat