Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/AngleSharp.Css.Tests/Values/Calc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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($"<style>p {{ width: {expression} }}</style><p></p>");
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($"<style>p {{ width: {expression} }}</style><p></p>");
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($"<style>p {{ width: {expression} }}</style><p></p>");
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($"<style>p {{ {property}: {expression} }}</style><p></p>");
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($"<style>p {{ {property}: {expression} }}</style><p></p>");
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($"<style>p {{ {property}: {expression} }}</style><p></p>");
var style = document.QuerySelector("p").ComputeCurrentStyle();
Assert.AreEqual(expected, style.GetPropertyValue(property));
}

[TestCase(typeof(CssAngleValue))]
[TestCase(typeof(CssFrequencyValue))]
[TestCase(typeof(CssIntegerValue))]
Expand Down
6 changes: 5 additions & 1 deletion src/AngleSharp.Css/Extensions/CssMetricValueExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
58 changes: 12 additions & 46 deletions src/AngleSharp.Css/Parser/Micro/CalcParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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;
Expand Down
21 changes: 13 additions & 8 deletions src/AngleSharp.Css/Values/Expressions/CssCalcDivExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading