diff --git a/docs/input/best-practices.md b/docs/input/best-practices.md index 17b8403..7d7c13c 100644 --- a/docs/input/best-practices.md +++ b/docs/input/best-practices.md @@ -60,6 +60,10 @@ Spectre.Console will tell your terminal to use the color that is configured in t If you are using an 8 or 24-bit color for the foreground text, it is recommended that you also set an appropriate background color to match. +**Do** escape data when outputting any user input or any external data via Markup using the [`EscapeMarkup`](xref:M:Spectre.Console.Markup.Escape(System.String)) method on the data. Any user input containing `[` or `]` will likely cause a runtime error while rendering otherwise. + +**Consider** replacing `Markup` and `MarkupLine` with [`MarkupInterpolated`](xref:M:Spectre.Console.AnsiConsole.MarkupInterpolated(System.FormattableString)) and [`MarkupLineInterpolated`](xref:M:Spectre.Console.AnsiConsole.MarkupLineInterpolated(System.FormattableString)). Both these methods will automatically escape all data in the interpolated string holes. When working with widgets such as the Table and Tree, consider using [`Markup.FromInterpolated`](xref:M:Spectre.Console.Markup.FromInterpolated(System.FormattableString,Spectre.Console.Style)) to generate an `IRenderable` from an interpolated string. + ### Live-Rendering Best Practices Spectre.Console has a variety of [live-rendering capabilities](live) widgets. These widgets can be used to display data diff --git a/docs/input/markup.md b/docs/input/markup.md index 3632605..a7b3e4b 100644 --- a/docs/input/markup.md +++ b/docs/input/markup.md @@ -63,6 +63,15 @@ You can also use the `Markup.Escape` method. ```csharp AnsiConsole.Markup("[red]{0}[/]", Markup.Escape("Hello [World]")); ``` + +## Escaping Interpolated Strings + +When working with interpolated string, you can use the `MarkupInterpolated` and `MarkupInterpolatedLine` methods to automatically escape the values in the interpolated string holes. + +```csharp +AnsiConsole.MarkupInterpolated("[red]{0}[/]", "Hello [World]"); +``` + ## Setting background color You can set the background color in markup by prefixing the color with diff --git a/src/Spectre.Console/AnsiConsole.Markup.cs b/src/Spectre.Console/AnsiConsole.Markup.cs index 87b35ca..b9d47f0 100644 --- a/src/Spectre.Console/AnsiConsole.Markup.cs +++ b/src/Spectre.Console/AnsiConsole.Markup.cs @@ -24,6 +24,24 @@ public static partial class AnsiConsole Console.Markup(format, args); } + /// + /// Writes the specified markup to the console. + /// + /// All interpolation holes which contain a string are automatically escaped so you must not call . + /// + /// + /// + /// string input = args[0]; + /// string output = Process(input); + /// AnsiConsole.MarkupInterpolated($"[blue]{input}[/] -> [green]{output}[/]"); + /// + /// + /// The interpolated string value to write. + public static void MarkupInterpolated(FormattableString value) + { + Console.MarkupInterpolated(value); + } + /// /// Writes the specified markup to the console. /// @@ -35,6 +53,25 @@ public static partial class AnsiConsole Console.Markup(provider, format, args); } + /// + /// Writes the specified markup to the console. + /// + /// All interpolation holes which contain a string are automatically escaped so you must not call . + /// + /// + /// + /// string input = args[0]; + /// string output = Process(input); + /// AnsiConsole.MarkupInterpolated(CultureInfo.InvariantCulture, $"[blue]{input}[/] -> [green]{output}[/]"); + /// + /// + /// An object that supplies culture-specific formatting information. + /// The interpolated string value to write. + public static void MarkupInterpolated(IFormatProvider provider, FormattableString value) + { + Console.MarkupInterpolated(provider, value); + } + /// /// Writes the specified markup, followed by the current line terminator, to the console. /// @@ -54,6 +91,24 @@ public static partial class AnsiConsole Console.MarkupLine(format, args); } + /// + /// Writes the specified markup, followed by the current line terminator, to the console. + /// + /// All interpolation holes which contain a string are automatically escaped so you must not call . + /// + /// + /// + /// string input = args[0]; + /// string output = Process(input); + /// AnsiConsole.MarkupLineInterpolated($"[blue]{input}[/] -> [green]{output}[/]"); + /// + /// + /// The interpolated string value to write. + public static void MarkupLineInterpolated(FormattableString value) + { + Console.MarkupLineInterpolated(value); + } + /// /// Writes the specified markup, followed by the current line terminator, to the console. /// @@ -64,4 +119,23 @@ public static partial class AnsiConsole { Console.MarkupLine(provider, format, args); } + + /// + /// Writes the specified markup, followed by the current line terminator, to the console. + /// + /// All interpolation holes which contain a string are automatically escaped so you must not call . + /// + /// + /// + /// string input = args[0]; + /// string output = Process(input); + /// AnsiConsole.MarkupLineInterpolated(CultureInfo.InvariantCulture, $"[blue]{input}[/] -> [green]{output}[/]"); + /// + /// + /// An object that supplies culture-specific formatting information. + /// The interpolated string value to write. + public static void MarkupLineInterpolated(IFormatProvider provider, FormattableString value) + { + Console.MarkupLineInterpolated(provider, value); + } } \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Markup.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Markup.cs index b6d8139..dc5023d 100644 --- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Markup.cs +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Markup.cs @@ -16,6 +16,25 @@ public static partial class AnsiConsoleExtensions Markup(console, CultureInfo.CurrentCulture, format, args); } + /// + /// Writes the specified markup to the console. + /// + /// All interpolation holes which contain a string are automatically escaped so you must not call . + /// + /// + /// + /// string input = args[0]; + /// string output = Process(input); + /// console.MarkupInterpolated($"[blue]{input}[/] -> [green]{output}[/]"); + /// + /// + /// The console to write to. + /// The interpolated string value to write. + public static void MarkupInterpolated(this IAnsiConsole console, FormattableString value) + { + MarkupInterpolated(console, CultureInfo.CurrentCulture, value); + } + /// /// Writes the specified markup to the console. /// @@ -28,6 +47,26 @@ public static partial class AnsiConsoleExtensions Markup(console, string.Format(provider, format, args)); } + /// + /// Writes the specified markup to the console. + /// + /// All interpolation holes which contain a string are automatically escaped so you must not call . + /// + /// + /// + /// string input = args[0]; + /// string output = Process(input); + /// console.MarkupInterpolated(CultureInfo.InvariantCulture, $"[blue]{input}[/] -> [green]{output}[/]"); + /// + /// + /// The console to write to. + /// An object that supplies culture-specific formatting information. + /// The interpolated string value to write. + public static void MarkupInterpolated(this IAnsiConsole console, IFormatProvider provider, FormattableString value) + { + Markup(console, Console.Markup.EscapeInterpolated(provider, value)); + } + /// /// Writes the specified markup to the console. /// @@ -49,6 +88,25 @@ public static partial class AnsiConsoleExtensions MarkupLine(console, CultureInfo.CurrentCulture, format, args); } + /// + /// Writes the specified markup, followed by the current line terminator, to the console. + /// + /// All interpolation holes which contain a string are automatically escaped so you must not call . + /// + /// + /// + /// string input = args[0]; + /// string output = Process(input); + /// console.MarkupLineInterpolated($"[blue]{input}[/] -> [green]{output}[/]"); + /// + /// + /// The console to write to. + /// The interpolated string value to write. + public static void MarkupLineInterpolated(this IAnsiConsole console, FormattableString value) + { + MarkupLineInterpolated(console, CultureInfo.CurrentCulture, value); + } + /// /// Writes the specified markup, followed by the current line terminator, to the console. /// @@ -70,4 +128,24 @@ public static partial class AnsiConsoleExtensions { Markup(console, provider, format + Environment.NewLine, args); } + + /// + /// Writes the specified markup, followed by the current line terminator, to the console. + /// + /// All interpolation holes which contain a string are automatically escaped so you must not call . + /// + /// + /// + /// string input = args[0]; + /// string output = Process(input); + /// console.MarkupLineInterpolated(CultureInfo.InvariantCulture, $"[blue]{input}[/] -> [green]{output}[/]"); + /// + /// + /// The console to write to. + /// An object that supplies culture-specific formatting information. + /// The interpolated string value to write. + public static void MarkupLineInterpolated(this IAnsiConsole console, IFormatProvider provider, FormattableString value) + { + MarkupLine(console, Console.Markup.EscapeInterpolated(provider, value)); + } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Markup.cs b/src/Spectre.Console/Widgets/Markup.cs index e598a1b..7b23376 100644 --- a/src/Spectre.Console/Widgets/Markup.cs +++ b/src/Spectre.Console/Widgets/Markup.cs @@ -54,6 +54,29 @@ public sealed class Markup : Renderable, IAlignable, IOverflowable return ((IRenderable)_paragraph).Render(context, maxWidth); } + /// + /// Returns a new instance of a Markup widget from an interpolated string. + /// + /// The interpolated string value to write. + /// The style of the text. + /// A new markup instance. + public static Markup FromInterpolated(FormattableString value, Style? style = null) + { + return FromInterpolated(CultureInfo.CurrentCulture, value, style); + } + + /// + /// Returns a new instance of a Markup widget from an interpolated string. + /// + /// The format provider to use. + /// The interpolated string value to write. + /// The style of the text. + /// A new markup instance. + public static Markup FromInterpolated(IFormatProvider provider, FormattableString value, Style? style = null) + { + return new Markup(EscapeInterpolated(provider, value), style); + } + /// /// Escapes text so that it won’t be interpreted as markup. /// @@ -83,4 +106,10 @@ public sealed class Markup : Renderable, IAlignable, IOverflowable return text.RemoveMarkup(); } + + internal static string EscapeInterpolated(IFormatProvider provider, FormattableString value) + { + object?[] args = value.GetArguments().Select(arg => arg is string s ? s.EscapeMarkup() : arg).ToArray(); + return string.Format(provider, value.Format, args); + } } \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Unit/Widgets/MarkupTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/MarkupTests.cs index fc9220b..aad415c 100644 --- a/test/Spectre.Console.Tests/Unit/Widgets/MarkupTests.cs +++ b/test/Spectre.Console.Tests/Unit/Widgets/MarkupTests.cs @@ -72,6 +72,25 @@ public sealed class MarkupTests // Then result.ShouldBe(expected); } + + [Theory] + [InlineData("Hello", "World", "\x1B[38;5;11mHello\x1B[0m \x1B[38;5;9mWorld\x1B[0m 2021-02-03")] + [InlineData("Hello", "World [", "\x1B[38;5;11mHello\x1B[0m \x1B[38;5;9mWorld [\x1B[0m 2021-02-03")] + [InlineData("Hello", "World ]", "\x1B[38;5;11mHello\x1B[0m \x1B[38;5;9mWorld ]\x1B[0m 2021-02-03")] + [InlineData("[Hello]", "World", "\x1B[38;5;11m[Hello]\x1B[0m \x1B[38;5;9mWorld\x1B[0m 2021-02-03")] + [InlineData("[[Hello]]", "[World]", "\x1B[38;5;11m[[Hello]]\x1B[0m \x1B[38;5;9m[World]\x1B[0m 2021-02-03")] + public void Should_Escape_Markup_When_Using_MarkupInterpolated(string input1, string input2, string expected) + { + // Given + var console = new TestConsole().EmitAnsiSequences(); + var date = new DateTime(2021, 2, 3); + + // When + console.MarkupInterpolated($"[yellow]{input1}[/] [red]{input2}[/] {date:yyyy-MM-dd}"); + + // Then + console.Output.ShouldBe(expected); + } } [Theory] @@ -134,4 +153,25 @@ public sealed class MarkupTests console.Output.NormalizeLineEndings() .ShouldBe("{\n"); } + + [Fact] + public void Can_Use_Interpolated_Markup_As_IRenderable() + { + // Given + var console = new TestConsole(); + const string Num = "[value["; + var table = new Table().AddColumns("First Column"); + table.AddRow(Markup.FromInterpolated($"Result: {Num}")); + + // When + console.Write(table); + + // Then + console.Output.NormalizeLineEndings().ShouldBe(@"┌─────────────────┐ +│ First Column │ +├─────────────────┤ +│ Result: [value[ │ +└─────────────────┘ +".NormalizeLineEndings()); + } }