From 6a0c0925e5784455268c81a4849dffe6d2886d12 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Wed, 6 May 2026 08:15:05 -0400 Subject: [PATCH] Issue #1826 - Serialize special calc() number values --- layout/style/nsCSSParser.cpp | 181 +++++++++++++++++- layout/style/nsCSSValue.cpp | 28 ++- layout/style/nsCSSValue.h | 2 - .../style/test/test_calc_numeric_types.html | 38 ++++ 4 files changed, 235 insertions(+), 14 deletions(-) diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index 7153d5cba3..9383b51ff6 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -262,6 +262,49 @@ RoundFloatToCSSInteger(float aValue) return int32_t(rounded); } +static bool +IsCalcSpecialNumberIdent(const nsAString& aIdent, float& aValue) +{ + if (aIdent.LowerCaseEqualsLiteral("nan")) { + aValue = std::numeric_limits::quiet_NaN(); + return true; + } + + if (aIdent.LowerCaseEqualsLiteral("infinity")) { + aValue = std::numeric_limits::infinity(); + return true; + } + + if (aIdent.LowerCaseEqualsLiteral("-infinity")) { + aValue = -std::numeric_limits::infinity(); + return true; + } + + return false; +} + +static bool +IsCalcNumberFunctionName(const nsAString& aIdent) +{ + return aIdent.LowerCaseEqualsLiteral("min") || + aIdent.LowerCaseEqualsLiteral("max") || + aIdent.LowerCaseEqualsLiteral("clamp"); +} + +static void +WrapCalcNumberValue(nsCSSValue& aValue, float aNumber) +{ + RefPtr arr = nsCSSValue::Array::Create(1); + arr->Item(0).SetFloatValue(aNumber, eCSSUnit_Number); + aValue.SetArrayValue(arr, eCSSUnit_Calc); +} + +static bool +IsFiniteCalcNumber(float aValue) +{ + return !std::isnan(aValue) && !std::isinf(aValue); +} + static bool GetCalcLengthTypedArithmeticExponent(const nsCSSValue& aValue, int32_t& aExponent) @@ -324,7 +367,8 @@ GetCalcLengthTypedArithmeticExponent(const nsCSSValue& aValue, static bool NormalizeCalcForVariant(nsCSSValue& aValue, uint32_t aPropertyVariantMask, - uint32_t aResultVariantMask) + uint32_t aResultVariantMask, + bool aSawSpecialNumericValues = false) { if (ShouldPreserveCalcValue(aPropertyVariantMask)) { return true; @@ -335,7 +379,13 @@ NormalizeCalcForVariant(nsCSSValue& aValue, float value = mozilla::css::ComputeCalc(aValue, ops); if (aPropertyVariantMask & VARIANT_INTEGER) { + if (!IsFiniteCalcNumber(value)) { + return false; + } aValue.SetIntValue(RoundFloatToCSSInteger(value), eCSSUnit_Integer); + } else if ((aPropertyVariantMask & VARIANT_OPACITY) && + aSawSpecialNumericValues) { + WrapCalcNumberValue(aValue, value); } else { aValue.SetFloatValue(value, eCSSUnit_Number); } @@ -347,7 +397,11 @@ NormalizeCalcForVariant(nsCSSValue& aValue, float value = mozilla::css::ComputeCalc(aValue, ops); if (aPropertyVariantMask & VARIANT_OPACITY) { - aValue.SetFloatValue(value, eCSSUnit_Number); + if (aSawSpecialNumericValues) { + WrapCalcNumberValue(aValue, value); + } else { + aValue.SetFloatValue(value, eCSSUnit_Number); + } } else { aValue.SetPercentValue(value); } @@ -1187,13 +1241,16 @@ protected: bool ParseBorderWidth(); bool ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, - uint32_t* aResultVariantMask = nullptr); + uint32_t* aResultVariantMask = nullptr, + bool* aSawSpecialNumericValues = nullptr); bool ParseCalcAdditiveExpression(nsCSSValue& aValue, uint32_t& aVariantMask); bool ParseCalcMultiplicativeExpression(nsCSSValue& aValue, uint32_t& aVariantMask, bool *aHadFinalWS); bool ParseCalcTerm(nsCSSValue& aValue, uint32_t& aVariantMask); + bool ParseCalcNumberExpressionValue(float& aValue); + bool ParseCalcNumberFunction(nsCSSValue& aValue, uint32_t& aVariantMask); bool RequireWhitespace(); // For "flex" shorthand property, defined in CSS Flexbox spec @@ -1787,6 +1844,7 @@ protected: // out of the parser. bool mSheetPrincipalRequired; bool mCalcAllowsTypedArithmetic; + bool mCalcHasSpecialNumericValues; // This enum helps us track whether we've unprefixed "display: -webkit-box" // (treating it as "display: flex") in an earlier declaration within a series @@ -1892,6 +1950,7 @@ CSSParserImpl::CSSParserImpl() mSuppressErrors(false), mSheetPrincipalRequired(true), mCalcAllowsTypedArithmetic(false), + mCalcHasSpecialNumericValues(false), mWebkitBoxUnprefixState(eNotParsingDecls), mNextFree(nullptr) { @@ -9190,8 +9249,11 @@ CSSParserImpl::ParseVariant(nsCSSValue& aValue, !ShouldPreserveCalcValue(calcVariantMask)) && IsCalcFunctionToken(*tk)) { uint32_t calcResultVariantMask = calcVariantMask; - if (!ParseCalc(aValue, calcVariantMask, &calcResultVariantMask) || - !NormalizeCalcForVariant(aValue, aVariantMask, calcResultVariantMask)) { + bool sawSpecialNumericValues = false; + if (!ParseCalc(aValue, calcVariantMask, &calcResultVariantMask, + &sawSpecialNumericValues) || + !NormalizeCalcForVariant(aValue, aVariantMask, calcResultVariantMask, + sawSpecialNumericValues)) { return CSSParseResult::Error; } return CSSParseResult::Ok; @@ -14893,7 +14955,8 @@ CSSParserImpl::ParseBorderColors(nsCSSPropertyID aProperty) // Parse the top level of a calc() expression. bool CSSParserImpl::ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, - uint32_t* aResultVariantMask) + uint32_t* aResultVariantMask, + bool* aSawSpecialNumericValues) { // Parsing calc expressions requires, in a number of cases, looking // for a token that is *either* a value of the property or a number. @@ -14902,7 +14965,9 @@ CSSParserImpl::ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, MOZ_ASSERT(aVariantMask != 0, "unexpected variant mask"); bool oldUnitlessLengthQuirk = mUnitlessLengthQuirk; + bool oldCalcHasSpecialNumericValues = mCalcHasSpecialNumericValues; mUnitlessLengthQuirk = false; + mCalcHasSpecialNumericValues = false; // One-iteration loop so we can break to the error-handling case. do { @@ -14939,11 +15004,21 @@ CSSParserImpl::ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, if (aResultVariantMask) { *aResultVariantMask = resultVariantMask; } + if (aSawSpecialNumericValues) { + *aSawSpecialNumericValues = mCalcHasSpecialNumericValues; + } + mCalcHasSpecialNumericValues = + oldCalcHasSpecialNumericValues || mCalcHasSpecialNumericValues; mUnitlessLengthQuirk = oldUnitlessLengthQuirk; return true; } while (false); SkipUntil(')'); + if (aSawSpecialNumericValues) { + *aSawSpecialNumericValues = mCalcHasSpecialNumericValues; + } + mCalcHasSpecialNumericValues = + oldCalcHasSpecialNumericValues || mCalcHasSpecialNumericValues; mUnitlessLengthQuirk = oldUnitlessLengthQuirk; return false; } @@ -15144,6 +15219,24 @@ CSSParserImpl::ParseCalcTerm(nsCSSValue& aValue, uint32_t& aVariantMask) } return true; } + if (mToken.mType == eCSSToken_Function && + IsCalcNumberFunctionName(mToken.mIdent)) { + if (!ParseCalcNumberFunction(aValue, aVariantMask)) { + SkipUntil(')'); + return false; + } + return true; + } + if ((aVariantMask & VARIANT_NUMBER) != 0 && + mToken.mType == eCSSToken_Ident) { + float specialValue; + if (IsCalcSpecialNumberIdent(mToken.mIdent, specialValue)) { + mCalcHasSpecialNumericValues = true; + aValue.SetFloatValue(specialValue, eCSSUnit_Number); + aVariantMask = VARIANT_NUMBER; + return true; + } + } // ... or just a value UngetToken(); // Always pass VARIANT_NUMBER to ParseVariant so that unitless zero @@ -15168,6 +15261,82 @@ CSSParserImpl::ParseCalcTerm(nsCSSValue& aValue, uint32_t& aVariantMask) return true; } +bool +CSSParserImpl::ParseCalcNumberExpressionValue(float& aValue) +{ + nsCSSValue expression; + uint32_t variantMask = VARIANT_NUMBER; + if (!ParseCalcAdditiveExpression(expression, variantMask) || + variantMask != VARIANT_NUMBER) { + return false; + } + + ReduceNumberCalcOps ops; + aValue = mozilla::css::ComputeCalc(expression, ops); + return true; +} + +bool +CSSParserImpl::ParseCalcNumberFunction(nsCSSValue& aValue, + uint32_t& aVariantMask) +{ + MOZ_ASSERT(mToken.mType == eCSSToken_Function, "expected function token"); + MOZ_ASSERT(IsCalcNumberFunctionName(mToken.mIdent), + "unexpected calc() number function"); + + float result; + + if (mToken.mIdent.LowerCaseEqualsLiteral("clamp")) { + float minValue; + float centerValue; + float maxValue; + if (!ParseCalcNumberExpressionValue(minValue) || + !ExpectSymbol(',', true) || + !ParseCalcNumberExpressionValue(centerValue) || + !ExpectSymbol(',', true) || + !ParseCalcNumberExpressionValue(maxValue) || + !ExpectSymbol(')', true)) { + return false; + } + + if (std::isnan(minValue) || std::isnan(centerValue) || + std::isnan(maxValue)) { + result = std::numeric_limits::quiet_NaN(); + } else { + result = std::max(minValue, std::min(centerValue, maxValue)); + } + } else { + const bool isMax = mToken.mIdent.LowerCaseEqualsLiteral("max"); + bool sawComma = false; + + if (!ParseCalcNumberExpressionValue(result)) { + return false; + } + + while (ExpectSymbol(',', true)) { + sawComma = true; + float candidate; + if (!ParseCalcNumberExpressionValue(candidate)) { + return false; + } + if (std::isnan(result) || std::isnan(candidate)) { + result = std::numeric_limits::quiet_NaN(); + } else { + result = isMax ? std::max(result, candidate) + : std::min(result, candidate); + } + } + + if (!sawComma || !ExpectSymbol(')', true)) { + return false; + } + } + + aValue.SetFloatValue(result, eCSSUnit_Number); + aVariantMask = VARIANT_NUMBER; + return true; +} + // This function consumes all consecutive whitespace and returns whether // there was any. bool diff --git a/layout/style/nsCSSValue.cpp b/layout/style/nsCSSValue.cpp index 226820abd7..4a3e9b1b4f 100644 --- a/layout/style/nsCSSValue.cpp +++ b/layout/style/nsCSSValue.cpp @@ -6,6 +6,7 @@ /* representation of simple property values within CSS declarations */ #include "mozilla/ArrayUtils.h" +#include "mozilla/FloatingPoint.h" #include "nsCSSValue.h" @@ -30,6 +31,25 @@ using namespace mozilla; using namespace mozilla::css; +static void +AppendSerializedCSSFloat(float aValue, nsAString& aResult) +{ + if (mozilla::IsNaN(aValue)) { + aResult.AppendLiteral("NaN"); + return; + } + + if (mozilla::IsInfinite(aValue)) { + if (aValue < 0.0f) { + aResult.Append('-'); + } + aResult.AppendLiteral("infinity"); + return; + } + + aResult.AppendFloat(aValue); +} + static bool IsLocalRefURL(nsStringBuffer* aString) { @@ -68,7 +88,6 @@ nsCSSValue::nsCSSValue(float aValue, nsCSSUnit aUnit) MOZ_ASSERT(eCSSUnit_Percent <= aUnit, "not a float value"); if (eCSSUnit_Percent <= aUnit) { mValue.mFloat = aValue; - MOZ_ASSERT(!mozilla::IsNaN(mValue.mFloat)); } else { mUnit = eCSSUnit_Null; @@ -147,7 +166,6 @@ nsCSSValue::nsCSSValue(const nsCSSValue& aCopy) } else if (eCSSUnit_Percent <= mUnit) { mValue.mFloat = aCopy.mValue.mFloat; - MOZ_ASSERT(!mozilla::IsNaN(mValue.mFloat)); } else if (UnitHasStringValue()) { mValue.mString = aCopy.mValue.mString; @@ -484,7 +502,6 @@ void nsCSSValue::SetPercentValue(float aValue) Reset(); mUnit = eCSSUnit_Percent; mValue.mFloat = aValue; - MOZ_ASSERT(!mozilla::IsNaN(mValue.mFloat)); } void nsCSSValue::SetFloatValue(float aValue, nsCSSUnit aUnit) @@ -494,7 +511,6 @@ void nsCSSValue::SetFloatValue(float aValue, nsCSSUnit aUnit) if (IsFloatUnit(aUnit)) { mUnit = aUnit; mValue.mFloat = aValue; - MOZ_ASSERT(!mozilla::IsNaN(mValue.mFloat)); } } @@ -1980,10 +1996,10 @@ nsCSSValue::AppendToString(nsCSSPropertyID aProperty, nsAString& aResult, aResult.Append(')'); } else if (eCSSUnit_Percent == unit) { - aResult.AppendFloat(GetPercentValue() * 100.0f); + AppendSerializedCSSFloat(GetPercentValue() * 100.0f, aResult); } else if (eCSSUnit_Percent < unit) { // length unit - aResult.AppendFloat(GetFloatValue()); + AppendSerializedCSSFloat(GetFloatValue(), aResult); } else if (eCSSUnit_Gradient == unit) { nsCSSValueGradient* gradient = GetGradientValue(); diff --git a/layout/style/nsCSSValue.h b/layout/style/nsCSSValue.h index e457db18ad..60f3417fb3 100644 --- a/layout/style/nsCSSValue.h +++ b/layout/style/nsCSSValue.h @@ -782,7 +782,6 @@ public: float GetFloatValue() const { MOZ_ASSERT(eCSSUnit_Number <= mUnit, "not a float value"); - MOZ_ASSERT(!mozilla::IsNaN(mValue.mFloat)); return mValue.mFloat; } @@ -2040,4 +2039,3 @@ private: } // namespace mozilla #endif /* nsCSSValue_h___ */ - diff --git a/layout/style/test/test_calc_numeric_types.html b/layout/style/test/test_calc_numeric_types.html index 267136dfe2..3d27cee31b 100644 --- a/layout/style/test/test_calc_numeric_types.html +++ b/layout/style/test/test_calc_numeric_types.html @@ -106,6 +106,44 @@ function parse2dMatrix(transformValue) { div.remove(); })(); + +(function testSpecialNumberCalcSpecifiedSerialization() { + const div = appendTestNode(); + const cases = [ + ["calc(NaN)", "calc(NaN)"], + ["calc(infinity)", "calc(infinity)"], + ["calc(-infinity)", "calc(-infinity)"], + ["calc(1 * NaN)", "calc(NaN)"], + ["calc(1 * infinity / infinity)", "calc(NaN)"], + ["calc(1 * 0 * infinity)", "calc(NaN)"], + ["calc(1 * (infinity + -infinity))", "calc(NaN)"], + ["calc(1 * (infinity - infinity))", "calc(NaN)"], + ["calc(1 * infinity)", "calc(infinity)"], + ["calc(1 * -infinity)", "calc(-infinity)"], + ["calc(1 * 1/infinity)", "calc(0)"], + ["calc(1 * infinity * infinity)", "calc(infinity)"], + ["calc(1 * max(INFinity*3, 0))", "calc(infinity)"], + ["calc(1 * min(inFInity*4, 0))", "calc(0)"], + ["calc(1 * max(nAn*2, 0))", "calc(NaN)"], + ["calc(1 * clamp(-INFINITY*20, 0, infiniTY*10))", "calc(0)"], + ["calc(1 * max(NaN, min(0,10)))", "calc(NaN)"], + ["calc(1 * clamp(NaN, 0, 10))", "calc(NaN)"], + ["calc(1 * max(0, min(10, NaN)))", "calc(NaN)"], + ["calc(1 * clamp(0, 10, NaN))", "calc(NaN)"], + ["calc(1 * max(0, min(NaN, 10)))", "calc(NaN)"], + ["calc(1 * clamp(0, NaN, 10))", "calc(NaN)"], + ["calc(1 * clamp(-Infinity, 0, infinity))", "calc(0)"], + ["calc(1 * clamp(-inFinity, infinity, 10))", "calc(10)"], + ]; + + for (const [input, expected] of cases) { + div.style.setProperty("opacity", input, ""); + is(div.style.getPropertyValue("opacity"), expected, + `opacity should serialize ${input} as ${expected}`); + } + + div.remove(); +})();