diff --git a/src/ExCSS.Tests/Gradient.cs b/src/ExCSS.Tests/Gradient.cs index 8d11581b..8fecb111 100644 --- a/src/ExCSS.Tests/Gradient.cs +++ b/src/ExCSS.Tests/Gradient.cs @@ -242,5 +242,45 @@ public void BackgroundImageRepeatingRadialGradientFunky() Assert.True(backgroundImage.HasValue); Assert.False(backgroundImage.IsInitial); } + + [Theory] + // = {1,2} (CSS Images 4 3.5.1): a stop may carry two + // positions, equivalent to two same-colour stops. Both positions are preserved when serialized. + [InlineData("background-image: linear-gradient(red 0 50%, blue 50% 100%)", + "linear-gradient(rgb(255, 0, 0) 0 50%, rgb(0, 0, 255) 50% 100%)")] + [InlineData("background-image: linear-gradient(90deg, red 0 8px, blue)", + "linear-gradient(90deg, rgb(255, 0, 0) 0 8px, rgb(0, 0, 255))")] + [InlineData("background-image: radial-gradient(red 0 8px, blue)", + "radial-gradient(rgb(255, 0, 0) 0 8px, rgb(0, 0, 255))")] + public void GradientTwoPositionColorStopLegal(string snippet, string expected) + { + var property = ParseDeclaration(snippet); + 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)")] + [InlineData("background-image: linear-gradient(red 50%, blue)")] + [InlineData("background-image: linear-gradient(red, 25%, blue)")] + public void GradientSinglePositionAndHintStillLegal(string snippet) + { + var property = ParseDeclaration(snippet); + Assert.IsType(property); + Assert.True(((BackgroundImageProperty)property).HasValue); + } + + [Theory] + // Three positions on one stop, and two bare positions with no colour, remain invalid. + [InlineData("background-image: linear-gradient(red 0 25% 50%, blue)")] + [InlineData("background-image: linear-gradient(0 50%, blue)")] + public void GradientMalformedColorStopIllegal(string snippet) + { + var property = ParseDeclaration(snippet); + Assert.False(((BackgroundImageProperty)property).HasValue); + } } } diff --git a/src/ExCSS/ValueConverters/GradientConverter.cs b/src/ExCSS/ValueConverters/GradientConverter.cs index aa55bd47..6643eb34 100644 --- a/src/ExCSS/ValueConverters/GradientConverter.cs +++ b/src/ExCSS/ValueConverters/GradientConverter.cs @@ -37,14 +37,31 @@ private static IPropertyValue[] ToGradientStops(List> values, int of private static IPropertyValue ToGradientStop(List value) { var color = default(IPropertyValue); - var position = default(IPropertyValue); + var firstPosition = default(IPropertyValue); + var secondPosition = default(IPropertyValue); var items = value.ToItems(); if (items.Count != 0) { - position = LengthOrPercentConverter.Convert(items[items.Count - 1]); + firstPosition = LengthOrPercentConverter.Convert(items[items.Count - 1]); - if (position != null) items.RemoveAt(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. + if (firstPosition != null && items.Count != 0) + { + var earlier = LengthOrPercentConverter.Convert(items[items.Count - 1]); + + if (earlier != null) + { + secondPosition = firstPosition; + firstPosition = earlier; + items.RemoveAt(items.Count - 1); + } } if (items.Count != 0) @@ -54,7 +71,11 @@ private static IPropertyValue ToGradientStop(List value) if (color != null) items.RemoveAt(items.Count - 1); } - return items.Count == 0 ? new StopValue(color, position, value) : null; + // The two-position form is only defined for a ; a bare with no + // colour is a , which takes exactly one position. + if (secondPosition != null && color == null) return null; + + return items.Count == 0 ? new StopValue(color, firstPosition, secondPosition, value) : null; } protected abstract IPropertyValue ConvertFirstArgument(IEnumerable value); @@ -62,12 +83,15 @@ private static IPropertyValue ToGradientStop(List value) private sealed class StopValue : IPropertyValue { private readonly IPropertyValue _color; - private readonly IPropertyValue _position; + private readonly IPropertyValue _firstPosition; + private readonly IPropertyValue _secondPosition; - public StopValue(IPropertyValue color, IPropertyValue position, IEnumerable tokens) + public StopValue(IPropertyValue color, IPropertyValue firstPosition, IPropertyValue secondPosition, + IEnumerable tokens) { _color = color; - _position = position; + _firstPosition = firstPosition; + _secondPosition = secondPosition; Original = new TokenValue(tokens); } @@ -75,11 +99,13 @@ public string CssText { get { - if (_color == null && _position != null) return _position.CssText; + var parts = new List(3); - if (_color != null && _position == null) return _color.CssText; + if (_color != null) parts.Add(_color.CssText); + if (_firstPosition != null) parts.Add(_firstPosition.CssText); + if (_secondPosition != null) parts.Add(_secondPosition.CssText); - return string.Concat(_color?.CssText, " ", _position.CssText); + return string.Join(" ", parts); } }