Skip to content

Commit 5ddfc39

Browse files
authored
Merge pull request #207 from jhaygood86/feature/conic-gradient
Add conic-gradient() and repeating-conic-gradient()
2 parents 53d3f56 + db26728 commit 5ddfc39

4 files changed

Lines changed: 108 additions & 9 deletions

File tree

src/ExCSS.Tests/Gradient.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,35 @@ public void GradientTwoPositionColorStopLegal(string snippet, string expected)
261261
Assert.Equal(expected, backgroundImage.Value);
262262
}
263263

264+
[Theory]
265+
// conic-gradient()/repeating-conic-gradient() (CSS Images 4 3.4): an optional
266+
// "[ from <angle> ]? [ at <position> ]?" prelude, then an <angular-color-stop> list whose stops are
267+
// positioned by <angle-percentage>.
268+
[InlineData("background-image: conic-gradient(red, blue)",
269+
"conic-gradient(rgb(255, 0, 0), rgb(0, 0, 255))")]
270+
[InlineData("background-image: conic-gradient(from 90deg, red, blue)",
271+
"conic-gradient(from 90deg, rgb(255, 0, 0), rgb(0, 0, 255))")]
272+
[InlineData("background-image: conic-gradient(at center, red, blue)",
273+
"conic-gradient(at center, rgb(255, 0, 0), rgb(0, 0, 255))")]
274+
[InlineData("background-image: conic-gradient(from 45deg at 30% 70%, red, blue)",
275+
"conic-gradient(from 45deg at 30% 70%, rgb(255, 0, 0), rgb(0, 0, 255))")]
276+
[InlineData("background-image: conic-gradient(red 0deg, blue 90deg, lime 180deg)",
277+
"conic-gradient(rgb(255, 0, 0) 0deg, rgb(0, 0, 255) 90deg, rgb(0, 255, 0) 180deg)")]
278+
[InlineData("background-image: conic-gradient(red 25%, blue 50%)",
279+
"conic-gradient(rgb(255, 0, 0) 25%, rgb(0, 0, 255) 50%)")]
280+
[InlineData("background-image: conic-gradient(from 1turn, red, blue)",
281+
"conic-gradient(from 1turn, rgb(255, 0, 0), rgb(0, 0, 255))")]
282+
[InlineData("background-image: repeating-conic-gradient(red, blue 30deg)",
283+
"repeating-conic-gradient(rgb(255, 0, 0), rgb(0, 0, 255) 30deg)")]
284+
public void ConicGradientLegal(string source, string expected)
285+
{
286+
var property = ParseDeclaration(source);
287+
Assert.IsType<BackgroundImageProperty>(property);
288+
var backgroundImage = (BackgroundImageProperty)property;
289+
Assert.True(backgroundImage.HasValue);
290+
Assert.Equal(expected, backgroundImage.Value);
291+
}
292+
264293
[Theory]
265294
// Single-position and no-position stops, plus a bare-position colour hint, must be unaffected.
266295
[InlineData("background-image: linear-gradient(red, blue)")]
@@ -282,5 +311,24 @@ public void GradientMalformedColorStopIllegal(string snippet)
282311
var property = ParseDeclaration(snippet);
283312
Assert.False(((BackgroundImageProperty)property).HasValue);
284313
}
314+
315+
[Theory]
316+
[InlineData("background-image: conic-gradient(from red, blue, green)")] // from needs an <angle>
317+
[InlineData("background-image: conic-gradient(at, red, blue)")] // at needs a <position>
318+
[InlineData("background-image: conic-gradient(red 0px, blue 90px)")] // conic stops use angles
319+
public void ConicGradientIllegal(string source)
320+
{
321+
var property = ParseDeclaration(source);
322+
Assert.False(((BackgroundImageProperty)property).HasValue);
323+
}
324+
325+
[Fact]
326+
public void ConicGradientDoesNotDisturbLinearOrRadial()
327+
{
328+
Assert.True(((BackgroundImageProperty)ParseDeclaration(
329+
"background-image: linear-gradient(90deg, red, blue 50%)")).HasValue);
330+
Assert.True(((BackgroundImageProperty)ParseDeclaration(
331+
"background-image: radial-gradient(circle at center, red, blue)")).HasValue);
332+
}
285333
}
286334
}

src/ExCSS/Enumerations/FunctionNames.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public static class FunctionNames
2323
public static readonly string RadialGradient = "radial-gradient";
2424
public static readonly string RepeatingLinearGradient = "repeating-linear-gradient";
2525
public static readonly string RepeatingRadialGradient = "repeating-radial-gradient";
26+
public static readonly string ConicGradient = "conic-gradient";
27+
public static readonly string RepeatingConicGradient = "repeating-conic-gradient";
2628
public static readonly string Image = "image";
2729
public static readonly string ImageSet = "image-set";
2830
public static readonly string CrossFade = "cross-fade";

src/ExCSS/Model/Converters.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ public static readonly IValueConverter
154154
new FunctionValueConverter(FunctionNames.RadialGradient, new RadialGradientConverter()).Or(
155155
new FunctionValueConverter(FunctionNames.RepeatingRadialGradient, new RadialGradientConverter())));
156156

157+
public static readonly IValueConverter ConicGradientConverter = Construct(() =>
158+
new FunctionValueConverter(FunctionNames.ConicGradient, new ConicGradientConverter()).Or(
159+
new FunctionValueConverter(FunctionNames.RepeatingConicGradient, new ConicGradientConverter())));
160+
157161
public static readonly IValueConverter RgbColorConverter = Construct(() =>
158162
{
159163
var number = RgbComponentConverter.Required();
@@ -455,7 +459,8 @@ public static readonly IValueConverter
455459
public static readonly IValueConverter TransitionConverter = new DictionaryValueConverter<ITimingFunction>(
456460
Map.TimingFunctions).Or(StepsConverter).Or(CubicBezierConverter);
457461

458-
public static readonly IValueConverter GradientConverter = LinearGradientConverter.Or(RadialGradientConverter);
462+
public static readonly IValueConverter GradientConverter =
463+
LinearGradientConverter.Or(RadialGradientConverter).Or(ConicGradientConverter);
459464

460465
public static readonly IValueConverter TransformConverter = MatrixTransformConverter
461466
.Or(ScaleTransformConverter)

src/ExCSS/ValueConverters/GradientConverter.cs

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ public IPropertyValue Construct(Property[] properties)
2020
return properties.Guard<GradientValue>();
2121
}
2222

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

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

37-
private static IPropertyValue ToGradientStop(List<Token> value)
41+
private IPropertyValue ToGradientStop(List<Token> value)
3842
{
3943
var color = default(IPropertyValue);
4044
var firstPosition = default(IPropertyValue);
@@ -43,18 +47,19 @@ private static IPropertyValue ToGradientStop(List<Token> value)
4347

4448
if (items.Count != 0)
4549
{
46-
firstPosition = LengthOrPercentConverter.Convert(items[items.Count - 1]);
50+
firstPosition = StopPositionConverter.Convert(items[items.Count - 1]);
4751

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

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

5964
if (earlier != null)
6065
{
@@ -199,4 +204,43 @@ protected override IPropertyValue ConvertFirstArgument(IEnumerable<Token> value)
199204
return _converter.Convert(value);
200205
}
201206
}
207+
208+
internal sealed class ConicGradientConverter : GradientConverter
209+
{
210+
private readonly IValueConverter _converter;
211+
212+
public ConicGradientConverter()
213+
{
214+
// The conic prelude is "[ from <angle> ]? [ at <position> ]?" (CSS Images 4 3.4).
215+
var from = AngleConverter.StartsWithKeyword(Keywords.From).Option();
216+
var at = PointConverter.StartsWithKeyword(Keywords.At).Option();
217+
218+
_converter = WithOrder(from, at);
219+
}
220+
221+
// A conic gradient's stops are positioned by angle, not length: <angular-color-stop> uses an
222+
// <angle-percentage> (CSS Images 4 3.4.1).
223+
protected override IValueConverter StopPositionConverter { get; } =
224+
AngleConverter.Or(PercentConverter);
225+
226+
protected override IPropertyValue ConvertFirstArgument(IEnumerable<Token> value)
227+
{
228+
// The prelude is optional, but this is only called for the gradient's first comma group. If
229+
// that group is actually the first color stop (no from/at), it must not be swallowed here - a
230+
// group that begins with a color, an angle, or a percentage is a stop, so reject it as a
231+
// prelude and let the caller treat it as the first stop.
232+
foreach (var token in value)
233+
{
234+
if (token.Type == TokenType.Whitespace) continue;
235+
236+
var isPreludeKeyword = token.Type == TokenType.Ident &&
237+
(token.Data.Isi(Keywords.From) || token.Data.Isi(Keywords.At));
238+
239+
if (!isPreludeKeyword) return null;
240+
break;
241+
}
242+
243+
return _converter.Convert(value);
244+
}
245+
}
202246
}

0 commit comments

Comments
 (0)