LibWeb: Produce computed values for custom properties

Custom properties are required to produce a computed value just like
regular properties. The computed value is defined in the spec as
"specified value with variables substituted, or the guaranteed-invalid
value", though in reality all arbitrary substitution functions should be
substituted, not just `var()`.

To support this, we parse the CSS-wide keywords normally in custom
properties, instead of ignoring them. We don't yet handle all of them
properly, and because that will require us to cascade them like regular
properties. This is just enough to prevent regressions when implementing
ASFs.

Our output in this new test is not quite correct, because of the awkward
way we handle whitespace in property values - so it has 3 spaces in the
middle instead of 1, until that's fixed.

It's possible this computed-value production should go in
cascade_custom_properties(), but I had issues with that. Hopefully once
we start cascading custom properties properly, it'll be clearer how
this should all work.
This commit is contained in:
Sam Atkins 2025-06-26 16:48:33 +01:00 committed by Tim Ledbetter
commit 26acd897bf
Notes: github-actions[bot] 2025-07-09 15:45:57 +00:00
9 changed files with 119 additions and 32 deletions

View file

@ -134,7 +134,7 @@ public:
Vector<ComponentValue> parse_as_list_of_component_values(); Vector<ComponentValue> parse_as_list_of_component_values();
static NonnullRefPtr<CSSStyleValue const> resolve_unresolved_style_value(ParsingParams const&, DOM::Element&, Optional<PseudoElement>, PropertyID, UnresolvedStyleValue const&); static NonnullRefPtr<CSSStyleValue const> resolve_unresolved_style_value(ParsingParams const&, DOM::Element&, Optional<PseudoElement>, PropertyIDOrCustomPropertyName, UnresolvedStyleValue const&);
[[nodiscard]] LengthOrCalculated parse_as_sizes_attribute(DOM::Element const& element, HTML::HTMLImageElement const* img = nullptr); [[nodiscard]] LengthOrCalculated parse_as_sizes_attribute(DOM::Element const& element, HTML::HTMLImageElement const* img = nullptr);
@ -507,7 +507,7 @@ private:
OwnPtr<BooleanExpression> parse_supports_feature(TokenStream<ComponentValue>&); OwnPtr<BooleanExpression> parse_supports_feature(TokenStream<ComponentValue>&);
NonnullRefPtr<CSSStyleValue const> resolve_unresolved_style_value(DOM::Element&, Optional<PseudoElement>, PropertyID, UnresolvedStyleValue const&); NonnullRefPtr<CSSStyleValue const> resolve_unresolved_style_value(DOM::Element&, Optional<PseudoElement>, PropertyIDOrCustomPropertyName, UnresolvedStyleValue const&);
bool expand_variables(DOM::Element&, Optional<PseudoElement>, FlyString const& property_name, HashMap<FlyString, NonnullRefPtr<PropertyDependencyNode>>& dependencies, TokenStream<ComponentValue>& source, Vector<ComponentValue>& dest); bool expand_variables(DOM::Element&, Optional<PseudoElement>, FlyString const& property_name, HashMap<FlyString, NonnullRefPtr<PropertyDependencyNode>>& dependencies, TokenStream<ComponentValue>& source, Vector<ComponentValue>& dest);
bool expand_unresolved_values(DOM::Element&, FlyString const& property_name, TokenStream<ComponentValue>& source, Vector<ComponentValue>& dest); bool expand_unresolved_values(DOM::Element&, FlyString const& property_name, TokenStream<ComponentValue>& source, Vector<ComponentValue>& dest);
bool substitute_attr_function(DOM::Element& element, FlyString const& property_name, Function const& attr_function, Vector<ComponentValue>& dest); bool substitute_attr_function(DOM::Element& element, FlyString const& property_name, Function const& attr_function, Vector<ComponentValue>& dest);

View file

@ -464,6 +464,12 @@ Parser::ParseErrorOr<NonnullRefPtr<CSSStyleValue const>> Parser::parse_css_value
component_values.append(token); component_values.append(token);
} }
if (component_values.size() == 1) {
auto tokens = TokenStream { component_values };
if (auto parsed_value = parse_builtin_value(tokens))
return parsed_value.release_nonnull();
}
if (property_id == PropertyID::Custom || contains_arbitrary_substitution_function) if (property_id == PropertyID::Custom || contains_arbitrary_substitution_function)
return UnresolvedStyleValue::create(move(component_values), contains_arbitrary_substitution_function, original_source_text); return UnresolvedStyleValue::create(move(component_values), contains_arbitrary_substitution_function, original_source_text);
@ -472,11 +478,6 @@ Parser::ParseErrorOr<NonnullRefPtr<CSSStyleValue const>> Parser::parse_css_value
auto tokens = TokenStream { component_values }; auto tokens = TokenStream { component_values };
if (component_values.size() == 1) {
if (auto parsed_value = parse_builtin_value(tokens))
return parsed_value.release_nonnull();
}
// Special-case property handling // Special-case property handling
switch (property_id) { switch (property_id) {
case PropertyID::All: case PropertyID::All:

View file

@ -4218,14 +4218,14 @@ RefPtr<FontSourceStyleValue const> Parser::parse_font_source_value(TokenStream<C
return FontSourceStyleValue::create(url.release_value(), move(format), move(tech)); return FontSourceStyleValue::create(url.release_value(), move(format), move(tech));
} }
NonnullRefPtr<CSSStyleValue const> Parser::resolve_unresolved_style_value(ParsingParams const& context, DOM::Element& element, Optional<PseudoElement> pseudo_element, PropertyID property_id, UnresolvedStyleValue const& unresolved) NonnullRefPtr<CSSStyleValue const> Parser::resolve_unresolved_style_value(ParsingParams const& context, DOM::Element& element, Optional<PseudoElement> pseudo_element, PropertyIDOrCustomPropertyName property, UnresolvedStyleValue const& unresolved)
{ {
// Unresolved always contains a var() or attr(), unless it is a custom property's value, in which case we shouldn't be trying // Unresolved always contains a var() or attr(), unless it is a custom property's value, in which case we shouldn't be trying
// to produce a different CSSStyleValue from it. // to produce a different CSSStyleValue from it.
VERIFY(unresolved.contains_arbitrary_substitution_function()); VERIFY(unresolved.contains_arbitrary_substitution_function());
auto parser = Parser::create(context, ""sv); auto parser = Parser::create(context, ""sv);
return parser.resolve_unresolved_style_value(element, pseudo_element, property_id, unresolved); return parser.resolve_unresolved_style_value(element, pseudo_element, property, unresolved);
} }
class PropertyDependencyNode : public RefCounted<PropertyDependencyNode> { class PropertyDependencyNode : public RefCounted<PropertyDependencyNode> {
@ -4271,11 +4271,15 @@ private:
bool m_marked { false }; bool m_marked { false };
}; };
NonnullRefPtr<CSSStyleValue const> Parser::resolve_unresolved_style_value(DOM::Element& element, Optional<PseudoElement> pseudo_element, PropertyID property_id, UnresolvedStyleValue const& unresolved) NonnullRefPtr<CSSStyleValue const> Parser::resolve_unresolved_style_value(DOM::Element& element, Optional<PseudoElement> pseudo_element, PropertyIDOrCustomPropertyName property, UnresolvedStyleValue const& unresolved)
{ {
TokenStream unresolved_values_without_variables_expanded { unresolved.values() }; TokenStream unresolved_values_without_variables_expanded { unresolved.values() };
Vector<ComponentValue> values_with_variables_expanded; Vector<ComponentValue> values_with_variables_expanded;
auto const& property_name = property.visit(
[](PropertyID const& property_id) { return string_from_property_id(property_id); },
[](FlyString const& name) { return name; });
HashMap<FlyString, NonnullRefPtr<PropertyDependencyNode>> dependencies; HashMap<FlyString, NonnullRefPtr<PropertyDependencyNode>> dependencies;
ScopeGuard mark_element_if_uses_custom_properties = [&] { ScopeGuard mark_element_if_uses_custom_properties = [&] {
for (auto const& name : dependencies.keys()) { for (auto const& name : dependencies.keys()) {
@ -4285,19 +4289,25 @@ NonnullRefPtr<CSSStyleValue const> Parser::resolve_unresolved_style_value(DOM::E
} }
} }
}; };
if (!expand_variables(element, pseudo_element, string_from_property_id(property_id), dependencies, unresolved_values_without_variables_expanded, values_with_variables_expanded)) if (!expand_variables(element, pseudo_element, property_name, dependencies, unresolved_values_without_variables_expanded, values_with_variables_expanded))
return GuaranteedInvalidStyleValue::create(); return GuaranteedInvalidStyleValue::create();
TokenStream unresolved_values_with_variables_expanded { values_with_variables_expanded }; TokenStream unresolved_values_with_variables_expanded { values_with_variables_expanded };
Vector<ComponentValue> expanded_values; Vector<ComponentValue> expanded_values;
if (!expand_unresolved_values(element, string_from_property_id(property_id), unresolved_values_with_variables_expanded, expanded_values)) if (!expand_unresolved_values(element, property_name, unresolved_values_with_variables_expanded, expanded_values))
return GuaranteedInvalidStyleValue::create(); return GuaranteedInvalidStyleValue::create();
auto expanded_value_tokens = TokenStream { expanded_values }; return property.visit(
if (auto parsed_value = parse_css_value(property_id, expanded_value_tokens); !parsed_value.is_error()) [&](PropertyID const& property_id) -> NonnullRefPtr<CSSStyleValue const> {
return parsed_value.release_value(); auto expanded_value_tokens = TokenStream { expanded_values };
if (auto parsed_value = parse_css_value(property_id, expanded_value_tokens); !parsed_value.is_error())
return parsed_value.release_value();
return GuaranteedInvalidStyleValue::create(); return GuaranteedInvalidStyleValue::create();
},
[&](FlyString const&) -> NonnullRefPtr<CSSStyleValue const> {
return UnresolvedStyleValue::create(move(expanded_values), false, {});
});
} }
static RefPtr<CSSStyleValue const> get_custom_property(DOM::Element const& element, Optional<CSS::PseudoElement> pseudo_element, FlyString const& custom_property_name) static RefPtr<CSSStyleValue const> get_custom_property(DOM::Element const& element, Optional<CSS::PseudoElement> pseudo_element, FlyString const& custom_property_name)
@ -4333,9 +4343,6 @@ bool Parser::expand_variables(DOM::Element& element, Optional<PseudoElement> pse
}; };
while (source.has_next_token()) { while (source.has_next_token()) {
// FIXME: We should properly cascade here instead of doing a basic fallback for CSS-wide keywords.
if (auto builtin_value = parse_builtin_value(source))
continue;
auto const& value = source.consume_a_token(); auto const& value = source.consume_a_token();
if (value.is_block()) { if (value.is_block()) {
auto const& source_block = value.block(); auto const& source_block = value.block();
@ -4383,8 +4390,9 @@ bool Parser::expand_variables(DOM::Element& element, Optional<PseudoElement> pse
if (parent->has_cycles()) if (parent->has_cycles())
return false; return false;
if (auto custom_property_value = get_custom_property(element, pseudo_element, custom_property_name)) { if (auto custom_property_value = get_custom_property(element, pseudo_element, custom_property_name);
VERIFY(custom_property_value->is_unresolved()); custom_property_value && custom_property_value->is_unresolved()) {
// FIXME: We should properly cascade here instead of doing a basic fallback for CSS-wide keywords.
TokenStream custom_property_tokens { custom_property_value->as_unresolved().values() }; TokenStream custom_property_tokens { custom_property_value->as_unresolved().values() };
auto dest_size_before = dest.size(); auto dest_size_before = dest.size();

View file

@ -55,6 +55,7 @@
#include <LibWeb/CSS/StyleValues/FrequencyStyleValue.h> #include <LibWeb/CSS/StyleValues/FrequencyStyleValue.h>
#include <LibWeb/CSS/StyleValues/GridTrackPlacementStyleValue.h> #include <LibWeb/CSS/StyleValues/GridTrackPlacementStyleValue.h>
#include <LibWeb/CSS/StyleValues/GridTrackSizeListStyleValue.h> #include <LibWeb/CSS/StyleValues/GridTrackSizeListStyleValue.h>
#include <LibWeb/CSS/StyleValues/GuaranteedInvalidStyleValue.h>
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h> #include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h> #include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/CSS/StyleValues/MathDepthStyleValue.h> #include <LibWeb/CSS/StyleValues/MathDepthStyleValue.h>
@ -2569,6 +2570,7 @@ RefPtr<CSSStyleValue const> StyleComputer::recascade_font_size_if_needed(
GC::Ref<ComputedProperties> StyleComputer::compute_properties(DOM::Element& element, Optional<PseudoElement> pseudo_element, CascadedProperties& cascaded_properties) const GC::Ref<ComputedProperties> StyleComputer::compute_properties(DOM::Element& element, Optional<PseudoElement> pseudo_element, CascadedProperties& cascaded_properties) const
{ {
DOM::AbstractElement abstract_element { element, pseudo_element };
auto computed_style = document().heap().allocate<CSS::ComputedProperties>(); auto computed_style = document().heap().allocate<CSS::ComputedProperties>();
auto new_font_size = recascade_font_size_if_needed(element, pseudo_element, cascaded_properties); auto new_font_size = recascade_font_size_if_needed(element, pseudo_element, cascaded_properties);
@ -2682,6 +2684,9 @@ GC::Ref<ComputedProperties> StyleComputer::compute_properties(DOM::Element& elem
} }
} }
// Compute the value of custom properties
compute_custom_properties(computed_style, abstract_element);
// 2. Compute the math-depth property, since that might affect the font-size // 2. Compute the math-depth property, since that might affect the font-size
compute_math_depth(computed_style, &element, pseudo_element); compute_math_depth(computed_style, &element, pseudo_element);
@ -3090,6 +3095,63 @@ void StyleComputer::unload_fonts_from_sheet(CSSStyleSheet& sheet)
} }
} }
NonnullRefPtr<CSSStyleValue const> StyleComputer::compute_value_of_custom_property(DOM::AbstractElement abstract_element, FlyString const& name)
{
// https://drafts.csswg.org/css-variables/#propdef-
// The computed value of a custom property is its specified value with any arbitrary-substitution functions replaced.
// FIXME: These should probably be part of ComputedProperties.
auto value = abstract_element.get_custom_property(name);
if (!value)
return GuaranteedInvalidStyleValue::create();
// Initial value is the guaranteed-invalid value.
if (value->is_initial())
return GuaranteedInvalidStyleValue::create();
// Unset is the same as inherit for inherited properties, and by default all custom properties are inherited.
// FIXME: Update handling of css-wide keywords once we support @property properly.
if (value->is_inherit() || value->is_unset()) {
auto inherited_value = DOM::AbstractElement { const_cast<DOM::Element&>(*abstract_element.parent_element()) }.get_custom_property(name);
if (!inherited_value)
return GuaranteedInvalidStyleValue::create();
return inherited_value.release_nonnull();
}
if (value->is_revert()) {
// FIXME: Implement reverting custom properties.
}
if (value->is_revert_layer()) {
// FIXME: Implement reverting custom properties.
}
if (!value->is_unresolved() || !value->as_unresolved().contains_arbitrary_substitution_function())
return value.release_nonnull();
auto& unresolved = value->as_unresolved();
return Parser::Parser::resolve_unresolved_style_value(Parser::ParsingParams {}, abstract_element.element(), abstract_element.pseudo_element(), name, unresolved);
}
void StyleComputer::compute_custom_properties(ComputedProperties&, DOM::AbstractElement abstract_element) const
{
// https://drafts.csswg.org/css-variables/#propdef-
// The computed value of a custom property is its specified value with any arbitrary-substitution functions replaced.
// FIXME: These should probably be part of ComputedProperties.
auto custom_properties = abstract_element.custom_properties();
decltype(custom_properties) resolved_custom_properties;
for (auto const& [name, style_property] : custom_properties) {
resolved_custom_properties.set(name,
StyleProperty {
.important = style_property.important,
.property_id = style_property.property_id,
.value = compute_value_of_custom_property(abstract_element, name),
.custom_name = style_property.custom_name,
});
}
abstract_element.set_custom_properties(move(resolved_custom_properties));
}
void StyleComputer::compute_math_depth(ComputedProperties& style, DOM::Element const* element, Optional<CSS::PseudoElement> pseudo_element) const void StyleComputer::compute_math_depth(ComputedProperties& style, DOM::Element const* element, Optional<CSS::PseudoElement> pseudo_element) const
{ {
// https://w3c.github.io/mathml-core/#propdef-math-depth // https://w3c.github.io/mathml-core/#propdef-math-depth

View file

@ -193,6 +193,8 @@ public:
[[nodiscard]] inline bool should_reject_with_ancestor_filter(Selector const&) const; [[nodiscard]] inline bool should_reject_with_ancestor_filter(Selector const&) const;
static NonnullRefPtr<CSSStyleValue const> compute_value_of_custom_property(DOM::AbstractElement, FlyString const& custom_property);
private: private:
enum class ComputeStyleMode { enum class ComputeStyleMode {
Normal, Normal,
@ -207,6 +209,7 @@ private:
static RefPtr<Gfx::FontCascadeList const> find_matching_font_weight_ascending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, bool inclusive); static RefPtr<Gfx::FontCascadeList const> find_matching_font_weight_ascending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, bool inclusive);
static RefPtr<Gfx::FontCascadeList const> find_matching_font_weight_descending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, bool inclusive); static RefPtr<Gfx::FontCascadeList const> find_matching_font_weight_descending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, bool inclusive);
RefPtr<Gfx::FontCascadeList const> font_matching_algorithm(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const; RefPtr<Gfx::FontCascadeList const> font_matching_algorithm(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const;
void compute_custom_properties(ComputedProperties&, DOM::AbstractElement) const;
void compute_math_depth(ComputedProperties&, DOM::Element const*, Optional<CSS::PseudoElement>) const; void compute_math_depth(ComputedProperties&, DOM::Element const*, Optional<CSS::PseudoElement>) const;
void compute_defaulted_values(ComputedProperties&, DOM::Element const*, Optional<CSS::PseudoElement>) const; void compute_defaulted_values(ComputedProperties&, DOM::Element const*, Optional<CSS::PseudoElement>) const;
void start_needed_transitions(ComputedProperties const& old_style, ComputedProperties& new_style, DOM::Element&, Optional<PseudoElement>) const; void start_needed_transitions(ComputedProperties const& old_style, ComputedProperties& new_style, DOM::Element&, Optional<PseudoElement>) const;

View file

@ -0,0 +1 @@
"Hello" "world!"

View file

@ -2,13 +2,13 @@ Harness status: OK
Found 30 tests Found 30 tests
2 Pass 4 Pass
28 Fail 26 Fail
Pass `initial` as a value for an unregistered custom property Pass `initial` as a value for an unregistered custom property
Fail `inherit` as a value for an unregistered custom property Fail `inherit` as a value for an unregistered custom property
Fail `unset` as a value for an unregistered custom property Fail `unset` as a value for an unregistered custom property
Fail `revert` as a value for an unregistered custom property Fail `revert` as a value for an unregistered custom property
Fail `revert-layer` as a value for an unregistered custom property Pass `revert-layer` as a value for an unregistered custom property
Fail `initial` as a value for a non-inheriting registered custom property Fail `initial` as a value for a non-inheriting registered custom property
Fail `initial` as a value for an inheriting registered custom property Fail `initial` as a value for an inheriting registered custom property
Fail `inherit` as a value for a non-inheriting registered custom property Fail `inherit` as a value for a non-inheriting registered custom property
@ -17,9 +17,9 @@ Fail `unset` as a value for a non-inheriting registered custom property
Fail `unset` as a value for an inheriting registered custom property Fail `unset` as a value for an inheriting registered custom property
Fail `revert` as a value for a non-inheriting registered custom property Fail `revert` as a value for a non-inheriting registered custom property
Fail `revert` as a value for an inheriting registered custom property Fail `revert` as a value for an inheriting registered custom property
Fail `revert-layer` as a value for a non-inheriting registered custom property Pass `revert-layer` as a value for a non-inheriting registered custom property
Fail `revert-layer` as a value for an inheriting registered custom property Pass `revert-layer` as a value for an inheriting registered custom property
Pass `initial` as a `var()` fallback for an unregistered custom property Fail `initial` as a `var()` fallback for an unregistered custom property
Fail `inherit` as a `var()` fallback for an unregistered custom property Fail `inherit` as a `var()` fallback for an unregistered custom property
Fail `unset` as a `var()` fallback for an unregistered custom property Fail `unset` as a `var()` fallback for an unregistered custom property
Fail `revert` as a `var()` fallback for an unregistered custom property Fail `revert` as a `var()` fallback for an unregistered custom property

View file

@ -2,11 +2,11 @@ Harness status: OK
Found 11 tests Found 11 tests
1 Pass 4 Pass
10 Fail 7 Fail
Fail Self-cycle Pass Self-cycle
Fail Simple a/b cycle Pass Simple a/b cycle
Fail Three-var cycle Pass Three-var cycle
Fail Cycle that starts in the middle of a chain Fail Cycle that starts in the middle of a chain
Fail Cycle with extra edge Fail Cycle with extra edge
Fail Cycle with extra edge (2) Fail Cycle with extra edge (2)

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<style>
#a { --foo: "Hello"; }
#b { --bar: var(--foo) "world!"; }
</style>
<div id="a"><div id="b"></div></div>
<script>
test(() => {
println(getComputedStyle(document.getElementById("b")).getPropertyValue("--bar"));
});
</script>