Issue #1826 - Canonicalize nested calc() serialization

This commit is contained in:
Basilisk-Dev 2026-05-05 21:18:31 -04:00 • committed by EAZYBLACK
commit 8af3f489da
2 changed files with 227 additions and 2 deletions

View file

@ -1019,6 +1019,209 @@ private:
nsCSSValue::Serialization mValueSerialization;
};
struct CSSValueReduceNumberCalcOps : public BasicFloatCalcOps,
public CSSValueInputCalcOps
{
result_type ComputeLeafValue(const nsCSSValue& aValue)
{
MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_Number, "unexpected unit");
return aValue.GetFloatValue();
}
float ComputeNumber(const nsCSSValue& aValue)
{
return css::ComputeCalc(aValue, *this);
}
};
struct CSSValueCalcLengthTerm {
nsCSSUnit mUnit;
float mValue;
};
struct CSSValueLengthPercentCalcAccumulator {
AutoTArray<CSSValueCalcLengthTerm, 8> mLengths;
float mPercent = 0.0f;
bool mSawPercent = false;
void AddLength(nsCSSUnit aUnit, float aValue)
{
for (CSSValueCalcLengthTerm& term : mLengths) {
if (term.mUnit == aUnit) {
term.mValue += aValue;
return;
}
}
CSSValueCalcLengthTerm& term = *mLengths.AppendElement();
term.mUnit = aUnit;
term.mValue = aValue;
}
};
static bool
IsNearlyZero(float aValue)
{
return aValue > -0.000001f && aValue < 0.000001f;
}
static bool
ComputeCalcNumberValue(const nsCSSValue& aValue, float& aResult)
{
if (aValue.GetUnit() == eCSSUnit_Number) {
aResult = aValue.GetFloatValue();
return true;
}
if (!aValue.IsCalcUnit()) {
return false;
}
CSSValueReduceNumberCalcOps ops;
aResult = css::ComputeCalc(aValue, ops);
return true;
}
static bool
AccumulateCalcLengthPercentTerms(
const nsCSSValue& aValue,
float aScale,
CSSValueLengthPercentCalcAccumulator& aAccumulator)
{
switch (aValue.GetUnit()) {
case eCSSUnit_Calc: {
const nsCSSValue::Array* array = aValue.GetArrayValue();
MOZ_ASSERT(array->Count() == 1, "unexpected length");
return AccumulateCalcLengthPercentTerms(array->Item(0), aScale,
aAccumulator);
}
case eCSSUnit_Calc_Plus:
case eCSSUnit_Calc_Minus: {
const nsCSSValue::Array* array = aValue.GetArrayValue();
MOZ_ASSERT(array->Count() == 2, "unexpected length");
if (!AccumulateCalcLengthPercentTerms(array->Item(0), aScale,
aAccumulator)) {
return false;
}
float rhsScale = aValue.GetUnit() == eCSSUnit_Calc_Plus
? aScale
: -aScale;
return AccumulateCalcLengthPercentTerms(array->Item(1), rhsScale,
aAccumulator);
}
case eCSSUnit_Calc_Times_L:
case eCSSUnit_Calc_Times_R:
case eCSSUnit_Calc_Divided: {
const nsCSSValue::Array* array = aValue.GetArrayValue();
MOZ_ASSERT(array->Count() == 2, "unexpected length");
const nsCSSValue* value = &array->Item(0);
const nsCSSValue* factorValue = &array->Item(1);
if (aValue.GetUnit() == eCSSUnit_Calc_Times_L) {
value = &array->Item(1);
factorValue = &array->Item(0);
}
float factor;
if (!ComputeCalcNumberValue(*factorValue, factor)) {
return false;
}
if (aValue.GetUnit() == eCSSUnit_Calc_Divided) {
factor = 1.0f / factor;
}
return AccumulateCalcLengthPercentTerms(*value, aScale * factor,
aAccumulator);
}
case eCSSUnit_Percent:
aAccumulator.mSawPercent = true;
aAccumulator.mPercent += aScale * aValue.GetPercentValue();
return true;
default:
break;
}
if (aValue.IsLengthUnit()) {
aAccumulator.AddLength(aValue.GetUnit(), aScale * aValue.GetFloatValue());
return true;
}
return false;
}
static void
AppendSerializedCalcLeafValue(nsCSSPropertyID aProperty,
nsAString& aResult,
nsCSSValue::Serialization aSerialization,
nsCSSUnit aUnit,
float aValue)
{
nsCSSValue value;
if (aUnit == eCSSUnit_Percent) {
value.SetPercentValue(aValue);
} else {
value.SetFloatValue(aValue, aUnit);
}
value.AppendToString(aProperty, aResult, aSerialization);
}
static bool
AppendNormalizedLengthPercentCalcToString(
const nsCSSValue& aValue,
nsCSSPropertyID aProperty,
nsAString& aResult,
nsCSSValue::Serialization aSerialization)
{
CSSValueLengthPercentCalcAccumulator accumulator;
if (!AccumulateCalcLengthPercentTerms(aValue, 1.0f, accumulator)) {
return false;
}
AutoTArray<CSSValueCalcLengthTerm, 9> serializedTerms;
if (!IsNearlyZero(accumulator.mPercent) || accumulator.mSawPercent) {
CSSValueCalcLengthTerm& term = *serializedTerms.AppendElement();
term.mUnit = eCSSUnit_Percent;
term.mValue = accumulator.mPercent;
}
for (const CSSValueCalcLengthTerm& term : accumulator.mLengths) {
if (IsNearlyZero(term.mValue)) {
continue;
}
serializedTerms.AppendElement(term);
}
if (serializedTerms.IsEmpty()) {
CSSValueCalcLengthTerm& term = *serializedTerms.AppendElement();
term.mUnit = accumulator.mSawPercent ? eCSSUnit_Percent : eCSSUnit_Pixel;
term.mValue = 0.0f;
}
aResult.AppendLiteral("calc(");
AppendSerializedCalcLeafValue(aProperty, aResult, aSerialization,
serializedTerms[0].mUnit,
serializedTerms[0].mValue);
for (uint32_t i = 1; i < serializedTerms.Length(); ++i) {
const CSSValueCalcLengthTerm& term = serializedTerms[i];
if (term.mValue < 0.0f) {
aResult.AppendLiteral(" - ");
AppendSerializedCalcLeafValue(aProperty, aResult, aSerialization,
term.mUnit, -term.mValue);
} else {
aResult.AppendLiteral(" + ");
AppendSerializedCalcLeafValue(aProperty, aResult, aSerialization,
term.mUnit, term.mValue);
}
}
aResult.Append(')');
return true;
}
} // namespace
void
@ -1475,8 +1678,11 @@ nsCSSValue::AppendToString(nsCSSPropertyID aProperty, nsAString& aResult,
}
else if (IsCalcUnit()) {
MOZ_ASSERT(GetUnit() == eCSSUnit_Calc, "unexpected unit");
CSSValueSerializeCalcOps ops(aProperty, aResult, aSerialization);
css::SerializeCalc(*this, ops);
if (!AppendNormalizedLengthPercentCalcToString(*this, aProperty, aResult,
aSerialization)) {
CSSValueSerializeCalcOps ops(aProperty, aResult, aSerialization);
css::SerializeCalc(*this, ops);
}
}
else if (eCSSUnit_Integer == unit) {
aResult.AppendInt(GetIntValue(), 10);

View file

@ -87,6 +87,25 @@ function parse2dMatrix(transformValue) {
div.remove();
})();
(function testNestedCalcSpecifiedSerialization() {
const div = appendTestNode();
const cases = [
["left", "calc(calc(20px) + calc(80px))", "calc(100px)"],
["left", "calc(calc(2) * calc(50px))", "calc(100px)"],
["left", "calc(calc(150px * 2 / 3))", "calc(100px)"],
["left", "calc(50px + calc(40%))", "calc(40% + 50px)"],
["border", "calc(calc(10px)) solid pink", "calc(10px) solid pink"],
];
for (const [property, input, expected] of cases) {
div.style.setProperty(property, input, "");
is(div.style.getPropertyValue(property), expected,
`${property} should canonicalize nested calc() serialization`);
}
div.remove();
})();
</script>
</pre>
</body>