LibWeb: Add support for the 'all' CSS property

The "longhands" array is populated in the code generator to avoid the
overhead of manually maintaining the list in Properties.json

There is one subtest that still fails in
'cssstyledeclaration-csstext-all-shorthand', this is related to
us not maintaining the relative order of CSS declarations for custom vs
non-custom properties.
This commit is contained in:
Callum Law 2025-06-10 01:31:14 +12:00 committed by Sam Atkins
commit d31a58a7d6
Notes: github-actions[bot] 2025-06-12 14:26:43 +00:00
16 changed files with 199 additions and 82 deletions

View file

@ -408,6 +408,11 @@ Parser::ParseErrorOr<NonnullRefPtr<CSSStyleValue const>> Parser::parse_css_value
// Special-case property handling // Special-case property handling
switch (property_id) { switch (property_id) {
case PropertyID::All:
// NOTE: The 'all' property, unlike some other shorthands, doesn't support directly listing sub-property
// values, only the CSS-wide keywords - this is handled above, and thus, if we have gotten to here, there
// is an invalid value which is a syntax error.
return ParseError::SyntaxError;
case PropertyID::AspectRatio: case PropertyID::AspectRatio:
if (auto parsed_value = parse_aspect_ratio_value(tokens); parsed_value && !tokens.has_next_token()) if (auto parsed_value = parse_aspect_ratio_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull(); return parsed_value.release_nonnull();

View file

@ -183,6 +183,14 @@
"align-self" "align-self"
] ]
}, },
"all": {
"affects-layout": true,
"affects-stacking-context": true,
"inherited": false,
"initial": "initial",
"longhands": [],
"_comment": "The 'longhands' array is populated in the code generator to avoid having to maintain it manually"
},
"animation": { "animation": {
"affects-layout": false, "affects-layout": false,
"inherited": false, "inherited": false,

View file

@ -996,57 +996,6 @@ void StyleComputer::for_each_property_expanding_shorthands(PropertyID property_i
set_longhand_property(property_id, value); set_longhand_property(property_id, value);
} }
void StyleComputer::set_property_expanding_shorthands(
CascadedProperties& cascaded_properties,
PropertyID property_id,
CSSStyleValue const& value,
GC::Ptr<CSSStyleDeclaration const> declaration,
CascadeOrigin cascade_origin,
Important important,
Optional<FlyString> layer_name)
{
for_each_property_expanding_shorthands(property_id, value, [&](PropertyID longhand_id, CSSStyleValue const& longhand_value) {
if (longhand_value.is_revert()) {
cascaded_properties.revert_property(longhand_id, important, cascade_origin);
} else if (longhand_value.is_revert_layer()) {
cascaded_properties.revert_layer_property(longhand_id, important, layer_name);
} else {
cascaded_properties.set_property(longhand_id, longhand_value, important, cascade_origin, layer_name, declaration);
}
});
}
void StyleComputer::set_all_properties(
CascadedProperties& cascaded_properties,
DOM::Element& element,
Optional<PseudoElement> pseudo_element,
CSSStyleValue const& value,
DOM::Document& document,
GC::Ptr<CSSStyleDeclaration const> declaration,
CascadeOrigin cascade_origin,
Important important,
Optional<FlyString> layer_name) const
{
for (auto i = to_underlying(CSS::first_longhand_property_id); i <= to_underlying(CSS::last_longhand_property_id); ++i) {
auto property_id = (CSS::PropertyID)i;
if (value.is_revert()) {
cascaded_properties.revert_property(property_id, important, cascade_origin);
continue;
}
if (value.is_revert_layer()) {
cascaded_properties.revert_layer_property(property_id, important, layer_name);
continue;
}
NonnullRefPtr<CSSStyleValue const> property_value = value;
if (property_value->is_unresolved())
property_value = Parser::Parser::resolve_unresolved_style_value(Parser::ParsingParams { document }, element, pseudo_element, property_id, property_value->as_unresolved());
set_property_expanding_shorthands(cascaded_properties, property_id, property_value, declaration, cascade_origin, important, layer_name);
}
}
void StyleComputer::cascade_declarations( void StyleComputer::cascade_declarations(
CascadedProperties& cascaded_properties, CascadedProperties& cascaded_properties,
DOM::Element& element, DOM::Element& element,
@ -1091,12 +1040,6 @@ void StyleComputer::cascade_declarations(
} }
} }
if (property.property_id == PropertyID::All) {
set_all_properties(cascaded_properties, element, pseudo_element, property_value, m_document, &declaration, cascade_origin, important, layer_name);
continue;
}
// NOTE: This is a duplicate of set_property_expanding_shorthands() with some extra checks.
for_each_property_expanding_shorthands(property.property_id, property_value, [&](PropertyID longhand_id, CSSStyleValue const& longhand_value) { for_each_property_expanding_shorthands(property.property_id, property_value, [&](PropertyID longhand_id, CSSStyleValue const& longhand_value) {
// If we're a PSV that's already been seen, that should mean that our shorthand already got // If we're a PSV that's already been seen, that should mean that our shorthand already got
// resolved and gave us a value, so we don't want to overwrite it with a PSV. // resolved and gave us a value, so we don't want to overwrite it with a PSV.

View file

@ -129,14 +129,6 @@ class FontLoader;
class StyleComputer { class StyleComputer {
public: public:
static void for_each_property_expanding_shorthands(PropertyID, CSSStyleValue const&, Function<void(PropertyID, CSSStyleValue const&)> const& set_longhand_property); static void for_each_property_expanding_shorthands(PropertyID, CSSStyleValue const&, Function<void(PropertyID, CSSStyleValue const&)> const& set_longhand_property);
static void set_property_expanding_shorthands(
CascadedProperties&,
PropertyID,
CSSStyleValue const&,
GC::Ptr<CSSStyleDeclaration const>,
CascadeOrigin,
Important,
Optional<FlyString> layer_name);
static NonnullRefPtr<CSSStyleValue const> get_inherit_value(CSS::PropertyID, DOM::Element const*, Optional<CSS::PseudoElement> = {}); static NonnullRefPtr<CSSStyleValue const> get_inherit_value(CSS::PropertyID, DOM::Element const*, Optional<CSS::PseudoElement> = {});
static Optional<String> user_agent_style_sheet_source(StringView name); static Optional<String> user_agent_style_sheet_source(StringView name);
@ -222,17 +214,6 @@ private:
void compute_defaulted_property_value(ComputedProperties&, DOM::Element const*, CSS::PropertyID, Optional<CSS::PseudoElement>) const; void compute_defaulted_property_value(ComputedProperties&, DOM::Element const*, CSS::PropertyID, Optional<CSS::PseudoElement>) const;
void set_all_properties(
CascadedProperties&,
DOM::Element&,
Optional<PseudoElement>,
CSSStyleValue const&,
DOM::Document&,
GC::Ptr<CSSStyleDeclaration const>,
CascadeOrigin,
Important,
Optional<FlyString> layer_name) const;
template<typename Callback> template<typename Callback>
void for_each_stylesheet(CascadeOrigin, Callback) const; void for_each_stylesheet(CascadeOrigin, Callback) const;

View file

@ -95,6 +95,11 @@ String ShorthandStyleValue::to_string(SerializationMode mode) const
// Then special cases // Then special cases
switch (m_properties.shorthand_property) { switch (m_properties.shorthand_property) {
case PropertyID::All: {
// NOTE: 'all' can only be serialized in the case all sub-properties share the same CSS-wide keyword, this is
// handled above, thus, if we get to here that mustn't be the case and we should return the empty string.
return ""_string;
}
case PropertyID::Background: { case PropertyID::Background: {
auto color = longhand(PropertyID::BackgroundColor); auto color = longhand(PropertyID::BackgroundColor);
auto image = longhand(PropertyID::BackgroundImage); auto image = longhand(PropertyID::BackgroundImage);

View file

@ -15,6 +15,7 @@
#include <LibMain/Main.h> #include <LibMain/Main.h>
void replace_logical_aliases(JsonObject& properties); void replace_logical_aliases(JsonObject& properties);
void populate_all_property_longhands(JsonObject& properties);
ErrorOr<void> generate_header_file(JsonObject& properties, Core::File& file); ErrorOr<void> generate_header_file(JsonObject& properties, Core::File& file);
ErrorOr<void> generate_implementation_file(JsonObject& properties, Core::File& file); ErrorOr<void> generate_implementation_file(JsonObject& properties, Core::File& file);
void generate_bounds_checking_function(JsonObject& properties, SourceGenerator& parent_generator, StringView css_type_name, StringView type_name, Optional<StringView> default_unit_name = {}, Optional<StringView> value_getter = {}); void generate_bounds_checking_function(JsonObject& properties, SourceGenerator& parent_generator, StringView css_type_name, StringView type_name, Optional<StringView> default_unit_name = {}, Optional<StringView> value_getter = {});
@ -81,6 +82,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
}); });
replace_logical_aliases(properties); replace_logical_aliases(properties);
populate_all_property_longhands(properties);
auto generated_header_file = TRY(Core::File::open(generated_header_path, Core::File::OpenMode::Write)); auto generated_header_file = TRY(Core::File::open(generated_header_path, Core::File::OpenMode::Write));
auto generated_implementation_file = TRY(Core::File::open(generated_implementation_path, Core::File::OpenMode::Write)); auto generated_implementation_file = TRY(Core::File::open(generated_implementation_path, Core::File::OpenMode::Write));
@ -123,6 +125,20 @@ void replace_logical_aliases(JsonObject& properties)
} }
} }
void populate_all_property_longhands(JsonObject& properties)
{
auto all_entry = properties.get_object("all"sv);
VERIFY(all_entry.has_value());
properties.for_each_member([&](auto name, auto value) {
if (value.as_object().has_array("longhands"sv) || value.as_object().has_array("logical-alias-for"sv) || value.as_object().has_string("legacy-alias-for"sv) || name == "direction" || name == "unicode-bidi")
return;
MUST(all_entry->get_array("longhands"sv)->append(JsonValue { name }));
});
}
ErrorOr<void> generate_header_file(JsonObject& properties, Core::File& file) ErrorOr<void> generate_header_file(JsonObject& properties, Core::File& file)
{ {
StringBuilder builder; StringBuilder builder;
@ -142,7 +158,6 @@ namespace Web::CSS {
enum class PropertyID : @property_id_underlying_type@ { enum class PropertyID : @property_id_underlying_type@ {
Invalid, Invalid,
Custom, Custom,
All,
)~~~"); )~~~");
Vector<String> inherited_shorthand_property_ids; Vector<String> inherited_shorthand_property_ids;
@ -448,8 +463,6 @@ Optional<PropertyID> property_id_from_string(StringView string)
if (is_a_custom_property_name_string(string)) if (is_a_custom_property_name_string(string))
return PropertyID::Custom; return PropertyID::Custom;
if (string.equals_ignoring_ascii_case("all"sv))
return PropertyID::All;
)~~~"); )~~~");
properties.for_each_member([&](auto& name, auto& value) { properties.for_each_member([&](auto& name, auto& value) {

View file

@ -152,6 +152,7 @@ All supported properties and their default values exposed from CSSStylePropertie
'align-items': 'normal' 'align-items': 'normal'
'alignSelf': 'auto' 'alignSelf': 'auto'
'align-self': 'auto' 'align-self': 'auto'
'all': ''
'animation': 'none' 'animation': 'none'
'animationDelay': '0s' 'animationDelay': '0s'
'animation-delay': '0s' 'animation-delay': '0s'

View file

@ -0,0 +1 @@
div { }

View file

@ -0,0 +1,11 @@
Harness status: OK
Found 6 tests
6 Pass
Pass getPropertyValue('all') returns empty string
Pass getPropertyValue('all') returns css-wide keyword if possible
Pass getPropertyValue('all') returns empty string when single property overriden
Pass setProperty('all') sets all property values
Pass removeProperty('all') removes all declarations affected by 'all'
Pass removeProperty('all') removes an 'all' declaration

View file

@ -0,0 +1,12 @@
Harness status: OK
Found 6 tests
5 Pass
1 Fail
Pass 'all' shorthand alone
Pass 'all' shorthand with 'width' and 'height'
Pass 'all' shorthand with 'direction' and 'unicode-bidi'
Fail 'all' shorthand with 'width', 'height' and custom properties
Pass 'all' shorthand with all longhands
Pass all:initial should not exclude custom from cssText

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass CSSStyleDeclaration.removeProperty("all")

View file

@ -2,7 +2,6 @@ Harness status: OK
Found 2 tests Found 2 tests
1 Pass 2 Pass
1 Fail Pass Specified style
Fail Specified style
Pass Computed style Pass Computed style

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<style>
div {
all: red;
}
</style>
<script src="../include.js"></script>
<script>
test(() => {
println(document.styleSheets[0].cssRules[0].cssText);
});
</script>
</html>

View file

@ -0,0 +1,54 @@
<!DOCTYPE html>
<title>CSSOM Test: Passing "all" shorthand to property methods</title>
<link rel="help" href="https://drafts.csswg.org/css-cascade/#all-shorthand">
<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-removeproperty">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script>
const style = document.createElement("div").style;
test((t) => {
t.add_cleanup(() => { style.cssText = ""; } );
style.cssText = "width: 50px";
assert_equals(style.getPropertyValue("all"), "");
}, "getPropertyValue('all') returns empty string");
test((t) => {
t.add_cleanup(() => { style.cssText = ""; } );
style.cssText = "all: revert";
assert_equals(style.getPropertyValue("all"), "revert");
}, "getPropertyValue('all') returns css-wide keyword if possible");
test((t) => {
t.add_cleanup(() => { style.cssText = ""; } );
style.cssText = "all: revert; width: 50px";
assert_equals(style.getPropertyValue("all"), "");
}, "getPropertyValue('all') returns empty string when single property overriden");
test((t) => {
t.add_cleanup(() => { style.cssText = ""; } );
style.setProperty("all", "revert");
assert_equals(style.getPropertyValue("width"), "revert");
assert_equals(style.getPropertyValue("color"), "revert");
}, "setProperty('all') sets all property values");
test((t) => {
t.add_cleanup(() => { style.cssText = ""; } );
style.cssText = "width: 50px; color: green; direction: rtl";
assert_equals(style.getPropertyValue("width"), "50px");
assert_equals(style.getPropertyValue("color"), "green");
assert_equals(style.getPropertyValue("direction"), "rtl");
style.removeProperty("all");
assert_equals(style.getPropertyValue("width"), "");
assert_equals(style.getPropertyValue("color"), "");
assert_equals(style.getPropertyValue("direction"), "rtl");
}, "removeProperty('all') removes all declarations affected by 'all'");
test((t) => {
t.add_cleanup(() => { style.cssText = ""; } );
style.cssText = "all: revert";
style.removeProperty("all");
assert_equals(style.getPropertyValue("all"), "");
}, "removeProperty('all') removes an 'all' declaration");
</script>

View file

@ -0,0 +1,47 @@
<!DOCTYPE html>
<title>CSSOM test: serialization of the 'all' shorthand in cssText</title>
<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com">
<link rel="help" href="https://drafts.csswg.org/cssom-1/#dom-cssstyledeclaration-csstext">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script>
const style = document.createElement("div").style;
test(function() {
style.cssText = "all: inherit";
assert_equals(style.cssText, "all: inherit;");
}, "'all' shorthand alone");
test(function() {
style.cssText = "width: 100px; all: inherit; height: inherit";
assert_equals(style.cssText, "all: inherit;");
}, "'all' shorthand with 'width' and 'height'");
test(function() {
style.cssText = "direction: ltr; all: inherit; unicode-bidi: plaintext";
assert_equals(style.cssText, "direction: ltr; all: inherit; unicode-bidi: plaintext;");
}, "'all' shorthand with 'direction' and 'unicode-bidi'");
test(function() {
style.cssText = "width: 100px; --a: a; all: inherit; --b: b; height: inherit";
assert_equals(style.cssText, "--a: a; all: inherit; --b: b;");
}, "'all' shorthand with 'width', 'height' and custom properties");
test(function() {
let cssText = "all: inherit; ";
for (let longhand of getComputedStyle(document.documentElement)) {
cssText += longhand + ": inherit; ";
}
style.cssText = cssText;
if (CSS.supports("interactivity:inert")) {
assert_equals(style.cssText, "all: inherit; direction: inherit; interactivity: inherit; unicode-bidi: inherit;");
} else {
assert_equals(style.cssText, "all: inherit; direction: inherit; unicode-bidi: inherit;");
}
}, "'all' shorthand with all longhands");
test(function() {
style.cssText = "--foo: bar; all: initial";
assert_true(style.cssText.includes("--foo: bar"), "cssText serialization includes custom property");
}, "all:initial should not exclude custom from cssText");
</script>

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSSStyleDeclaration.removeProperty("all")</title>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="author" title="Mozilla" href="https://mozilla.org">
<link rel="help" href="https://drafts.csswg.org/cssom-1/#dom-cssstyledeclaration-removeproperty">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script>
test(function() {
let style = document.createElement("div").style;
style.width = "40px";
assert_equals(style.length, 1, "setter should work as expected");
style.removeProperty("all");
assert_equals(style.length, 0, "all is a shorthand of all properties, so should remove the property");
});
</script>