diff --git a/src/ExCSS.Tests/Gradient.cs b/src/ExCSS.Tests/Gradient.cs index 8fecb11..db228fa 100644 --- a/src/ExCSS.Tests/Gradient.cs +++ b/src/ExCSS.Tests/Gradient.cs @@ -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 ]? [ at ]?" prelude, then an list whose stops are + // positioned by . + [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(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)")] @@ -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 + [InlineData("background-image: conic-gradient(at, red, blue)")] // at needs a + [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); + } } } diff --git a/src/ExCSS/Enumerations/FunctionNames.cs b/src/ExCSS/Enumerations/FunctionNames.cs index afd8eb8..d53c996 100644 --- a/src/ExCSS/Enumerations/FunctionNames.cs +++ b/src/ExCSS/Enumerations/FunctionNames.cs @@ -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"; diff --git a/src/ExCSS/Model/Converters.cs b/src/ExCSS/Model/Converters.cs index 0cd8c44..a00291b 100644 --- a/src/ExCSS/Model/Converters.cs +++ b/src/ExCSS/Model/Converters.cs @@ -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(); @@ -409,7 +413,8 @@ public static readonly IValueConverter public static readonly IValueConverter TransitionConverter = new DictionaryValueConverter( 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) diff --git a/src/ExCSS/ValueConverters/GradientConverter.cs b/src/ExCSS/ValueConverters/GradientConverter.cs index 6643eb3..6b987d6 100644 --- a/src/ExCSS/ValueConverters/GradientConverter.cs +++ b/src/ExCSS/ValueConverters/GradientConverter.cs @@ -20,7 +20,11 @@ public IPropertyValue Construct(Property[] properties) return properties.Guard(); } - private static IPropertyValue[] ToGradientStops(List> values, int offset) + // The converter that parses a stop's position. Linear/radial stops are positioned with a + // ; a conic gradient overrides this with an (CSS Images 4). + protected virtual IValueConverter StopPositionConverter => LengthOrPercentConverter; + + private IPropertyValue[] ToGradientStops(List> values, int offset) { var stops = new IPropertyValue[values.Count - offset]; @@ -34,7 +38,7 @@ private static IPropertyValue[] ToGradientStops(List> values, int of return stops; } - private static IPropertyValue ToGradientStop(List value) + private IPropertyValue ToGradientStop(List value) { var color = default(IPropertyValue); var firstPosition = default(IPropertyValue); @@ -43,18 +47,19 @@ private static IPropertyValue ToGradientStop(List 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); } - // = {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. + // = {1,2} (CSS Images 4 3.5.1) - or + // {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) { @@ -199,4 +204,43 @@ protected override IPropertyValue ConvertFirstArgument(IEnumerable value) return _converter.Convert(value); } } + + internal sealed class ConicGradientConverter : GradientConverter + { + private readonly IValueConverter _converter; + + public ConicGradientConverter() + { + // The conic prelude is "[ from ]? [ at ]?" (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: uses an + // (CSS Images 4 3.4.1). + protected override IValueConverter StopPositionConverter { get; } = + AngleConverter.Or(PercentConverter); + + protected override IPropertyValue ConvertFirstArgument(IEnumerable 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); + } + } } \ No newline at end of file