Issue #1826 - Implement broader CSS calc() parsing

This commit is contained in:
Basilisk-Dev 2026-05-05 21:00:15 -04:00 • committed by EAZYBLACK
commit 41cd9dbe88
4 changed files with 284 additions and 16 deletions

View file

@ -170,6 +170,153 @@ struct ReducePercentageCalcOps : ReduceNumberCalcOps
} }
}; };
struct ReduceDimensionCalcOps : public mozilla::css::BasicFloatCalcOps,
public mozilla::css::CSSValueInputCalcOps,
public mozilla::css::NumbersAlreadyNormalizedOps
{
enum class DimensionType {
Angle,
Time,
Frequency,
};
explicit ReduceDimensionCalcOps(DimensionType aDimensionType)
: mDimensionType(aDimensionType)
{
}
result_type ComputeLeafValue(const nsCSSValue& aValue)
{
switch (mDimensionType) {
case DimensionType::Angle:
MOZ_ASSERT(aValue.IsAngularUnit(), "unexpected unit");
return aValue.GetAngleValueInDegrees();
case DimensionType::Time:
MOZ_ASSERT(aValue.IsTimeUnit(), "unexpected unit");
return aValue.GetUnit() == eCSSUnit_Seconds
? aValue.GetFloatValue()
: aValue.GetFloatValue() / float(PR_MSEC_PER_SEC);
case DimensionType::Frequency:
MOZ_ASSERT(aValue.IsFrequencyUnit(), "unexpected unit");
return aValue.GetUnit() == eCSSUnit_Kilohertz
? aValue.GetFloatValue() * 1000.0f
: aValue.GetFloatValue();
}
MOZ_ASSERT_UNREACHABLE("unexpected dimension type");
return 0.0f;
}
DimensionType mDimensionType;
};
static uint32_t
GetCalcParseVariantMask(uint32_t aVariantMask)
{
uint32_t calcVariantMask = 0;
if (aVariantMask & VARIANT_LENGTH) {
calcVariantMask |= VARIANT_LENGTH | (aVariantMask & VARIANT_ABSOLUTE_DIMENSION);
}
if (aVariantMask & VARIANT_PERCENT) {
calcVariantMask |= VARIANT_PERCENT;
}
if (aVariantMask & VARIANT_ANGLE) {
calcVariantMask |= VARIANT_ANGLE;
}
if (aVariantMask & VARIANT_TIME) {
calcVariantMask |= VARIANT_TIME;
}
if (aVariantMask & VARIANT_FREQUENCY) {
calcVariantMask |= VARIANT_FREQUENCY;
}
if (aVariantMask & (VARIANT_NUMBER | VARIANT_INTEGER)) {
calcVariantMask |= VARIANT_NUMBER;
}
if (aVariantMask & VARIANT_OPACITY) {
calcVariantMask |= VARIANT_PN;
}
return calcVariantMask;
}
static bool
ShouldPreserveCalcValue(uint32_t aVariantMask)
{
return (aVariantMask & VARIANT_LENGTH) != 0 ||
((aVariantMask & VARIANT_PERCENT) != 0 &&
(aVariantMask & (VARIANT_NUMBER | VARIANT_INTEGER |
VARIANT_OPACITY)) == 0);
}
static int32_t
RoundFloatToCSSInteger(float aValue)
{
double rounded = std::floor(double(aValue) + 0.5);
rounded = mozilla::clamped(
rounded,
double(std::numeric_limits<int32_t>::min()),
double(std::numeric_limits<int32_t>::max()));
return int32_t(rounded);
}
static bool
NormalizeCalcForVariant(nsCSSValue& aValue,
uint32_t aPropertyVariantMask,
uint32_t aResultVariantMask)
{
if (ShouldPreserveCalcValue(aPropertyVariantMask)) {
return true;
}
if (aResultVariantMask == VARIANT_NUMBER) {
ReduceNumberCalcOps ops;
float value = mozilla::css::ComputeCalc(aValue, ops);
if (aPropertyVariantMask & VARIANT_INTEGER) {
aValue.SetIntValue(RoundFloatToCSSInteger(value), eCSSUnit_Integer);
} else {
aValue.SetFloatValue(value, eCSSUnit_Number);
}
return true;
}
if (aResultVariantMask & VARIANT_PERCENT) {
ReducePercentageCalcOps ops;
float value = mozilla::css::ComputeCalc(aValue, ops);
if (aPropertyVariantMask & VARIANT_OPACITY) {
aValue.SetFloatValue(value, eCSSUnit_Number);
} else {
aValue.SetPercentValue(value);
}
return true;
}
if (aResultVariantMask & VARIANT_ANGLE) {
ReduceDimensionCalcOps ops(ReduceDimensionCalcOps::DimensionType::Angle);
aValue.SetFloatValue(mozilla::css::ComputeCalc(aValue, ops), eCSSUnit_Degree);
return true;
}
if (aResultVariantMask & VARIANT_TIME) {
ReduceDimensionCalcOps ops(ReduceDimensionCalcOps::DimensionType::Time);
aValue.SetFloatValue(mozilla::css::ComputeCalc(aValue, ops), eCSSUnit_Seconds);
return true;
}
if (aResultVariantMask & VARIANT_FREQUENCY) {
ReduceDimensionCalcOps ops(ReduceDimensionCalcOps::DimensionType::Frequency);
aValue.SetFloatValue(mozilla::css::ComputeCalc(aValue, ops), eCSSUnit_Hertz);
return true;
}
MOZ_ASSERT_UNREACHABLE("unsupported calc result type");
return false;
}
static_assert(css::eAuthorSheetFeatures == 0 && static_assert(css::eAuthorSheetFeatures == 0 &&
css::eUserSheetFeatures == 1 && css::eUserSheetFeatures == 1 &&
css::eAgentSheetFeatures == 2, css::eAgentSheetFeatures == 2,
@ -980,7 +1127,8 @@ protected:
bool ParseBorderStyle(); bool ParseBorderStyle();
bool ParseBorderWidth(); bool ParseBorderWidth();
bool ParseCalc(nsCSSValue &aValue, uint32_t aVariantMask); bool ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask,
uint32_t* aResultVariantMask = nullptr);
bool ParseCalcAdditiveExpression(nsCSSValue& aValue, bool ParseCalcAdditiveExpression(nsCSSValue& aValue,
uint32_t& aVariantMask); uint32_t& aVariantMask);
bool ParseCalcMultiplicativeExpression(nsCSSValue& aValue, bool ParseCalcMultiplicativeExpression(nsCSSValue& aValue,
@ -8598,14 +8746,18 @@ CSSParserImpl::ParseNonNegativeVariant(nsCSSValue& aValue,
VARIANT_NUMBER | VARIANT_NUMBER |
VARIANT_LENGTH | VARIANT_LENGTH |
VARIANT_PERCENT | VARIANT_PERCENT |
VARIANT_FREQUENCY |
VARIANT_OPACITY | VARIANT_OPACITY |
VARIANT_TIME |
VARIANT_INTEGER)) == 0, VARIANT_INTEGER)) == 0,
"need to update code below to handle additional variants"); "need to update code below to handle additional variants");
CSSParseResult result = ParseVariant(aValue, aVariantMask, aKeywordTable); CSSParseResult result = ParseVariant(aValue, aVariantMask, aKeywordTable);
if (result == CSSParseResult::Ok) { if (result == CSSParseResult::Ok) {
if (eCSSUnit_Number == aValue.GetUnit() || if (eCSSUnit_Number == aValue.GetUnit() ||
aValue.IsLengthUnit()){ aValue.IsLengthUnit() ||
aValue.IsFrequencyUnit() ||
aValue.IsTimeUnit()) {
if (aValue.GetFloatValue() < 0) { if (aValue.GetFloatValue() < 0) {
UngetToken(); UngetToken();
return CSSParseResult::NotFound; return CSSParseResult::NotFound;
@ -8967,12 +9119,14 @@ CSSParserImpl::ParseVariant(nsCSSValue& aValue,
return CSSParseResult::Ok; return CSSParseResult::Ok;
} }
} }
if ((aVariantMask & VARIANT_CALC) && uint32_t calcVariantMask = GetCalcParseVariantMask(aVariantMask);
if (calcVariantMask &&
(((aVariantMask & VARIANT_CALC) != 0) ||
!ShouldPreserveCalcValue(calcVariantMask)) &&
IsCalcFunctionToken(*tk)) { IsCalcFunctionToken(*tk)) {
// calc() currently allows only lengths and percents and number inside it. uint32_t calcResultVariantMask = calcVariantMask;
// And note that in current implementation, number cannot be mixed with if (!ParseCalc(aValue, calcVariantMask, &calcResultVariantMask) ||
// length and percent. !NormalizeCalcForVariant(aValue, aVariantMask, calcResultVariantMask)) {
if (!ParseCalc(aValue, aVariantMask & VARIANT_LPN)) {
return CSSParseResult::Error; return CSSParseResult::Error;
} }
return CSSParseResult::Ok; return CSSParseResult::Ok;
@ -14673,7 +14827,8 @@ CSSParserImpl::ParseBorderColors(nsCSSPropertyID aProperty)
// Parse the top level of a calc() expression. // Parse the top level of a calc() expression.
bool bool
CSSParserImpl::ParseCalc(nsCSSValue &aValue, uint32_t aVariantMask) CSSParserImpl::ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask,
uint32_t* aResultVariantMask)
{ {
// Parsing calc expressions requires, in a number of cases, looking // Parsing calc expressions requires, in a number of cases, looking
// for a token that is *either* a value of the property or a number. // for a token that is *either* a value of the property or a number.
@ -14688,14 +14843,18 @@ CSSParserImpl::ParseCalc(nsCSSValue &aValue, uint32_t aVariantMask)
do { do {
// The toplevel of a calc() is always an nsCSSValue::Array of length 1. // The toplevel of a calc() is always an nsCSSValue::Array of length 1.
RefPtr<nsCSSValue::Array> arr = nsCSSValue::Array::Create(1); RefPtr<nsCSSValue::Array> arr = nsCSSValue::Array::Create(1);
uint32_t resultVariantMask = aVariantMask;
if (!ParseCalcAdditiveExpression(arr->Item(0), aVariantMask)) if (!ParseCalcAdditiveExpression(arr->Item(0), resultVariantMask))
break; break;
if (!ExpectSymbol(')', true)) if (!ExpectSymbol(')', true))
break; break;
aValue.SetArrayValue(arr, eCSSUnit_Calc); aValue.SetArrayValue(arr, eCSSUnit_Calc);
if (aResultVariantMask) {
*aResultVariantMask = resultVariantMask;
}
mUnitlessLengthQuirk = oldUnitlessLengthQuirk; mUnitlessLengthQuirk = oldUnitlessLengthQuirk;
return true; return true;
} while (false); } while (false);

View file

@ -159,6 +159,8 @@ support-files = file_bug1089417_iframe.html
[test_clip-path_polygon.html] [test_clip-path_polygon.html]
[test_compute_data_with_start_struct.html] [test_compute_data_with_start_struct.html]
[test_computed_style.html] [test_computed_style.html]
[test_calc_numeric_types.html]
prefs = layout.css.filters.enabled=true
[test_computed_style_min_size_auto.html] [test_computed_style_min_size_auto.html]
[test_computed_style_no_pseudo.html] [test_computed_style_no_pseudo.html]
[test_computed_style_prefs.html] [test_computed_style_prefs.html]

View file

@ -122,6 +122,8 @@ var validGradientAndElementValues = [
"linear-gradient(10deg, red, blue)", "linear-gradient(10deg, red, blue)",
"linear-gradient(1turn, red, blue)", "linear-gradient(1turn, red, blue)",
"linear-gradient(.414rad, red, blue)", "linear-gradient(.414rad, red, blue)",
"linear-gradient(calc(90deg / 2), red, blue)",
"linear-gradient(calc(calc(0.25turn) + 45deg), red, blue)",
"linear-gradient(90deg in srgb, yellow, purple)", "linear-gradient(90deg in srgb, yellow, purple)",
"linear-gradient(90deg in hsl, yellow, purple)", "linear-gradient(90deg in hsl, yellow, purple)",
"linear-gradient(90deg in lch, yellow, purple)", "linear-gradient(90deg in lch, yellow, purple)",
@ -1005,7 +1007,8 @@ var gCSSProperties = {
inherited: false, inherited: false,
type: CSS_TYPE_LONGHAND, type: CSS_TYPE_LONGHAND,
initial_values: ["0s", "0ms"], initial_values: ["0s", "0ms"],
other_values: ["1s", "250ms", "-100ms", "-1s", "1s, 250ms, 2.3s"], other_values: ["1s", "250ms", "-100ms", "-1s", "1s, 250ms, 2.3s",
"calc(250ms - 0.5s)", "calc(calc(500ms) - 250ms)"],
invalid_values: ["0", "0px"], invalid_values: ["0", "0px"],
}, },
"animation-direction": { "animation-direction": {
@ -1030,7 +1033,8 @@ var gCSSProperties = {
inherited: false, inherited: false,
type: CSS_TYPE_LONGHAND, type: CSS_TYPE_LONGHAND,
initial_values: ["0s", "0ms"], initial_values: ["0s", "0ms"],
other_values: ["1s", "250ms", "1s, 250ms, 2.3s"], other_values: ["1s", "250ms", "1s, 250ms, 2.3s",
"calc(calc(0.25s) + 250ms)"],
invalid_values: ["0", "0px", "-1ms", "-2s"], invalid_values: ["0", "0px", "-1ms", "-2s"],
}, },
"animation-fill-mode": { "animation-fill-mode": {
@ -2862,6 +2866,9 @@ var gCSSProperties = {
"translate(calc(5px - 10% * 3))", "translate(calc(5px - 10% * 3))",
"translate(calc(5px - 3 * 10%), 50px)", "translate(calc(5px - 3 * 10%), 50px)",
"translate(-50px, calc(5px - 10% * 3))", "translate(-50px, calc(5px - 10% * 3))",
"rotate(calc(45deg + 45deg))",
"rotate(calc(calc(0.125turn) + 45deg))",
"scale(calc(1 + 0.5))",
"translatez(1px)", "translatez(1px)",
"translatez(4em)", "translatez(4em)",
"translatez(-4px)", "translatez(-4px)",
@ -5584,7 +5591,8 @@ var gCSSProperties = {
"3e+0", "3e+0",
"3e-0", "3e-0",
], ],
other_values: ["0", "0.4", "0.0000", "-3", "3e-1", "-100%", "50%"], other_values: ["0", "0.4", "0.0000", "-3", "3e-1", "-100%", "50%",
"calc(25% * 2)", "calc(calc(0.25) + 0.25)"],
invalid_values: ["0px", "1px"], invalid_values: ["0px", "1px"],
}, },
"-moz-orient": { "-moz-orient": {
@ -6413,7 +6421,8 @@ var gCSSProperties = {
inherited: false, inherited: false,
type: CSS_TYPE_LONGHAND, type: CSS_TYPE_LONGHAND,
initial_values: ["0s", "0ms"], initial_values: ["0s", "0ms"],
other_values: ["1s", "250ms", "-100ms", "-1s", "1s, 250ms, 2.3s"], other_values: ["1s", "250ms", "-100ms", "-1s", "1s, 250ms, 2.3s",
"calc(250ms - 0.5s)", "calc(calc(500ms) - 250ms)"],
invalid_values: ["0", "0px"], invalid_values: ["0", "0px"],
}, },
"transition-duration": { "transition-duration": {
@ -6421,7 +6430,8 @@ var gCSSProperties = {
inherited: false, inherited: false,
type: CSS_TYPE_LONGHAND, type: CSS_TYPE_LONGHAND,
initial_values: ["0s", "0ms"], initial_values: ["0s", "0ms"],
other_values: ["1s", "250ms", "1s, 250ms, 2.3s"], other_values: ["1s", "250ms", "1s, 250ms, 2.3s",
"calc(calc(0.25s) + 250ms)"],
invalid_values: ["0", "0px", "-1ms", "-2s"], invalid_values: ["0", "0px", "-1ms", "-2s"],
}, },
"transition-property": { "transition-property": {
@ -6785,7 +6795,7 @@ var gCSSProperties = {
type: CSS_TYPE_LONGHAND, type: CSS_TYPE_LONGHAND,
/* XXX requires position */ /* XXX requires position */
initial_values: ["auto"], initial_values: ["auto"],
other_values: ["0", "3", "-7000", "12000"], other_values: ["0", "3", "-7000", "12000", "calc(2.5)"],
invalid_values: ["3.0", "17.5", "3e1"], invalid_values: ["3.0", "17.5", "3e1"],
}, },
"clip-path": { "clip-path": {
@ -7645,7 +7655,8 @@ var gCSSProperties = {
inherited: false, inherited: false,
type: CSS_TYPE_LONGHAND, type: CSS_TYPE_LONGHAND,
initial_values: ["0"], initial_values: ["0"],
other_values: ["1", "99999", "-1", "-50"], other_values: ["1", "99999", "-1", "-50", "calc(1.5)",
"calc(calc(-2) + 1)"],
invalid_values: ["0px", "1.0", "1.", "1%", "0.2", "3em", "stretch"], invalid_values: ["0px", "1.0", "1.", "1%", "0.2", "3em", "stretch"],
}, },
@ -9305,6 +9316,8 @@ if (IsCSSPropertyPrefEnabled("layout.css.filters.enabled")) {
"brightness(2)", "brightness(2)",
"brightness(350%)", "brightness(350%)",
"brightness(4.567)", "brightness(4.567)",
"brightness(calc(25% * 2))",
"brightness(calc(calc(0.25) + 0.25))",
"contrast(0)", "contrast(0)",
"contrast(50%)", "contrast(50%)",
@ -9349,6 +9362,7 @@ if (IsCSSPropertyPrefEnabled("layout.css.filters.enabled")) {
"hue-rotate(-1.6rad)", "hue-rotate(-1.6rad)",
"hue-rotate(0.5turn)", "hue-rotate(0.5turn)",
"hue-rotate(-2turn)", "hue-rotate(-2turn)",
"hue-rotate(calc(90deg + 0.125turn))",
"invert(0)", "invert(0)",
"invert(50%)", "invert(50%)",

View file

@ -0,0 +1,93 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test calc() numeric type support</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css">
</head>
<body>
<div id="display"></div>
<pre id="test">
<script>
"use strict";
function appendTestNode(tag = "div") {
const node = document.createElement(tag);
document.getElementById("display").appendChild(node);
return node;
}
function parse2dMatrix(transformValue) {
const match = /^matrix\(([^)]+)\)$/.exec(transformValue);
ok(match, `expected a 2d matrix, got "${transformValue}"`);
return match[1].split(",").map(value => parseFloat(value.trim()));
}
(function testNestedCalcLengthPercentage() {
const container = appendTestNode();
container.style.width = "200px";
container.style.position = "absolute";
const child = document.createElement("div");
child.style.width = "calc(calc(50% + 10px) - calc(5px + 10%))";
container.appendChild(child);
is(getComputedStyle(child).width, "85px",
"nested calc() should resolve length-percentage expressions correctly");
container.remove();
})();
(function testTimeCalc() {
const div = appendTestNode();
div.style.transitionDuration = "calc(calc(0.25s) + 250ms)";
div.style.animationDelay = "calc(250ms - 0.5s)";
is(getComputedStyle(div).transitionDuration, "0.5s",
"transition-duration should accept calc() time values");
is(getComputedStyle(div).animationDelay, "-0.25s",
"animation-delay should accept calc() time values");
div.remove();
})();
(function testNumericAndIntegerCalc() {
const div = appendTestNode();
div.style.opacity = "calc(25% * 2)";
div.style.order = "calc(1.5)";
div.style.zIndex = "calc(2.5)";
is(getComputedStyle(div).opacity, "0.5",
"opacity should accept calc() number/percent values");
is(getComputedStyle(div).order, "2",
"order should round calc() results to the nearest integer");
is(getComputedStyle(div).zIndex, "3",
"z-index should round calc() results to the nearest integer");
div.remove();
})();
(function testAngleAndNumberCalcInFunctions() {
const div = appendTestNode();
div.style.transform = "rotate(calc(45deg + 45deg)) scale(calc(1 + 0.5))";
div.style.filter = "hue-rotate(calc(90deg + 0.125turn)) brightness(calc(0.25 + 0.25))";
div.style.backgroundImage = "linear-gradient(calc(90deg / 2), red, blue)";
ok(div.style.filter !== "",
"filter should accept calc() in angle and number function arguments");
ok(div.style.backgroundImage !== "",
"gradients should accept calc() in angle arguments");
const matrix = parse2dMatrix(getComputedStyle(div).transform);
ok(Math.abs(matrix[0]) < 1e-6, "rotate(calc()) should zero out the xx entry");
ok(Math.abs(matrix[1] - 1.5) < 1e-6, "scale(calc()) should affect the xy entry");
ok(Math.abs(matrix[2] + 1.5) < 1e-6, "scale(calc()) should affect the yx entry");
ok(Math.abs(matrix[3]) < 1e-6, "rotate(calc()) should zero out the yy entry");
div.remove();
})();
</script>
</pre>
</body>
</html>