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());
+ }
}