LibWeb/CSS: Reify math functions into CSSMathValue types

This commit is contained in:
Sam Atkins 2025-08-22 11:10:24 +01:00 committed by Andreas Kling
commit d29084997e
Notes: github-actions[bot] 2025-08-29 09:58:30 +00:00
5 changed files with 121 additions and 11 deletions

View file

@ -10,6 +10,16 @@
#include "CalculatedStyleValue.h"
#include <AK/QuickSort.h>
#include <AK/TypeCasts.h>
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/CSS/CSSMathClamp.h>
#include <LibWeb/CSS/CSSMathInvert.h>
#include <LibWeb/CSS/CSSMathMax.h>
#include <LibWeb/CSS/CSSMathMin.h>
#include <LibWeb/CSS/CSSMathNegate.h>
#include <LibWeb/CSS/CSSMathProduct.h>
#include <LibWeb/CSS/CSSMathSum.h>
#include <LibWeb/CSS/CSSNumericArray.h>
#include <LibWeb/CSS/CSSUnitValue.h>
#include <LibWeb/CSS/Percentage.h>
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
@ -190,6 +200,18 @@ static CalculationNode::NumericValue clamp_and_censor_numeric_value(NumericCalcu
});
}
static GC::Ptr<CSSNumericArray> reify_children(JS::Realm& realm, ReadonlySpan<NonnullRefPtr<CalculationNode const>> children)
{
GC::RootVector<GC::Ref<CSSNumericValue>> reified_children { realm.heap() };
for (auto const& child : children) {
auto reified_child = child->reify(realm);
if (!reified_child)
return nullptr;
reified_children.append(reified_child.as_nonnull());
}
return CSSNumericArray::create(realm, move(reified_children));
}
static String serialize_a_calculation_tree(CalculationNode const&, CalculationContext const&, SerializationMode);
// https://drafts.csswg.org/css-values-4/#serialize-a-math-function
@ -803,6 +825,14 @@ bool NumericCalculationNode::equals(CalculationNode const& other) const
return m_value == static_cast<NumericCalculationNode const&>(other).m_value;
}
GC::Ptr<CSSNumericValue> NumericCalculationNode::reify(JS::Realm& realm) const
{
return m_value.visit(
[&realm](Number const& number) { return CSSUnitValue::create(realm, number.value(), "number"_fly_string); },
[&realm](Percentage const& percentage) { return CSSUnitValue::create(realm, percentage.value(), "percent"_fly_string); },
[&realm](auto const& dimension) { return CSSUnitValue::create(realm, dimension.raw_value(), FlyString::from_utf8_without_validation(dimension.unit_name().bytes())); });
}
NonnullRefPtr<SumCalculationNode const> SumCalculationNode::create(Vector<NonnullRefPtr<CalculationNode const>> values)
{
// https://www.w3.org/TR/css-values-4/#determine-the-type-of-a-calculation
@ -874,6 +904,14 @@ bool SumCalculationNode::equals(CalculationNode const& other) const
return true;
}
GC::Ptr<CSSNumericValue> SumCalculationNode::reify(JS::Realm& realm) const
{
auto reified_children = reify_children(realm, m_values);
if (!reified_children)
return nullptr;
return CSSMathSum::create(realm, numeric_type().value(), reified_children.as_nonnull());
}
NonnullRefPtr<ProductCalculationNode const> ProductCalculationNode::create(Vector<NonnullRefPtr<CalculationNode const>> values)
{
// https://drafts.csswg.org/css-values-4/#determine-the-type-of-a-calculation
@ -944,6 +982,14 @@ bool ProductCalculationNode::equals(CalculationNode const& other) const
return true;
}
GC::Ptr<CSSNumericValue> ProductCalculationNode::reify(JS::Realm& realm) const
{
auto reified_children = reify_children(realm, m_values);
if (!reified_children)
return nullptr;
return CSSMathProduct::create(realm, numeric_type().value(), reified_children.as_nonnull());
}
NonnullRefPtr<NegateCalculationNode const> NegateCalculationNode::create(NonnullRefPtr<CalculationNode const> value)
{
return adopt_ref(*new (nothrow) NegateCalculationNode(move(value)));
@ -990,6 +1036,14 @@ bool NegateCalculationNode::equals(CalculationNode const& other) const
return m_value->equals(*static_cast<NegateCalculationNode const&>(other).m_value);
}
GC::Ptr<CSSNumericValue> NegateCalculationNode::reify(JS::Realm& realm) const
{
auto reified_value = m_value->reify(realm);
if (!reified_value)
return nullptr;
return CSSMathNegate::create(realm, numeric_type().value(), reified_value.as_nonnull());
}
NonnullRefPtr<InvertCalculationNode const> InvertCalculationNode::create(NonnullRefPtr<CalculationNode const> value)
{
// https://drafts.csswg.org/css-values-4/#determine-the-type-of-a-calculation
@ -1042,6 +1096,14 @@ bool InvertCalculationNode::equals(CalculationNode const& other) const
return m_value->equals(*static_cast<InvertCalculationNode const&>(other).m_value);
}
GC::Ptr<CSSNumericValue> InvertCalculationNode::reify(JS::Realm& realm) const
{
auto reified_value = m_value->reify(realm);
if (!reified_value)
return nullptr;
return CSSMathInvert::create(realm, numeric_type().value(), reified_value.as_nonnull());
}
NonnullRefPtr<MinCalculationNode const> MinCalculationNode::create(Vector<NonnullRefPtr<CalculationNode const>> values)
{
// https://drafts.csswg.org/css-values-4/#determine-the-type-of-a-calculation
@ -1166,6 +1228,14 @@ bool MinCalculationNode::equals(CalculationNode const& other) const
return true;
}
GC::Ptr<CSSNumericValue> MinCalculationNode::reify(JS::Realm& realm) const
{
auto reified_children = reify_children(realm, m_values);
if (!reified_children)
return nullptr;
return CSSMathMin::create(realm, numeric_type().value(), reified_children.as_nonnull());
}
NonnullRefPtr<MaxCalculationNode const> MaxCalculationNode::create(Vector<NonnullRefPtr<CalculationNode const>> values)
{
// https://drafts.csswg.org/css-values-4/#determine-the-type-of-a-calculation
@ -1243,6 +1313,14 @@ bool MaxCalculationNode::equals(CalculationNode const& other) const
return true;
}
GC::Ptr<CSSNumericValue> MaxCalculationNode::reify(JS::Realm& realm) const
{
auto reified_children = reify_children(realm, m_values);
if (!reified_children)
return nullptr;
return CSSMathMax::create(realm, numeric_type().value(), reified_children.as_nonnull());
}
NonnullRefPtr<ClampCalculationNode const> ClampCalculationNode::create(NonnullRefPtr<CalculationNode const> min, NonnullRefPtr<CalculationNode const> center, NonnullRefPtr<CalculationNode const> max)
{
// https://drafts.csswg.org/css-values-4/#determine-the-type-of-a-calculation
@ -1358,6 +1436,17 @@ bool ClampCalculationNode::equals(CalculationNode const& other) const
&& m_max_value->equals(*static_cast<ClampCalculationNode const&>(other).m_max_value);
}
GC::Ptr<CSSNumericValue> ClampCalculationNode::reify(JS::Realm& realm) const
{
auto lower = m_min_value->reify(realm);
auto value = m_center_value->reify(realm);
auto upper = m_max_value->reify(realm);
if (!lower || !value || !upper)
return nullptr;
return CSSMathClamp::create(realm, numeric_type().value(), lower.as_nonnull(), value.as_nonnull(), upper.as_nonnull());
}
NonnullRefPtr<AbsCalculationNode const> AbsCalculationNode::create(NonnullRefPtr<CalculationNode const> value)
{
return adopt_ref(*new (nothrow) AbsCalculationNode(move(value)));
@ -3037,6 +3126,18 @@ String CalculatedStyleValue::dump() const
return builder.to_string_without_validation();
}
// https://drafts.css-houdini.org/css-typed-om-1/#reify-a-math-expression
GC::Ref<CSSStyleValue> CalculatedStyleValue::reify(JS::Realm& realm, String const& associated_property) const
{
// NB: This spec algorithm isn't really implementable here - it's incomplete, and assumes we don't already have a
// calculation tree. So we have a per-node method instead.
if (auto reified = m_calculation->reify(realm))
return *reified;
// Some math functions are not reifiable yet. If we contain one, we have to fall back to CSSStyleValue.
// https://github.com/w3c/css-houdini-drafts/issues/1090
return StyleValue::reify(realm, associated_property);
}
struct NumericChildAndIndex {
NonnullRefPtr<NumericCalculationNode const> child;
size_t index;

View file

@ -117,6 +117,8 @@ public:
String dump() const;
virtual GC::Ref<CSSStyleValue> reify(JS::Realm&, String const& associated_property) const override;
private:
explicit CalculatedStyleValue(NonnullRefPtr<CalculationNode const> calculation, NumericType resolved_type, CalculationContext context)
: StyleValue(Type::Calculated)
@ -249,6 +251,7 @@ public:
virtual void dump(StringBuilder&, int indent) const = 0;
virtual bool equals(CalculationNode const&) const = 0;
virtual GC::Ptr<CSSNumericValue> reify(JS::Realm&) const { return nullptr; }
protected:
CalculationNode(Type, Optional<NumericType>);
@ -287,6 +290,7 @@ public:
virtual void dump(StringBuilder&, int indent) const override;
virtual bool equals(CalculationNode const&) const override;
virtual GC::Ptr<CSSNumericValue> reify(JS::Realm&) const override;
private:
NumericCalculationNode(NumericValue, NumericType);
@ -306,6 +310,7 @@ public:
virtual void dump(StringBuilder&, int indent) const override;
virtual bool equals(CalculationNode const&) const override;
virtual GC::Ptr<CSSNumericValue> reify(JS::Realm&) const override;
private:
SumCalculationNode(Vector<NonnullRefPtr<CalculationNode const>>, Optional<NumericType>);
@ -325,6 +330,7 @@ public:
virtual void dump(StringBuilder&, int indent) const override;
virtual bool equals(CalculationNode const&) const override;
virtual GC::Ptr<CSSNumericValue> reify(JS::Realm&) const override;
private:
ProductCalculationNode(Vector<NonnullRefPtr<CalculationNode const>>, Optional<NumericType>);
@ -345,6 +351,7 @@ public:
virtual void dump(StringBuilder&, int indent) const override;
virtual bool equals(CalculationNode const&) const override;
virtual GC::Ptr<CSSNumericValue> reify(JS::Realm&) const override;
private:
explicit NegateCalculationNode(NonnullRefPtr<CalculationNode const>);
@ -365,6 +372,7 @@ public:
virtual void dump(StringBuilder&, int indent) const override;
virtual bool equals(CalculationNode const&) const override;
virtual GC::Ptr<CSSNumericValue> reify(JS::Realm&) const override;
private:
InvertCalculationNode(NonnullRefPtr<CalculationNode const>, Optional<NumericType>);
@ -385,6 +393,7 @@ public:
virtual void dump(StringBuilder&, int indent) const override;
virtual bool equals(CalculationNode const&) const override;
virtual GC::Ptr<CSSNumericValue> reify(JS::Realm&) const override;
private:
MinCalculationNode(Vector<NonnullRefPtr<CalculationNode const>>, Optional<NumericType>);
@ -405,6 +414,7 @@ public:
virtual void dump(StringBuilder&, int indent) const override;
virtual bool equals(CalculationNode const&) const override;
virtual GC::Ptr<CSSNumericValue> reify(JS::Realm&) const override;
private:
MaxCalculationNode(Vector<NonnullRefPtr<CalculationNode const>>, Optional<NumericType>);
@ -425,6 +435,7 @@ public:
virtual void dump(StringBuilder&, int indent) const override;
virtual bool equals(CalculationNode const&) const override;
virtual GC::Ptr<CSSNumericValue> reify(JS::Realm&) const override;
private:
ClampCalculationNode(NonnullRefPtr<CalculationNode const>, NonnullRefPtr<CalculationNode const>, NonnullRefPtr<CalculationNode const>, Optional<NumericType>);

View file

@ -2,6 +2,6 @@ Harness status: OK
Found 2 tests
2 Fail
Fail Parsing calc(1% + 2em + 3px)
Fail Parsing calc(1px + 2% + 3em)
2 Pass
Pass Parsing calc(1% + 2em + 3px)
Pass Parsing calc(1px + 2% + 3em)

View file

@ -2,11 +2,10 @@ Harness status: OK
Found 6 tests
5 Pass
1 Fail
6 Pass
Pass Normalizing a <number> returns a number CSSUnitValue
Pass Normalizing a <percentage> returns a percent CSSUnitValue
Pass Normalizing a <dimension> returns a CSSUnitValue with the correct unit
Pass Normalizing a <number> with a unitless zero returns 0
Fail Normalizing a <calc> returns simplified expression
Pass Normalizing a <calc> returns simplified expression
Pass Normalizing a <dimension> with a unitless zero returns 0px

View file

@ -2,16 +2,15 @@ Harness status: OK
Found 11 tests
8 Pass
3 Fail
11 Pass
Pass Parsing an invalid string throws SyntaxError
Pass Parsing a string with a non numeric token throws SyntaxError
Pass Parsing a string with left over numeric tokens throws SyntaxError
Pass Parsing a calc with incompatible units throws a SyntaxError
Pass Parsing a <dimension-token> with invalid units throws a SyntaxError
Pass Parsing ignores surrounding spaces
Fail Parsing min() is successful
Fail Parsing max() is successful
Fail Parsing clamp() is successful
Pass Parsing min() is successful
Pass Parsing max() is successful
Pass Parsing clamp() is successful
Pass Parsing sum of multiple min() is successful
Pass Parsing product of multiple min() is successful