diff --git a/src/AngleSharp.Css.Tests/Values/Calc.cs b/src/AngleSharp.Css.Tests/Values/Calc.cs index 85f01b1..1b3cae7 100644 --- a/src/AngleSharp.Css.Tests/Values/Calc.cs +++ b/src/AngleSharp.Css.Tests/Values/Calc.cs @@ -140,6 +140,71 @@ public void CalcLengthWithUnitlessOperandIsComputed(String expression, String ex Assert.AreEqual(expected, style.GetWidth()); } + [TestCase("calc(10px - 2px - 3px)", "5px")] + [TestCase("calc(100px - 10px - 20px - 30px)", "40px")] + [TestCase("calc(30px - 10px + 5px)", "25px")] + [TestCase("calc(10px + 20px - 5px)", "25px")] + [TestCase("calc(50px - (10px - 5px))", "45px")] + public void CalcSameOperatorChainIsLeftAssociative(String expression, String expected) + { + var document = ParseDocument($"
"); + var style = document.QuerySelector("p").ComputeCurrentStyle(); + Assert.AreEqual(expected, style.GetWidth()); + } + + [TestCase("calc(100px / 2 / 5)", "10px")] + [TestCase("calc(100px / 2 * 5)", "250px")] + [TestCase("calc(100px * 2 / 5)", "40px")] + [TestCase("calc(1px * 2 * 3)", "6px")] + public void CalcMultiplicativeChainIsLeftAssociative(String expression, String expected) + { + var document = ParseDocument($""); + var style = document.QuerySelector("p").ComputeCurrentStyle(); + Assert.AreEqual(expected, style.GetWidth()); + } + + [TestCase("calc(2 * 3px + 1px)", "7px")] + [TestCase("calc(21px + 5px - 4px * 2)", "18px")] + public void CalcMixedPrecedenceIsComputed(String expression, String expected) + { + var document = ParseDocument($""); + var style = document.QuerySelector("p").ComputeCurrentStyle(); + Assert.AreEqual(expected, style.GetWidth()); + } + + [TestCase("opacity", "calc(10px / 20px)", "0.5")] + [TestCase("opacity", "calc(2s / 8s)", "0.25")] + [TestCase("flex-grow", "calc(100px / 50px)", "2")] + [TestCase("z-index", "calc(100px / 25px)", "4")] + [TestCase("line-height", "calc(40px / 20px)", "2")] + public void CalcDivisionOfEqualUnitsYieldsNumber(String property, String expression, String expected) + { + var document = ParseDocument($""); + var style = document.QuerySelector("p").ComputeCurrentStyle(); + Assert.AreEqual(expected, style.GetPropertyValue(property)); + } + + [TestCase("width", "calc(100px / 2)", "50px")] + [TestCase("width", "calc(100px * 3 / 2)", "150px")] + [TestCase("width", "calc(100px / 2px * 3px)", "150px")] + [TestCase("transition-duration", "calc(2s / 4)", "500ms")] + public void CalcDivisionByNumberKeepsUnitOfLeftOperand(String property, String expression, String expected) + { + var document = ParseDocument($""); + var style = document.QuerySelector("p").ComputeCurrentStyle(); + Assert.AreEqual(expected, style.GetPropertyValue(property)); + } + + [TestCase("opacity", "calc(1 / 4)", "0.25")] + [TestCase("opacity", "calc(2 * 3)", "6")] + [TestCase("flex-shrink", "calc(20 / 8)", "2.5")] + public void CalcOfUnitlessOperandsStaysUnitless(String property, String expression, String expected) + { + var document = ParseDocument($""); + var style = document.QuerySelector("p").ComputeCurrentStyle(); + Assert.AreEqual(expected, style.GetPropertyValue(property)); + } + [TestCase(typeof(CssAngleValue))] [TestCase(typeof(CssFrequencyValue))] [TestCase(typeof(CssIntegerValue))] diff --git a/src/AngleSharp.Css/Extensions/CssMetricValueExtensions.cs b/src/AngleSharp.Css/Extensions/CssMetricValueExtensions.cs index caf9d9f..828229f 100644 --- a/src/AngleSharp.Css/Extensions/CssMetricValueExtensions.cs +++ b/src/AngleSharp.Css/Extensions/CssMetricValueExtensions.cs @@ -34,6 +34,10 @@ static class CssMetricValueExtensions "public constructor taking a single Double themselves, e.g., via DynamicDependency.")] #endif public static ICssValue WithValue(this ICssMetricValue template, Double value) => - (ICssValue)Activator.CreateInstance(template.GetType(), value); + // The single argument constructor of CssLengthValue defaults to pixels, which would + // turn a unitless length (e.g., the result of calc(1 / 4)) into a length in pixels. + template is CssLengthValue length ? + new CssLengthValue(value, length.Type) : + (ICssValue)Activator.CreateInstance(template.GetType(), value); } } diff --git a/src/AngleSharp.Css/Parser/Micro/CalcParser.cs b/src/AngleSharp.Css/Parser/Micro/CalcParser.cs index 49bbd36..7a72f4b 100644 --- a/src/AngleSharp.Css/Parser/Micro/CalcParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/CalcParser.cs @@ -47,51 +47,12 @@ private static ICssValue ParseExpression(this StringSource source) } private static ICssValue ParseAddExpression(this StringSource source) - { - var left = ParseSubExpression(source); - - if (source.Current == Symbols.Plus) - { - source.SkipCurrentAndSpaces(); - var right = ParseAddExpression(source); - - if (right == null) - { - return null; - } - - return new CssCalcAddExpression(left, right); - } - - return left; - } - - private static ICssValue ParseSubExpression(this StringSource source) { var left = ParseMulExpression(source); - if (source.Current == Symbols.Minus) - { - source.SkipCurrentAndSpaces(); - var right = ParseSubExpression(source); - - if (right == null) - { - return null; - } - - return new CssCalcSubExpression(left, right); - } - - return left; - } - - private static ICssValue ParseMulExpression(this StringSource source) - { - var left = ParseDivExpression(source); - - if (source.Current == Symbols.Asterisk) + while (left != null && (source.Current == Symbols.Plus || source.Current == Symbols.Minus)) { + var add = source.Current == Symbols.Plus; source.SkipCurrentAndSpaces(); var right = ParseMulExpression(source); @@ -100,27 +61,32 @@ private static ICssValue ParseMulExpression(this StringSource source) return null; } - return new CssCalcMulExpression(left, right); + left = add ? + new CssCalcAddExpression(left, right) : + (ICssValue)new CssCalcSubExpression(left, right); } return left; } - private static ICssValue ParseDivExpression(this StringSource source) + private static ICssValue ParseMulExpression(this StringSource source) { var left = ParseBracketExpression(source); - if (source.Current == Symbols.Solidus) + while (left != null && (source.Current == Symbols.Asterisk || source.Current == Symbols.Solidus)) { + var mul = source.Current == Symbols.Asterisk; source.SkipCurrentAndSpaces(); - var right = ParseDivExpression(source); + var right = ParseBracketExpression(source); if (right == null) { return null; } - return new CssCalcDivExpression(left, right); + left = mul ? + new CssCalcMulExpression(left, right) : + (ICssValue)new CssCalcDivExpression(left, right); } return left; diff --git a/src/AngleSharp.Css/Values/Expressions/CssCalcDivExpression.cs b/src/AngleSharp.Css/Values/Expressions/CssCalcDivExpression.cs index 01ef850..68245cc 100644 --- a/src/AngleSharp.Css/Values/Expressions/CssCalcDivExpression.cs +++ b/src/AngleSharp.Css/Values/Expressions/CssCalcDivExpression.cs @@ -57,15 +57,20 @@ ICssValue ICssValue.Compute(ICssComputeContext context) var left = ComputeValue(_left, context); var right = ComputeValue(_right, context); - if (left is ICssMetricValue x && right is ICssMetricValue y && x.UnitString == y.UnitString) + if (left is ICssMetricValue x && right is ICssMetricValue y) { - var result = x.Value / y.Value; - return x.WithValue(result); - } - - if (left is ICssMetricValue unitLeft && right is ICssMetricValue unitlessRight && unitlessRight.UnitString.Length == 0) - { - return unitLeft.WithValue(unitLeft.Value / unitlessRight.Value); + // Dividing by a plain number scales the left operand, keeping its unit. + if (y.UnitString.Length == 0) + { + return x.WithValue(x.Value / y.Value); + } + + // Dividing two values sharing a unit cancels the unit out, i.e. the + // result is a plain number (calc(40px / 20px) is 2, not 2px). + if (x.UnitString == y.UnitString) + { + return new CssLengthValue(x.Value / y.Value, CssLengthValue.Unit.None); + } } return null;