diff --git a/src/Spectre.Console.Tests/Unit/StyleTests.cs b/src/Spectre.Console.Tests/Unit/StyleTests.cs index 0efa9dd..3280373 100644 --- a/src/Spectre.Console.Tests/Unit/StyleTests.cs +++ b/src/Spectre.Console.Tests/Unit/StyleTests.cs @@ -126,108 +126,54 @@ namespace Spectre.Console.Tests.Unit result.ShouldBeOfType(); result.Message.ShouldBe("Could not find color 'lol'."); } + + [Theory] + [InlineData("#FF0000 on #0000FF")] + [InlineData("#F00 on #00F")] + public void Should_Parse_Hex_Colors_Correctly(string style) + { + // Given, When + var result = Style.Parse(style); + + // Then + result.Foreground.ShouldBe(Color.Red); + result.Background.ShouldBe(Color.Blue); + } + + [Theory] + [InlineData("#", "Invalid hex color '#'.")] + [InlineData("#FF00FF00FF", "Invalid hex color '#FF00FF00FF'.")] + [InlineData("#FOO", "Invalid hex color '#FOO'. Could not find any recognizable digits.")] + public void Should_Return_Error_If_Hex_Color_Is_Invalid(string style, string expected) + { + // Given, When + var result = Record.Exception(() => Style.Parse(style)); + + // Then + result.ShouldNotBeNull(); + result.Message.ShouldBe(expected); + } } public sealed class TheTryParseMethod { [Fact] - public void Default_Keyword_Should_Return_Default_Style() + public void Should_Return_True_If_Parsing_Succeeded() { // Given, When - var result = Style.TryParse("default", out var style); + var result = Style.TryParse("bold", out var style); // Then result.ShouldBeTrue(); style.ShouldNotBeNull(); - style.Foreground.ShouldBe(Color.Default); - style.Background.ShouldBe(Color.Default); - style.Decoration.ShouldBe(Decoration.None); - } - - [Theory] - [InlineData("bold", Decoration.Bold)] - [InlineData("dim", Decoration.Dim)] - [InlineData("italic", Decoration.Italic)] - [InlineData("underline", Decoration.Underline)] - [InlineData("invert", Decoration.Invert)] - [InlineData("conceal", Decoration.Conceal)] - [InlineData("slowblink", Decoration.SlowBlink)] - [InlineData("rapidblink", Decoration.RapidBlink)] - [InlineData("strikethrough", Decoration.Strikethrough)] - public void Should_Parse_Decoration(string text, Decoration decoration) - { - // Given, When - var result = Style.TryParse(text, out var style); - - // Then - result.ShouldBeTrue(); - style.ShouldNotBeNull(); - style.Decoration.ShouldBe(decoration); + style.Decoration.ShouldBe(Decoration.Bold); } [Fact] - public void Should_Parse_Text_And_Decoration() + public void Should_Return_False_If_Parsing_Failed() { // Given, When - var result = Style.TryParse("bold underline blue on green", out var style); - - // Then - result.ShouldBeTrue(); - style.ShouldNotBeNull(); - style.Decoration.ShouldBe(Decoration.Bold | Decoration.Underline); - style.Foreground.ShouldBe(Color.Blue); - style.Background.ShouldBe(Color.Green); - } - - [Fact] - public void Should_Parse_Background_If_Foreground_Is_Set_To_Default() - { - // Given, When - var result = Style.TryParse("default on green", out var style); - - // Then - result.ShouldBeTrue(); - style.ShouldNotBeNull(); - style.Decoration.ShouldBe(Decoration.None); - style.Foreground.ShouldBe(Color.Default); - style.Background.ShouldBe(Color.Green); - } - - [Fact] - public void Should_Throw_If_Foreground_Is_Set_Twice() - { - // Given, When - var result = Style.TryParse("green yellow", out _); - - // Then - result.ShouldBeFalse(); - } - - [Fact] - public void Should_Throw_If_Background_Is_Set_Twice() - { - // Given, When - var result = Style.TryParse("green on blue yellow", out _); - - // Then - result.ShouldBeFalse(); - } - - [Fact] - public void Should_Throw_If_Color_Name_Could_Not_Be_Found() - { - // Given, When - var result = Style.TryParse("bold lol", out _); - - // Then - result.ShouldBeFalse(); - } - - [Fact] - public void Should_Throw_If_Background_Color_Name_Could_Not_Be_Found() - { - // Given, When - var result = Style.TryParse("blue on lol", out _); + var result = Style.TryParse("lol", out _); // Then result.ShouldBeFalse(); diff --git a/src/Spectre.Console/Internal/Text/StyleParser.cs b/src/Spectre.Console/Internal/Text/StyleParser.cs index efcdf4a..d432e91 100644 --- a/src/Spectre.Console/Internal/Text/StyleParser.cs +++ b/src/Spectre.Console/Internal/Text/StyleParser.cs @@ -57,16 +57,22 @@ namespace Spectre.Console.Internal var color = ColorTable.GetColor(part); if (color == null) { - if (!foreground) + if (part.StartsWith("#", StringComparison.OrdinalIgnoreCase)) { - error = $"Could not find color '{part}'."; + color = ParseHexColor(part, out error); + if (!string.IsNullOrWhiteSpace(error)) + { + return null; + } } else { - error = $"Could not find color or style '{part}'."; - } + error = !foreground + ? $"Could not find color '{part}'." + : $"Could not find color or style '{part}'."; - return null; + return null; + } } if (foreground) @@ -95,5 +101,42 @@ namespace Spectre.Console.Internal error = null; return new Style(effectiveForeground, effectiveBackground, effectiveDecoration); } + + private static Color? ParseHexColor(string hex, out string error) + { + error = null; + + hex = hex ?? string.Empty; + hex = hex.Replace("#", string.Empty).Trim(); + + try + { + if (!string.IsNullOrWhiteSpace(hex)) + { + if (hex.Length == 6) + { + return new Color( + (byte)Convert.ToUInt32(hex.Substring(0, 2), 16), + (byte)Convert.ToUInt32(hex.Substring(2, 2), 16), + (byte)Convert.ToUInt32(hex.Substring(4, 2), 16)); + } + else if (hex.Length == 3) + { + return new Color( + (byte)Convert.ToUInt32(new string(hex[0], 2), 16), + (byte)Convert.ToUInt32(new string(hex[1], 2), 16), + (byte)Convert.ToUInt32(new string(hex[2], 2), 16)); + } + } + } + catch (Exception ex) + { + error = $"Invalid hex color '#{hex}'. {ex.Message}"; + return null; + } + + error = $"Invalid hex color '#{hex}'."; + return null; + } } }