mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-16 00:42:51 +08:00
Improve text composite
- A `Text` object should not be able to justify itself. All justification needs to be done by a parent. - Apply colors and styles to part of a `Text` object - Markup parser should return a `Text` object
This commit is contained in:
parent
8e4f33bba4
commit
f19202b427
@ -75,3 +75,6 @@ dotnet_diagnostic.CA1032.severity = none
|
|||||||
|
|
||||||
# CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly
|
# CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly
|
||||||
dotnet_diagnostic.CA1826.severity = none
|
dotnet_diagnostic.CA1826.severity = none
|
||||||
|
|
||||||
|
# RCS1079: Throwing of new NotImplementedException.
|
||||||
|
dotnet_diagnostic.RCS1079.severity = warning
|
@ -53,9 +53,10 @@ namespace Sample
|
|||||||
AnsiConsole.Foreground = Color.Maroon;
|
AnsiConsole.Foreground = Color.Maroon;
|
||||||
AnsiConsole.Render(new Panel(new Panel(new Panel(new Panel(
|
AnsiConsole.Render(new Panel(new Panel(new Panel(new Panel(
|
||||||
Text.New(
|
Text.New(
|
||||||
"I heard you like 📦\n\n\n\nSo I put a 📦 in a 📦",
|
"[underline]I[/] heard [underline on blue]you[/] like 📦\n\n\n\n" +
|
||||||
foreground: Color.White,
|
"So I put a 📦 in a 📦\nin a 📦 in a 📦\n\n" +
|
||||||
justify: Justify.Center))))));
|
"😅",
|
||||||
|
foreground: Color.White), content: Justify.Center)))));
|
||||||
|
|
||||||
// Reset colors
|
// Reset colors
|
||||||
AnsiConsole.ResetColors();
|
AnsiConsole.ResetColors();
|
||||||
@ -69,16 +70,14 @@ namespace Sample
|
|||||||
// Centered panel with text
|
// Centered panel with text
|
||||||
AnsiConsole.Render(new Panel(
|
AnsiConsole.Render(new Panel(
|
||||||
Text.New("Centered\nCenter",
|
Text.New("Centered\nCenter",
|
||||||
foreground: Color.White,
|
foreground: Color.White),
|
||||||
justify: Justify.Center),
|
fit: true, content: Justify.Center));
|
||||||
fit: true));
|
|
||||||
|
|
||||||
// Right adjusted panel with text
|
// Right adjusted panel with text
|
||||||
AnsiConsole.Render(new Panel(
|
AnsiConsole.Render(new Panel(
|
||||||
Text.New("Right adjusted\nRight",
|
Text.New("Right adjusted\nRight",
|
||||||
foreground: Color.White,
|
foreground: Color.White),
|
||||||
justify: Justify.Right),
|
fit: true, content: Justify.Right));
|
||||||
fit: true));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -21,3 +21,6 @@ dotnet_diagnostic.CA1034.severity = none
|
|||||||
|
|
||||||
# CA2000: Dispose objects before losing scope
|
# CA2000: Dispose objects before losing scope
|
||||||
dotnet_diagnostic.CA2000.severity = none
|
dotnet_diagnostic.CA2000.severity = none
|
||||||
|
|
||||||
|
# SA1118: Parameter should not span multiple lines
|
||||||
|
dotnet_diagnostic.SA1118.severity = none
|
||||||
|
13
src/Spectre.Console.Tests/Extensions/StringExtensions.cs
Normal file
13
src/Spectre.Console.Tests/Extensions/StringExtensions.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Spectre.Console.Tests
|
||||||
|
{
|
||||||
|
public static class StringExtensions
|
||||||
|
{
|
||||||
|
public static string NormalizeLineEndings(this string text)
|
||||||
|
{
|
||||||
|
return text?.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase)
|
||||||
|
?.Replace("\r", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,16 +11,17 @@ namespace Spectre.Console.Tests
|
|||||||
|
|
||||||
public string Output => _writer.ToString();
|
public string Output => _writer.ToString();
|
||||||
|
|
||||||
public AnsiConsoleFixture(ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes)
|
public AnsiConsoleFixture(ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes, int width = 80)
|
||||||
{
|
{
|
||||||
_writer = new StringWriter();
|
_writer = new StringWriter();
|
||||||
|
|
||||||
Console = AnsiConsole.Create(new AnsiConsoleSettings
|
Console = new ConsoleWithWidth(
|
||||||
{
|
AnsiConsole.Create(new AnsiConsoleSettings
|
||||||
Ansi = ansi,
|
{
|
||||||
ColorSystem = (ColorSystemSupport)system,
|
Ansi = ansi,
|
||||||
Out = _writer,
|
ColorSystem = (ColorSystemSupport)system,
|
||||||
});
|
Out = _writer,
|
||||||
|
}), width);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
31
src/Spectre.Console.Tests/Fixtures/ConsoleWithWidth.cs
Normal file
31
src/Spectre.Console.Tests/Fixtures/ConsoleWithWidth.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Spectre.Console.Tests
|
||||||
|
{
|
||||||
|
public sealed class ConsoleWithWidth : IAnsiConsole
|
||||||
|
{
|
||||||
|
private readonly IAnsiConsole _console;
|
||||||
|
|
||||||
|
public Capabilities Capabilities => _console.Capabilities;
|
||||||
|
|
||||||
|
public int Width { get; }
|
||||||
|
public int Height => _console.Height;
|
||||||
|
|
||||||
|
public Encoding Encoding => _console.Encoding;
|
||||||
|
|
||||||
|
public Styles Style { get => _console.Style; set => _console.Style = value; }
|
||||||
|
public Color Foreground { get => _console.Foreground; set => _console.Foreground = value; }
|
||||||
|
public Color Background { get => _console.Background; set => _console.Background = value; }
|
||||||
|
|
||||||
|
public ConsoleWithWidth(IAnsiConsole console, int width)
|
||||||
|
{
|
||||||
|
_console = console;
|
||||||
|
Width = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(string text)
|
||||||
|
{
|
||||||
|
_console.Write(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Spectre.Console.Tests
|
namespace Spectre.Console.Tests.Unit
|
||||||
{
|
{
|
||||||
public partial class AnsiConsoleTests
|
public partial class AnsiConsoleTests
|
||||||
{
|
{
|
||||||
|
@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Spectre.Console.Tests
|
namespace Spectre.Console.Tests.Unit
|
||||||
{
|
{
|
||||||
public partial class AnsiConsoleTests
|
public partial class AnsiConsoleTests
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Spectre.Console.Tests
|
namespace Spectre.Console.Tests.Unit
|
||||||
{
|
{
|
||||||
public partial class AnsiConsoleTests
|
public partial class AnsiConsoleTests
|
||||||
{
|
{
|
||||||
|
@ -3,7 +3,7 @@ using System.Globalization;
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Spectre.Console.Tests
|
namespace Spectre.Console.Tests.Unit
|
||||||
{
|
{
|
||||||
public partial class AnsiConsoleTests
|
public partial class AnsiConsoleTests
|
||||||
{
|
{
|
||||||
|
24
src/Spectre.Console.Tests/Unit/AppearanceTests.cs
Normal file
24
src/Spectre.Console.Tests/Unit/AppearanceTests.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Spectre.Console.Tests.Unit
|
||||||
|
{
|
||||||
|
public sealed class AppearanceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Should_Combine_Two_Appearances_As_Expected()
|
||||||
|
{
|
||||||
|
// Given
|
||||||
|
var first = new Appearance(Color.White, Color.Yellow, Styles.Bold | Styles.Italic);
|
||||||
|
var other = new Appearance(Color.Green, Color.Silver, Styles.Underline);
|
||||||
|
|
||||||
|
// When
|
||||||
|
var result = first.Combine(other);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
result.Foreground.ShouldBe(Color.Green);
|
||||||
|
result.Background.ShouldBe(Color.Silver);
|
||||||
|
result.Style.ShouldBe(Styles.Bold | Styles.Italic | Styles.Underline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,7 @@
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Spectre.Console.Composition;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Spectre.Console.Tests.Unit.Composition
|
namespace Spectre.Console.Tests.Unit
|
||||||
{
|
{
|
||||||
public sealed class PanelTests
|
public sealed class PanelTests
|
||||||
{
|
{
|
||||||
@ -13,7 +12,7 @@ namespace Spectre.Console.Tests.Unit.Composition
|
|||||||
var console = new PlainConsole(width: 80);
|
var console = new PlainConsole(width: 80);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
console.Render(new Panel(new Text("Hello World")));
|
console.Render(new Panel(Text.New("Hello World")));
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
console.Lines.Count.ShouldBe(3);
|
console.Lines.Count.ShouldBe(3);
|
||||||
@ -29,7 +28,7 @@ namespace Spectre.Console.Tests.Unit.Composition
|
|||||||
var console = new PlainConsole(width: 80);
|
var console = new PlainConsole(width: 80);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
console.Render(new Panel(new Text(" \n💩\n ")));
|
console.Render(new Panel(Text.New(" \n💩\n ")));
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
console.Lines.Count.ShouldBe(5);
|
console.Lines.Count.ShouldBe(5);
|
||||||
@ -47,7 +46,7 @@ namespace Spectre.Console.Tests.Unit.Composition
|
|||||||
var console = new PlainConsole(width: 80);
|
var console = new PlainConsole(width: 80);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
console.Render(new Panel(new Text("Hello World\nFoo Bar")));
|
console.Render(new Panel(Text.New("Hello World\nFoo Bar")));
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
console.Lines.Count.ShouldBe(4);
|
console.Lines.Count.ShouldBe(4);
|
||||||
@ -57,6 +56,29 @@ namespace Spectre.Console.Tests.Unit.Composition
|
|||||||
console.Lines[3].ShouldBe("└─────────────┘");
|
console.Lines[3].ShouldBe("└─────────────┘");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Should_Preserve_Explicit_Line_Ending()
|
||||||
|
{
|
||||||
|
// 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 📦"),
|
||||||
|
content: Justify.Center);
|
||||||
|
|
||||||
|
// When
|
||||||
|
console.Render(text);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
console.Lines.Count.ShouldBe(7);
|
||||||
|
console.Lines[0].ShouldBe("┌───────────────────────┐");
|
||||||
|
console.Lines[1].ShouldBe("│ I heard you like 📦 │");
|
||||||
|
console.Lines[2].ShouldBe("│ │");
|
||||||
|
console.Lines[3].ShouldBe("│ │");
|
||||||
|
console.Lines[4].ShouldBe("│ │");
|
||||||
|
console.Lines[5].ShouldBe("│ So I put a 📦 in a 📦 │");
|
||||||
|
console.Lines[6].ShouldBe("└───────────────────────┘");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Fit_Panel_To_Parent_If_Enabled()
|
public void Should_Fit_Panel_To_Parent_If_Enabled()
|
||||||
{
|
{
|
||||||
@ -64,7 +86,7 @@ namespace Spectre.Console.Tests.Unit.Composition
|
|||||||
var console = new PlainConsole(width: 25);
|
var console = new PlainConsole(width: 25);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
console.Render(new Panel(new Text("Hello World"), fit: true));
|
console.Render(new Panel(Text.New("Hello World"), fit: true));
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
console.Lines.Count.ShouldBe(3);
|
console.Lines.Count.ShouldBe(3);
|
||||||
@ -80,7 +102,7 @@ namespace Spectre.Console.Tests.Unit.Composition
|
|||||||
var console = new PlainConsole(width: 25);
|
var console = new PlainConsole(width: 25);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
console.Render(new Panel(new Text("Hello World", justify: Justify.Right), fit: true));
|
console.Render(new Panel(Text.New("Hello World"), fit: true, content: Justify.Right));
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
console.Lines.Count.ShouldBe(3);
|
console.Lines.Count.ShouldBe(3);
|
||||||
@ -96,7 +118,7 @@ namespace Spectre.Console.Tests.Unit.Composition
|
|||||||
var console = new PlainConsole(width: 25);
|
var console = new PlainConsole(width: 25);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
console.Render(new Panel(new Text("Hello World", justify: Justify.Center), fit: true));
|
console.Render(new Panel(Text.New("Hello World"), fit: true, content: Justify.Center));
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
console.Lines.Count.ShouldBe(3);
|
console.Lines.Count.ShouldBe(3);
|
||||||
@ -112,7 +134,7 @@ namespace Spectre.Console.Tests.Unit.Composition
|
|||||||
var console = new PlainConsole(width: 80);
|
var console = new PlainConsole(width: 80);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
console.Render(new Panel(new Panel(new Text("Hello World"))));
|
console.Render(new Panel(new Panel(Text.New("Hello World"))));
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
console.Lines.Count.ShouldBe(5);
|
console.Lines.Count.ShouldBe(5);
|
||||||
|
@ -2,66 +2,91 @@ using Shouldly;
|
|||||||
using Spectre.Console.Composition;
|
using Spectre.Console.Composition;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Spectre.Console.Tests.Unit.Composition
|
namespace Spectre.Console.Tests.Unit
|
||||||
{
|
{
|
||||||
public sealed class SegmentTests
|
public sealed class SegmentTests
|
||||||
{
|
{
|
||||||
[Fact]
|
public sealed class TheSplitMethod
|
||||||
public void Should_Split_Segment()
|
|
||||||
{
|
{
|
||||||
var lines = Segment.Split(new[]
|
[Fact]
|
||||||
|
public void Should_Split_Segment_Correctly()
|
||||||
{
|
{
|
||||||
new Segment("Foo"),
|
// Given
|
||||||
new Segment("Bar"),
|
var appearance = new Appearance(Color.Red, Color.Green, Styles.Bold);
|
||||||
new Segment("\n"),
|
var segment = new Segment("Foo Bar", appearance);
|
||||||
new Segment("Baz"),
|
|
||||||
new Segment("Qux"),
|
|
||||||
new Segment("\n"),
|
|
||||||
new Segment("Corgi"),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then
|
// When
|
||||||
lines.Count.ShouldBe(3);
|
var (first, second) = segment.Split(3);
|
||||||
|
|
||||||
lines[0].Count.ShouldBe(2);
|
// Then
|
||||||
lines[0][0].Text.ShouldBe("Foo");
|
first.Text.ShouldBe("Foo");
|
||||||
lines[0][1].Text.ShouldBe("Bar");
|
first.Appearance.ShouldBe(appearance);
|
||||||
|
second.Text.ShouldBe(" Bar");
|
||||||
lines[1].Count.ShouldBe(2);
|
second.Appearance.ShouldBe(appearance);
|
||||||
lines[1][0].Text.ShouldBe("Baz");
|
}
|
||||||
lines[1][1].Text.ShouldBe("Qux");
|
|
||||||
|
|
||||||
lines[2].Count.ShouldBe(1);
|
|
||||||
lines[2][0].Text.ShouldBe("Corgi");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
public sealed class TheSplitLinesMethod
|
||||||
public void Should_Split_Segments_With_Linebreak_In_Text()
|
|
||||||
{
|
{
|
||||||
var lines = Segment.Split(new[]
|
[Fact]
|
||||||
|
public void Should_Split_Segment()
|
||||||
{
|
{
|
||||||
new Segment("Foo\n"),
|
var lines = Segment.SplitLines(
|
||||||
new Segment("Bar\n"),
|
new[]
|
||||||
new Segment("Baz"),
|
{
|
||||||
new Segment("Qux\n"),
|
new Segment("Foo"),
|
||||||
new Segment("Corgi"),
|
new Segment("Bar"),
|
||||||
});
|
new Segment("\n"),
|
||||||
|
new Segment("Baz"),
|
||||||
|
new Segment("Qux"),
|
||||||
|
new Segment("\n"),
|
||||||
|
new Segment("Corgi"),
|
||||||
|
});
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
lines.Count.ShouldBe(4);
|
lines.Count.ShouldBe(3);
|
||||||
|
|
||||||
lines[0].Count.ShouldBe(1);
|
lines[0].Count.ShouldBe(2);
|
||||||
lines[0][0].Text.ShouldBe("Foo");
|
lines[0][0].Text.ShouldBe("Foo");
|
||||||
|
lines[0][1].Text.ShouldBe("Bar");
|
||||||
|
|
||||||
lines[1].Count.ShouldBe(1);
|
lines[1].Count.ShouldBe(2);
|
||||||
lines[1][0].Text.ShouldBe("Bar");
|
lines[1][0].Text.ShouldBe("Baz");
|
||||||
|
lines[1][1].Text.ShouldBe("Qux");
|
||||||
|
|
||||||
lines[2].Count.ShouldBe(2);
|
lines[2].Count.ShouldBe(1);
|
||||||
lines[2][0].Text.ShouldBe("Baz");
|
lines[2][0].Text.ShouldBe("Corgi");
|
||||||
lines[2][1].Text.ShouldBe("Qux");
|
}
|
||||||
|
|
||||||
lines[3].Count.ShouldBe(1);
|
[Fact]
|
||||||
lines[3][0].Text.ShouldBe("Corgi");
|
public void Should_Split_Segments_With_Linebreak_In_Text()
|
||||||
|
{
|
||||||
|
var lines = Segment.SplitLines(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new Segment("Foo\n"),
|
||||||
|
new Segment("Bar\n"),
|
||||||
|
new Segment("Baz"),
|
||||||
|
new Segment("Qux\n"),
|
||||||
|
new Segment("Corgi"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
lines.Count.ShouldBe(4);
|
||||||
|
|
||||||
|
lines[0].Count.ShouldBe(1);
|
||||||
|
lines[0][0].Text.ShouldBe("Foo");
|
||||||
|
|
||||||
|
lines[1].Count.ShouldBe(1);
|
||||||
|
lines[1][0].Text.ShouldBe("Bar");
|
||||||
|
|
||||||
|
lines[2].Count.ShouldBe(2);
|
||||||
|
lines[2][0].Text.ShouldBe("Baz");
|
||||||
|
lines[2][1].Text.ShouldBe("Qux");
|
||||||
|
|
||||||
|
lines[3].Count.ShouldBe(1);
|
||||||
|
lines[3][0].Text.ShouldBe("Corgi");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,61 +1,77 @@
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Spectre.Console.Composition;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Spectre.Console.Tests.Composition
|
namespace Spectre.Console.Tests.Unit
|
||||||
{
|
{
|
||||||
public sealed class TextTests
|
public sealed class TextTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Render_Text_To_Console()
|
public void Should_Render_Unstyled_Text_As_Expected()
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var console = new PlainConsole();
|
var fixture = new PlainConsole(width: 80);
|
||||||
|
var text = Text.New("Hello World");
|
||||||
|
|
||||||
// When
|
// When
|
||||||
console.Render(new Text("Hello World"));
|
fixture.Render(text);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
console.Output.ShouldBe("Hello World");
|
fixture.Output
|
||||||
|
.NormalizeLineEndings()
|
||||||
|
.ShouldBe("Hello World");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Right_Align_Text_To_Parent()
|
public void Should_Split_Unstyled_Text_To_New_Lines_If_Width_Exceeds_Console_Width()
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var console = new PlainConsole(width: 15);
|
var fixture = new PlainConsole(width: 5);
|
||||||
|
var text = Text.New("Hello World");
|
||||||
|
|
||||||
// When
|
// When
|
||||||
console.Render(new Text("Hello World", justify: Justify.Right));
|
fixture.Render(text);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
console.Output.ShouldBe(" Hello World");
|
fixture.Output
|
||||||
|
.NormalizeLineEndings()
|
||||||
|
.ShouldBe("Hello\n Worl\nd");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
public sealed class TheStylizeMethod
|
||||||
public void Should_Center_Text_To_Parent()
|
|
||||||
{
|
{
|
||||||
// Given
|
[Fact]
|
||||||
var console = new PlainConsole(width: 15);
|
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 Appearance(style: Styles.Underline));
|
||||||
|
|
||||||
// When
|
// When
|
||||||
console.Render(new Text("Hello World", justify: Justify.Center));
|
fixture.Console.Render(text);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
console.Output.ShouldBe(" Hello World ");
|
fixture.Output
|
||||||
}
|
.NormalizeLineEndings()
|
||||||
|
.ShouldBe("Hel[4mlo Wo[0mrld");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Should_Split_Text_To_Multiple_Lines_If_It_Does_Not_Fit()
|
public void Should_Apply_Style_To_Text_Which_Spans_Over_Multiple_Lines()
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var console = new PlainConsole(width: 5);
|
var fixture = new AnsiConsoleFixture(ColorSystem.Standard, width: 5);
|
||||||
|
var text = Text.New("Hello World");
|
||||||
|
text.Stylize(start: 3, end: 8, new Appearance(style: Styles.Underline));
|
||||||
|
|
||||||
// When
|
// When
|
||||||
console.Render(new Text("Hello World"));
|
fixture.Console.Render(text);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
console.Output.ShouldBe("Hello\n Worl\nd");
|
fixture.Output
|
||||||
|
.NormalizeLineEndings()
|
||||||
|
.ShouldBe("Hel[4mlo[0m\n[4m Wo[0mrl\nd");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,12 +26,7 @@ namespace Spectre.Console
|
|||||||
/// Gets an <see cref="Appearance"/> with the
|
/// Gets an <see cref="Appearance"/> with the
|
||||||
/// default color and without style.
|
/// default color and without style.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Appearance Plain { get; }
|
public static Appearance Plain { get; } = new Appearance();
|
||||||
|
|
||||||
static Appearance()
|
|
||||||
{
|
|
||||||
Plain = new Appearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Appearance()
|
private Appearance()
|
||||||
: this(null, null, null)
|
: this(null, null, null)
|
||||||
@ -51,6 +46,33 @@ namespace Spectre.Console
|
|||||||
Style = style ?? Styles.None;
|
Style = style ?? Styles.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Combines this appearance with another one.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="other">The item to combine with this.</param>
|
||||||
|
/// <returns>A new appearance representing a combination of this and the other one.</returns>
|
||||||
|
public Appearance Combine(Appearance other)
|
||||||
|
{
|
||||||
|
if (other is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(other));
|
||||||
|
}
|
||||||
|
|
||||||
|
var foreground = Foreground;
|
||||||
|
if (!other.Foreground.IsDefault)
|
||||||
|
{
|
||||||
|
foreground = other.Foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
var background = Background;
|
||||||
|
if (!other.Background.IsDefault)
|
||||||
|
{
|
||||||
|
background = other.Background;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Appearance(foreground, background, Style | other.Style);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
|
@ -12,16 +12,19 @@ namespace Spectre.Console
|
|||||||
{
|
{
|
||||||
private readonly IRenderable _child;
|
private readonly IRenderable _child;
|
||||||
private readonly bool _fit;
|
private readonly bool _fit;
|
||||||
|
private readonly Justify _content;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Panel"/> class.
|
/// Initializes a new instance of the <see cref="Panel"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="child">The child.</param>
|
/// <param name="child">The child.</param>
|
||||||
/// <param name="fit">Whether or not to fit the panel to it's parent.</param>
|
/// <param name="fit">Whether or not to fit the panel to it's parent.</param>
|
||||||
public Panel(IRenderable child, bool fit = false)
|
/// <param name="content">The justification of the panel content.</param>
|
||||||
|
public Panel(IRenderable child, bool fit = false, Justify content = Justify.Left)
|
||||||
{
|
{
|
||||||
_child = child;
|
_child = child;
|
||||||
_fit = fit;
|
_fit = fit;
|
||||||
|
_content = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@ -48,23 +51,59 @@ namespace Spectre.Console
|
|||||||
result.Add(new Segment("┐"));
|
result.Add(new Segment("┐"));
|
||||||
result.Add(new Segment("\n"));
|
result.Add(new Segment("\n"));
|
||||||
|
|
||||||
|
// Render the child.
|
||||||
var childSegments = _child.Render(encoding, childWidth);
|
var childSegments = _child.Render(encoding, childWidth);
|
||||||
foreach (var line in Segment.Split(childSegments))
|
|
||||||
|
// Split the child segments into lines.
|
||||||
|
var lines = Segment.SplitLines(childSegments, childWidth);
|
||||||
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
result.Add(new Segment("│ "));
|
result.Add(new Segment("│ "));
|
||||||
|
|
||||||
foreach (var segment in line)
|
var content = new List<Segment>();
|
||||||
{
|
|
||||||
result.Add(segment.StripLineEndings());
|
|
||||||
}
|
|
||||||
|
|
||||||
var length = line.Sum(segment => segment.CellLength(encoding));
|
var length = line.Sum(segment => segment.CellLength(encoding));
|
||||||
if (length < childWidth)
|
if (length < childWidth)
|
||||||
{
|
{
|
||||||
var diff = childWidth - length;
|
if (_content == Justify.Right)
|
||||||
result.Add(new Segment(new string(' ', diff)));
|
{
|
||||||
|
var diff = childWidth - length;
|
||||||
|
content.Add(new Segment(new string(' ', diff)));
|
||||||
|
}
|
||||||
|
else if (_content == Justify.Center)
|
||||||
|
{
|
||||||
|
var diff = (childWidth - length) / 2;
|
||||||
|
content.Add(new Segment(new string(' ', diff)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var segment in line)
|
||||||
|
{
|
||||||
|
content.Add(segment.StripLineEndings());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length < childWidth)
|
||||||
|
{
|
||||||
|
if (_content == Justify.Left)
|
||||||
|
{
|
||||||
|
var diff = childWidth - length;
|
||||||
|
content.Add(new Segment(new string(' ', diff)));
|
||||||
|
}
|
||||||
|
else if (_content == Justify.Center)
|
||||||
|
{
|
||||||
|
var diff = (childWidth - length) / 2;
|
||||||
|
content.Add(new Segment(new string(' ', diff)));
|
||||||
|
|
||||||
|
var remainder = (childWidth - length) % 2;
|
||||||
|
if (remainder != 0)
|
||||||
|
{
|
||||||
|
content.Add(new Segment(new string(' ', remainder)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.AddRange(content);
|
||||||
|
|
||||||
result.Add(new Segment(" │"));
|
result.Add(new Segment(" │"));
|
||||||
result.Add(new Segment("\n"));
|
result.Add(new Segment("\n"));
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Spectre.Console.Internal;
|
using Spectre.Console.Internal;
|
||||||
@ -9,13 +10,20 @@ namespace Spectre.Console.Composition
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a renderable segment.
|
/// Represents a renderable segment.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Segment
|
[DebuggerDisplay("{Text,nq}")]
|
||||||
|
public class Segment
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the segment text.
|
/// Gets the segment text.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Text { get; }
|
public string Text { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether or not this is an expicit line break
|
||||||
|
/// that should be preserved.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsLineBreak { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the appearance of the segment.
|
/// Gets the appearance of the segment.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -36,9 +44,24 @@ namespace Spectre.Console.Composition
|
|||||||
/// <param name="text">The segment text.</param>
|
/// <param name="text">The segment text.</param>
|
||||||
/// <param name="appearance">The segment appearance.</param>
|
/// <param name="appearance">The segment appearance.</param>
|
||||||
public Segment(string text, Appearance appearance)
|
public Segment(string text, Appearance appearance)
|
||||||
|
: this(text, appearance, false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private Segment(string text, Appearance appearance, bool lineBreak)
|
||||||
{
|
{
|
||||||
Text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text));
|
Text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text));
|
||||||
Appearance = appearance;
|
Appearance = appearance;
|
||||||
|
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", Appearance.Plain, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -61,12 +84,45 @@ namespace Spectre.Console.Composition
|
|||||||
return new Segment(Text.TrimEnd('\n'), Appearance);
|
return new Segment(Text.TrimEnd('\n'), Appearance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Splits the segment at the offset.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="offset">The offset where to split the segment.</param>
|
||||||
|
/// <returns>One or two new segments representing the split.</returns>
|
||||||
|
public (Segment First, Segment Second) Split(int offset)
|
||||||
|
{
|
||||||
|
if (offset < 0)
|
||||||
|
{
|
||||||
|
return (this, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset >= Text.Length)
|
||||||
|
{
|
||||||
|
return (this, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
new Segment(Text.Substring(0, offset), Appearance),
|
||||||
|
new Segment(Text.Substring(offset, Text.Length - offset), Appearance));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Splits the provided segments into lines.
|
/// Splits the provided segments into lines.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="segments">The segments to split.</param>
|
/// <param name="segments">The segments to split.</param>
|
||||||
/// <returns>A collection of lines.</returns>
|
/// <returns>A collection of lines.</returns>
|
||||||
public static List<SegmentLine> Split(IEnumerable<Segment> segments)
|
public static List<SegmentLine> SplitLines(IEnumerable<Segment> segments)
|
||||||
|
{
|
||||||
|
return SplitLines(segments, int.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Splits the provided segments into lines with a maximum width.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="segments">The segments to split into lines.</param>
|
||||||
|
/// <param name="maxWidth">The maximum width.</param>
|
||||||
|
/// <returns>A list of lines.</returns>
|
||||||
|
public static List<SegmentLine> SplitLines(IEnumerable<Segment> segments, int maxWidth)
|
||||||
{
|
{
|
||||||
if (segments is null)
|
if (segments is null)
|
||||||
{
|
{
|
||||||
@ -76,14 +132,41 @@ namespace Spectre.Console.Composition
|
|||||||
var lines = new List<SegmentLine>();
|
var lines = new List<SegmentLine>();
|
||||||
var line = new SegmentLine();
|
var line = new SegmentLine();
|
||||||
|
|
||||||
foreach (var segment in segments)
|
var stack = new Stack<Segment>(segments.Reverse());
|
||||||
|
|
||||||
|
while (stack.Count > 0)
|
||||||
{
|
{
|
||||||
|
var segment = stack.Pop();
|
||||||
|
|
||||||
|
if (line.Length + segment.Text.Length > maxWidth)
|
||||||
|
{
|
||||||
|
var diff = -(maxWidth - (line.Length + segment.Text.Length));
|
||||||
|
var offset = segment.Text.Length - diff;
|
||||||
|
|
||||||
|
var (first, second) = segment.Split(offset);
|
||||||
|
|
||||||
|
line.Add(first);
|
||||||
|
lines.Add(line);
|
||||||
|
line = new SegmentLine();
|
||||||
|
|
||||||
|
if (second != null)
|
||||||
|
{
|
||||||
|
stack.Push(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (segment.Text.Contains("\n"))
|
if (segment.Text.Contains("\n"))
|
||||||
{
|
{
|
||||||
if (segment.Text == "\n")
|
if (segment.Text == "\n")
|
||||||
{
|
{
|
||||||
lines.Add(line);
|
if (line.Length > 0 || segment.IsLineBreak)
|
||||||
line = new SegmentLine();
|
{
|
||||||
|
lines.Add(line);
|
||||||
|
line = new SegmentLine();
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,19 +176,21 @@ namespace Spectre.Console.Composition
|
|||||||
var parts = text.SplitLines();
|
var parts = text.SplitLines();
|
||||||
if (parts.Length > 0)
|
if (parts.Length > 0)
|
||||||
{
|
{
|
||||||
line.Add(new Segment(parts[0], segment.Appearance));
|
if (parts[0].Length > 0)
|
||||||
|
{
|
||||||
|
line.Add(new Segment(parts[0], segment.Appearance));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parts.Length > 1)
|
if (parts.Length > 1)
|
||||||
{
|
{
|
||||||
lines.Add(line);
|
if (line.Length > 0)
|
||||||
line = new SegmentLine();
|
{
|
||||||
|
lines.Add(line);
|
||||||
|
line = new SegmentLine();
|
||||||
|
}
|
||||||
|
|
||||||
text = string.Concat(parts.Skip(1).Take(parts.Length - 1));
|
text = string.Concat(parts.Skip(1).Take(parts.Length - 1));
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
|
||||||
{
|
|
||||||
text = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Spectre.Console.Composition
|
namespace Spectre.Console.Composition
|
||||||
{
|
{
|
||||||
@ -9,5 +10,9 @@ namespace Spectre.Console.Composition
|
|||||||
[SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix")]
|
[SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix")]
|
||||||
public sealed class SegmentLine : List<Segment>
|
public sealed class SegmentLine : List<Segment>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the length of the line.
|
||||||
|
/// </summary>
|
||||||
|
public int Length => this.Sum(line => line.Text.Length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
216
src/Spectre.Console/Composition/Text.cs
Normal file
216
src/Spectre.Console/Composition/Text.cs
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Spectre.Console.Composition;
|
||||||
|
using Spectre.Console.Internal;
|
||||||
|
|
||||||
|
namespace Spectre.Console
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents text with color and style.
|
||||||
|
/// </summary>
|
||||||
|
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
|
||||||
|
public sealed class Text : IRenderable
|
||||||
|
{
|
||||||
|
private readonly List<Span> _spans;
|
||||||
|
private string _text;
|
||||||
|
|
||||||
|
private sealed class Span
|
||||||
|
{
|
||||||
|
public int Start { get; }
|
||||||
|
public int End { get; }
|
||||||
|
public Appearance Appearance { get; }
|
||||||
|
|
||||||
|
public Span(int start, int end, Appearance appearance)
|
||||||
|
{
|
||||||
|
Start = start;
|
||||||
|
End = end;
|
||||||
|
Appearance = appearance ?? Appearance.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 ?? 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="style">The style.</param>
|
||||||
|
/// <returns>A <see cref="Text"/> instance.</returns>
|
||||||
|
public static Text New(
|
||||||
|
string text, Color? foreground = null, Color? background = null, Styles? style = null)
|
||||||
|
{
|
||||||
|
var result = MarkupParser.Parse(text, new Appearance(foreground, background, style));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Appends some text with a style.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text to append.</param>
|
||||||
|
/// <param name="appearance">The appearance of the text.</param>
|
||||||
|
public void Append(string text, Appearance appearance)
|
||||||
|
{
|
||||||
|
if (text == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = _text.Length;
|
||||||
|
var end = _text.Length + text.Length;
|
||||||
|
|
||||||
|
_text += text;
|
||||||
|
|
||||||
|
Stylize(start, end, appearance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stylizes a part of the text.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="start">The start position.</param>
|
||||||
|
/// <param name="end">The end position.</param>
|
||||||
|
/// <param name="appearance">The color and style to apply.</param>
|
||||||
|
public void Stylize(int start, int end, Appearance appearance)
|
||||||
|
{
|
||||||
|
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, appearance));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public int Measure(Encoding encoding, int maxWidth)
|
||||||
|
{
|
||||||
|
var lines = _text.SplitLines();
|
||||||
|
return lines.Max(x => x.CellLength(encoding));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IEnumerable<Segment> Render(Encoding encoding, int width)
|
||||||
|
{
|
||||||
|
var result = new List<Segment>();
|
||||||
|
|
||||||
|
var segments = SplitLineBreaks(CreateSegments());
|
||||||
|
|
||||||
|
foreach (var (_, _, last, line) in Segment.SplitLines(segments, width).Enumerate())
|
||||||
|
{
|
||||||
|
foreach (var segment in line)
|
||||||
|
{
|
||||||
|
result.Add(segment.StripLineEndings());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!last)
|
||||||
|
{
|
||||||
|
result.Add(Segment.LineBreak());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<Segment> SplitLineBreaks(IEnumerable<Segment> segments)
|
||||||
|
{
|
||||||
|
// Creates individual segments of line breaks.
|
||||||
|
var result = new List<Segment>();
|
||||||
|
var queue = new Queue<Segment>(segments);
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var segment = queue.Dequeue();
|
||||||
|
|
||||||
|
var index = segment.Text.IndexOf("\n", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (index == -1)
|
||||||
|
{
|
||||||
|
result.Add(segment);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var (first, second) = segment.Split(index);
|
||||||
|
if (!string.IsNullOrEmpty(first.Text))
|
||||||
|
{
|
||||||
|
result.Add(first);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(Segment.LineBreak());
|
||||||
|
queue.Enqueue(new Segment(second.Text.Substring(1), second.Appearance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Appearance);
|
||||||
|
styleMap[0] = Appearance.Plain;
|
||||||
|
|
||||||
|
// Create a span list.
|
||||||
|
var spans = new List<(int Offset, bool Leaving, int Style)>();
|
||||||
|
spans.Add((0, false, 0));
|
||||||
|
spans.AddRange(_spans.SelectIndex((span, index) => (span.Start, false, index + 1)));
|
||||||
|
spans.AddRange(_spans.SelectIndex((span, index) => (span.End, true, index + 1)));
|
||||||
|
spans.Add((_text.Length, true, 0));
|
||||||
|
spans = spans.OrderBy(x => x.Offset).ThenBy(x => !x.Leaving).ToList();
|
||||||
|
|
||||||
|
// Keep track of applied appearances 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 = Appearance.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,8 +29,7 @@ namespace Spectre.Console
|
|||||||
/// <param name="args">An array of objects to write.</param>
|
/// <param name="args">An array of objects to write.</param>
|
||||||
public static void Markup(this IAnsiConsole console, IFormatProvider provider, string format, params object[] args)
|
public static void Markup(this IAnsiConsole console, IFormatProvider provider, string format, params object[] args)
|
||||||
{
|
{
|
||||||
var result = MarkupParser.Parse(string.Format(provider, format, args));
|
console.Render(MarkupParser.Parse(string.Format(provider, format, args)));
|
||||||
result.Render(console);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -92,12 +92,7 @@ namespace Spectre.Console.Internal
|
|||||||
internal static Color ExactOrClosest(ColorSystem system, Color color)
|
internal static Color ExactOrClosest(ColorSystem system, Color color)
|
||||||
{
|
{
|
||||||
var exact = Exact(system, color);
|
var exact = Exact(system, color);
|
||||||
if (exact != null)
|
return exact ?? Closest(system, color);
|
||||||
{
|
|
||||||
return exact.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Closest(system, color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Color? Exact(ColorSystem system, Color color)
|
private static Color? Exact(ColorSystem system, Color color)
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Spectre.Console.Internal
|
||||||
|
{
|
||||||
|
internal static class AppearanceExtensions
|
||||||
|
{
|
||||||
|
public static Appearance Combine(this Appearance appearance, IEnumerable<Appearance> source)
|
||||||
|
{
|
||||||
|
var current = appearance;
|
||||||
|
foreach (var item in source)
|
||||||
|
{
|
||||||
|
current = current.Combine(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Spectre.Console.Internal
|
|
||||||
{
|
|
||||||
internal static class CharExtensions
|
|
||||||
{
|
|
||||||
public static int CellLength(this char token, Encoding encoding)
|
|
||||||
{
|
|
||||||
return Cell.GetCellLength(encoding, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Spectre.Console.Composition;
|
|
||||||
|
|
||||||
namespace Spectre.Console.Internal
|
namespace Spectre.Console.Internal
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Spectre.Console.Internal
|
||||||
|
{
|
||||||
|
internal static class EnumerableExtensions
|
||||||
|
{
|
||||||
|
public static IEnumerable<(int Index, bool First, bool Last, T Item)> Enumerate<T>(this IEnumerable<T> source)
|
||||||
|
{
|
||||||
|
if (source is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Enumerate(source.GetEnumerator());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<(int Index, bool First, bool Last, T Item)> Enumerate<T>(this IEnumerator<T> source)
|
||||||
|
{
|
||||||
|
if (source is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
var first = true;
|
||||||
|
var last = !source.MoveNext();
|
||||||
|
T current;
|
||||||
|
|
||||||
|
for (var index = 0; !last; index++)
|
||||||
|
{
|
||||||
|
current = source.Current;
|
||||||
|
last = !source.MoveNext();
|
||||||
|
yield return (index, first, last, current);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<TResult> SelectIndex<T, TResult>(this IEnumerable<T> source, Func<T, int, TResult> func)
|
||||||
|
{
|
||||||
|
return source.Select((value, index) => func(value, index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Spectre.Console.Internal
|
|
||||||
{
|
|
||||||
internal sealed class MarkupBlockNode : IMarkupNode
|
|
||||||
{
|
|
||||||
private readonly List<IMarkupNode> _elements;
|
|
||||||
|
|
||||||
public MarkupBlockNode()
|
|
||||||
{
|
|
||||||
_elements = new List<IMarkupNode>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Append(IMarkupNode element)
|
|
||||||
{
|
|
||||||
if (element != null)
|
|
||||||
{
|
|
||||||
_elements.Add(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Render(IAnsiConsole renderer)
|
|
||||||
{
|
|
||||||
foreach (var element in _elements)
|
|
||||||
{
|
|
||||||
element.Render(renderer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Spectre.Console.Internal
|
|
||||||
{
|
|
||||||
internal sealed class MarkupStyleNode : IMarkupNode
|
|
||||||
{
|
|
||||||
private readonly Styles? _style;
|
|
||||||
private readonly Color? _foreground;
|
|
||||||
private readonly Color? _background;
|
|
||||||
private readonly IMarkupNode _element;
|
|
||||||
|
|
||||||
public MarkupStyleNode(
|
|
||||||
Styles? style,
|
|
||||||
Color? foreground,
|
|
||||||
Color? background,
|
|
||||||
IMarkupNode element)
|
|
||||||
{
|
|
||||||
_style = style;
|
|
||||||
_foreground = foreground;
|
|
||||||
_background = background;
|
|
||||||
_element = element ?? throw new ArgumentNullException(nameof(element));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Render(IAnsiConsole renderer)
|
|
||||||
{
|
|
||||||
var style = (IDisposable)null;
|
|
||||||
var foreground = (IDisposable)null;
|
|
||||||
var background = (IDisposable)null;
|
|
||||||
|
|
||||||
if (_style != null)
|
|
||||||
{
|
|
||||||
style = renderer.PushStyle(_style.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_foreground != null)
|
|
||||||
{
|
|
||||||
foreground = renderer.PushColor(_foreground.Value, foreground: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_background != null)
|
|
||||||
{
|
|
||||||
background = renderer.PushColor(_background.Value, foreground: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
_element.Render(renderer);
|
|
||||||
|
|
||||||
background?.Dispose();
|
|
||||||
foreground?.Dispose();
|
|
||||||
style?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Spectre.Console.Internal
|
|
||||||
{
|
|
||||||
internal sealed class MarkupTextNode : IMarkupNode
|
|
||||||
{
|
|
||||||
public string Text { get; }
|
|
||||||
|
|
||||||
public MarkupTextNode(string text)
|
|
||||||
{
|
|
||||||
Text = text ?? throw new ArgumentNullException(nameof(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Render(IAnsiConsole renderer)
|
|
||||||
{
|
|
||||||
renderer.Write(Text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
namespace Spectre.Console.Internal
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a parsed markup node.
|
|
||||||
/// </summary>
|
|
||||||
internal interface IMarkupNode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Renders the node using the specified renderer.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="renderer">The renderer to use.</param>
|
|
||||||
void Render(IAnsiConsole renderer);
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,37 +5,23 @@ namespace Spectre.Console.Internal
|
|||||||
{
|
{
|
||||||
internal static class MarkupParser
|
internal static class MarkupParser
|
||||||
{
|
{
|
||||||
public static IMarkupNode Parse(string text)
|
public static Text Parse(string text, Appearance appearance = null)
|
||||||
{
|
{
|
||||||
|
appearance ??= Appearance.Plain;
|
||||||
|
|
||||||
|
var result = new Text(string.Empty);
|
||||||
using var tokenizer = new MarkupTokenizer(text);
|
using var tokenizer = new MarkupTokenizer(text);
|
||||||
var root = new MarkupBlockNode();
|
|
||||||
|
|
||||||
var stack = new Stack<MarkupBlockNode>();
|
var stack = new Stack<Appearance>();
|
||||||
var current = root;
|
|
||||||
|
|
||||||
while (true)
|
while (tokenizer.MoveNext())
|
||||||
{
|
{
|
||||||
var token = tokenizer.GetNext();
|
var token = tokenizer.Current;
|
||||||
if (token == null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token.Kind == MarkupTokenKind.Text)
|
if (token.Kind == MarkupTokenKind.Open)
|
||||||
{
|
|
||||||
current.Append(new MarkupTextNode(token.Value));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else if (token.Kind == MarkupTokenKind.Open)
|
|
||||||
{
|
{
|
||||||
var (style, foreground, background) = MarkupStyleParser.Parse(token.Value);
|
var (style, foreground, background) = MarkupStyleParser.Parse(token.Value);
|
||||||
var content = new MarkupBlockNode();
|
stack.Push(new Appearance(foreground, background, style));
|
||||||
current.Append(new MarkupStyleNode(style, foreground, background, content));
|
|
||||||
|
|
||||||
current = content;
|
|
||||||
stack.Push(current);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
else if (token.Kind == MarkupTokenKind.Close)
|
else if (token.Kind == MarkupTokenKind.Close)
|
||||||
{
|
{
|
||||||
@ -45,20 +31,17 @@ namespace Spectre.Console.Internal
|
|||||||
}
|
}
|
||||||
|
|
||||||
stack.Pop();
|
stack.Pop();
|
||||||
|
|
||||||
if (stack.Count == 0)
|
|
||||||
{
|
|
||||||
current = root;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
current = stack.Peek();
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
else if (token.Kind == MarkupTokenKind.Text)
|
||||||
throw new InvalidOperationException("Encountered unkown markup token.");
|
{
|
||||||
|
// Get the effecive style.
|
||||||
|
var style = appearance.Combine(stack);
|
||||||
|
result.Append(token.Value, style);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Encountered unkown markup token.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stack.Count > 0)
|
if (stack.Count > 0)
|
||||||
@ -66,7 +49,7 @@ namespace Spectre.Console.Internal
|
|||||||
throw new InvalidOperationException("Unbalanced markup stack. Did you forget to close a tag?");
|
throw new InvalidOperationException("Unbalanced markup stack. Did you forget to close a tag?");
|
||||||
}
|
}
|
||||||
|
|
||||||
return root;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ namespace Spectre.Console.Internal
|
|||||||
{
|
{
|
||||||
private readonly StringBuffer _reader;
|
private readonly StringBuffer _reader;
|
||||||
|
|
||||||
|
public MarkupToken Current { get; private set; }
|
||||||
|
|
||||||
public MarkupTokenizer(string text)
|
public MarkupTokenizer(string text)
|
||||||
{
|
{
|
||||||
_reader = new StringBuffer(text ?? throw new ArgumentNullException(nameof(text)));
|
_reader = new StringBuffer(text ?? throw new ArgumentNullException(nameof(text)));
|
||||||
@ -17,11 +19,11 @@ namespace Spectre.Console.Internal
|
|||||||
_reader.Dispose();
|
_reader.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarkupToken GetNext()
|
public bool MoveNext()
|
||||||
{
|
{
|
||||||
if (_reader.Eof)
|
if (_reader.Eof)
|
||||||
{
|
{
|
||||||
return null;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var current = _reader.Peek();
|
var current = _reader.Peek();
|
||||||
@ -40,7 +42,8 @@ namespace Spectre.Console.Internal
|
|||||||
if (current == '[')
|
if (current == '[')
|
||||||
{
|
{
|
||||||
_reader.Read();
|
_reader.Read();
|
||||||
return new MarkupToken(MarkupTokenKind.Text, "[", position);
|
Current = new MarkupToken(MarkupTokenKind.Text, "[", position);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current == '/')
|
if (current == '/')
|
||||||
@ -59,7 +62,8 @@ namespace Spectre.Console.Internal
|
|||||||
}
|
}
|
||||||
|
|
||||||
_reader.Read();
|
_reader.Read();
|
||||||
return new MarkupToken(MarkupTokenKind.Close, string.Empty, position);
|
Current = new MarkupToken(MarkupTokenKind.Close, string.Empty, position);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
@ -80,7 +84,8 @@ namespace Spectre.Console.Internal
|
|||||||
}
|
}
|
||||||
|
|
||||||
_reader.Read();
|
_reader.Read();
|
||||||
return new MarkupToken(MarkupTokenKind.Open, builder.ToString(), position);
|
Current = new MarkupToken(MarkupTokenKind.Open, builder.ToString(), position);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -97,7 +102,8 @@ namespace Spectre.Console.Internal
|
|||||||
builder.Append(_reader.Read());
|
builder.Append(_reader.Read());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MarkupToken(MarkupTokenKind.Text, builder.ToString(), position);
|
Current = new MarkupToken(MarkupTokenKind.Text, builder.ToString(), position);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,127 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using Spectre.Console.Composition;
|
|
||||||
using Spectre.Console.Internal;
|
|
||||||
|
|
||||||
namespace Spectre.Console
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Represents text with color and style.
|
|
||||||
/// </summary>
|
|
||||||
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
|
|
||||||
public sealed class Text : IRenderable
|
|
||||||
{
|
|
||||||
private readonly string _text;
|
|
||||||
private readonly Appearance _appearance;
|
|
||||||
private readonly Justify _justify;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="Text"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="text">The text.</param>
|
|
||||||
/// <param name="appearance">The appearance.</param>
|
|
||||||
/// <param name="justify">The justification.</param>
|
|
||||||
public Text(string text, Appearance appearance = null, Justify justify = Justify.Left)
|
|
||||||
{
|
|
||||||
_text = text ?? throw new ArgumentNullException(nameof(text));
|
|
||||||
_appearance = appearance ?? Appearance.Plain;
|
|
||||||
_justify = justify;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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="style">The style.</param>
|
|
||||||
/// <param name="justify">The justification.</param>
|
|
||||||
/// <returns>A <see cref="Text"/> instance.</returns>
|
|
||||||
public static Text New(
|
|
||||||
string text, Color? foreground = null, Color? background = null,
|
|
||||||
Styles? style = null, Justify justify = Justify.Left)
|
|
||||||
{
|
|
||||||
return new Text(text, new Appearance(foreground, background, style), justify);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public int Measure(Encoding encoding, int maxWidth)
|
|
||||||
{
|
|
||||||
return _text.SplitLines().Max(x => x.CellLength(encoding));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IEnumerable<Segment> Render(Encoding encoding, int width)
|
|
||||||
{
|
|
||||||
var result = new List<Segment>();
|
|
||||||
|
|
||||||
foreach (var line in Partition(encoding, _text, width))
|
|
||||||
{
|
|
||||||
result.Add(new Segment(line, _appearance));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<string> Partition(Encoding encoding, string text, int width)
|
|
||||||
{
|
|
||||||
var lines = new List<string>();
|
|
||||||
var line = new StringBuilder();
|
|
||||||
|
|
||||||
var position = 0;
|
|
||||||
foreach (var token in text)
|
|
||||||
{
|
|
||||||
if (token == '\n')
|
|
||||||
{
|
|
||||||
lines.Add(line.ToString());
|
|
||||||
line.Clear();
|
|
||||||
position = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position >= width)
|
|
||||||
{
|
|
||||||
lines.Add(line.ToString());
|
|
||||||
line.Clear();
|
|
||||||
position = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
line.Append(token);
|
|
||||||
position += token.CellLength(encoding);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.Length > 0)
|
|
||||||
{
|
|
||||||
lines.Add(line.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Justify lines
|
|
||||||
for (var i = 0; i < lines.Count; i++)
|
|
||||||
{
|
|
||||||
if (_justify != Justify.Left && lines[i].CellLength(encoding) < width)
|
|
||||||
{
|
|
||||||
if (_justify == Justify.Right)
|
|
||||||
{
|
|
||||||
var diff = width - lines[i].CellLength(encoding);
|
|
||||||
lines[i] = new string(' ', diff) + lines[i];
|
|
||||||
}
|
|
||||||
else if (_justify == Justify.Center)
|
|
||||||
{
|
|
||||||
var diff = (width - lines[i].CellLength(encoding)) / 2;
|
|
||||||
lines[i] = new string(' ', diff) + lines[i] + new string(' ', diff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i < lines.Count - 1)
|
|
||||||
{
|
|
||||||
lines[i] += "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user