Skip to content
Open
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
48 changes: 48 additions & 0 deletions src/ExCSS.Tests/Gradient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,35 @@ public void GradientTwoPositionColorStopLegal(string snippet, string expected)
Assert.Equal(expected, backgroundImage.Value);
}

[Theory]
// conic-gradient()/repeating-conic-gradient() (CSS Images 4 3.4): an optional
// "[ from <angle> ]? [ at <position> ]?" prelude, then an <angular-color-stop> list whose stops are
// positioned by <angle-percentage>.
[InlineData("background-image: conic-gradient(red, blue)",
"conic-gradient(rgb(255, 0, 0), rgb(0, 0, 255))")]
[InlineData("background-image: conic-gradient(from 90deg, red, blue)",
"conic-gradient(from 90deg, rgb(255, 0, 0), rgb(0, 0, 255))")]
[InlineData("background-image: conic-gradient(at center, red, blue)",
"conic-gradient(at center, rgb(255, 0, 0), rgb(0, 0, 255))")]
[InlineData("background-image: conic-gradient(from 45deg at 30% 70%, red, blue)",
"conic-gradient(from 45deg at 30% 70%, rgb(255, 0, 0), rgb(0, 0, 255))")]
[InlineData("background-image: conic-gradient(red 0deg, blue 90deg, lime 180deg)",
"conic-gradient(rgb(255, 0, 0) 0deg, rgb(0, 0, 255) 90deg, rgb(0, 255, 0) 180deg)")]
[InlineData("background-image: conic-gradient(red 25%, blue 50%)",
"conic-gradient(rgb(255, 0, 0) 25%, rgb(0, 0, 255) 50%)")]
[InlineData("background-image: conic-gradient(from 1turn, red, blue)",
"conic-gradient(from 1turn, rgb(255, 0, 0), rgb(0, 0, 255))")]
[InlineData("background-image: repeating-conic-gradient(red, blue 30deg)",
"repeating-conic-gradient(rgb(255, 0, 0), rgb(0, 0, 255) 30deg)")]
public void ConicGradientLegal(string source, string expected)
{
var property = ParseDeclaration(source);
Assert.IsType<BackgroundImageProperty>(property);
var backgroundImage = (BackgroundImageProperty)property;
Assert.True(backgroundImage.HasValue);
Assert.Equal(expected, backgroundImage.Value);
}

[Theory]
// Single-position and no-position stops, plus a bare-position colour hint, must be unaffected.
[InlineData("background-image: linear-gradient(red, blue)")]
Expand All @@ -282,5 +311,24 @@ public void GradientMalformedColorStopIllegal(string snippet)
var property = ParseDeclaration(snippet);
Assert.False(((BackgroundImageProperty)property).HasValue);
}

[Theory]
[InlineData("background-image: conic-gradient(from red, blue, green)")] // from needs an <angle>
[InlineData("background-image: conic-gradient(at, red, blue)")] // at needs a <position>
[InlineData("background-image: conic-gradient(red 0px, blue 90px)")] // conic stops use angles
public void ConicGradientIllegal(string source)
{
var property = ParseDeclaration(source);
Assert.False(((BackgroundImageProperty)property).HasValue);
}

[Fact]
public void ConicGradientDoesNotDisturbLinearOrRadial()
{
Assert.True(((BackgroundImageProperty)ParseDeclaration(
"background-image: linear-gradient(90deg, red, blue 50%)")).HasValue);
Assert.True(((BackgroundImageProperty)ParseDeclaration(
"background-image: radial-gradient(circle at center, red, blue)")).HasValue);
}
}
}
2 changes: 2 additions & 0 deletions src/ExCSS/Enumerations/FunctionNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public static class FunctionNames
public static readonly string RadialGradient = "radial-gradient";
public static readonly string RepeatingLinearGradient = "repeating-linear-gradient";
public static readonly string RepeatingRadialGradient = "repeating-radial-gradient";
public static readonly string ConicGradient = "conic-gradient";
public static readonly string RepeatingConicGradient = "repeating-conic-gradient";
public static readonly string Image = "image";
public static readonly string Counter = "counter";
public static readonly string Counters = "counters";
Expand Down
7 changes: 6 additions & 1 deletion src/ExCSS/Model/Converters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ public static readonly IValueConverter
new FunctionValueConverter(FunctionNames.RadialGradient, new RadialGradientConverter()).Or(
new FunctionValueConverter(FunctionNames.RepeatingRadialGradient, new RadialGradientConverter())));

public static readonly IValueConverter ConicGradientConverter = Construct(() =>
new FunctionValueConverter(FunctionNames.ConicGradient, new ConicGradientConverter()).Or(
new FunctionValueConverter(FunctionNames.RepeatingConicGradient, new ConicGradientConverter())));

public static readonly IValueConverter RgbColorConverter = Construct(() =>
{
var number = RgbComponentConverter.Required();
Expand Down Expand Up @@ -409,7 +413,8 @@ public static readonly IValueConverter
public static readonly IValueConverter TransitionConverter = new DictionaryValueConverter<ITimingFunction>(
Map.TimingFunctions).Or(StepsConverter).Or(CubicBezierConverter);

public static readonly IValueConverter GradientConverter = LinearGradientConverter.Or(RadialGradientConverter);
public static readonly IValueConverter GradientConverter =
LinearGradientConverter.Or(RadialGradientConverter).Or(ConicGradientConverter);

public static readonly IValueConverter TransformConverter = MatrixTransformConverter
.Or(ScaleTransformConverter)
Expand Down
60 changes: 52 additions & 8 deletions src/ExCSS/ValueConverters/GradientConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ public IPropertyValue Construct(Property[] properties)
return properties.Guard<GradientValue>();
}

private static IPropertyValue[] ToGradientStops(List<List<Token>> values, int offset)
// The converter that parses a stop's position. Linear/radial stops are positioned with a
// <length-percentage>; a conic gradient overrides this with an <angle-percentage> (CSS Images 4).
protected virtual IValueConverter StopPositionConverter => LengthOrPercentConverter;

private IPropertyValue[] ToGradientStops(List<List<Token>> values, int offset)
{
var stops = new IPropertyValue[values.Count - offset];

Expand All @@ -34,7 +38,7 @@ private static IPropertyValue[] ToGradientStops(List<List<Token>> values, int of
return stops;
}

private static IPropertyValue ToGradientStop(List<Token> value)
private IPropertyValue ToGradientStop(List<Token> value)
{
var color = default(IPropertyValue);
var firstPosition = default(IPropertyValue);
Expand All @@ -43,18 +47,19 @@ private static IPropertyValue ToGradientStop(List<Token> value)

if (items.Count != 0)
{
firstPosition = LengthOrPercentConverter.Convert(items[items.Count - 1]);
firstPosition = StopPositionConverter.Convert(items[items.Count - 1]);

if (firstPosition != null) items.RemoveAt(items.Count - 1);
}

// <color-stop-length> = <length-percentage>{1,2} (CSS Images 4 3.5.1): a stop may carry two
// positions, equivalent to two same-colour stops, one at each position. Parsing right-to-left,
// the position taken above is the second (rightmost); a position immediately before it is the
// first, and the two are kept in source order for serialization.
// <color-stop-length> = <length-percentage>{1,2} (CSS Images 4 3.5.1) - or
// <angle-percentage>{1,2} for a conic gradient, hence StopPositionConverter: a stop may carry
// two positions, equivalent to two same-colour stops, one at each position. Parsing
// right-to-left, the position taken above is the second (rightmost); a position immediately
// before it is the first, and the two are kept in source order for serialization.
if (firstPosition != null && items.Count != 0)
{
var earlier = LengthOrPercentConverter.Convert(items[items.Count - 1]);
var earlier = StopPositionConverter.Convert(items[items.Count - 1]);

if (earlier != null)
{
Expand Down Expand Up @@ -199,4 +204,43 @@ protected override IPropertyValue ConvertFirstArgument(IEnumerable<Token> value)
return _converter.Convert(value);
}
}

internal sealed class ConicGradientConverter : GradientConverter
{
private readonly IValueConverter _converter;

public ConicGradientConverter()
{
// The conic prelude is "[ from <angle> ]? [ at <position> ]?" (CSS Images 4 3.4).
var from = AngleConverter.StartsWithKeyword(Keywords.From).Option();
var at = PointConverter.StartsWithKeyword(Keywords.At).Option();

_converter = WithOrder(from, at);
}

// A conic gradient's stops are positioned by angle, not length: <angular-color-stop> uses an
// <angle-percentage> (CSS Images 4 3.4.1).
protected override IValueConverter StopPositionConverter { get; } =
AngleConverter.Or(PercentConverter);

protected override IPropertyValue ConvertFirstArgument(IEnumerable<Token> value)
{
// The prelude is optional, but this is only called for the gradient's first comma group. If
// that group is actually the first color stop (no from/at), it must not be swallowed here - a
// group that begins with a color, an angle, or a percentage is a stop, so reject it as a
// prelude and let the caller treat it as the first stop.
foreach (var token in value)
{
if (token.Type == TokenType.Whitespace) continue;

var isPreludeKeyword = token.Type == TokenType.Ident &&
(token.Data.Isi(Keywords.From) || token.Data.Isi(Keywords.At));

if (!isPreludeKeyword) return null;
break;
}

return _converter.Convert(value);
}
}
}