Skip to content

Commit 36dc734

Browse files
authored
Merge pull request #285 from csf-dev/craigfowler/issue275
Resolves #275 by adding a new shared Color type which may be used to deal with color in a cross-browser manner.
2 parents 538da37 + b01e965 commit 36dc734

9 files changed

Lines changed: 1205 additions & 4 deletions

File tree

CSF.Screenplay.Selenium/CSF.Screenplay.Selenium.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
<Import Project="..\Tools\Version.props" />
55
<Import Project="..\Tools\PackageReadmes.props" />
66

7+
<PropertyGroup>
8+
<TargetFrameworks>$(TargetFrameworks);net6.0</TargetFrameworks>
9+
<DefineConstants Condition="'$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == '$(DotNetLatestLts)'">$(DefineConstants);RECORD_STRUCT_SUPPORT</DefineConstants>
10+
</PropertyGroup>
11+
12+
713
<PropertyGroup>
814
<RootNamespace>CSF.Screenplay.Selenium</RootNamespace>
915
<DocumentationFile>$(MSBuildProjectDirectory)\bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>

CSF.Screenplay.Selenium/Color.cs

Lines changed: 445 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
using System;
2+
using System.Text.RegularExpressions;
3+
4+
namespace CSF.Screenplay.Selenium
5+
{
6+
/// <summary>
7+
/// An object which can get an instance of <see cref="Color"/>.
8+
/// </summary>
9+
internal interface IParsesColor
10+
{
11+
/// <summary>
12+
/// Attempts to parse an instance of <see cref="Color"/> from the specified <paramref name="colorValue"/>.
13+
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// If the parsing is successful then this method will return <see langword="true"/> and the <paramref name="color"/>
17+
/// will contain the parsed color value.
18+
/// Otherwise it will return <see langword="false"/> and the <paramref name="color"/> parameter must be ignored.
19+
/// </para>
20+
/// </remarks>
21+
/// <param name="colorValue">A string representing a color.</param>
22+
/// <param name="color">If this method returns <see langword="true"/> then this parameter exposes the parsed
23+
/// color; if not then this value must be ignored.</param>
24+
/// <returns><see langword="true"/> if the parsing was a success; <see langword="false"/> if not.</returns>
25+
bool TryParseColor(string colorValue, out Color color);
26+
}
27+
28+
internal abstract class NumericColorParser : IParsesColor
29+
{
30+
protected static readonly string
31+
BytePart = @"\s*(\d{1,3})\s*",
32+
DecimalAlphaPart = @"\s*(0|1|0\.\d+)\s*",
33+
PercentDecimalPart = @"\s*(\d{1,3}|\d{1,2}\.\d+)%\s*",
34+
PercentPart = @"\s*(\d{1,3})%\s*",
35+
Hex2Part = @"([0-9a-f]{2})",
36+
Hex1Part = @"([0-9a-f])";
37+
38+
/// <inheritdoc/>
39+
public virtual bool TryParseColor(string colorValue, out Color color)
40+
{
41+
color = default;
42+
var match = GetParsingRegex(out var includesAlpha).Match(colorValue);
43+
if(!match.Success) return false;
44+
45+
color = new Color(GetByte(match.Groups[1]),
46+
GetByte(match.Groups[2]),
47+
GetByte(match.Groups[3]),
48+
includesAlpha ? double.Parse(match.Groups[4].Value) : 1);
49+
return true;
50+
}
51+
52+
/// <summary>
53+
/// Gets a regex which parses a string color representation.
54+
/// </summary>
55+
/// <param name="includesAlpha"><see langword="true"/> if this regex includes a value for the alpha channel; <see langword="false"/> if not.</param>
56+
/// <returns>A regex</returns>
57+
protected virtual Regex GetParsingRegex(out bool includesAlpha)
58+
=> new Regex(GetParsingPattern(out includesAlpha),
59+
RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.IgnoreCase,
60+
TimeSpan.FromMilliseconds(500));
61+
62+
/// <summary>
63+
/// Gets a regex pattern which parses a string color representation.
64+
/// </summary>
65+
/// <param name="includesAlpha"><see langword="true"/> if this regex includes a value for the alpha channel; <see langword="false"/> if not.</param>
66+
/// <returns>A regex</returns>
67+
protected abstract string GetParsingPattern(out bool includesAlpha);
68+
69+
/// <summary>
70+
/// Parses a byte value from the value of the match group.
71+
/// </summary>
72+
/// <param name="matchGroup">A regex match group</param>
73+
/// <returns>A byte value</returns>
74+
protected virtual byte GetByte(Group matchGroup) => byte.Parse(matchGroup.Value);
75+
}
76+
77+
internal class RgbColorParser : NumericColorParser
78+
{
79+
/// <inheritdoc/>
80+
protected override string GetParsingPattern(out bool includesAlpha)
81+
{
82+
includesAlpha = false;
83+
return $"^\\s*rgb\\({BytePart},{BytePart},{BytePart}\\)\\s*$";
84+
}
85+
}
86+
87+
internal class RgbaColorParser : NumericColorParser
88+
{
89+
/// <inheritdoc/>
90+
protected override string GetParsingPattern(out bool includesAlpha)
91+
{
92+
includesAlpha = true;
93+
return $"^\\s*rgba\\({BytePart},{BytePart},{BytePart},{DecimalAlphaPart}\\)\\s*$";
94+
}
95+
}
96+
97+
internal class RgbPercentageColorParser : NumericColorParser
98+
{
99+
/// <inheritdoc/>
100+
protected override string GetParsingPattern(out bool includesAlpha)
101+
{
102+
includesAlpha = false;
103+
return $"^\\s*rgb\\({PercentDecimalPart},{PercentDecimalPart},{PercentDecimalPart}\\)\\s*$";
104+
}
105+
106+
/// <inheritdoc/>
107+
protected override byte GetByte(Group matchGroup) => (byte) (double.Parse(matchGroup.Value) / 100D * 255D);
108+
}
109+
110+
internal class RgbaPercentageColorParser : RgbPercentageColorParser
111+
{
112+
/// <inheritdoc/>
113+
protected override string GetParsingPattern(out bool includesAlpha)
114+
{
115+
includesAlpha = true;
116+
return $"^\\s*rgba\\({PercentDecimalPart},{PercentDecimalPart},{PercentDecimalPart},{DecimalAlphaPart}\\)\\s*$";
117+
}
118+
}
119+
120+
internal class Hex3ColorParser : NumericColorParser
121+
{
122+
/// <inheritdoc/>
123+
protected override string GetParsingPattern(out bool includesAlpha)
124+
{
125+
includesAlpha = false;
126+
return $"^\\s*#{Hex1Part}{Hex1Part}{Hex1Part}\\s*$";
127+
}
128+
129+
/// <inheritdoc/>
130+
protected override byte GetByte(Group matchGroup) => Convert.ToByte(matchGroup.Value + matchGroup.Value, 16);
131+
}
132+
133+
internal class Hex6ColorParser : NumericColorParser
134+
{
135+
/// <inheritdoc/>
136+
protected override string GetParsingPattern(out bool includesAlpha)
137+
{
138+
includesAlpha = false;
139+
return $"^\\s*#{Hex2Part}{Hex2Part}{Hex2Part}\\s*$";
140+
}
141+
142+
/// <inheritdoc/>
143+
protected override byte GetByte(Group matchGroup) => Convert.ToByte(matchGroup.Value, 16);
144+
}
145+
146+
internal class HslColorParser : NumericColorParser
147+
{
148+
/// <inheritdoc/>
149+
public override bool TryParseColor(string colorValue, out Color color)
150+
{
151+
color = default;
152+
var match = GetParsingRegex(out var includesAlpha).Match(colorValue);
153+
if(!match.Success) return false;
154+
155+
color = HslToRgb(short.Parse(match.Groups[1].Value),
156+
byte.Parse(match.Groups[2].Value),
157+
byte.Parse(match.Groups[3].Value),
158+
includesAlpha ? double.Parse(match.Groups[4].Value) : 1);
159+
return true;
160+
}
161+
162+
/// <inheritdoc/>
163+
protected override string GetParsingPattern(out bool includesAlpha)
164+
{
165+
includesAlpha = false;
166+
return $"^\\s*hsl\\({BytePart},{PercentPart},{PercentPart}\\)\\s*$";
167+
}
168+
169+
/// <summary>
170+
/// Converts an HSL color to the sRGB color space.
171+
/// </summary>
172+
/// <remarks>
173+
/// <para>
174+
/// This is based upon the algorithms available at
175+
/// <see href="https://github.com/SeleniumHQ/selenium/blob/selenium-4.40.0/java/src/org/openqa/selenium/support/Color.java#L258">The
176+
/// Selenium 4.40.0 HSL converter for Java</see>.
177+
/// </para>
178+
/// </remarks>
179+
/// <param name="h">The hue value in degrees (0 to 360)</param>
180+
/// <param name="s">The saturation percentage (0 to 100)</param>
181+
/// <param name="l">The lightness percentage (0 to 100)</param>
182+
/// <param name="alpha">The alpha (transparency) value (0 to 1)</param>
183+
/// <returns>An instance of <see cref="Color"/> in the sRGB color space</returns>
184+
static Color HslToRgb(short h, byte s, byte l, double alpha)
185+
{
186+
var hue = h / 360D;
187+
var saturation = s / 100D;
188+
var lightness = l / 100D;
189+
190+
// FYI: Saturation = zero means gray, making hue irrelevant
191+
if(s == 0)
192+
{
193+
var allChannels = (byte) Math.Round(lightness * 255D, MidpointRounding.AwayFromZero);
194+
return new Color(allChannels, allChannels, allChannels, alpha);
195+
}
196+
197+
var luminosity2 = (lightness < 0.5)
198+
? lightness * (saturation + 1D)
199+
: lightness + saturation - lightness * saturation;
200+
var luminosity1 = 2D * lightness - luminosity2;
201+
202+
return new Color(HueValueToRgbByte(luminosity1, luminosity2, hue + 1D / 3D),
203+
HueValueToRgbByte(luminosity1, luminosity2, hue),
204+
HueValueToRgbByte(luminosity1, luminosity2, hue - 1D / 3D),
205+
alpha);
206+
}
207+
208+
static byte HueValueToRgbByte(double luminosity1,
209+
double luminosity2,
210+
double hueValue)
211+
=> (byte) Math.Round(HueValueToRgb(luminosity1, luminosity2, hueValue) * 255D, MidpointRounding.AwayFromZero);
212+
213+
static double HueValueToRgb(double luminosity1,
214+
double luminosity2,
215+
double hueValue)
216+
{
217+
if (hueValue < 0D) hueValue += 1;
218+
if (hueValue > 1D) hueValue -= 1.0;
219+
if (hueValue < 1D / 6D) return luminosity1 + (luminosity2 - luminosity1) * 6D * hueValue;
220+
if (hueValue < 1D / 2D) return luminosity2;
221+
if (hueValue < 2D / 3D) return luminosity1 + (luminosity2 - luminosity1) * (2D / 3D - hueValue) * 6D;
222+
return luminosity1;
223+
}
224+
}
225+
226+
internal class HslaColorParser : HslColorParser
227+
{
228+
/// <inheritdoc/>
229+
protected override string GetParsingPattern(out bool includesAlpha)
230+
{
231+
includesAlpha = true;
232+
return $"^\\s*hsla\\({BytePart},{PercentPart},{PercentPart},{DecimalAlphaPart}\\)\\s*$";
233+
}
234+
}
235+
236+
internal class NamedColorParser : IParsesColor
237+
{
238+
/// <inheritdoc/>
239+
public bool TryParseColor(string colorValue, out Color color)
240+
=> Colors.TryGetNamedColor(colorValue, out color);
241+
}
242+
243+
}

0 commit comments

Comments
 (0)