Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce MarkupInterpolated and MarkupLineInterpolated extensions #761

Merged
merged 4 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/input/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions docs/input/markup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions src/Spectre.Console/AnsiConsole.Markup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ public static void Markup(string format, params object[] args)
Console.Markup(format, args);
}

/// <summary>
/// Writes the specified markup to the console.
/// <para/>
/// All interpolation holes which contain a string are automatically escaped so you must not call <see cref="StringExtensions.EscapeMarkup"/>.
/// </summary>
/// <example>
/// <code>
/// string input = args[0];
/// string output = Process(input);
/// AnsiConsole.MarkupInterpolated($"[blue]{input}[/] -> [green]{output}[/]");
/// </code>
/// </example>
/// <param name="value">The interpolated string value to write.</param>
public static void MarkupInterpolated(FormattableString value)
{
Console.MarkupInterpolated(value);
}

/// <summary>
/// Writes the specified markup to the console.
/// </summary>
Expand All @@ -35,6 +53,25 @@ public static void Markup(IFormatProvider provider, string format, params object
Console.Markup(provider, format, args);
}

/// <summary>
/// Writes the specified markup to the console.
/// <para/>
/// All interpolation holes which contain a string are automatically escaped so you must not call <see cref="StringExtensions.EscapeMarkup"/>.
/// </summary>
/// <example>
/// <code>
/// string input = args[0];
/// string output = Process(input);
/// AnsiConsole.MarkupInterpolated(CultureInfo.InvariantCulture, $"[blue]{input}[/] -> [green]{output}[/]");
/// </code>
/// </example>
/// <param name="provider">An object that supplies culture-specific formatting information.</param>
/// <param name="value">The interpolated string value to write.</param>
public static void MarkupInterpolated(IFormatProvider provider, FormattableString value)
{
Console.MarkupInterpolated(provider, value);
}

/// <summary>
/// Writes the specified markup, followed by the current line terminator, to the console.
/// </summary>
Expand All @@ -54,6 +91,24 @@ public static void MarkupLine(string format, params object[] args)
Console.MarkupLine(format, args);
}

/// <summary>
/// Writes the specified markup, followed by the current line terminator, to the console.
/// <para/>
/// All interpolation holes which contain a string are automatically escaped so you must not call <see cref="StringExtensions.EscapeMarkup"/>.
/// </summary>
/// <example>
/// <code>
/// string input = args[0];
/// string output = Process(input);
/// AnsiConsole.MarkupLineInterpolated($"[blue]{input}[/] -> [green]{output}[/]");
/// </code>
/// </example>
/// <param name="value">The interpolated string value to write.</param>
public static void MarkupLineInterpolated(FormattableString value)
{
Console.MarkupLineInterpolated(value);
}

/// <summary>
/// Writes the specified markup, followed by the current line terminator, to the console.
/// </summary>
Expand All @@ -64,4 +119,23 @@ public static void MarkupLine(IFormatProvider provider, string format, params ob
{
Console.MarkupLine(provider, format, args);
}

/// <summary>
/// Writes the specified markup, followed by the current line terminator, to the console.
/// <para/>
/// All interpolation holes which contain a string are automatically escaped so you must not call <see cref="StringExtensions.EscapeMarkup"/>.
/// </summary>
/// <example>
/// <code>
/// string input = args[0];
/// string output = Process(input);
/// AnsiConsole.MarkupLineInterpolated(CultureInfo.InvariantCulture, $"[blue]{input}[/] -> [green]{output}[/]");
/// </code>
/// </example>
/// <param name="provider">An object that supplies culture-specific formatting information.</param>
/// <param name="value">The interpolated string value to write.</param>
public static void MarkupLineInterpolated(IFormatProvider provider, FormattableString value)
{
Console.MarkupLineInterpolated(provider, value);
}
}
78 changes: 78 additions & 0 deletions src/Spectre.Console/Extensions/AnsiConsoleExtensions.Markup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ public static void Markup(this IAnsiConsole console, string format, params objec
Markup(console, CultureInfo.CurrentCulture, format, args);
}

/// <summary>
/// Writes the specified markup to the console.
/// <para/>
/// All interpolation holes which contain a string are automatically escaped so you must not call <see cref="StringExtensions.EscapeMarkup"/>.
/// </summary>
/// <example>
/// <code>
/// string input = args[0];
/// string output = Process(input);
/// console.MarkupInterpolated($"[blue]{input}[/] -> [green]{output}[/]");
/// </code>
/// </example>
/// <param name="console">The console to write to.</param>
/// <param name="value">The interpolated string value to write.</param>
public static void MarkupInterpolated(this IAnsiConsole console, FormattableString value)
{
MarkupInterpolated(console, CultureInfo.CurrentCulture, value);
}

/// <summary>
/// Writes the specified markup to the console.
/// </summary>
Expand All @@ -28,6 +47,26 @@ public static void Markup(this IAnsiConsole console, IFormatProvider provider, s
Markup(console, string.Format(provider, format, args));
}

/// <summary>
/// Writes the specified markup to the console.
/// <para/>
/// All interpolation holes which contain a string are automatically escaped so you must not call <see cref="StringExtensions.EscapeMarkup"/>.
/// </summary>
/// <example>
/// <code>
/// string input = args[0];
/// string output = Process(input);
/// console.MarkupInterpolated(CultureInfo.InvariantCulture, $"[blue]{input}[/] -> [green]{output}[/]");
/// </code>
/// </example>
/// <param name="console">The console to write to.</param>
/// <param name="provider">An object that supplies culture-specific formatting information.</param>
/// <param name="value">The interpolated string value to write.</param>
public static void MarkupInterpolated(this IAnsiConsole console, IFormatProvider provider, FormattableString value)
{
Markup(console, Console.Markup.EscapeInterpolated(provider, value));
}

/// <summary>
/// Writes the specified markup to the console.
/// </summary>
Expand All @@ -49,6 +88,25 @@ public static void MarkupLine(this IAnsiConsole console, string format, params o
MarkupLine(console, CultureInfo.CurrentCulture, format, args);
}

/// <summary>
/// Writes the specified markup, followed by the current line terminator, to the console.
/// <para/>
/// All interpolation holes which contain a string are automatically escaped so you must not call <see cref="StringExtensions.EscapeMarkup"/>.
/// </summary>
/// <example>
/// <code>
/// string input = args[0];
/// string output = Process(input);
/// console.MarkupLineInterpolated($"[blue]{input}[/] -> [green]{output}[/]");
/// </code>
/// </example>
/// <param name="console">The console to write to.</param>
/// <param name="value">The interpolated string value to write.</param>
public static void MarkupLineInterpolated(this IAnsiConsole console, FormattableString value)
{
MarkupLineInterpolated(console, CultureInfo.CurrentCulture, value);
}

/// <summary>
/// Writes the specified markup, followed by the current line terminator, to the console.
/// </summary>
Expand All @@ -70,4 +128,24 @@ public static void MarkupLine(this IAnsiConsole console, IFormatProvider provide
{
Markup(console, provider, format + Environment.NewLine, args);
}

/// <summary>
/// Writes the specified markup, followed by the current line terminator, to the console.
/// <para/>
/// All interpolation holes which contain a string are automatically escaped so you must not call <see cref="StringExtensions.EscapeMarkup"/>.
/// </summary>
/// <example>
/// <code>
/// string input = args[0];
/// string output = Process(input);
/// console.MarkupLineInterpolated(CultureInfo.InvariantCulture, $"[blue]{input}[/] -> [green]{output}[/]");
/// </code>
/// </example>
/// <param name="console">The console to write to.</param>
/// <param name="provider">An object that supplies culture-specific formatting information.</param>
/// <param name="value">The interpolated string value to write.</param>
public static void MarkupLineInterpolated(this IAnsiConsole console, IFormatProvider provider, FormattableString value)
{
MarkupLine(console, Console.Markup.EscapeInterpolated(provider, value));
}
}
29 changes: 29 additions & 0 deletions src/Spectre.Console/Widgets/Markup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,29 @@ protected override IEnumerable<Segment> Render(RenderContext context, int maxWid
return ((IRenderable)_paragraph).Render(context, maxWidth);
}

/// <summary>
/// Returns a new instance of a Markup widget from an interpolated string.
/// </summary>
/// <param name="value">The interpolated string value to write.</param>
/// <param name="style">The style of the text.</param>
/// <returns>A new markup instance.</returns>
public static Markup FromInterpolated(FormattableString value, Style? style = null)
{
return FromInterpolated(CultureInfo.CurrentCulture, value, style);
}

/// <summary>
/// Returns a new instance of a Markup widget from an interpolated string.
/// </summary>
/// <param name="provider">The format provider to use.</param>
/// <param name="value">The interpolated string value to write.</param>
/// <param name="style">The style of the text.</param>
/// <returns>A new markup instance.</returns>
public static Markup FromInterpolated(IFormatProvider provider, FormattableString value, Style? style = null)
{
return new Markup(EscapeInterpolated(provider, value), style);
}

/// <summary>
/// Escapes text so that it won’t be interpreted as markup.
/// </summary>
Expand Down Expand Up @@ -83,4 +106,10 @@ public static string Remove(string text)

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);
}
}
40 changes: 40 additions & 0 deletions test/Spectre.Console.Tests/Unit/Widgets/MarkupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ public void Should_Remove_Markup_From_Text(string input, string expected)
// 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]
Expand Down Expand Up @@ -134,4 +153,25 @@ public void Should_Not_Fail_With_Brackets_On_Calls_Without_Args()
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());
}
}