mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-10-05 15:49:15 +00:00
LibWeb: Clamp and censor top-level calc results
We now clamp the values returned from calc into the allowed range (where we know it) and censor any `NaN`s to `0` both when we resolve and when we serialize. Gains us 76 WPT passes.
This commit is contained in:
parent
bc2ca96f50
commit
778da0175e
Notes:
github-actions[bot]
2025-08-11 16:11:10 +00:00
Author: https://github.com/Calme1709
Commit: 778da0175e
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/5734
Reviewed-by: https://github.com/AtkinsSJ ✅
Reviewed-by: https://github.com/Hendiadyoin1
Reviewed-by: https://github.com/gmta
Reviewed-by: https://github.com/tcl3
15 changed files with 361 additions and 74 deletions
|
@ -130,6 +130,66 @@ static NonnullRefPtr<CalculationNode const> simplify_2_children(T const& origina
|
|||
return original;
|
||||
}
|
||||
|
||||
static CalculationNode::NumericValue clamp_and_censor_numeric_value(NumericCalculationNode const& node, CalculationContext const& context)
|
||||
{
|
||||
auto value = node.value();
|
||||
|
||||
Optional<AcceptedTypeRange> accepted_range = value.visit(
|
||||
[&](Number const&) { return context.resolve_numbers_as_integers ? context.accepted_type_ranges.get(ValueType::Integer) : context.accepted_type_ranges.get(ValueType::Number); },
|
||||
[&](Angle const&) { return context.accepted_type_ranges.get(ValueType::Angle); },
|
||||
[&](Flex const&) { return context.accepted_type_ranges.get(ValueType::Flex); },
|
||||
[&](Frequency const&) { return context.accepted_type_ranges.get(ValueType::Frequency); },
|
||||
[&](Length const&) { return context.accepted_type_ranges.get(ValueType::Length); },
|
||||
[&](Percentage const&) { return context.accepted_type_ranges.get(ValueType::Percentage); },
|
||||
[&](Resolution const&) { return context.accepted_type_ranges.get(ValueType::Resolution); },
|
||||
[&](Time const&) { return context.accepted_type_ranges.get(ValueType::Time); });
|
||||
|
||||
if (!accepted_range.has_value()) {
|
||||
dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Calculation context missing accepted type range {}", node.numeric_type());
|
||||
// FIXME: Min and max values for Integer should be based on i32 rather than float
|
||||
accepted_range = { AK::NumericLimits<float>::lowest(), AK::NumericLimits<float>::max() };
|
||||
}
|
||||
|
||||
auto clamp_and_censor = [&](double value, double min, double max) {
|
||||
// https://drafts.csswg.org/css-values/#calc-ieee
|
||||
// NaN does not escape a top-level calculation; it’s censored into a zero value.
|
||||
if (isnan(value))
|
||||
value = 0;
|
||||
|
||||
// https://drafts.csswg.org/css-values/#calc-range
|
||||
// the value resulting from a top-level calculation must be clamped to the range allowed in the target context.
|
||||
// Clamping is performed on computed values to the extent possible, and also on used values if computation was
|
||||
// unable to sufficiently simplify the expression to allow range-checking.
|
||||
return clamp(value, min, max);
|
||||
};
|
||||
|
||||
return value.visit(
|
||||
[&](Number const& value) -> CalculationNode::NumericValue {
|
||||
return Number { value.type(), clamp_and_censor(context.resolve_numbers_as_integers ? value.integer_value() : value.value(), accepted_range->min, accepted_range->max) };
|
||||
},
|
||||
[&](Angle const& value) -> CalculationNode::NumericValue {
|
||||
return Angle { clamp_and_censor(value.raw_value(), accepted_range->min, accepted_range->max), value.type() };
|
||||
},
|
||||
[&](Flex const& value) -> CalculationNode::NumericValue {
|
||||
return Flex { clamp_and_censor(value.raw_value(), accepted_range->min, accepted_range->max), value.type() };
|
||||
},
|
||||
[&](Frequency const& value) -> CalculationNode::NumericValue {
|
||||
return Frequency { clamp_and_censor(value.raw_value(), accepted_range->min, accepted_range->max), value.type() };
|
||||
},
|
||||
[&](Length const& value) -> CalculationNode::NumericValue {
|
||||
return Length { clamp_and_censor(value.raw_value(), accepted_range->min, accepted_range->max), value.type() };
|
||||
},
|
||||
[&](Percentage const& value) -> CalculationNode::NumericValue {
|
||||
return Percentage { clamp_and_censor(value.value(), accepted_range->min, accepted_range->max) };
|
||||
},
|
||||
[&](Resolution const& value) -> CalculationNode::NumericValue {
|
||||
return Resolution { clamp_and_censor(value.raw_value(), accepted_range->min, accepted_range->max), value.type() };
|
||||
},
|
||||
[&](Time const& value) -> CalculationNode::NumericValue {
|
||||
return Time { clamp_and_censor(value.raw_value(), accepted_range->min, accepted_range->max), value.type() };
|
||||
});
|
||||
}
|
||||
|
||||
static String serialize_a_calculation_tree(CalculationNode const&, CalculationContext const&, SerializationMode);
|
||||
|
||||
// https://drafts.csswg.org/css-values-4/#serialize-a-math-function
|
||||
|
@ -141,8 +201,9 @@ static String serialize_a_math_function(CalculationNode const& fn, CalculationCo
|
|||
// the serialization being produced is of a computed value or later, then clamp the value to the range allowed
|
||||
// for its context (if necessary), then serialize the value as normal and return the result.
|
||||
if (fn.type() == CalculationNode::Type::Numeric && serialization_mode == SerializationMode::ResolvedValue) {
|
||||
// FIXME: Clamp the value. Note that we might have an infinite/nan value here.
|
||||
return static_cast<NumericCalculationNode const&>(fn).value_to_string();
|
||||
auto clamped_value = clamp_and_censor_numeric_value(static_cast<NumericCalculationNode const&>(fn), context);
|
||||
|
||||
return clamped_value.visit([&](auto const& value) { return value.to_string(serialization_mode); });
|
||||
}
|
||||
|
||||
// 2. If fn represents an infinite or NaN value:
|
||||
|
@ -2757,12 +2818,40 @@ Optional<CalculatedStyleValue::ResolvedValue> CalculatedStyleValue::resolve_valu
|
|||
auto raw_value = value->value();
|
||||
|
||||
// https://drafts.csswg.org/css-values/#calc-ieee
|
||||
// FIXME: NaN does not escape a top-level calculation; it’s censored into a zero value.
|
||||
// NaN does not escape a top-level calculation; it’s censored into a zero value.
|
||||
if (isnan(raw_value))
|
||||
raw_value = 0;
|
||||
|
||||
// https://drafts.csswg.org/css-values/#calc-range
|
||||
// FIXME: the value resulting from a top-level calculation must be clamped to the range allowed in the target
|
||||
// context. Clamping is performed on computed values to the extent possible, and also on used values if
|
||||
// computation was unable to sufficiently simplify the expression to allow range-checking.
|
||||
// the value resulting from a top-level calculation must be clamped to the range allowed in the target context.
|
||||
// Clamping is performed on computed values to the extent possible, and also on used values if computation was
|
||||
// unable to sufficiently simplify the expression to allow range-checking.
|
||||
Optional<AcceptedTypeRange> accepted_range;
|
||||
|
||||
if (value->type()->matches_number(m_context.percentages_resolve_as))
|
||||
accepted_range = m_context.resolve_numbers_as_integers ? m_context.accepted_type_ranges.get(ValueType::Integer) : m_context.accepted_type_ranges.get(ValueType::Number);
|
||||
else if (value->type()->matches_angle_percentage(m_context.percentages_resolve_as))
|
||||
accepted_range = m_context.accepted_type_ranges.get(ValueType::Angle);
|
||||
else if (value->type()->matches_flex(m_context.percentages_resolve_as))
|
||||
accepted_range = m_context.accepted_type_ranges.get(ValueType::Flex);
|
||||
else if (value->type()->matches_frequency_percentage(m_context.percentages_resolve_as))
|
||||
accepted_range = m_context.accepted_type_ranges.get(ValueType::Frequency);
|
||||
else if (value->type()->matches_length_percentage(m_context.percentages_resolve_as))
|
||||
accepted_range = m_context.accepted_type_ranges.get(ValueType::Length);
|
||||
else if (value->type()->matches_percentage())
|
||||
accepted_range = m_context.accepted_type_ranges.get(ValueType::Percentage);
|
||||
else if (value->type()->matches_resolution(m_context.percentages_resolve_as))
|
||||
accepted_range = m_context.accepted_type_ranges.get(ValueType::Resolution);
|
||||
else if (value->type()->matches_time_percentage(m_context.percentages_resolve_as))
|
||||
accepted_range = m_context.accepted_type_ranges.get(ValueType::Time);
|
||||
|
||||
if (!accepted_range.has_value()) {
|
||||
dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Calculation context missing accepted type range {}", value->type());
|
||||
// FIXME: Infinity for integers should be i32 max rather than float max
|
||||
accepted_range = { AK::NumericLimits<float>::lowest(), AK::NumericLimits<float>::max() };
|
||||
}
|
||||
|
||||
raw_value = clamp(raw_value, accepted_range->min, accepted_range->max);
|
||||
|
||||
return ResolvedValue { raw_value, value->type() };
|
||||
}
|
||||
|
@ -2912,15 +3001,8 @@ Optional<double> CalculatedStyleValue::resolve_number(CalculationResolutionConte
|
|||
{
|
||||
auto result = resolve_value(context);
|
||||
|
||||
if (result.has_value() && result->type.has_value() && result->type->matches_number(m_context.percentages_resolve_as)) {
|
||||
auto value = result->value;
|
||||
|
||||
// FIXME: This can be removed once it is upstreamed to `resolve_value`
|
||||
if (isnan(value))
|
||||
return 0.;
|
||||
|
||||
if (result.has_value() && result->type.has_value() && result->type->matches_number(m_context.percentages_resolve_as))
|
||||
return result->value;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue