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