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
40 changes: 40 additions & 0 deletions src/ExCSS.Tests/Gradient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,5 +242,45 @@ public void BackgroundImageRepeatingRadialGradientFunky()
Assert.True(backgroundImage.HasValue);
Assert.False(backgroundImage.IsInitial);
}

[Theory]
// <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. 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<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)")]
[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<BackgroundImageProperty>(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);
}
}
}
46 changes: 36 additions & 10 deletions src/ExCSS/ValueConverters/GradientConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,31 @@ private static IPropertyValue[] ToGradientStops(List<List<Token>> values, int of
private static IPropertyValue ToGradientStop(List<Token> 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);
}

// <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.
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)
Expand All @@ -54,32 +71,41 @@ private static IPropertyValue ToGradientStop(List<Token> 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 <color-stop>; a bare <length-percentage> with no
// colour is a <linear-color-hint>, 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<Token> 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<Token> tokens)
public StopValue(IPropertyValue color, IPropertyValue firstPosition, IPropertyValue secondPosition,
IEnumerable<Token> tokens)
{
_color = color;
_position = position;
_firstPosition = firstPosition;
_secondPosition = secondPosition;
Original = new TokenValue(tokens);
}

public string CssText
{
get
{
if (_color == null && _position != null) return _position.CssText;
var parts = new List<string>(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);
}
}

Expand Down