LibWeb/CSS: Support converting CSSUnitValue to a StyleValue

A lone CSSUnitValue can now be converted to a dimension StyleValue of
the relevant type, as long as the property allows that type. If the
value is out of the allowed range, it's wrapped in calc().

There are a few failing tests still, involving setting a negative
percentage and expecting to read the computed value as 0. Those also
fail in Chromium, and a similar negative-length test expects a negative
computed value (not 0), so this appears to be an incorrect test.

Also, we regress some of the `cursor` tests. This is because our "does
property X accept type Y?" code is too naive: `cursor` is defined to
accept "number [-∞,∞]" in the JSON, and that value range is used when
clamping the result of calculations or interpolation. But because that
entry is there, we think a single number is a valid value for `cursor`.
Solving this generally is a larger task than I want to take on right
now. :^)
This commit is contained in:
Sam Atkins 2025-10-03 11:33:37 +01:00 committed by Andreas Kling
parent bd545af210
commit b2d55e4caa
9 changed files with 199 additions and 62 deletions

View File

@ -7,7 +7,19 @@
#include "CSSUnitValue.h"
#include <LibWeb/Bindings/CSSUnitValuePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/PropertyNameAndID.h>
#include <LibWeb/CSS/Serialize.h>
#include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
#include <LibWeb/CSS/StyleValues/FlexStyleValue.h>
#include <LibWeb/CSS/StyleValues/FrequencyStyleValue.h>
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/ResolutionStyleValue.h>
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
#include <LibWeb/CSS/StyleValues/UnresolvedStyleValue.h>
#include <LibWeb/CSS/Units.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
@ -285,4 +297,127 @@ Optional<SumValue> CSSUnitValue::create_a_sum_value() const
return SumValue { SumValueItem { value, { { unit, 1 } } } };
}
// https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation
WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSUnitValue::create_an_internal_representation(PropertyNameAndID const& property) const
{
// If value is a CSSStyleValue subclass,
// If value does not match the grammar of a list-valued property iteration of property, throw a TypeError.
//
// If any component of propertys CSS grammar has a limited numeric range, and the corresponding part of value
// is a CSSUnitValue that is outside of that range, replace that value with the result of wrapping it in a
// fresh CSSMathSum whose values internal slot contains only that part of value.
//
// Return the value.
// NB: We store all custom properties as UnresolvedStyleValue, so we always need to create one here.
if (property.is_custom_property()) {
auto token = [this]() {
if (m_unit == "number"_fly_string)
return Parser::Token::create_number(Number { Number::Type::Number, m_value });
if (m_unit == "percent"_fly_string)
return Parser::Token::create_percentage(Number { Number::Type::Number, m_value });
return Parser::Token::create_dimension(m_value, m_unit);
}();
return UnresolvedStyleValue::create({ Parser::ComponentValue { move(token) } }, Parser::SubstitutionFunctionsPresence {});
}
auto wrap_in_math_sum = [this, &property](auto&& value) -> NonnullRefPtr<StyleValue const> {
CalculationContext context {
.percentages_resolve_as = property_resolves_percentages_relative_to(property.id()),
.resolve_numbers_as_integers = property_accepts_type(property.id(), ValueType::Integer),
.accepted_type_ranges = property_accepted_type_ranges(property.id()),
};
auto numeric_node = NumericCalculationNode::create(value, context);
auto math_sum_node = SumCalculationNode::create({ move(numeric_node) });
return CalculatedStyleValue::create(move(math_sum_node), NumericType::create_from_unit(m_unit).release_value(), context);
};
if (m_unit == "number"_fly_string) {
// NB: Number before Integer, because a custom property accepts either and we want to avoid rounding in that case.
if (property_accepts_type(property.id(), ValueType::Number)) {
if (property_accepts_number(property.id(), m_value))
return NumberStyleValue::create(m_value);
return wrap_in_math_sum(Number { Number::Type::Number, m_value });
}
if (property_accepts_type(property.id(), ValueType::Integer)) {
// NB: Same rounding as CalculatedStyleValue::resolve_integer(). Maybe this should go somewhere central?
auto integer = llround(m_value);
if (property_accepts_integer(property.id(), integer))
return IntegerStyleValue::create(integer);
return wrap_in_math_sum(Number { Number::Type::Number, m_value });
}
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Property does not accept values of this type."sv };
}
if (m_unit == "percent"_fly_string) {
if (property_accepts_type(property.id(), ValueType::Percentage)) {
Percentage percentage { m_value };
if (property_accepts_percentage(property.id(), percentage))
return PercentageStyleValue::create(percentage);
return wrap_in_math_sum(percentage);
}
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Property does not accept values of this type."sv };
}
auto dimension_type = dimension_for_unit(m_unit);
if (!dimension_type.has_value())
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Unrecognized unit '{}'.", m_unit)) };
switch (*dimension_type) {
case DimensionType::Angle:
if (property_accepts_type(property.id(), ValueType::Angle)) {
Angle value { m_value, string_to_angle_unit(m_unit).release_value() };
if (property_accepts_angle(property.id(), value))
return AngleStyleValue::create(value);
return wrap_in_math_sum(value);
}
break;
case DimensionType::Flex:
if (property_accepts_type(property.id(), ValueType::Flex)) {
Flex value { m_value, string_to_flex_unit(m_unit).release_value() };
if (property_accepts_flex(property.id(), value))
return FlexStyleValue::create(value);
return wrap_in_math_sum(value);
}
break;
case DimensionType::Frequency:
if (property_accepts_type(property.id(), ValueType::Frequency)) {
Frequency value { m_value, string_to_frequency_unit(m_unit).release_value() };
if (property_accepts_frequency(property.id(), value))
return FrequencyStyleValue::create(value);
return wrap_in_math_sum(value);
}
break;
case DimensionType::Length:
if (property_accepts_type(property.id(), ValueType::Length)) {
Length value { m_value, string_to_length_unit(m_unit).release_value() };
if (property_accepts_length(property.id(), value))
return LengthStyleValue::create(value);
return wrap_in_math_sum(value);
}
break;
case DimensionType::Resolution:
if (property_accepts_type(property.id(), ValueType::Resolution)) {
Resolution value { m_value, string_to_resolution_unit(m_unit).release_value() };
if (property_accepts_resolution(property.id(), value))
return ResolutionStyleValue::create(value);
return wrap_in_math_sum(value);
}
break;
case DimensionType::Time:
if (property_accepts_type(property.id(), ValueType::Time)) {
Time value { m_value, string_to_time_unit(m_unit).release_value() };
if (property_accepts_time(property.id(), value))
return TimeStyleValue::create(value);
return wrap_in_math_sum(value);
}
break;
}
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Property does not accept values of this type."sv };
}
}

View File

@ -35,6 +35,8 @@ public:
virtual bool is_equal_numeric_value(GC::Ref<CSSNumericValue> other) const override;
virtual Optional<SumValue> create_a_sum_value() const override;
virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&) const override;
private:
explicit CSSUnitValue(JS::Realm&, double value, FlyString unit, NumericType type);

View File

@ -2,12 +2,12 @@ Harness status: OK
Found 7 tests
5 Pass
2 Fail
6 Pass
1 Fail
Fail Declared StylePropertyMap only contains properties in the style rule
Pass Declared StylePropertyMap contains CSS property declarations in style rules
Pass Declared StylePropertyMap does not contain inline styles
Pass Declared StylePropertyMap contains custom property declarations
Pass Declared StylePropertyMap does not contain properties with invalid values
Pass Declared StylePropertyMap contains properties with their last valid value
Fail Declared StylePropertyMap is live
Pass Declared StylePropertyMap is live

View File

@ -2,8 +2,8 @@ Harness status: OK
Found 14 tests
9 Pass
5 Fail
10 Pass
4 Fail
Pass Setting a StylePropertyMap with an unsupported property name throws TypeError
Pass Setting a StylePropertyMap with an null property name throws TypeError
Pass Setting a StylePropertyMap with a descriptor throws TypeError
@ -13,7 +13,7 @@ Pass Setting a non list-valued property with multiple arguments throws TypeError
Pass Setting a non list-valued property with list-valued string throws TypeError
Pass Setting a list-valued property with a CSSUnparsedValue and other values throws TypeError
Pass Setting a list-valued property with a var ref() and other values throws TypeError
Fail Setting a property with CSSStyleValue or String updates its value
Pass Setting a property with CSSStyleValue or String updates its value
Fail Setting a list-valued property with CSSStyleValue or String updates its values
Fail Setting a list-valued property with a list-valued string updates its value
Fail Setting a custom property with CSSStyleValue or String updates its value

View File

@ -2,8 +2,8 @@ Harness status: OK
Found 14 tests
9 Pass
5 Fail
10 Pass
4 Fail
Pass Setting a StylePropertyMap with an unsupported property name throws TypeError
Pass Setting a StylePropertyMap with an null property name throws TypeError
Pass Setting a StylePropertyMap with a descriptor throws TypeError
@ -13,7 +13,7 @@ Pass Setting a non list-valued property with multiple arguments throws TypeError
Pass Setting a non list-valued property with list-valued string throws TypeError
Pass Setting a list-valued property with a CSSUnparsedValue and other values throws TypeError
Pass Setting a list-valued property with a var ref() and other values throws TypeError
Fail Setting a property with CSSStyleValue or String updates its value
Pass Setting a property with CSSStyleValue or String updates its value
Fail Setting a list-valued property with CSSStyleValue or String updates its values
Fail Setting a list-valued property with a list-valued string updates its value
Fail Setting a custom property with CSSStyleValue or String updates its value

View File

@ -2,8 +2,8 @@ Harness status: OK
Found 69 tests
64 Pass
5 Fail
61 Pass
8 Fail
Fail Can set 'cursor' to CSS-wide keywords: initial
Fail Can set 'cursor' to CSS-wide keywords: inherit
Fail Can set 'cursor' to CSS-wide keywords: unset
@ -64,9 +64,9 @@ Pass Setting 'cursor' to an angle: calc(0rad + 0deg) throws TypeError
Pass Setting 'cursor' to a flexible length: 0fr throws TypeError
Pass Setting 'cursor' to a flexible length: 1fr throws TypeError
Pass Setting 'cursor' to a flexible length: -3.14fr throws TypeError
Pass Setting 'cursor' to a number: 0 throws TypeError
Pass Setting 'cursor' to a number: -3.14 throws TypeError
Pass Setting 'cursor' to a number: 3.14 throws TypeError
Fail Setting 'cursor' to a number: 0 throws TypeError
Fail Setting 'cursor' to a number: -3.14 throws TypeError
Fail Setting 'cursor' to a number: 3.14 throws TypeError
Pass Setting 'cursor' to a number: calc(2 + 3) throws TypeError
Pass Setting 'cursor' to a transform: translate(50%, 50%) throws TypeError
Pass Setting 'cursor' to a transform: perspective(10em) throws TypeError

View File

@ -2,25 +2,25 @@ Harness status: OK
Found 32 tests
15 Pass
17 Fail
22 Pass
10 Fail
Fail Can set 'line-height' to CSS-wide keywords: initial
Fail Can set 'line-height' to CSS-wide keywords: inherit
Fail Can set 'line-height' to CSS-wide keywords: unset
Fail Can set 'line-height' to CSS-wide keywords: revert
Fail Can set 'line-height' to var() references: var(--A)
Pass Can set 'line-height' to the 'normal' keyword: normal
Fail Can set 'line-height' to a length: 0px
Fail Can set 'line-height' to a length: -3.14em
Fail Can set 'line-height' to a length: 3.14cm
Pass Can set 'line-height' to a length: 0px
Pass Can set 'line-height' to a length: -3.14em
Pass Can set 'line-height' to a length: 3.14cm
Fail Can set 'line-height' to a length: calc(0px + 0em)
Fail Can set 'line-height' to a number: 0
Fail Can set 'line-height' to a number: -3.14
Fail Can set 'line-height' to a number: 3.14
Pass Can set 'line-height' to a number: 3.14
Fail Can set 'line-height' to a number: calc(2 + 3)
Fail Can set 'line-height' to a percent: 0%
Fail Can set 'line-height' to a percent: -3.14%
Fail Can set 'line-height' to a percent: 3.14%
Pass Can set 'line-height' to a percent: 0%
Pass Can set 'line-height' to a percent: -3.14%
Pass Can set 'line-height' to a percent: 3.14%
Fail Can set 'line-height' to a percent: calc(0% + 0%)
Pass Setting 'line-height' to a time: 0s throws TypeError
Pass Setting 'line-height' to a time: -3.14ms throws TypeError

View File

@ -2,20 +2,20 @@ Harness status: OK
Found 124 tests
72 Pass
52 Fail
92 Pass
32 Fail
Fail Can set 'padding-top' to CSS-wide keywords: initial
Fail Can set 'padding-top' to CSS-wide keywords: inherit
Fail Can set 'padding-top' to CSS-wide keywords: unset
Fail Can set 'padding-top' to CSS-wide keywords: revert
Fail Can set 'padding-top' to var() references: var(--A)
Fail Can set 'padding-top' to a percent: 0%
Pass Can set 'padding-top' to a percent: 0%
Fail Can set 'padding-top' to a percent: -3.14%
Fail Can set 'padding-top' to a percent: 3.14%
Pass Can set 'padding-top' to a percent: 3.14%
Fail Can set 'padding-top' to a percent: calc(0% + 0%)
Fail Can set 'padding-top' to a length: 0px
Fail Can set 'padding-top' to a length: -3.14em
Fail Can set 'padding-top' to a length: 3.14cm
Pass Can set 'padding-top' to a length: 0px
Pass Can set 'padding-top' to a length: -3.14em
Pass Can set 'padding-top' to a length: 3.14cm
Fail Can set 'padding-top' to a length: calc(0px + 0em)
Pass Setting 'padding-top' to a time: 0s throws TypeError
Pass Setting 'padding-top' to a time: -3.14ms throws TypeError
@ -40,13 +40,13 @@ Fail Can set 'padding-left' to CSS-wide keywords: inherit
Fail Can set 'padding-left' to CSS-wide keywords: unset
Fail Can set 'padding-left' to CSS-wide keywords: revert
Fail Can set 'padding-left' to var() references: var(--A)
Fail Can set 'padding-left' to a percent: 0%
Pass Can set 'padding-left' to a percent: 0%
Fail Can set 'padding-left' to a percent: -3.14%
Fail Can set 'padding-left' to a percent: 3.14%
Pass Can set 'padding-left' to a percent: 3.14%
Fail Can set 'padding-left' to a percent: calc(0% + 0%)
Fail Can set 'padding-left' to a length: 0px
Fail Can set 'padding-left' to a length: -3.14em
Fail Can set 'padding-left' to a length: 3.14cm
Pass Can set 'padding-left' to a length: 0px
Pass Can set 'padding-left' to a length: -3.14em
Pass Can set 'padding-left' to a length: 3.14cm
Fail Can set 'padding-left' to a length: calc(0px + 0em)
Pass Setting 'padding-left' to a time: 0s throws TypeError
Pass Setting 'padding-left' to a time: -3.14ms throws TypeError
@ -71,13 +71,13 @@ Fail Can set 'padding-right' to CSS-wide keywords: inherit
Fail Can set 'padding-right' to CSS-wide keywords: unset
Fail Can set 'padding-right' to CSS-wide keywords: revert
Fail Can set 'padding-right' to var() references: var(--A)
Fail Can set 'padding-right' to a percent: 0%
Pass Can set 'padding-right' to a percent: 0%
Fail Can set 'padding-right' to a percent: -3.14%
Fail Can set 'padding-right' to a percent: 3.14%
Pass Can set 'padding-right' to a percent: 3.14%
Fail Can set 'padding-right' to a percent: calc(0% + 0%)
Fail Can set 'padding-right' to a length: 0px
Fail Can set 'padding-right' to a length: -3.14em
Fail Can set 'padding-right' to a length: 3.14cm
Pass Can set 'padding-right' to a length: 0px
Pass Can set 'padding-right' to a length: -3.14em
Pass Can set 'padding-right' to a length: 3.14cm
Fail Can set 'padding-right' to a length: calc(0px + 0em)
Pass Setting 'padding-right' to a time: 0s throws TypeError
Pass Setting 'padding-right' to a time: -3.14ms throws TypeError
@ -102,13 +102,13 @@ Fail Can set 'padding-bottom' to CSS-wide keywords: inherit
Fail Can set 'padding-bottom' to CSS-wide keywords: unset
Fail Can set 'padding-bottom' to CSS-wide keywords: revert
Fail Can set 'padding-bottom' to var() references: var(--A)
Fail Can set 'padding-bottom' to a percent: 0%
Pass Can set 'padding-bottom' to a percent: 0%
Fail Can set 'padding-bottom' to a percent: -3.14%
Fail Can set 'padding-bottom' to a percent: 3.14%
Pass Can set 'padding-bottom' to a percent: 3.14%
Fail Can set 'padding-bottom' to a percent: calc(0% + 0%)
Fail Can set 'padding-bottom' to a length: 0px
Fail Can set 'padding-bottom' to a length: -3.14em
Fail Can set 'padding-bottom' to a length: 3.14cm
Pass Can set 'padding-bottom' to a length: 0px
Pass Can set 'padding-bottom' to a length: -3.14em
Pass Can set 'padding-bottom' to a length: 3.14cm
Fail Can set 'padding-bottom' to a length: calc(0px + 0em)
Pass Setting 'padding-bottom' to a time: 0s throws TypeError
Pass Setting 'padding-bottom' to a time: -3.14ms throws TypeError

View File

@ -2,21 +2,21 @@ Harness status: OK
Found 95 tests
56 Pass
39 Fail
71 Pass
24 Fail
Fail Can set 'width' to CSS-wide keywords: initial
Fail Can set 'width' to CSS-wide keywords: inherit
Fail Can set 'width' to CSS-wide keywords: unset
Fail Can set 'width' to CSS-wide keywords: revert
Fail Can set 'width' to var() references: var(--A)
Pass Can set 'width' to the 'auto' keyword: auto
Fail Can set 'width' to a percent: 0%
Pass Can set 'width' to a percent: 0%
Fail Can set 'width' to a percent: -3.14%
Fail Can set 'width' to a percent: 3.14%
Pass Can set 'width' to a percent: 3.14%
Fail Can set 'width' to a percent: calc(0% + 0%)
Fail Can set 'width' to a length: 0px
Fail Can set 'width' to a length: -3.14em
Fail Can set 'width' to a length: 3.14cm
Pass Can set 'width' to a length: 0px
Pass Can set 'width' to a length: -3.14em
Pass Can set 'width' to a length: 3.14cm
Fail Can set 'width' to a length: calc(0px + 0em)
Pass Setting 'width' to a time: 0s throws TypeError
Pass Setting 'width' to a time: -3.14ms throws TypeError
@ -41,13 +41,13 @@ Fail Can set 'min-width' to CSS-wide keywords: inherit
Fail Can set 'min-width' to CSS-wide keywords: unset
Fail Can set 'min-width' to CSS-wide keywords: revert
Fail Can set 'min-width' to var() references: var(--A)
Fail Can set 'min-width' to a percent: 0%
Pass Can set 'min-width' to a percent: 0%
Fail Can set 'min-width' to a percent: -3.14%
Fail Can set 'min-width' to a percent: 3.14%
Pass Can set 'min-width' to a percent: 3.14%
Fail Can set 'min-width' to a percent: calc(0% + 0%)
Fail Can set 'min-width' to a length: 0px
Fail Can set 'min-width' to a length: -3.14em
Fail Can set 'min-width' to a length: 3.14cm
Pass Can set 'min-width' to a length: 0px
Pass Can set 'min-width' to a length: -3.14em
Pass Can set 'min-width' to a length: 3.14cm
Fail Can set 'min-width' to a length: calc(0px + 0em)
Pass Setting 'min-width' to a time: 0s throws TypeError
Pass Setting 'min-width' to a time: -3.14ms throws TypeError
@ -73,13 +73,13 @@ Fail Can set 'max-width' to CSS-wide keywords: unset
Fail Can set 'max-width' to CSS-wide keywords: revert
Fail Can set 'max-width' to var() references: var(--A)
Pass Can set 'max-width' to the 'none' keyword: none
Fail Can set 'max-width' to a percent: 0%
Pass Can set 'max-width' to a percent: 0%
Fail Can set 'max-width' to a percent: -3.14%
Fail Can set 'max-width' to a percent: 3.14%
Pass Can set 'max-width' to a percent: 3.14%
Fail Can set 'max-width' to a percent: calc(0% + 0%)
Fail Can set 'max-width' to a length: 0px
Fail Can set 'max-width' to a length: -3.14em
Fail Can set 'max-width' to a length: 3.14cm
Pass Can set 'max-width' to a length: 0px
Pass Can set 'max-width' to a length: -3.14em
Pass Can set 'max-width' to a length: 3.14cm
Fail Can set 'max-width' to a length: calc(0px + 0em)
Pass Setting 'max-width' to a time: 0s throws TypeError
Pass Setting 'max-width' to a time: -3.14ms throws TypeError