From bad9efac22b1264bcc4b0bf4f1c9f5662ef1fc86 Mon Sep 17 00:00:00 2001 From: Callum Law Date: Tue, 4 Nov 2025 15:18:14 +1300 Subject: [PATCH] LibWeb: Support CSS `random()` function step argument --- Libraries/LibWeb/CSS/MathFunctions.json | 5 + .../CSS/StyleValues/CalculatedStyleValue.cpp | 93 +++++++++++++++---- .../CSS/StyleValues/CalculatedStyleValue.h | 5 +- .../css-values/random-computed.tentative.txt | 68 +++++++------- .../css-values/random-serialize.tentative.txt | 6 +- 5 files changed, 122 insertions(+), 55 deletions(-) diff --git a/Libraries/LibWeb/CSS/MathFunctions.json b/Libraries/LibWeb/CSS/MathFunctions.json index e1492e5c866..cd4c4ea138a 100644 --- a/Libraries/LibWeb/CSS/MathFunctions.json +++ b/Libraries/LibWeb/CSS/MathFunctions.json @@ -187,6 +187,11 @@ "name": "maximum", "type": "||", "required": true + }, + { + "name": "step", + "type": "||", + "required": false } ] }, diff --git a/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp index 80b2ef9d17d..489980c601d 100644 --- a/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp @@ -286,7 +286,7 @@ static String serialize_a_math_function(CalculationNode const& fn, CalculationCo } // AD-HOC: We serialize random() directly since it has abnormal children (e.g. m_random_value_sharing which is not a - // calculation node). + // calculation node and m_step which is nullable). if (fn.type() == CalculationNode::Type::Random) return as(fn).to_string(context, serialization_mode); @@ -2442,18 +2442,24 @@ bool ModCalculationNode::equals(CalculationNode const& other) const && m_y->equals(*static_cast(other).m_y); } -NonnullRefPtr RandomCalculationNode::create(NonnullRefPtr random_value_sharing, NonnullRefPtr minimum, NonnullRefPtr maximum) +NonnullRefPtr RandomCalculationNode::create(NonnullRefPtr random_value_sharing, NonnullRefPtr minimum, NonnullRefPtr maximum, RefPtr step) { - Optional numeric_type = add_the_types(*minimum, *maximum); + Optional numeric_type; - return adopt_ref(*new (nothrow) RandomCalculationNode(move(random_value_sharing), move(minimum), move(maximum), move(numeric_type))); + if (step) + numeric_type = add_the_types(*minimum, *maximum, *step); + else + numeric_type = add_the_types(*minimum, *maximum); + + return adopt_ref(*new (nothrow) RandomCalculationNode(move(random_value_sharing), move(minimum), move(maximum), move(step), move(numeric_type))); } -RandomCalculationNode::RandomCalculationNode(NonnullRefPtr random_value_sharing, NonnullRefPtr minimum, NonnullRefPtr maximum, Optional numeric_type) +RandomCalculationNode::RandomCalculationNode(NonnullRefPtr random_value_sharing, NonnullRefPtr minimum, NonnullRefPtr maximum, RefPtr step, Optional numeric_type) : CalculationNode(Type::Random, move(numeric_type)) , m_random_value_sharing(move(random_value_sharing)) , m_minimum(move(minimum)) , m_maximum(move(maximum)) + , m_step(move(step)) { } @@ -2461,7 +2467,7 @@ RandomCalculationNode::~RandomCalculationNode() = default; bool RandomCalculationNode::contains_percentage() const { - return m_minimum->contains_percentage() || m_maximum->contains_percentage(); + return m_minimum->contains_percentage() || m_maximum->contains_percentage() || (m_step && m_step->contains_percentage()); } NonnullRefPtr RandomCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const @@ -2483,10 +2489,14 @@ NonnullRefPtr RandomCalculationNode::with_simplified_chil ValueComparingNonnullRefPtr simplified_minimum = simplify_a_calculation_tree(m_minimum, context, resolution_context); ValueComparingNonnullRefPtr simplified_maximum = simplify_a_calculation_tree(m_maximum, context, resolution_context); - if (simplified_random_value_sharing == m_random_value_sharing && simplified_minimum == m_minimum && simplified_maximum == m_maximum) + ValueComparingRefPtr simplified_step; + if (m_step) + simplified_step = simplify_a_calculation_tree(*m_step, context, resolution_context); + + if (simplified_random_value_sharing == m_random_value_sharing && simplified_minimum == m_minimum && simplified_maximum == m_maximum && simplified_step == m_step) return *this; - return RandomCalculationNode::create(simplified_random_value_sharing.release_nonnull(), move(simplified_minimum), move(simplified_maximum)); + return RandomCalculationNode::create(simplified_random_value_sharing.release_nonnull(), move(simplified_minimum), move(simplified_maximum), move(simplified_step)); } // https://drafts.csswg.org/css-values-5/#random-evaluation @@ -2506,6 +2516,16 @@ Optional RandomCalculationNode::run_ope auto minimum_value = minimum->value(); auto maximum_value = maximum->value(); + double step_value = 0; + + if (m_step) { + auto step = try_get_value_with_canonical_unit(*m_step, context, resolution_context); + + if (!step.has_value()) + return {}; + + step_value = step->value(); + } // https://drafts.csswg.org/css-values-5/#random-infinities // If the maximum value is less than the minimum value, it behaves as if it’s equal to the minimum value. @@ -2522,17 +2542,52 @@ Optional RandomCalculationNode::run_ope if (isinf(maximum_value)) return CalculatedStyleValue::CalculationResult { AK::NaN, numeric_type() }; + // If C is infinite, the result is A. + if (isinf(step_value)) + return CalculatedStyleValue::CalculationResult { minimum_value, numeric_type() }; + // Note: As usual for math functions, if any argument calculation is NaN, the result is NaN. - if (isnan(minimum_value) || isnan(maximum_value)) + if (isnan(minimum_value) || isnan(maximum_value) || isnan(step_value)) return CalculatedStyleValue::CalculationResult { AK::NaN, numeric_type() }; + // If C is negative, zero, or positive but close enough to zero that the range for the step multiplier (the N + // mentioned in § 9.3 Evaluating Random Values) would be infinite in the user agent, the step must be ignored. (The + // function is treated as if only A and B were provided.) + auto has_step = step_value > AK::NumericLimits::epsilon() * 1000; + // Given a random function with a random base value R, the value of the function is: // - for a random() function with min and max, but no step - // Return min + R * (max - min) - return CalculatedStyleValue::CalculationResult { - minimum_value + (random_base_value * (maximum_value - minimum_value)), - numeric_type() - }; + if (!has_step) { + // Return min + R * (max - min) + return CalculatedStyleValue::CalculationResult { + minimum_value + (random_base_value * (maximum_value - minimum_value)), + numeric_type() + }; + } + + // for a random() function with min, max, and step + // Let epsilon be step / 1000, or the smallest representable value greater than zero in the numeric type being used if epsilon would round to zero. + auto epsilon = step_value / 1000; + + // Let N be the largest integer such that min + N * step is less than or equal to max. + auto n = floor((maximum_value - minimum_value) / step_value); + + // If N produces a value that is not within epsilon of max, but N+1 would produce a value within epsilon of max, set N to N+1. + if (abs(maximum_value - (n * step_value + minimum_value)) > epsilon && abs(maximum_value - ((n + 1) * step_value + minimum_value)) < epsilon) + n = n + 1; + + // Let step index be a random integer less than N+1, given R. + auto step_index = floor((n + 1) * random_base_value); + + // Let value be min + step index * step. + auto value = minimum_value + (step_index * step_value); + + // If step index is N and value is within epsilon of max, return max. + if (step_index == n && abs(maximum_value - value) < epsilon) + return CalculatedStyleValue::CalculationResult { maximum_value, numeric_type() }; + + // Otherwise, return value. + return CalculatedStyleValue::CalculationResult { value, numeric_type() }; } String RandomCalculationNode::to_string(CalculationContext const& context, SerializationMode serialization_mode) const @@ -2542,7 +2597,10 @@ String RandomCalculationNode::to_string(CalculationContext const& context, Seria builder.append("random("sv); builder.appendff("{}, ", m_random_value_sharing->to_string(serialization_mode)); builder.appendff("{}, ", serialize_a_calculation_tree(m_minimum, context, serialization_mode)); - builder.appendff("{})", serialize_a_calculation_tree(m_maximum, context, serialization_mode)); + builder.append(serialize_a_calculation_tree(m_maximum, context, serialization_mode)); + if (m_step) + builder.appendff(", {}", serialize_a_calculation_tree(*m_step, context, serialization_mode)); + builder.append(')'); return builder.to_string_without_validation(); } @@ -2553,6 +2611,8 @@ void RandomCalculationNode::dump(StringBuilder& builder, int indent) const builder.appendff("{}\n", m_random_value_sharing->to_string(SerializationMode::Normal)); m_minimum->dump(builder, indent + 2); m_maximum->dump(builder, indent + 2); + if (m_step) + m_step->dump(builder, indent + 2); } bool RandomCalculationNode::equals(CalculationNode const& other) const @@ -2567,7 +2627,8 @@ bool RandomCalculationNode::equals(CalculationNode const& other) const return m_random_value_sharing == other_random.m_random_value_sharing && m_minimum == other_random.m_minimum - && m_maximum == other_random.m_maximum; + && m_maximum == other_random.m_maximum + && m_step == other_random.m_step; } NonnullRefPtr RemCalculationNode::create(NonnullRefPtr x, NonnullRefPtr y) diff --git a/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.h index 02927e37346..728ae29b61c 100644 --- a/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.h +++ b/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.h @@ -754,7 +754,7 @@ private: class RandomCalculationNode final : public CalculationNode { public: - static NonnullRefPtr create(NonnullRefPtr, NonnullRefPtr minimum, NonnullRefPtr maximum); + static NonnullRefPtr create(NonnullRefPtr, NonnullRefPtr minimum, NonnullRefPtr maximum, RefPtr step); ~RandomCalculationNode(); virtual bool contains_percentage() const override; @@ -770,10 +770,11 @@ public: virtual bool equals(CalculationNode const&) const override; private: - RandomCalculationNode(NonnullRefPtr, NonnullRefPtr, NonnullRefPtr, Optional); + RandomCalculationNode(NonnullRefPtr, NonnullRefPtr, NonnullRefPtr, RefPtr, Optional); ValueComparingNonnullRefPtr m_random_value_sharing; ValueComparingNonnullRefPtr m_minimum; ValueComparingNonnullRefPtr m_maximum; + ValueComparingRefPtr m_step; }; class RemCalculationNode final : public CalculationNode { diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-values/random-computed.tentative.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-values/random-computed.tentative.txt index 2326681733a..3a8c6c1969a 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-values/random-computed.tentative.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-values/random-computed.tentative.txt @@ -2,64 +2,64 @@ Harness status: OK Found 72 tests -27 Pass -45 Fail +59 Pass +13 Fail Pass Property scale value 'random(1, 11)' Fail Property scale value 'random(--foo, 2, 12)' Fail Property scale value 'random(--foo element-shared, 3, 13)' Fail Property scale value 'random(element-shared --foo, 4, 14)' -Fail Property scale value 'random(0, 10, 5)' +Pass Property scale value 'random(0, 10, 5)' Fail Property scale value 'random(--foo, 10, 20, 5)' Fail Property scale value 'random(--foo element-shared, 20, 30, 5)' Fail Property scale value 'random(element-shared --foo, 30, 40, 5)' Pass Property scale value 'random(100, 10)' Pass Property scale value 'random(-10, -100)' Pass Property scale value 'random(-100, -10)' -Fail Property scale value 'random(40, 50, -5)' +Pass Property scale value 'random(40, 50, -5)' Pass Property scale value 'random(5 * 1, 30 / 2)' Pass Property scale value 'calc(2 * random(6, 16))' Pass Property scale value 'random(NaN, 100)' Pass Property scale value 'random(10, NaN)' Pass Property scale value 'random(NaN, NaN)' -Fail Property scale value 'random(NaN, 100, 10)' -Fail Property scale value 'random(10, NaN, 10)' -Fail Property scale value 'random(NaN, NaN, 10)' -Fail Property scale value 'random(NaN, 100, NaN)' -Fail Property scale value 'random(10, NaN, NaN)' -Fail Property scale value 'random(NaN, NaN, NaN)' -Fail Property scale value 'random(10, 100, NaN)' +Pass Property scale value 'random(NaN, 100, 10)' +Pass Property scale value 'random(10, NaN, 10)' +Pass Property scale value 'random(NaN, NaN, 10)' +Pass Property scale value 'random(NaN, 100, NaN)' +Pass Property scale value 'random(10, NaN, NaN)' +Pass Property scale value 'random(NaN, NaN, NaN)' +Pass Property scale value 'random(10, 100, NaN)' Pass Property scale value 'calc(10 + random(NaN, 100))' Pass Property scale value 'calc(10 + random(10, NaN))' Pass Property scale value 'calc(10 + random(NaN, NaN))' -Fail Property scale value 'calc(10 + random(NaN, 100, 10))' -Fail Property scale value 'calc(10 + random(10, NaN, 10))' -Fail Property scale value 'calc(10 + random(NaN, NaN, 10))' -Fail Property scale value 'calc(10 + random(NaN, 100, NaN))' -Fail Property scale value 'calc(10 + random(10, NaN, NaN))' -Fail Property scale value 'calc(10 + random(NaN, NaN, NaN))' -Fail Property scale value 'calc(10 + random(10, 100, NaN))' +Pass Property scale value 'calc(10 + random(NaN, 100, 10))' +Pass Property scale value 'calc(10 + random(10, NaN, 10))' +Pass Property scale value 'calc(10 + random(NaN, NaN, 10))' +Pass Property scale value 'calc(10 + random(NaN, 100, NaN))' +Pass Property scale value 'calc(10 + random(10, NaN, NaN))' +Pass Property scale value 'calc(10 + random(NaN, NaN, NaN))' +Pass Property scale value 'calc(10 + random(10, 100, NaN))' Pass Property scale value 'random(infinity, 100)' Pass Property scale value 'random(infinity, infinity)' -Fail Property scale value 'random(infinity, 100, 10)' -Fail Property scale value 'random(infinity, infinity, 10)' -Fail Property scale value 'random(infinity, 100, infinity)' -Fail Property scale value 'random(infinity, infinity, infinity)' +Pass Property scale value 'random(infinity, 100, 10)' +Pass Property scale value 'random(infinity, infinity, 10)' +Pass Property scale value 'random(infinity, 100, infinity)' +Pass Property scale value 'random(infinity, infinity, infinity)' Pass Property scale value 'calc(10 + random(infinity, 100))' Pass Property scale value 'calc(10 + random(infinity, infinity))' -Fail Property scale value 'calc(10 + random(infinity, infinity, 10))' -Fail Property scale value 'calc(10 + random(infinity, 100, infinity))' -Fail Property scale value 'calc(10 + random(infinity, infinity, infinity))' -Fail Property scale value 'calc(10 + random(infinity, 100, 10))' +Pass Property scale value 'calc(10 + random(infinity, infinity, 10))' +Pass Property scale value 'calc(10 + random(infinity, 100, infinity))' +Pass Property scale value 'calc(10 + random(infinity, infinity, infinity))' +Pass Property scale value 'calc(10 + random(infinity, 100, 10))' Pass Property scale value 'random(10, infinity)' -Fail Property scale value 'random(10, infinity, 10)' -Fail Property scale value 'random(10, infinity, infinity)' +Pass Property scale value 'random(10, infinity, 10)' +Pass Property scale value 'random(10, infinity, infinity)' Pass Property scale value 'calc(10 + random(10, infinity))' -Fail Property scale value 'calc(10 + random(10, infinity, 10))' -Fail Property scale value 'calc(10 + random(10, infinity, infinity))' -Fail Property scale value 'random(10, 100, infinity)' -Fail Property scale value 'calc(10 + random(10, 100, infinity))' -Fail Property scale value 'random(10, 100, -infinity)' -Fail Property scale value 'calc(10 + random(10, 100, -infinity))' +Pass Property scale value 'calc(10 + random(10, infinity, 10))' +Pass Property scale value 'calc(10 + random(10, infinity, infinity))' +Pass Property scale value 'random(10, 100, infinity)' +Pass Property scale value 'calc(10 + random(10, 100, infinity))' +Pass Property scale value 'random(10, 100, -infinity)' +Pass Property scale value 'calc(10 + random(10, 100, -infinity))' Pass Property scale value on pseudo element '::before' 'random(7, 17)' Fail Property scale value on pseudo element '::before' 'random(--bar, 8, 18)' Fail Property scale value on pseudo element '::before' 'random(element-shared, 9, 19)' diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-values/random-serialize.tentative.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-values/random-serialize.tentative.txt index b5aaa9e0efb..8d7e3e2ded0 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-values/random-serialize.tentative.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-values/random-serialize.tentative.txt @@ -2,8 +2,8 @@ Harness status: OK Found 32 tests -2 Pass -30 Fail +3 Pass +29 Fail Fail e.style['width'] = "random(0px, 100px)" should set the property value Fail e.style['width'] = "random(0px, 100px, 50px)" should set the property value Fail e.style['width'] = "random(--foo, 0px, 100px)" should set the property value @@ -19,7 +19,7 @@ Fail e.style['width'] = "random(--foo element-shared, 0px, 100px, 50px)" should Fail e.style['width'] = "random(auto element-shared, 0px, 100px, 50px)" should set the property value Fail e.style['width'] = "random(element-shared --foo, 0px, 100px, 50px)" should set the property value Fail e.style['width'] = "random(element-shared auto, 0px, 100px, 50px)" should set the property value -Fail e.style['width'] = "random(fixed 0.5, 0px, 100px, 50px)" should set the property value +Pass e.style['width'] = "random(fixed 0.5, 0px, 100px, 50px)" should set the property value Fail e.style['width'] = "random(10px, 20%)" should set the property value Fail e.style['width'] = "random(100px, 0px)" should set the property value Fail e.style['width'] = "random(-100px, -10px)" should set the property value