mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-16 00:42:51 +08:00
parent
0119364728
commit
d7bbaf4a85
@ -7,10 +7,10 @@ namespace PanelExample
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
var content = Text.New(
|
||||
var content = Text.Markup(
|
||||
"[underline]I[/] heard [underline on blue]you[/] like 📦\n\n\n\n" +
|
||||
"So I put a 📦 in a 📦\n\n" +
|
||||
"😅", foreground: Color.White);
|
||||
"😅");
|
||||
|
||||
AnsiConsole.Render(
|
||||
new Panel(
|
||||
@ -22,7 +22,7 @@ namespace PanelExample
|
||||
|
||||
// Left adjusted panel with text
|
||||
AnsiConsole.Render(new Panel(
|
||||
Text.New("Left adjusted\nLeft"))
|
||||
new Text("Left adjusted\nLeft"))
|
||||
{
|
||||
Expand = true,
|
||||
Alignment = Justify.Left,
|
||||
@ -30,7 +30,7 @@ namespace PanelExample
|
||||
|
||||
// Centered ASCII panel with text
|
||||
AnsiConsole.Render(new Panel(
|
||||
Text.New("Centered\nCenter"))
|
||||
new Text("Centered\nCenter"))
|
||||
{
|
||||
Expand = true,
|
||||
Alignment = Justify.Center,
|
||||
@ -39,7 +39,7 @@ namespace PanelExample
|
||||
|
||||
// Right adjusted, rounded panel with text
|
||||
AnsiConsole.Render(new Panel(
|
||||
Text.New("Right adjusted\nRight"))
|
||||
new Text("Right adjusted\nRight"))
|
||||
{
|
||||
Expand = true,
|
||||
Alignment = Justify.Right,
|
||||
|
@ -39,7 +39,7 @@ namespace TableExample
|
||||
|
||||
// Add some rows
|
||||
table.AddRow("[blue][underline]Hell[/]o[/]", "World 🌍");
|
||||
table.AddRow("[yellow]Patrik [green]\"Hello World[/]\" Svensson[/]", "Was [underline]here[/]!");
|
||||
table.AddRow("[yellow]Patrik [green]\"Hello World\"[/] Svensson[/]", "Was [underline]here[/]!");
|
||||
table.AddEmptyRow();
|
||||
table.AddRow(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit,sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " +
|
||||
|
@ -1,8 +0,0 @@
|
||||
root = false
|
||||
[*.cs]
|
||||
|
||||
# Default severity for all analyzer diagnostics
|
||||
dotnet_analyzer_diagnostic.severity = none
|
||||
|
||||
# CS1591: Missing XML comment for publicly visible type or member
|
||||
dotnet_diagnostic.CS1591.severity = none
|
@ -1,169 +0,0 @@
|
||||
using System;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace Sample
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// Use the static API to write some things to the console.
|
||||
AnsiConsole.Foreground = Color.Chartreuse2;
|
||||
AnsiConsole.Decoration = Decoration.Underline | Decoration.Bold;
|
||||
AnsiConsole.WriteLine("Hello World!");
|
||||
AnsiConsole.Reset();
|
||||
AnsiConsole.MarkupLine("Capabilities: [yellow underline]{0}[/]", AnsiConsole.Capabilities);
|
||||
AnsiConsole.MarkupLine("Encoding: [yellow underline]{0}[/]", AnsiConsole.Console.Encoding.EncodingName);
|
||||
AnsiConsole.MarkupLine("Width=[yellow]{0}[/], Height=[yellow]{1}[/]", AnsiConsole.Width, AnsiConsole.Height);
|
||||
AnsiConsole.MarkupLine("[white on red]Good[/] [red]bye[/]!");
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
// We can also use System.ConsoleColor with AnsiConsole
|
||||
// to set the foreground and background color.
|
||||
foreach (ConsoleColor value in Enum.GetValues(typeof(ConsoleColor)))
|
||||
{
|
||||
var foreground = value;
|
||||
var background = (ConsoleColor)(15 - (int)value);
|
||||
|
||||
AnsiConsole.Foreground = foreground;
|
||||
AnsiConsole.Background = background;
|
||||
AnsiConsole.WriteLine("{0} on {1}", foreground, background);
|
||||
AnsiConsole.ResetColors();
|
||||
}
|
||||
|
||||
// We can get the default console via the static API.
|
||||
var console = AnsiConsole.Console;
|
||||
|
||||
// Or you can build it yourself the old fashion way.
|
||||
console = AnsiConsole.Create(
|
||||
new AnsiConsoleSettings()
|
||||
{
|
||||
Ansi = AnsiSupport.Yes,
|
||||
ColorSystem = ColorSystemSupport.Standard,
|
||||
Out = Console.Out,
|
||||
});
|
||||
|
||||
// In this case, we will find the closest colors
|
||||
// and downgrade them to the specified color system.
|
||||
console.WriteLine();
|
||||
console.Foreground = Color.Chartreuse2;
|
||||
console.Decoration = Decoration.Underline | Decoration.Bold;
|
||||
console.WriteLine("Hello World!");
|
||||
console.ResetColors();
|
||||
console.ResetDecoration();
|
||||
console.MarkupLine("Capabilities: [yellow underline]{0}[/]", console.Capabilities);
|
||||
console.MarkupLine("Encoding: [yellow underline]{0}[/]", AnsiConsole.Console.Encoding.EncodingName);
|
||||
console.MarkupLine("Width=[yellow]{0}[/], Height=[yellow]{1}[/]", console.Width, console.Height);
|
||||
console.MarkupLine("[white on red]Good[/] [red]bye[/]!");
|
||||
console.WriteLine();
|
||||
|
||||
// Nest some panels and text
|
||||
AnsiConsole.Foreground = Color.Maroon;
|
||||
AnsiConsole.Render(
|
||||
new Panel(
|
||||
new Panel(
|
||||
new Panel(
|
||||
new Panel(
|
||||
Text.New(
|
||||
"[underline]I[/] heard [underline on blue]you[/] like 📦\n\n\n\n" +
|
||||
"So I put a 📦 in a 📦\nin a 📦 in a 📦\n\n" +
|
||||
"😅", foreground: Color.White))
|
||||
{ Alignment = Justify.Center, Border = BorderKind.Rounded })))
|
||||
{
|
||||
Border = BorderKind.Ascii
|
||||
});
|
||||
|
||||
// Reset colors
|
||||
AnsiConsole.ResetColors();
|
||||
|
||||
// Left adjusted panel with text
|
||||
AnsiConsole.Render(new Panel(
|
||||
Text.New("Left adjusted\nLeft"))
|
||||
{
|
||||
Expand = true,
|
||||
Alignment = Justify.Left,
|
||||
});
|
||||
|
||||
// Centered panel with text
|
||||
AnsiConsole.Render(new Panel(
|
||||
Text.New("Centered\nCenter"))
|
||||
{
|
||||
Expand = true,
|
||||
Alignment = Justify.Center,
|
||||
});
|
||||
|
||||
// Right adjusted panel with text
|
||||
AnsiConsole.Render(new Panel(
|
||||
Text.New("Right adjusted\nRight"))
|
||||
{
|
||||
Expand = true,
|
||||
Alignment = Justify.Right,
|
||||
});
|
||||
|
||||
// A normal, square table
|
||||
var table = new Table();
|
||||
table.AddColumns("[red underline]Foo[/]", "Bar");
|
||||
table.AddRow("[blue][underline]Hell[/]o[/]", "World 🌍");
|
||||
table.AddRow("[yellow]Patrik [green]\"Lol[/]\" Svensson[/]", "Was [underline]here[/]!");
|
||||
table.AddRow("Lorem ipsum dolor sit amet, consectetur adipiscing elit,sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum", "◀ Strange language");
|
||||
table.AddRow("Hej 👋", "[green]Världen[/]");
|
||||
AnsiConsole.Render(table);
|
||||
|
||||
// A rounded table
|
||||
table = new Table { Border = BorderKind.Rounded };
|
||||
table.AddColumns("[red underline]Foo[/]", "Bar");
|
||||
table.AddRow("[blue][underline]Hell[/]o[/]", "World 🌍");
|
||||
table.AddRow("[yellow]Patrik [green]\"Lol[/]\" Svensson[/]", "Was [underline]here[/]!");
|
||||
table.AddRow("Lorem ipsum dolor sit amet, consectetur [blue]adipiscing[/] elit,sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum", "◀ Strange language");
|
||||
table.AddRow("Hej 👋", "[green]Världen[/]");
|
||||
AnsiConsole.Render(table);
|
||||
|
||||
// A rounded table without headers
|
||||
table = new Table { Border = BorderKind.Rounded, ShowHeaders = false };
|
||||
table.AddColumns("[red underline]Foo[/]", "Bar");
|
||||
table.AddRow("[blue][underline]Hell[/]o[/]", "World 🌍");
|
||||
table.AddRow("[yellow]Patrik [green]\"Lol[/]\" Svensson[/]", "Was [underline]here[/]!");
|
||||
table.AddRow("Lorem ipsum dolor sit amet, consectetur [blue]adipiscing[/] elit,sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum", "◀ Strange language");
|
||||
table.AddRow("Hej 👋", "[green]Världen[/]");
|
||||
AnsiConsole.Render(table);
|
||||
|
||||
// Emulate the usage information for "dotnet run"
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.MarkupLine("Usage: [grey]dotnet [blue]run[/] [[options] [[[[--] <additional arguments>...]][/]");
|
||||
AnsiConsole.WriteLine();
|
||||
var grid = new Grid();
|
||||
grid.AddColumn(new GridColumn { NoWrap = true });
|
||||
grid.AddColumn(new GridColumn { NoWrap = true, Width = 2 });
|
||||
grid.AddColumn();
|
||||
grid.AddRow("Options:", "", "");
|
||||
grid.AddRow(" [blue]-h[/], [blue]--help[/]", "", "Show command line help.");
|
||||
grid.AddRow(" [blue]-c[/], [blue]--configuration[/] <CONFIGURATION>", "", "The configuration to run for.\nThe default for most projects is [green]Debug[/].");
|
||||
grid.AddRow(" [blue]-v[/], [blue]--verbosity[/] <LEVEL>", "", "Set the MSBuild verbosity level. Allowed values are \nq[grey][[uiet][/], m[grey][[inimal][/], n[grey][[ormal][/], d[grey][[etailed][/], and diag[grey][[nostic][/].");
|
||||
AnsiConsole.Render(grid);
|
||||
|
||||
// A simple table
|
||||
AnsiConsole.WriteLine();
|
||||
table = new Table { Border = BorderKind.Rounded };
|
||||
table.AddColumn("Foo");
|
||||
table.AddColumn("Bar");
|
||||
table.AddColumn("Baz");
|
||||
table.AddRow("Qux\nQuuuuuux", "[blue]Corgi[/]", "Waldo");
|
||||
table.AddRow("Grault", "Garply", "Fred");
|
||||
AnsiConsole.Render(table);
|
||||
|
||||
// Render a table in some panels.
|
||||
AnsiConsole.Render(new Panel(new Panel(table) { Border = BorderKind.Ascii }) { Padding = new Padding(0, 0) });
|
||||
|
||||
// Draw another table
|
||||
table = new Table { Expand = false };
|
||||
table.AddColumn(new TableColumn("Date"));
|
||||
table.AddColumn(new TableColumn("Title"));
|
||||
table.AddColumn(new TableColumn("Production\nBudget"));
|
||||
table.AddColumn(new TableColumn("Box Office"));
|
||||
table.AddRow("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$275,000,000", "[red]$375,126,118[/]");
|
||||
table.AddRow("May 25, 2018", "[yellow]Solo[/]: A Star Wars Story", "$275,000,000", "$393,151,347");
|
||||
table.AddRow("Dec 15, 2017", "Star Wars Ep. VIII: The Last Jedi", "$262,000,000", "[bold green]$1,332,539,889[/]");
|
||||
AnsiConsole.Render(table);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Spectre.Console\Spectre.Console.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -12,7 +12,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("[yellow]Hello[/]", "[93mHello[0m")]
|
||||
[InlineData("[yellow]Hello [italic]World[/]![/]", "[93mHello [0m[3;93mWorld[0m[93m![0m")]
|
||||
[InlineData("[yellow]Hello [italic]World[/]![/]", "[93mHello[0m[93m [0m[3;93mWorld[0m[93m![0m")]
|
||||
public void Should_Output_Expected_Ansi_For_Markup(string markup, string expected)
|
||||
{
|
||||
// Given
|
||||
@ -26,7 +26,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("[yellow]Hello [[ World[/]", "[93mHello [0m[93m[[0m[93m World[0m")]
|
||||
[InlineData("[yellow]Hello [[ World[/]", "[93mHello[0m[93m [0m[93m[[0m[93m [0m[93mWorld[0m")]
|
||||
public void Should_Be_Able_To_Escape_Tags(string markup, string expected)
|
||||
{
|
||||
// Given
|
||||
|
@ -3,7 +3,7 @@ using Shouldly;
|
||||
using Spectre.Console.Composition;
|
||||
using Xunit;
|
||||
|
||||
namespace Spectre.Console.Tests.Unit.Composition
|
||||
namespace Spectre.Console.Tests.Unit
|
||||
{
|
||||
public sealed class BorderTests
|
||||
{
|
@ -1,91 +0,0 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace Spectre.Console.Tests.Unit
|
||||
{
|
||||
public sealed class TextTests
|
||||
{
|
||||
[Fact]
|
||||
public void Should_Render_Unstyled_Text_As_Expected()
|
||||
{
|
||||
// Given
|
||||
var fixture = new PlainConsole(width: 80);
|
||||
var text = Text.New("Hello World");
|
||||
|
||||
// When
|
||||
fixture.Render(text);
|
||||
|
||||
// Then
|
||||
fixture.Output
|
||||
.NormalizeLineEndings()
|
||||
.ShouldBe("Hello World");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Write_Line_Breaks()
|
||||
{
|
||||
// Given
|
||||
var fixture = new PlainConsole(width: 5);
|
||||
var text = Text.New("\n\n");
|
||||
|
||||
// When
|
||||
fixture.Render(text);
|
||||
|
||||
// Then
|
||||
fixture.RawOutput.ShouldBe("\n\n");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Split_Unstyled_Text_To_New_Lines_If_Width_Exceeds_Console_Width()
|
||||
{
|
||||
// Given
|
||||
var fixture = new PlainConsole(width: 5);
|
||||
var text = Text.New("Hello World");
|
||||
|
||||
// When
|
||||
fixture.Render(text);
|
||||
|
||||
// Then
|
||||
fixture.Output
|
||||
.NormalizeLineEndings()
|
||||
.ShouldBe("Hello\n Worl\nd");
|
||||
}
|
||||
|
||||
public sealed class TheStylizeMethod
|
||||
{
|
||||
[Fact]
|
||||
public void Should_Apply_Style_To_Text()
|
||||
{
|
||||
// Given
|
||||
var fixture = new AnsiConsoleFixture(ColorSystem.Standard);
|
||||
var text = Text.New("Hello World");
|
||||
text.Stylize(start: 3, end: 8, new Style(decoration: Decoration.Underline));
|
||||
|
||||
// When
|
||||
fixture.Console.Render(text);
|
||||
|
||||
// Then
|
||||
fixture.Output
|
||||
.NormalizeLineEndings()
|
||||
.ShouldBe("Hel[4mlo Wo[0mrld");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Apply_Style_To_Text_Which_Spans_Over_Multiple_Lines()
|
||||
{
|
||||
// Given
|
||||
var fixture = new AnsiConsoleFixture(ColorSystem.Standard, width: 5);
|
||||
var text = Text.New("Hello World");
|
||||
text.Stylize(start: 3, end: 8, new Style(decoration: Decoration.Underline));
|
||||
|
||||
// When
|
||||
fixture.Console.Render(text);
|
||||
|
||||
// Then
|
||||
fixture.Output
|
||||
.NormalizeLineEndings()
|
||||
.ShouldBe("Hel[4mlo[0m\n[4m Wo[0mrl\nd");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ using System;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace Spectre.Console.Tests.Unit.Composition
|
||||
namespace Spectre.Console.Tests.Unit
|
||||
{
|
||||
public sealed class GridTests
|
||||
{
|
@ -12,7 +12,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
var console = new PlainConsole(width: 80);
|
||||
|
||||
// When
|
||||
console.Render(new Panel(Text.New("Hello World")));
|
||||
console.Render(new Panel(new Text("Hello World")));
|
||||
|
||||
// Then
|
||||
console.Lines.Count.ShouldBe(3);
|
||||
@ -28,7 +28,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
var console = new PlainConsole(width: 80);
|
||||
|
||||
// When
|
||||
console.Render(new Panel(Text.New("Hello World"))
|
||||
console.Render(new Panel(new Text("Hello World"))
|
||||
{
|
||||
Padding = new Padding(3, 5),
|
||||
});
|
||||
@ -47,7 +47,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
var console = new PlainConsole(width: 80);
|
||||
|
||||
// When
|
||||
console.Render(new Panel(Text.New(" \n💩\n ")));
|
||||
console.Render(new Panel(new Text(" \n💩\n ")));
|
||||
|
||||
// Then
|
||||
console.Lines.Count.ShouldBe(5);
|
||||
@ -65,7 +65,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
var console = new PlainConsole(width: 80);
|
||||
|
||||
// When
|
||||
console.Render(new Panel(Text.New("Hello World\nFoo Bar")));
|
||||
console.Render(new Panel(new Text("Hello World\nFoo Bar")));
|
||||
|
||||
// Then
|
||||
console.Lines.Count.ShouldBe(4);
|
||||
@ -81,7 +81,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
// Given
|
||||
var console = new PlainConsole(width: 80);
|
||||
var text = new Panel(
|
||||
Text.New("I heard [underline on blue]you[/] like 📦\n\n\n\nSo I put a 📦 in a 📦"));
|
||||
Text.Markup("I heard [underline on blue]you[/] like 📦\n\n\n\nSo I put a 📦 in a 📦"));
|
||||
|
||||
// When
|
||||
console.Render(text);
|
||||
@ -104,7 +104,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
var console = new PlainConsole(width: 80);
|
||||
|
||||
// When
|
||||
console.Render(new Panel(Text.New("Hello World"))
|
||||
console.Render(new Panel(new Text("Hello World"))
|
||||
{
|
||||
Expand = true,
|
||||
});
|
||||
@ -126,7 +126,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
// When
|
||||
console.Render(
|
||||
new Panel(
|
||||
Text.New("Hello World").WithAlignment(Justify.Right))
|
||||
new Text("Hello World").WithAlignment(Justify.Right))
|
||||
{
|
||||
Expand = true,
|
||||
});
|
||||
@ -147,7 +147,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
// When
|
||||
console.Render(
|
||||
new Panel(
|
||||
Text.New("Hello World").WithAlignment(Justify.Center))
|
||||
new Text("Hello World").WithAlignment(Justify.Center))
|
||||
{
|
||||
Expand = true,
|
||||
});
|
||||
@ -166,7 +166,7 @@ namespace Spectre.Console.Tests.Unit
|
||||
var console = new PlainConsole(width: 80);
|
||||
|
||||
// When
|
||||
console.Render(new Panel(new Panel(Text.New("Hello World"))));
|
||||
console.Render(new Panel(new Panel(new Text("Hello World"))));
|
||||
|
||||
// Then
|
||||
console.Lines.Count.ShouldBe(5);
|
@ -2,7 +2,7 @@ using System;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace Spectre.Console.Tests.Unit.Composition
|
||||
namespace Spectre.Console.Tests.Unit
|
||||
{
|
||||
public sealed class TableTests
|
||||
{
|
85
src/Spectre.Console.Tests/Unit/TextTests.cs
Normal file
85
src/Spectre.Console.Tests/Unit/TextTests.cs
Normal file
@ -0,0 +1,85 @@
|
||||
using System.Text;
|
||||
using Shouldly;
|
||||
using Spectre.Console.Composition;
|
||||
using Xunit;
|
||||
|
||||
namespace Spectre.Console.Tests.Unit
|
||||
{
|
||||
public sealed class TextTests
|
||||
{
|
||||
public sealed class Measuring
|
||||
{
|
||||
[Fact]
|
||||
public void Should_Return_The_Longest_Word_As_Minimum_Width()
|
||||
{
|
||||
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
|
||||
|
||||
var result = text.Measure(new RenderContext(Encoding.Unicode, false), 80);
|
||||
|
||||
result.Min.ShouldBe(6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Return_The_Longest_Line_As_Maximum_Width()
|
||||
{
|
||||
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
|
||||
|
||||
var result = text.Measure(new RenderContext(Encoding.Unicode, false), 80);
|
||||
|
||||
result.Max.ShouldBe(11);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class Rendering
|
||||
{
|
||||
[Fact]
|
||||
public void Should_Render_Unstyled_Text_As_Expected()
|
||||
{
|
||||
// Given
|
||||
var fixture = new PlainConsole(width: 80);
|
||||
var text = new Text("Hello World");
|
||||
|
||||
// When
|
||||
fixture.Render(text);
|
||||
|
||||
// Then
|
||||
fixture.Output
|
||||
.NormalizeLineEndings()
|
||||
.ShouldBe("Hello World");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Write_Line_Breaks()
|
||||
{
|
||||
// Given
|
||||
var fixture = new PlainConsole(width: 5);
|
||||
var text = new Text("Hello\n\nWorld");
|
||||
|
||||
// When
|
||||
fixture.Render(text);
|
||||
|
||||
// Then
|
||||
fixture.RawOutput.ShouldBe("Hello\n\nWorld");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(5, "Hello World", "Hello\nWorld")]
|
||||
[InlineData(10, "Hello Sweet Nice World", "Hello \nSweet Nice\nWorld")]
|
||||
public void Should_Split_Unstyled_Text_To_New_Lines_If_Width_Exceeds_Console_Width(
|
||||
int width, string input, string expected)
|
||||
{
|
||||
// Given
|
||||
var fixture = new PlainConsole(width);
|
||||
var text = new Text(input);
|
||||
|
||||
// When
|
||||
fixture.Render(text);
|
||||
|
||||
// Then
|
||||
fixture.Output
|
||||
.NormalizeLineEndings()
|
||||
.ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console", "Spectre.
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Tests", "Spectre.Console.Tests\Spectre.Console.Tests.csproj", "{9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "Sample\Sample.csproj", "{272E6092-BD31-4EB6-A9FF-F4179F91958F}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{20595AD4-8D75-4AF8-B6BC-9C38C160423F}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
@ -61,18 +59,6 @@ Global
|
||||
{9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{272E6092-BD31-4EB6-A9FF-F4179F91958F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{94ECCBA8-7EBF-4B53-8379-52EB2327417E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
|
@ -24,11 +24,28 @@ namespace Spectre.Console.Composition
|
||||
/// </summary>
|
||||
public bool IsLineBreak { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not this is a whitespace
|
||||
/// that should be preserved but not taken into account when
|
||||
/// layouting text.
|
||||
/// </summary>
|
||||
public bool IsWhiteSpace { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the segment style.
|
||||
/// </summary>
|
||||
public Style Style { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a segment representing a line break.
|
||||
/// </summary>
|
||||
public static Segment LineBreak { get; } = new Segment("\n", Style.Plain, true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an empty segment.
|
||||
/// </summary>
|
||||
public static Segment Empty { get; } = new Segment(string.Empty, Style.Plain);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Segment"/> class.
|
||||
/// </summary>
|
||||
@ -58,15 +75,7 @@ namespace Spectre.Console.Composition
|
||||
Text = text.NormalizeLineEndings();
|
||||
Style = style;
|
||||
IsLineBreak = lineBreak;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a segment that represents an implicit line break.
|
||||
/// </summary>
|
||||
/// <returns>A segment that represents an implicit line break.</returns>
|
||||
public static Segment LineBreak()
|
||||
{
|
||||
return new Segment("\n", Style.Plain, true);
|
||||
IsWhiteSpace = string.IsNullOrWhiteSpace(text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -143,9 +152,9 @@ namespace Spectre.Console.Composition
|
||||
{
|
||||
var segment = stack.Pop();
|
||||
|
||||
if (line.Length + segment.Text.Length > maxWidth)
|
||||
if (line.Width + segment.Text.Length > maxWidth)
|
||||
{
|
||||
var diff = -(maxWidth - (line.Length + segment.Text.Length));
|
||||
var diff = -(maxWidth - (line.Width + segment.Text.Length));
|
||||
var offset = segment.Text.Length - diff;
|
||||
|
||||
var (first, second) = segment.Split(offset);
|
||||
@ -166,7 +175,7 @@ namespace Spectre.Console.Composition
|
||||
{
|
||||
if (segment.Text == "\n")
|
||||
{
|
||||
if (line.Length > 0 || segment.IsLineBreak)
|
||||
if (line.Width > 0 || segment.IsLineBreak)
|
||||
{
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
@ -189,7 +198,7 @@ namespace Spectre.Console.Composition
|
||||
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
if (line.Length > 0)
|
||||
if (line.Width > 0)
|
||||
{
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
|
@ -1,18 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Spectre.Console.Composition
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a line of segments.
|
||||
/// Represents a collection of segments.
|
||||
/// </summary>
|
||||
[SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix")]
|
||||
public sealed class SegmentLine : List<Segment>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the length of the line.
|
||||
/// Gets the width of the line.
|
||||
/// </summary>
|
||||
public int Length => this.Sum(line => line.Text.Length);
|
||||
public int Width => this.Sum(line => line.Text.Length);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cell width of the segment line.
|
||||
/// </summary>
|
||||
/// <param name="encoding">The encoding to use.</param>
|
||||
/// <returns>The cell width of the segment line.</returns>
|
||||
public int CellWidth(Encoding encoding)
|
||||
{
|
||||
return this.Sum(line => line.CellLength(encoding));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Preprends a segment to the line.
|
||||
/// </summary>
|
||||
/// <param name="segment">The segment to prepend.</param>
|
||||
public void Prepend(Segment segment)
|
||||
{
|
||||
Insert(0, segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
25
src/Spectre.Console/Composition/SegmentLineEnumerator.cs
Normal file
25
src/Spectre.Console/Composition/SegmentLineEnumerator.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Spectre.Console.Composition
|
||||
{
|
||||
internal sealed class SegmentLineEnumerator : IEnumerable<Segment>
|
||||
{
|
||||
private readonly List<SegmentLine> _lines;
|
||||
|
||||
public SegmentLineEnumerator(List<SegmentLine> lines)
|
||||
{
|
||||
_lines = lines;
|
||||
}
|
||||
|
||||
public IEnumerator<Segment> GetEnumerator()
|
||||
{
|
||||
return new SegmentLineIterator(_lines);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
99
src/Spectre.Console/Composition/SegmentLineIterator.cs
Normal file
99
src/Spectre.Console/Composition/SegmentLineIterator.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Spectre.Console.Composition
|
||||
{
|
||||
internal sealed class SegmentLineIterator : IEnumerator<Segment>
|
||||
{
|
||||
private readonly List<SegmentLine> _lines;
|
||||
private int _currentLine;
|
||||
private int _currentIndex;
|
||||
private bool _lineBreakEmitted;
|
||||
|
||||
public Segment Current { get; private set; }
|
||||
object? IEnumerator.Current => Current;
|
||||
|
||||
public SegmentLineIterator(List<SegmentLine> lines)
|
||||
{
|
||||
_currentLine = 0;
|
||||
_currentIndex = -1;
|
||||
_lines = lines;
|
||||
|
||||
Current = Segment.Empty;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_currentLine > _lines.Count - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_currentIndex += 1;
|
||||
|
||||
// Did we go past the end of the line?
|
||||
if (_currentIndex > _lines[_currentLine].Count - 1)
|
||||
{
|
||||
// We haven't just emitted a line break?
|
||||
if (!_lineBreakEmitted)
|
||||
{
|
||||
// Got any more lines?
|
||||
if (_currentIndex + 1 > _lines[_currentLine].Count - 1)
|
||||
{
|
||||
// Only emit a line break if the next one isn't a line break.
|
||||
if ((_currentLine + 1 <= _lines.Count - 1)
|
||||
&& _lines[_currentLine + 1].Count > 0
|
||||
&& !_lines[_currentLine + 1][0].IsLineBreak)
|
||||
{
|
||||
_lineBreakEmitted = true;
|
||||
Current = Segment.LineBreak;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Increase the line and reset the index.
|
||||
_currentLine += 1;
|
||||
_currentIndex = 0;
|
||||
|
||||
_lineBreakEmitted = false;
|
||||
|
||||
// No more lines?
|
||||
if (_currentLine > _lines.Count - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Nothing on the line?
|
||||
while (_currentIndex > _lines[_currentLine].Count - 1)
|
||||
{
|
||||
_currentLine += 1;
|
||||
_currentIndex = 0;
|
||||
|
||||
if (_currentLine > _lines.Count - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the flag
|
||||
_lineBreakEmitted = false;
|
||||
|
||||
Current = _lines[_currentLine][_currentIndex];
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_currentLine = 0;
|
||||
_currentIndex = -1;
|
||||
|
||||
Current = Segment.Empty;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,297 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Composition;
|
||||
using Spectre.Console.Internal;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents text with color and decorations.
|
||||
/// </summary>
|
||||
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
|
||||
[DebuggerDisplay("{_text,nq}")]
|
||||
public sealed class Text : IRenderable
|
||||
{
|
||||
private readonly List<Span> _spans;
|
||||
private string _text;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text alignment.
|
||||
/// </summary>
|
||||
public Justify Alignment { get; set; } = Justify.Left;
|
||||
|
||||
private sealed class Span
|
||||
{
|
||||
public int Start { get; }
|
||||
public int End { get; }
|
||||
public Style Style { get; }
|
||||
|
||||
public Span(int start, int end, Style style)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
Style = style ?? Style.Plain;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Console.Text"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
internal Text(string text)
|
||||
{
|
||||
_text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text));
|
||||
_spans = new List<Span>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Text"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="foreground">The foreground.</param>
|
||||
/// <param name="background">The background.</param>
|
||||
/// <param name="decoration">The text decoration.</param>
|
||||
/// <returns>A <see cref="Text"/> instance.</returns>
|
||||
public static Text New(
|
||||
string text,
|
||||
Color? foreground = null,
|
||||
Color? background = null,
|
||||
Decoration? decoration = null)
|
||||
{
|
||||
var result = MarkupParser.Parse(text, new Style(foreground, background, decoration));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the text alignment.
|
||||
/// </summary>
|
||||
/// <param name="alignment">The text alignment.</param>
|
||||
/// <returns>The same <see cref="Text"/> instance.</returns>
|
||||
public Text WithAlignment(Justify alignment)
|
||||
{
|
||||
Alignment = alignment;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends some text with the specified color and decorations.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to append.</param>
|
||||
/// <param name="style">The text style.</param>
|
||||
public void Append(string text, Style style)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
var start = _text.Length;
|
||||
var end = _text.Length + text.Length;
|
||||
|
||||
_text += text;
|
||||
|
||||
Stylize(start, end, style);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stylizes a part of the text.
|
||||
/// </summary>
|
||||
/// <param name="start">The start position.</param>
|
||||
/// <param name="end">The end position.</param>
|
||||
/// <param name="style">The style to apply.</param>
|
||||
public void Stylize(int start, int end, Style style)
|
||||
{
|
||||
if (start >= end)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(start), "Start position must be less than the end position.");
|
||||
}
|
||||
|
||||
start = Math.Max(start, 0);
|
||||
end = Math.Min(end, _text.Length);
|
||||
|
||||
_spans.Add(new Span(start, end, style));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
Measurement IRenderable.Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_text))
|
||||
{
|
||||
return new Measurement(1, 1);
|
||||
}
|
||||
|
||||
// TODO: Write some kind of tokenizer for this
|
||||
var min = Segment.SplitLines(((IRenderable)this).Render(context, maxWidth))
|
||||
.SelectMany(line => line.Select(segment => segment.Text.Length))
|
||||
.Max();
|
||||
|
||||
var max = _text.SplitLines().Max(x => Cell.GetCellLength(context.Encoding, x));
|
||||
|
||||
return new Measurement(min, max);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerable<Segment> IRenderable.Render(RenderContext context, int width)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_text))
|
||||
{
|
||||
return Array.Empty<Segment>();
|
||||
}
|
||||
|
||||
if (width == 0)
|
||||
{
|
||||
return Array.Empty<Segment>();
|
||||
}
|
||||
|
||||
var result = new List<Segment>();
|
||||
var segments = SplitLineBreaks(CreateSegments());
|
||||
|
||||
var justification = context.Justification ?? Alignment;
|
||||
|
||||
foreach (var (_, _, last, line) in Segment.SplitLines(segments, width).Enumerate())
|
||||
{
|
||||
var length = line.Sum(l => l.StripLineEndings().CellLength(context.Encoding));
|
||||
|
||||
if (length < width)
|
||||
{
|
||||
// Justify right side
|
||||
if (justification == Justify.Right)
|
||||
{
|
||||
var diff = width - length;
|
||||
result.Add(new Segment(new string(' ', diff)));
|
||||
}
|
||||
else if (justification == Justify.Center)
|
||||
{
|
||||
var diff = (width - length) / 2;
|
||||
result.Add(new Segment(new string(' ', diff)));
|
||||
}
|
||||
}
|
||||
|
||||
// Render the line.
|
||||
foreach (var segment in line)
|
||||
{
|
||||
result.Add(segment.StripLineEndings());
|
||||
}
|
||||
|
||||
// Justify left side
|
||||
if (length < width)
|
||||
{
|
||||
if (justification == Justify.Center)
|
||||
{
|
||||
var diff = (width - length) / 2;
|
||||
result.Add(new Segment(new string(' ', diff)));
|
||||
|
||||
var remainder = (width - length) % 2;
|
||||
if (remainder != 0)
|
||||
{
|
||||
result.Add(new Segment(new string(' ', remainder)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!last || line.Count == 0)
|
||||
{
|
||||
result.Add(Segment.LineBreak());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IEnumerable<Segment> SplitLineBreaks(IEnumerable<Segment> segments)
|
||||
{
|
||||
// Creates individual segments of line breaks.
|
||||
var result = new List<Segment>();
|
||||
var queue = new Stack<Segment>(segments.Reverse());
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var segment = queue.Pop();
|
||||
|
||||
var index = segment.Text.IndexOf("\n", StringComparison.OrdinalIgnoreCase);
|
||||
if (index == -1)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(segment.Text))
|
||||
{
|
||||
result.Add(segment);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var (first, second) = segment.Split(index);
|
||||
if (!string.IsNullOrEmpty(first.Text))
|
||||
{
|
||||
result.Add(first);
|
||||
}
|
||||
|
||||
result.Add(Segment.LineBreak());
|
||||
|
||||
if (second != null)
|
||||
{
|
||||
queue.Push(new Segment(second.Text.Substring(1), second.Style));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IEnumerable<Segment> CreateSegments()
|
||||
{
|
||||
// This excellent algorithm to sort spans was ported and adapted from
|
||||
// https://github.com/willmcgugan/rich/blob/eb2f0d5277c159d8693636ec60c79c5442fd2e43/rich/text.py#L492
|
||||
|
||||
// Create the style map.
|
||||
var styleMap = _spans.SelectIndex((span, index) => (span, index)).ToDictionary(x => x.index + 1, x => x.span.Style);
|
||||
styleMap[0] = Style.Plain;
|
||||
|
||||
// Create a span list.
|
||||
var spans = new List<(int Offset, bool Leaving, int Style)>();
|
||||
spans.AddRange(_spans.SelectIndex((span, index) => (span.Start, false, index + 1)));
|
||||
spans.AddRange(_spans.SelectIndex((span, index) => (span.End, true, index + 1)));
|
||||
spans = spans.OrderBy(x => x.Offset).ThenBy(x => !x.Leaving).ToList();
|
||||
|
||||
// Keep track of applied styles using a stack
|
||||
var styleStack = new Stack<int>();
|
||||
|
||||
// Now build the segments.
|
||||
var result = new List<Segment>();
|
||||
foreach (var (offset, leaving, style, nextOffset) in BuildSkipList(spans))
|
||||
{
|
||||
if (leaving)
|
||||
{
|
||||
// Leaving
|
||||
styleStack.Pop();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Entering
|
||||
styleStack.Push(style);
|
||||
}
|
||||
|
||||
if (nextOffset > offset)
|
||||
{
|
||||
// Build the current style from the stack
|
||||
var styleIndices = styleStack.OrderBy(index => index).ToArray();
|
||||
var currentStyle = Style.Plain.Combine(styleIndices.Select(index => styleMap[index]));
|
||||
|
||||
// Create segment
|
||||
var text = _text.Substring(offset, Math.Min(_text.Length - offset, nextOffset - offset));
|
||||
result.Add(new Segment(text, currentStyle));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IEnumerable<(int Offset, bool Leaving, int Style, int NextOffset)> BuildSkipList(
|
||||
List<(int Offset, bool Leaving, int Style)> spans)
|
||||
{
|
||||
return spans.Zip(spans.Skip(1), (first, second) => (first, second)).Select(
|
||||
x => (x.first.Offset, x.first.Leaving, x.first.Style, NextOffset: x.second.Offset));
|
||||
}
|
||||
}
|
||||
}
|
@ -168,7 +168,7 @@ namespace Spectre.Console
|
||||
throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
|
||||
}
|
||||
|
||||
_rows.Add(columns.Select(column => Text.New(column)).ToList());
|
||||
_rows.Add(columns.Select(column => Text.Markup(column)).ToList());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@ -268,7 +268,7 @@ namespace Spectre.Console
|
||||
}
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight)));
|
||||
result.Add(Segment.LineBreak());
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Iterate through each cell row
|
||||
@ -327,7 +327,7 @@ namespace Spectre.Console
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(Segment.LineBreak());
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Show header separator?
|
||||
@ -349,7 +349,7 @@ namespace Spectre.Console
|
||||
}
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.HeaderBottomRight)));
|
||||
result.Add(Segment.LineBreak());
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
|
||||
// Show bottom of footer?
|
||||
@ -371,7 +371,7 @@ namespace Spectre.Console
|
||||
}
|
||||
|
||||
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight)));
|
||||
result.Add(Segment.LineBreak());
|
||||
result.Add(Segment.LineBreak);
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ namespace Spectre.Console
|
||||
/// <param name="text">The table column text.</param>
|
||||
public TableColumn(string text)
|
||||
{
|
||||
Text = Text.New(text ?? throw new ArgumentNullException(nameof(text)));
|
||||
Text = Text.Markup(text ?? throw new ArgumentNullException(nameof(text)));
|
||||
Width = null;
|
||||
Padding = new Padding(1, 1);
|
||||
NoWrap = false;
|
270
src/Spectre.Console/Composition/Widgets/Text.cs
Normal file
270
src/Spectre.Console/Composition/Widgets/Text.cs
Normal file
@ -0,0 +1,270 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Spectre.Console.Composition;
|
||||
using Spectre.Console.Internal;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a piece of text.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{_text,nq}")]
|
||||
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
|
||||
public sealed class Text : IRenderable
|
||||
{
|
||||
private readonly List<SegmentLine> _lines;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text alignment.
|
||||
/// </summary>
|
||||
public Justify Alignment { get; set; } = Justify.Left;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Text"/> class.
|
||||
/// </summary>
|
||||
public Text()
|
||||
{
|
||||
_lines = new List<SegmentLine>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Text"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="style">The style of the text.</param>
|
||||
public Text(string text, Style? style = null)
|
||||
: this()
|
||||
{
|
||||
if (text is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
Append(text, style);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Text"/> instance representing
|
||||
/// the specified markup text.
|
||||
/// </summary>
|
||||
/// <param name="text">The markup text.</param>
|
||||
/// <param name="style">The text style.</param>
|
||||
/// <returns>a <see cref="Text"/> instance representing the specified markup text.</returns>
|
||||
public static Text Markup(string text, Style? style = null)
|
||||
{
|
||||
var result = MarkupParser.Parse(text, style ?? Style.Plain);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Measurement Measure(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (_lines.Count == 0)
|
||||
{
|
||||
return new Measurement(0, 0);
|
||||
}
|
||||
|
||||
var min = _lines.Max(line => line.Max(segment => segment.CellLength(context.Encoding)));
|
||||
var max = _lines.Max(x => x.CellWidth(context.Encoding));
|
||||
|
||||
return new Measurement(min, max);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<Segment> Render(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (_lines.Count == 0)
|
||||
{
|
||||
return Array.Empty<Segment>();
|
||||
}
|
||||
|
||||
var justification = context.Justification ?? Alignment;
|
||||
|
||||
var lines = SplitLines(context, maxWidth);
|
||||
foreach (var (_, _, last, line) in lines.Enumerate())
|
||||
{
|
||||
var length = line.Sum(l => l.StripLineEndings().CellLength(context.Encoding));
|
||||
if (length < maxWidth)
|
||||
{
|
||||
// Justify right side
|
||||
if (justification == Justify.Right)
|
||||
{
|
||||
var diff = maxWidth - length;
|
||||
line.Prepend(new Segment(new string(' ', diff)));
|
||||
}
|
||||
else if (justification == Justify.Center)
|
||||
{
|
||||
// Left side.
|
||||
var diff = (maxWidth - length) / 2;
|
||||
line.Prepend(new Segment(new string(' ', diff)));
|
||||
|
||||
// Right side
|
||||
line.Add(new Segment(new string(' ', diff)));
|
||||
var remainder = (maxWidth - length) % 2;
|
||||
if (remainder != 0)
|
||||
{
|
||||
line.Add(new Segment(new string(' ', remainder)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SegmentLineEnumerator(lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a piece of text.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to append.</param>
|
||||
/// <param name="style">The style of the appended text.</param>
|
||||
public void Append(string text, Style? style = null)
|
||||
{
|
||||
if (text is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
foreach (var (_, first, last, part) in text.SplitLines().Enumerate())
|
||||
{
|
||||
var current = part;
|
||||
if (string.IsNullOrEmpty(current) && last)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (first)
|
||||
{
|
||||
var line = _lines.LastOrDefault();
|
||||
if (line == null)
|
||||
{
|
||||
_lines.Add(new SegmentLine());
|
||||
line = _lines.Last();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(current))
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var span in current.SplitWords())
|
||||
{
|
||||
line.Add(new Segment(span, style ?? Style.Plain));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var line = new SegmentLine();
|
||||
|
||||
if (string.IsNullOrEmpty(current))
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var span in current.SplitWords())
|
||||
{
|
||||
line.Add(new Segment(span, style ?? Style.Plain));
|
||||
}
|
||||
}
|
||||
|
||||
_lines.Add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<SegmentLine> Clone()
|
||||
{
|
||||
var result = new List<SegmentLine>();
|
||||
|
||||
foreach (var line in _lines)
|
||||
{
|
||||
var newLine = new SegmentLine();
|
||||
foreach (var segment in line)
|
||||
{
|
||||
newLine.Add(segment);
|
||||
}
|
||||
|
||||
result.Add(newLine);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<SegmentLine> SplitLines(RenderContext context, int maxWidth)
|
||||
{
|
||||
if (_lines.Max(x => x.CellWidth(context.Encoding)) <= maxWidth)
|
||||
{
|
||||
return Clone();
|
||||
}
|
||||
|
||||
var lines = new List<SegmentLine>();
|
||||
var line = new SegmentLine();
|
||||
|
||||
var newLine = true;
|
||||
using (var iterator = new SegmentLineIterator(_lines))
|
||||
{
|
||||
while (iterator.MoveNext())
|
||||
{
|
||||
var current = iterator.Current;
|
||||
if (current == null)
|
||||
{
|
||||
throw new InvalidOperationException("Iterator returned empty segment.");
|
||||
}
|
||||
|
||||
if (newLine && current.IsWhiteSpace && !current.IsLineBreak)
|
||||
{
|
||||
newLine = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
newLine = false;
|
||||
|
||||
if (current.IsLineBreak)
|
||||
{
|
||||
line.Add(current);
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
var length = current.CellLength(context.Encoding);
|
||||
if (line.CellWidth(context.Encoding) + length > maxWidth)
|
||||
{
|
||||
line.Add(Segment.Empty);
|
||||
lines.Add(line);
|
||||
line = new SegmentLine();
|
||||
newLine = true;
|
||||
}
|
||||
|
||||
if (newLine && current.IsWhiteSpace)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
newLine = false;
|
||||
|
||||
line.Add(current);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining.
|
||||
if (line.Count > 0)
|
||||
{
|
||||
lines.Add(line);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
}
|
27
src/Spectre.Console/Composition/Widgets/TextExtensions.cs
Normal file
27
src/Spectre.Console/Composition/Widgets/TextExtensions.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="Text"/>.
|
||||
/// </summary>
|
||||
public static class TextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the text alignment.
|
||||
/// </summary>
|
||||
/// <param name="text">The <see cref="Text"/> instance.</param>
|
||||
/// <param name="alignment">The text alignment.</param>
|
||||
/// <returns>The same <see cref="Text"/> instance.</returns>
|
||||
public static Text WithAlignment(this Text text, Justify alignment)
|
||||
{
|
||||
if (text is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
text.Alignment = alignment;
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
@ -28,17 +28,24 @@ namespace Spectre.Console
|
||||
|
||||
var options = new RenderContext(console.Encoding, console.Capabilities.LegacyConsole);
|
||||
|
||||
foreach (var segment in renderable.Render(options, console.Width))
|
||||
using (console.PushStyle(Style.Plain))
|
||||
{
|
||||
if (!segment.Style.Equals(Style.Plain))
|
||||
var current = Style.Plain;
|
||||
foreach (var segment in renderable.Render(options, console.Width))
|
||||
{
|
||||
using (var style = console.PushStyle(segment.Style))
|
||||
if (string.IsNullOrEmpty(segment.Text))
|
||||
{
|
||||
console.Write(segment.Text);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if (!segment.Style.Equals(current))
|
||||
{
|
||||
console.Foreground = segment.Style.Foreground;
|
||||
console.Background = segment.Style.Background;
|
||||
console.Decoration = segment.Style.Decoration;
|
||||
current = segment.Style;
|
||||
}
|
||||
|
||||
console.Write(segment.Text);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Spectre.Console.Internal
|
||||
@ -33,5 +34,49 @@ namespace Spectre.Console.Internal
|
||||
var result = text?.NormalizeLineEndings()?.Split(new[] { '\n' }, StringSplitOptions.None);
|
||||
return result ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public static string[] SplitWords(this string word, StringSplitOptions options = StringSplitOptions.None)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
static string Read(StringBuffer reader, Func<char, bool> criteria)
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
while (!reader.Eof)
|
||||
{
|
||||
var current = reader.Peek();
|
||||
if (!criteria(current))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
buffer.Append(reader.Read());
|
||||
}
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
using (var reader = new StringBuffer(word))
|
||||
{
|
||||
while (!reader.Eof)
|
||||
{
|
||||
var current = reader.Peek();
|
||||
if (char.IsWhiteSpace(current))
|
||||
{
|
||||
var x = Read(reader, c => char.IsWhiteSpace(c));
|
||||
if (options != StringSplitOptions.RemoveEmptyEntries)
|
||||
{
|
||||
result.Add(x);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(Read(reader, c => !char.IsWhiteSpace(c)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ namespace Spectre.Console.Internal
|
||||
{
|
||||
style ??= Style.Plain;
|
||||
|
||||
var result = new Text(string.Empty);
|
||||
var result = new Text();
|
||||
using var tokenizer = new MarkupTokenizer(text);
|
||||
|
||||
var stack = new Stack<Style>();
|
||||
|
@ -57,6 +57,15 @@ namespace Spectre.Console
|
||||
return StyleParser.Parse(text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the current <see cref="Style"/>.
|
||||
/// </summary>
|
||||
/// <returns>A copy of the current <see cref="Style"/>.</returns>
|
||||
public Style Clone()
|
||||
{
|
||||
return new Style(Foreground, Background, Decoration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the string representation of a style to its <see cref="Style"/> equivalent.
|
||||
/// A return value indicates whether the operation succeeded.
|
||||
|
Loading…
x
Reference in New Issue
Block a user