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
63 changes: 63 additions & 0 deletions src/ExCSS.Tests/PropertyTests/CalcPropertyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Xunit;

namespace ExCSS.Tests.PropertyTests
{
public class CalcPropertyTests : CssConstructionFunctions
{
[Theory]
// calc()/min()/max()/clamp() (CSS Values 4 10) are accepted wherever a compatible numeric value is,
// and the authored text is preserved.
[InlineData("width: calc(100% - 20px)", "width")]
[InlineData("width: calc(50% + 2em)", "width")]
[InlineData("width: calc((100% - 20px) / 3)", "width")]
[InlineData("margin-top: calc(10px * 2)", "margin-top")]
[InlineData("width: min(100%, 500px)", "width")]
[InlineData("width: max(50%, 200px)", "width")]
[InlineData("width: clamp(200px, 50%, 500px)", "width")]
[InlineData("width: calc(min(10px, 5%) + 2px)", "width")]
public void CalcAcceptedInLengthContext(string declaration, string name)
{
var property = ParseDeclaration(declaration);

Assert.Equal(name, property.Name);
Assert.True(property.HasValue);
}

[Fact]
public void CalcPreservesAuthoredText()
{
var property = ParseDeclaration("width: calc(100% - 20px)");

Assert.Equal("calc(100% - 20px)", property.Value);
}

[Theory]
// A calc() also works in the angle context.
[InlineData("transform: rotate(calc(45deg * 2))")]
[InlineData("transform: rotate(calc(1turn / 4))")]
public void CalcAcceptedInAngleContext(string declaration)
{
var property = ParseDeclaration(declaration);

Assert.True(property.HasValue);
}

[Theory]
// Type-checking (CSS Values 4 10.10): dimensionally-inconsistent or malformed expressions are
// rejected.
[InlineData("width: calc(1px + 1s)")] // length + time
[InlineData("width: calc(10px * 20px)")] // length * length
[InlineData("width: calc(100% / 0)")] // divide by a constant zero
[InlineData("width: calc(1px +1px)")] // "+" needs whitespace on both sides
[InlineData("width: calc(5)")] // bare number is not a length
[InlineData("width: calc()")] // empty
[InlineData("width: clamp(1px, 2px)")] // clamp() takes exactly three arguments
[InlineData("width: calc(1px + )")] // missing operand
public void CalcRejectsInvalidExpressions(string declaration)
{
var property = ParseDeclaration(declaration);

Assert.False(property.HasValue);
}
}
}
21 changes: 21 additions & 0 deletions src/ExCSS/Calc/CalcCategory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

namespace ExCSS
{
/// <summary>
/// The CSS calc() type-checking category a (sub-)expression resolves to. A flags enum so that
/// <c>Length | Percentage</c> can represent the combined <c>&lt;length-percentage&gt;</c> type once a
/// length and a percentage have been added or subtracted together.
/// </summary>
[Flags]
internal enum CalcCategory
{
Number = 1,
Length = 2,
Percentage = 4,
LengthPercentage = Length | Percentage,
Angle = 8,
Time = 16,
Resolution = 32
}
}
120 changes: 120 additions & 0 deletions src/ExCSS/Calc/CalcNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System.Collections.Generic;

namespace ExCSS
{
/// <summary>
/// AST node for a parsed calc()/min()/max()/clamp() expression (CSS Values 4 §10).
/// </summary>
internal abstract class CalcNode
{
}

internal sealed class NumberCalcNode : CalcNode
{
public NumberCalcNode(double value)
{
Value = value;
}

public double Value { get; }
}

internal sealed class DimensionCalcNode : CalcNode
{
public DimensionCalcNode(double value, Length.Unit unit)
{
Value = value;
Unit = unit;
}

public double Value { get; }
public Length.Unit Unit { get; }
}

internal sealed class PercentageCalcNode : CalcNode
{
public PercentageCalcNode(double value)
{
Value = value;
}

public double Value { get; }
}

internal sealed class AngleCalcNode : CalcNode
{
public AngleCalcNode(double value, Angle.Unit unit)
{
Value = value;
Unit = unit;
}

public double Value { get; }
public Angle.Unit Unit { get; }
}

internal sealed class TimeCalcNode : CalcNode
{
public TimeCalcNode(double value, Time.Unit unit)
{
Value = value;
Unit = unit;
}

public double Value { get; }
public Time.Unit Unit { get; }
}

internal sealed class ResolutionCalcNode : CalcNode
{
public ResolutionCalcNode(double value, Resolution.Unit unit)
{
Value = value;
Unit = unit;
}

public double Value { get; }
public Resolution.Unit Unit { get; }
}

/// <summary>A leading unary sign directly before a parenthesized group or a nested function call.</summary>
internal sealed class UnaryCalcNode : CalcNode
{
public UnaryCalcNode(bool negative, CalcNode operand)
{
Negative = negative;
Operand = operand;
}

public bool Negative { get; }
public CalcNode Operand { get; }
}

/// <summary>A binary <c>+</c>/<c>-</c>/<c>*</c>/<c>/</c> operation.</summary>
internal sealed class BinaryCalcNode : CalcNode
{
public BinaryCalcNode(char op, CalcNode left, CalcNode right)
{
Operator = op;
Left = left;
Right = right;
}

public char Operator { get; }
public CalcNode Left { get; }
public CalcNode Right { get; }
}

/// <summary>A <c>min()</c>, <c>max()</c>, or <c>clamp()</c> call over its comma-separated arguments.</summary>
internal sealed class CallCalcNode : CalcNode
{
public CallCalcNode(string name, IReadOnlyList<CalcNode> arguments)
{
Name = name;
Arguments = arguments;
}

public string Name { get; }
public IReadOnlyList<CalcNode> Arguments { get; }
}
}
Loading