diff --git a/docs/input/assets/images/custom_exception.png b/docs/input/assets/images/custom_exception.png new file mode 100644 index 0000000..23b730d Binary files /dev/null and b/docs/input/assets/images/custom_exception.png differ diff --git a/docs/input/exceptions.md b/docs/input/exceptions.md index 3ac7231..14d6bb5 100644 --- a/docs/input/exceptions.md +++ b/docs/input/exceptions.md @@ -9,9 +9,9 @@ You can make exception a bit more readable by using the `WriteException` method. AnsiConsole.WriteException(ex); ``` - - + +## Shortening parts You can also shorten specific parts of the exception to make it even more readable, and make paths clickable hyperlinks. Whether or not @@ -24,3 +24,29 @@ AnsiConsole.WriteException(ex, ``` + +## Customizing exception output + +In addition to shorten specific part of the exception, you can +also override the default styling. + +```csharp +AnsiConsole.WriteException(ex, new ExceptionSettings +{ + Format = ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks, + Style = new ExceptionStyle + { + Exception = Style.WithForeground(Color.Grey), + Message = Style.WithForeground(Color.White), + NonEmphasized = Style.WithForeground(Color.Cornsilk1), + Parenthesis = Style.WithForeground(Color.Cornsilk1), + Method = Style.WithForeground(Color.Red), + ParameterName = Style.WithForeground(Color.Cornsilk1), + ParameterType = Style.WithForeground(Color.Red), + Path = Style.WithForeground(Color.Red), + LineNumber = Style.WithForeground(Color.Cornsilk1), + } +}); +``` + + diff --git a/docs/input/markup.md b/docs/input/markup.md index 87f3d22..d3323a7 100644 --- a/docs/input/markup.md +++ b/docs/input/markup.md @@ -43,6 +43,12 @@ AnsiConsole.Markup("[[Hello]] "); // [Hello] AnsiConsole.Markup("[red][[World]][/]"); // [World] ``` +You can also use the `SafeMarkup` extension method. + +```csharp +AnsiConsole.Markup("[red]{0}[/]", "Hello [World]".SafeMarkup()); +``` + # Setting background color You can set the background color in markup by prefixing the color with diff --git a/examples/Exceptions/Program.cs b/examples/Exceptions/Program.cs index 7e10289..1a644cb 100644 --- a/examples/Exceptions/Program.cs +++ b/examples/Exceptions/Program.cs @@ -14,11 +14,35 @@ namespace Exceptions } catch (Exception ex) { + AnsiConsole.WriteLine(); + AnsiConsole.Render(new Panel("[u]Default[/]").Expand()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex); + AnsiConsole.WriteLine(); + AnsiConsole.Render(new Panel("[u]Compact[/]").Expand()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks); + + AnsiConsole.WriteLine(); + AnsiConsole.Render(new Panel("[u]Custom colors[/]").Expand()); + AnsiConsole.WriteLine(); + AnsiConsole.WriteException(ex, new ExceptionSettings + { + Format = ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks, + Style = new ExceptionStyle + { + Exception = Style.WithForeground(Color.Grey), + Message = Style.WithForeground(Color.White), + NonEmphasized = Style.WithForeground(Color.Cornsilk1), + Parenthesis = Style.WithForeground(Color.Cornsilk1), + Method = Style.WithForeground(Color.Red), + ParameterName = Style.WithForeground(Color.Cornsilk1), + ParameterType = Style.WithForeground(Color.Red), + Path = Style.WithForeground(Color.Red), + LineNumber = Style.WithForeground(Color.Cornsilk1), + } + }); } } diff --git a/src/Spectre.Console.Tests/Unit/ColorTests.cs b/src/Spectre.Console.Tests/Unit/ColorTests.cs index 1e6f1f6..259cd4c 100644 --- a/src/Spectre.Console.Tests/Unit/ColorTests.cs +++ b/src/Spectre.Console.Tests/Unit/ColorTests.cs @@ -228,6 +228,39 @@ namespace Spectre.Console.Tests.Unit } } + public sealed class TheToMarkupMethod + { + [Fact] + public void Should_Return_Expected_Markup_For_Default_Color() + { + // Given, When + var result = Color.Default.ToMarkup(); + + // Then + result.ShouldBe("default"); + } + + [Fact] + public void Should_Return_Expected_Markup_For_Known_Color() + { + // Given, When + var result = Color.Red.ToMarkup(); + + // Then + result.ShouldBe("red"); + } + + [Fact] + public void Should_Return_Expected_Markup_For_Custom_Color() + { + // Given, When + var result = new Color(255, 1, 12).ToMarkup(); + + // Then + result.ShouldBe("#FF010C"); + } + } + public sealed class TheToStringMethod { [Fact] diff --git a/src/Spectre.Console.Tests/Unit/StyleTests.cs b/src/Spectre.Console.Tests/Unit/StyleTests.cs index 9b246ac..f90bde0 100644 --- a/src/Spectre.Console.Tests/Unit/StyleTests.cs +++ b/src/Spectre.Console.Tests/Unit/StyleTests.cs @@ -317,5 +317,60 @@ namespace Spectre.Console.Tests.Unit result.ShouldBeFalse(); } } + + public sealed class TheToMarkupMethod + { + [Fact] + public void Should_Return_Expected_Markup_For_Style_With_Foreground_Color() + { + // Given + var style = new Style(Color.Red); + + // When + var result = style.ToMarkup(); + + // Then + result.ShouldBe("red"); + } + + [Fact] + public void Should_Return_Expected_Markup_For_Style_With_Foreground_And_Background_Color() + { + // Given + var style = new Style(Color.Red, Color.Green); + + // When + var result = style.ToMarkup(); + + // Then + result.ShouldBe("red on green"); + } + + [Fact] + public void Should_Return_Expected_Markup_For_Style_With_Foreground_And_Background_Color_And_Decoration() + { + // Given + var style = new Style(Color.Red, Color.Green, Decoration.Bold | Decoration.Underline); + + // When + var result = style.ToMarkup(); + + // Then + result.ShouldBe("bold underline red on green"); + } + + [Fact] + public void Should_Return_Expected_Markup_For_Style_With_Only_Background_Color() + { + // Given + var style = new Style(background: Color.Green); + + // When + var result = style.ToMarkup(); + + // Then + result.ShouldBe("default on green"); + } + } } } diff --git a/src/Spectre.Console/AnsiConsole.Exceptions.cs b/src/Spectre.Console/AnsiConsole.Exceptions.cs index e3730a5..133fff1 100644 --- a/src/Spectre.Console/AnsiConsole.Exceptions.cs +++ b/src/Spectre.Console/AnsiConsole.Exceptions.cs @@ -16,5 +16,15 @@ namespace Spectre.Console { Console.WriteException(exception, format); } + + /// + /// Writes an exception to the console. + /// + /// The exception to write to the console. + /// The exception settings. + public static void WriteException(Exception exception, ExceptionSettings settings) + { + Console.WriteException(exception, settings); + } } } diff --git a/src/Spectre.Console/Color.cs b/src/Spectre.Console/Color.cs index f79d452..434e7cd 100644 --- a/src/Spectre.Console/Color.cs +++ b/src/Spectre.Console/Color.cs @@ -249,8 +249,13 @@ namespace Spectre.Console /// Converts the color to a markup string. /// /// A representing the color as markup. - public string ToMarkupString() + public string ToMarkup() { + if (IsDefault) + { + return "default"; + } + if (Number != null) { var name = ColorTable.GetName(Number.Value); diff --git a/src/Spectre.Console/ExceptionSettings.cs b/src/Spectre.Console/ExceptionSettings.cs new file mode 100644 index 0000000..5b7ff18 --- /dev/null +++ b/src/Spectre.Console/ExceptionSettings.cs @@ -0,0 +1,27 @@ +namespace Spectre.Console +{ + /// + /// Exception settings. + /// + public sealed class ExceptionSettings + { + /// + /// Gets or sets the exception format. + /// + public ExceptionFormats Format { get; set; } + + /// + /// Gets or sets the exception style. + /// + public ExceptionStyle Style { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public ExceptionSettings() + { + Format = ExceptionFormats.Default; + Style = new ExceptionStyle(); + } + } +} diff --git a/src/Spectre.Console/ExceptionStyle.cs b/src/Spectre.Console/ExceptionStyle.cs new file mode 100644 index 0000000..b144992 --- /dev/null +++ b/src/Spectre.Console/ExceptionStyle.cs @@ -0,0 +1,58 @@ +namespace Spectre.Console +{ + /// + /// Represent an exception style. + /// + public sealed class ExceptionStyle + { + /// + /// Gets or sets the message color. + /// + public Style Message { get; set; } = new Style(Color.Red, Color.Default, Decoration.Bold); + + /// + /// Gets or sets the exception color. + /// + public Style Exception { get; set; } = new Style(Color.White); + + /// + /// Gets or sets the method color. + /// + public Style Method { get; set; } = new Style(Color.Yellow); + + /// + /// Gets or sets the parameter type color. + /// + public Style ParameterType { get; set; } = new Style(Color.Blue); + + /// + /// Gets or sets the parameter name color. + /// + public Style ParameterName { get; set; } = new Style(Color.Silver); + + /// + /// Gets or sets the parenthesis color. + /// + public Style Parenthesis { get; set; } = new Style(Color.Silver); + + /// + /// Gets or sets the path color. + /// + public Style Path { get; set; } = new Style(Color.Yellow, Color.Default, Decoration.Bold); + + /// + /// Gets or sets the line number color. + /// + public Style LineNumber { get; set; } = new Style(Color.Blue); + + /// + /// Gets or sets the color for dimmed text such as "at" or "in". + /// + public Style Dimmed { get; set; } = new Style(Color.Grey); + + /// + /// Gets or sets the color for non emphasized items. + /// + public Style NonEmphasized { get; set; } = new Style(Color.Silver); + } +} diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Exceptions.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Exceptions.cs index a8a445a..6319a5c 100644 --- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Exceptions.cs +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Exceptions.cs @@ -17,5 +17,16 @@ namespace Spectre.Console { Render(console, exception.GetRenderable(format)); } + + /// + /// Writes an exception to the console. + /// + /// The console. + /// The exception to write to the console. + /// The exception settings. + public static void WriteException(this IAnsiConsole console, Exception exception, ExceptionSettings settings) + { + Render(console, exception.GetRenderable(settings)); + } } } diff --git a/src/Spectre.Console/Extensions/ExceptionExtensions.cs b/src/Spectre.Console/Extensions/ExceptionExtensions.cs index 0d2ffda..aa0c0aa 100644 --- a/src/Spectre.Console/Extensions/ExceptionExtensions.cs +++ b/src/Spectre.Console/Extensions/ExceptionExtensions.cs @@ -16,7 +16,36 @@ namespace Spectre.Console /// A representing the exception. public static IRenderable GetRenderable(this Exception exception, ExceptionFormats format = ExceptionFormats.Default) { - return ExceptionFormatter.Format(exception, format); + if (exception is null) + { + throw new ArgumentNullException(nameof(exception)); + } + + return GetRenderable(exception, new ExceptionSettings + { + Format = format, + }); + } + + /// + /// Gets a representation of the exception. + /// + /// The exception to format. + /// The exception settings. + /// A representing the exception. + public static IRenderable GetRenderable(this Exception exception, ExceptionSettings settings) + { + if (exception is null) + { + throw new ArgumentNullException(nameof(exception)); + } + + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + + return ExceptionFormatter.Format(exception, settings); } } } diff --git a/src/Spectre.Console/Internal/DecorationTable.cs b/src/Spectre.Console/Internal/DecorationTable.cs index f84c4b7..2f2ac32 100644 --- a/src/Spectre.Console/Internal/DecorationTable.cs +++ b/src/Spectre.Console/Internal/DecorationTable.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Spectre.Console.Internal { internal static class DecorationTable { private static readonly Dictionary _lookup; + private static readonly Dictionary _reverseLookup; [SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline")] static DecorationTable() @@ -28,6 +30,21 @@ namespace Spectre.Console.Internal { "strikethrough", Decoration.Strikethrough }, { "s", Decoration.Strikethrough }, }; + + _reverseLookup = new Dictionary(); + foreach (var (name, decoration) in _lookup) + { + // Cannot happen, but the compiler thinks so... + if (decoration == null) + { + continue; + } + + if (!_reverseLookup.ContainsKey(decoration.Value)) + { + _reverseLookup[decoration.Value] = name; + } + } } public static Decoration? GetDecoration(string name) @@ -35,5 +52,23 @@ namespace Spectre.Console.Internal _lookup.TryGetValue(name, out var result); return result; } + + public static List GetMarkupNames(Decoration decoration) + { + var result = new List(); + + Enum.GetValues(typeof(Decoration)) + .Cast() + .Where(flag => (decoration & flag) != 0) + .ForEach(flag => + { + if (flag != Decoration.None && _reverseLookup.TryGetValue(flag, out var name)) + { + result.Add(name); + } + }); + + return result; + } } } diff --git a/src/Spectre.Console/Internal/ExceptionFormatter.cs b/src/Spectre.Console/Internal/ExceptionFormatter.cs index 2a305b2..8e3ecde 100644 --- a/src/Spectre.Console/Internal/ExceptionFormatter.cs +++ b/src/Spectre.Console/Internal/ExceptionFormatter.cs @@ -8,13 +8,7 @@ namespace Spectre.Console { internal static class ExceptionFormatter { - private static readonly Color _typeColor = Color.White; - private static readonly Color _methodColor = Color.Yellow; - private static readonly Color _parameterColor = Color.Blue; - private static readonly Color _pathColor = Color.Yellow; - private static readonly Color _dimmedColor = Color.Grey; - - public static IRenderable Format(Exception exception, ExceptionFormats format) + public static IRenderable Format(Exception exception, ExceptionSettings settings) { if (exception is null) { @@ -27,10 +21,10 @@ namespace Spectre.Console return new Text(exception.ToString()); } - return GetException(info, format); + return GetException(info, settings); } - private static IRenderable GetException(ExceptionInfo info, ExceptionFormats format) + private static IRenderable GetException(ExceptionInfo info, ExceptionSettings settings) { if (info is null) { @@ -39,20 +33,20 @@ namespace Spectre.Console return new Rows(new IRenderable[] { - GetMessage(info, format), - GetStackFrames(info, format), + GetMessage(info, settings), + GetStackFrames(info, settings), }).Expand(); } - private static Markup GetMessage(ExceptionInfo ex, ExceptionFormats format) + private static Markup GetMessage(ExceptionInfo ex, ExceptionSettings settings) { - var shortenTypes = (format & ExceptionFormats.ShortenTypes) != 0; - var type = Emphasize(ex.Type, new[] { '.' }, _typeColor.ToMarkupString(), shortenTypes); - var message = $"[b red]{ex.Message.SafeMarkup()}[/]"; + var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0; + var type = Emphasize(ex.Type, new[] { '.' }, settings.Style.Exception, shortenTypes, settings); + var message = $"[{settings.Style.Message.ToMarkup()}]{ex.Message.SafeMarkup()}[/]"; return new Markup(string.Concat(type, ": ", message)); } - private static Grid GetStackFrames(ExceptionInfo ex, ExceptionFormats format) + private static Grid GetStackFrames(ExceptionInfo ex, ExceptionSettings settings) { var grid = new Grid(); grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap()); @@ -63,7 +57,7 @@ namespace Spectre.Console { grid.AddRow( Text.Empty, - GetException(ex.Inner, format)); + GetException(ex.Inner, settings)); } // Stack frames @@ -72,47 +66,57 @@ namespace Spectre.Console var builder = new StringBuilder(); // Method - var shortenMethods = (format & ExceptionFormats.ShortenMethods) != 0; - builder.Append(Emphasize(frame.Method, new[] { '.' }, _methodColor.ToMarkupString(), shortenMethods)); - builder.Append('('); - builder.Append(string.Join(", ", frame.Parameters.Select(x => $"[{_parameterColor.ToMarkupString()}]{x.Type.SafeMarkup()}[/] {x.Name}"))); - builder.Append(')'); + var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0; + builder.Append(Emphasize(frame.Method, new[] { '.' }, settings.Style.Method, shortenMethods, settings)); + builder.Append('[').Append(settings.Style.Parenthesis.ToMarkup()).Append(']').Append('(').Append("[/]"); + AppendParameters(builder, frame, settings); + builder.Append('[').Append(settings.Style.Parenthesis.ToMarkup()).Append(']').Append(')').Append("[/]"); if (frame.Path != null) { - builder.Append(" [").Append(_dimmedColor.ToMarkupString()).Append("]in[/] "); + builder.Append(" [").Append(settings.Style.Dimmed.ToMarkup()).Append("]in[/] "); // Path - AppendPath(builder, frame, format); + AppendPath(builder, frame, settings); // Line number if (frame.LineNumber != null) { - builder.Append(':'); - builder.Append('[').Append(_parameterColor.ToMarkupString()).Append(']').Append(frame.LineNumber).Append("[/]"); + builder.Append('[').Append(settings.Style.Dimmed.ToMarkup()).Append("]:[/]"); + builder.Append('[').Append(settings.Style.LineNumber.ToMarkup()).Append(']').Append(frame.LineNumber).Append("[/]"); } } - grid.AddRow($"[{_dimmedColor.ToMarkupString()}]at[/]", builder.ToString()); + grid.AddRow( + $"[{settings.Style.Dimmed.ToMarkup()}]at[/]", + builder.ToString()); } return grid; } - private static void AppendPath(StringBuilder builder, StackFrameInfo frame, ExceptionFormats format) + private static void AppendParameters(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings) + { + var typeColor = settings.Style.ParameterType.ToMarkup(); + var nameColor = settings.Style.ParameterName.ToMarkup(); + var parameters = frame.Parameters.Select(x => $"[{typeColor}]{x.Type.SafeMarkup()}[/] [{nameColor}]{x.Name}[/]"); + builder.Append(string.Join(", ", parameters)); + } + + private static void AppendPath(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings) { if (frame?.Path is null) { return; } - void RenderLink() + void AppendPath() { - var shortenPaths = (format & ExceptionFormats.ShortenPaths) != 0; - builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, $"b {_pathColor.ToMarkupString()}", shortenPaths)); + var shortenPaths = (settings.Format & ExceptionFormats.ShortenPaths) != 0; + builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, settings.Style.Path, shortenPaths, settings)); } - if ((format & ExceptionFormats.ShowLinks) != 0) + if ((settings.Format & ExceptionFormats.ShowLinks) != 0) { var hasLink = frame.TryGetUri(out var uri); if (hasLink && uri != null) @@ -120,7 +124,7 @@ namespace Spectre.Console builder.Append("[link=").Append(uri.AbsoluteUri).Append(']'); } - RenderLink(); + AppendPath(); if (hasLink && uri != null) { @@ -129,11 +133,11 @@ namespace Spectre.Console } else { - RenderLink(); + AppendPath(); } } - private static string Emphasize(string input, char[] separators, string color, bool compact) + private static string Emphasize(string input, char[] separators, Style color, bool compact, ExceptionSettings settings) { var builder = new StringBuilder(); @@ -143,10 +147,12 @@ namespace Spectre.Console { if (!compact) { - builder.Append("[silver]").Append(type, 0, index + 1).Append("[/]"); + builder.Append('[').Append(settings.Style.NonEmphasized.ToMarkup()).Append(']') + .Append(type, 0, index + 1).Append("[/]"); } - builder.Append('[').Append(color).Append(']').Append(type, index + 1, type.Length - index - 1).Append("[/]"); + builder.Append('[').Append(color.ToMarkup()).Append(']') + .Append(type, index + 1, type.Length - index - 1).Append("[/]"); } else { diff --git a/src/Spectre.Console/Internal/Extensions/DictionaryExtensions.cs b/src/Spectre.Console/Internal/Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000..bee3b3d --- /dev/null +++ b/src/Spectre.Console/Internal/Extensions/DictionaryExtensions.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Spectre.Console.Internal +{ + internal static class DictionaryExtensions + { + public static void Deconstruct(this KeyValuePair tuple, out T1 key, out T2 value) + { + key = tuple.Key; + value = tuple.Value; + } + } +} diff --git a/src/Spectre.Console/Style.cs b/src/Spectre.Console/Style.cs index 4eb7bad..b74993e 100644 --- a/src/Spectre.Console/Style.cs +++ b/src/Spectre.Console/Style.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Spectre.Console.Internal; namespace Spectre.Console @@ -25,7 +27,7 @@ namespace Spectre.Console public Decoration Decoration { get; } /// - /// Gets the link. + /// Gets the link associated with the style. /// public string? Link { get; } @@ -191,6 +193,41 @@ namespace Spectre.Console } } + /// + /// Returns the markup representation of this style. + /// + /// The markup representation of this style. + public string ToMarkup() + { + var builder = new List(); + + if (Decoration != Decoration.None) + { + var result = DecorationTable.GetMarkupNames(Decoration); + if (result.Count != 0) + { + builder.AddRange(result); + } + } + + if (Foreground != Color.Default) + { + builder.Add(Foreground.ToMarkup()); + } + + if (Background != Color.Default) + { + if (builder.Count == 0) + { + builder.Add("default"); + } + + builder.Add("on " + Background.ToMarkup()); + } + + return string.Join(" ", builder); + } + /// public override bool Equals(object? obj) {