mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-29 04:09:13 +00:00
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
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:
parent
94f5a51820
commit
ed65d5b342
Notes:
github-actions[bot]
2025-05-29 10:05:38 +00:00
Author: https://github.com/Calme1709
Commit: ed65d5b342
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4832
Reviewed-by: https://github.com/AtkinsSJ
Reviewed-by: https://github.com/gmta ✅
9 changed files with 249 additions and 36 deletions
|
@ -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 block’s 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 there’s 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 property’s 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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"~~~(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
Loading…
Add table
Add a link
Reference in a new issue