diff --git a/src/.editorconfig b/src/.editorconfig index 369239d..a9a7a34 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -75,3 +75,6 @@ dotnet_diagnostic.CA1032.severity = none # CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly dotnet_diagnostic.CA1826.severity = none + +# RCS1079: Throwing of new NotImplementedException. +dotnet_diagnostic.RCS1079.severity = warning \ No newline at end of file diff --git a/src/Sample/Program.cs b/src/Sample/Program.cs index 57e6c2a..508f470 100644 --- a/src/Sample/Program.cs +++ b/src/Sample/Program.cs @@ -53,9 +53,10 @@ namespace Sample AnsiConsole.Foreground = Color.Maroon; AnsiConsole.Render(new Panel(new Panel(new Panel(new Panel( Text.New( - "I heard you like πŸ“¦\n\n\n\nSo I put a πŸ“¦ in a πŸ“¦", - foreground: Color.White, - justify: Justify.Center)))))); + "[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), content: Justify.Center))))); // Reset colors AnsiConsole.ResetColors(); @@ -69,16 +70,14 @@ namespace Sample // Centered panel with text AnsiConsole.Render(new Panel( Text.New("Centered\nCenter", - foreground: Color.White, - justify: Justify.Center), - fit: true)); + foreground: Color.White), + fit: true, content: Justify.Center)); // Right adjusted panel with text AnsiConsole.Render(new Panel( Text.New("Right adjusted\nRight", - foreground: Color.White, - justify: Justify.Right), - fit: true)); + foreground: Color.White), + fit: true, content: Justify.Right)); } } } \ No newline at end of file diff --git a/src/Spectre.Console.Tests/.editorconfig b/src/Spectre.Console.Tests/.editorconfig index 1cc45d2..90a63b8 100644 --- a/src/Spectre.Console.Tests/.editorconfig +++ b/src/Spectre.Console.Tests/.editorconfig @@ -21,3 +21,6 @@ dotnet_diagnostic.CA1034.severity = none # CA2000: Dispose objects before losing scope dotnet_diagnostic.CA2000.severity = none + +# SA1118: Parameter should not span multiple lines +dotnet_diagnostic.SA1118.severity = none diff --git a/src/Spectre.Console.Tests/Extensions/StringExtensions.cs b/src/Spectre.Console.Tests/Extensions/StringExtensions.cs new file mode 100644 index 0000000..7319815 --- /dev/null +++ b/src/Spectre.Console.Tests/Extensions/StringExtensions.cs @@ -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); + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleFixture.cs b/src/Spectre.Console.Tests/Fixtures/AnsiConsoleFixture.cs similarity index 60% rename from src/Spectre.Console.Tests/Unit/AnsiConsoleFixture.cs rename to src/Spectre.Console.Tests/Fixtures/AnsiConsoleFixture.cs index 70913bc..54ea64e 100644 --- a/src/Spectre.Console.Tests/Unit/AnsiConsoleFixture.cs +++ b/src/Spectre.Console.Tests/Fixtures/AnsiConsoleFixture.cs @@ -11,16 +11,17 @@ namespace Spectre.Console.Tests 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(); - Console = AnsiConsole.Create(new AnsiConsoleSettings - { - Ansi = ansi, - ColorSystem = (ColorSystemSupport)system, - Out = _writer, - }); + Console = new ConsoleWithWidth( + AnsiConsole.Create(new AnsiConsoleSettings + { + Ansi = ansi, + ColorSystem = (ColorSystemSupport)system, + Out = _writer, + }), width); } public void Dispose() diff --git a/src/Spectre.Console.Tests/Fixtures/ConsoleWithWidth.cs b/src/Spectre.Console.Tests/Fixtures/ConsoleWithWidth.cs new file mode 100644 index 0000000..3429478 --- /dev/null +++ b/src/Spectre.Console.Tests/Fixtures/ConsoleWithWidth.cs @@ -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); + } + } +} diff --git a/src/Spectre.Console.Tests/PlainConsole.cs b/src/Spectre.Console.Tests/Fixtures/PlainConsole.cs similarity index 100% rename from src/Spectre.Console.Tests/PlainConsole.cs rename to src/Spectre.Console.Tests/Fixtures/PlainConsole.cs diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Colors.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Colors.cs index 9b71eeb..d476ee0 100644 --- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Colors.cs +++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Colors.cs @@ -1,7 +1,7 @@ using Shouldly; using Xunit; -namespace Spectre.Console.Tests +namespace Spectre.Console.Tests.Unit { public partial class AnsiConsoleTests { diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs index 4633647..c27b24e 100644 --- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs +++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs @@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Shouldly; using Xunit; -namespace Spectre.Console.Tests +namespace Spectre.Console.Tests.Unit { public partial class AnsiConsoleTests { diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs index 2d7f9a1..624d52e 100644 --- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs +++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs @@ -1,7 +1,7 @@ using Shouldly; using Xunit; -namespace Spectre.Console.Tests +namespace Spectre.Console.Tests.Unit { public partial class AnsiConsoleTests { diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs index edc7aa6..aeecdfc 100644 --- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs +++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs @@ -3,7 +3,7 @@ using System.Globalization; using Shouldly; using Xunit; -namespace Spectre.Console.Tests +namespace Spectre.Console.Tests.Unit { public partial class AnsiConsoleTests { diff --git a/src/Spectre.Console.Tests/Unit/AppearanceTests.cs b/src/Spectre.Console.Tests/Unit/AppearanceTests.cs new file mode 100644 index 0000000..3261fa3 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/AppearanceTests.cs @@ -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); + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Composition/PanelTests.cs b/src/Spectre.Console.Tests/Unit/Composition/PanelTests.cs index 74383e7..a84a9b1 100644 --- a/src/Spectre.Console.Tests/Unit/Composition/PanelTests.cs +++ b/src/Spectre.Console.Tests/Unit/Composition/PanelTests.cs @@ -1,8 +1,7 @@ using Shouldly; -using Spectre.Console.Composition; using Xunit; -namespace Spectre.Console.Tests.Unit.Composition +namespace Spectre.Console.Tests.Unit { public sealed class PanelTests { @@ -13,7 +12,7 @@ namespace Spectre.Console.Tests.Unit.Composition var console = new PlainConsole(width: 80); // When - console.Render(new Panel(new Text("Hello World"))); + console.Render(new Panel(Text.New("Hello World"))); // Then console.Lines.Count.ShouldBe(3); @@ -29,7 +28,7 @@ namespace Spectre.Console.Tests.Unit.Composition var console = new PlainConsole(width: 80); // When - console.Render(new Panel(new Text(" \nπŸ’©\n "))); + console.Render(new Panel(Text.New(" \nπŸ’©\n "))); // Then console.Lines.Count.ShouldBe(5); @@ -47,7 +46,7 @@ namespace Spectre.Console.Tests.Unit.Composition var console = new PlainConsole(width: 80); // When - console.Render(new Panel(new Text("Hello World\nFoo Bar"))); + console.Render(new Panel(Text.New("Hello World\nFoo Bar"))); // Then console.Lines.Count.ShouldBe(4); @@ -57,6 +56,29 @@ namespace Spectre.Console.Tests.Unit.Composition 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] 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); // When - console.Render(new Panel(new Text("Hello World"), fit: true)); + console.Render(new Panel(Text.New("Hello World"), fit: true)); // Then console.Lines.Count.ShouldBe(3); @@ -80,7 +102,7 @@ namespace Spectre.Console.Tests.Unit.Composition var console = new PlainConsole(width: 25); // 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 console.Lines.Count.ShouldBe(3); @@ -96,7 +118,7 @@ namespace Spectre.Console.Tests.Unit.Composition var console = new PlainConsole(width: 25); // 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 console.Lines.Count.ShouldBe(3); @@ -112,7 +134,7 @@ namespace Spectre.Console.Tests.Unit.Composition var console = new PlainConsole(width: 80); // When - console.Render(new Panel(new Panel(new Text("Hello World")))); + console.Render(new Panel(new Panel(Text.New("Hello World")))); // Then console.Lines.Count.ShouldBe(5); diff --git a/src/Spectre.Console.Tests/Unit/Composition/SegmentTests.cs b/src/Spectre.Console.Tests/Unit/Composition/SegmentTests.cs index 1d7ec12..0e97b2a 100644 --- a/src/Spectre.Console.Tests/Unit/Composition/SegmentTests.cs +++ b/src/Spectre.Console.Tests/Unit/Composition/SegmentTests.cs @@ -2,66 +2,91 @@ using Shouldly; using Spectre.Console.Composition; using Xunit; -namespace Spectre.Console.Tests.Unit.Composition +namespace Spectre.Console.Tests.Unit { public sealed class SegmentTests { - [Fact] - public void Should_Split_Segment() + public sealed class TheSplitMethod { - var lines = Segment.Split(new[] + [Fact] + public void Should_Split_Segment_Correctly() { - new Segment("Foo"), - new Segment("Bar"), - new Segment("\n"), - new Segment("Baz"), - new Segment("Qux"), - new Segment("\n"), - new Segment("Corgi"), - }); + // Given + var appearance = new Appearance(Color.Red, Color.Green, Styles.Bold); + var segment = new Segment("Foo Bar", appearance); - // Then - lines.Count.ShouldBe(3); + // When + var (first, second) = segment.Split(3); - lines[0].Count.ShouldBe(2); - lines[0][0].Text.ShouldBe("Foo"); - lines[0][1].Text.ShouldBe("Bar"); - - lines[1].Count.ShouldBe(2); - lines[1][0].Text.ShouldBe("Baz"); - lines[1][1].Text.ShouldBe("Qux"); - - lines[2].Count.ShouldBe(1); - lines[2][0].Text.ShouldBe("Corgi"); + // Then + first.Text.ShouldBe("Foo"); + first.Appearance.ShouldBe(appearance); + second.Text.ShouldBe(" Bar"); + second.Appearance.ShouldBe(appearance); + } } - [Fact] - public void Should_Split_Segments_With_Linebreak_In_Text() + public sealed class TheSplitLinesMethod { - var lines = Segment.Split(new[] + [Fact] + public void Should_Split_Segment() { - new Segment("Foo\n"), - new Segment("Bar\n"), - new Segment("Baz"), - new Segment("Qux\n"), - new Segment("Corgi"), - }); + var lines = Segment.SplitLines( + new[] + { + new Segment("Foo"), + new Segment("Bar"), + new Segment("\n"), + new Segment("Baz"), + new Segment("Qux"), + new Segment("\n"), + new Segment("Corgi"), + }); - // Then - lines.Count.ShouldBe(4); + // Then + lines.Count.ShouldBe(3); - lines[0].Count.ShouldBe(1); - lines[0][0].Text.ShouldBe("Foo"); + lines[0].Count.ShouldBe(2); + lines[0][0].Text.ShouldBe("Foo"); + lines[0][1].Text.ShouldBe("Bar"); - lines[1].Count.ShouldBe(1); - lines[1][0].Text.ShouldBe("Bar"); + lines[1].Count.ShouldBe(2); + lines[1][0].Text.ShouldBe("Baz"); + lines[1][1].Text.ShouldBe("Qux"); - lines[2].Count.ShouldBe(2); - lines[2][0].Text.ShouldBe("Baz"); - lines[2][1].Text.ShouldBe("Qux"); + lines[2].Count.ShouldBe(1); + lines[2][0].Text.ShouldBe("Corgi"); + } - lines[3].Count.ShouldBe(1); - lines[3][0].Text.ShouldBe("Corgi"); + [Fact] + 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"); + } } } } diff --git a/src/Spectre.Console.Tests/Unit/Composition/TextTests.cs b/src/Spectre.Console.Tests/Unit/Composition/TextTests.cs index 4502678..3db878c 100644 --- a/src/Spectre.Console.Tests/Unit/Composition/TextTests.cs +++ b/src/Spectre.Console.Tests/Unit/Composition/TextTests.cs @@ -1,61 +1,77 @@ using Shouldly; -using Spectre.Console.Composition; using Xunit; -namespace Spectre.Console.Tests.Composition +namespace Spectre.Console.Tests.Unit { public sealed class TextTests { [Fact] - public void Should_Render_Text_To_Console() + public void Should_Render_Unstyled_Text_As_Expected() { // Given - var console = new PlainConsole(); + var fixture = new PlainConsole(width: 80); + var text = Text.New("Hello World"); // When - console.Render(new Text("Hello World")); + fixture.Render(text); // Then - console.Output.ShouldBe("Hello World"); + fixture.Output + .NormalizeLineEndings() + .ShouldBe("Hello World"); } [Fact] - public void Should_Right_Align_Text_To_Parent() + public void Should_Split_Unstyled_Text_To_New_Lines_If_Width_Exceeds_Console_Width() { // Given - var console = new PlainConsole(width: 15); + var fixture = new PlainConsole(width: 5); + var text = Text.New("Hello World"); // When - console.Render(new Text("Hello World", justify: Justify.Right)); + fixture.Render(text); // Then - console.Output.ShouldBe(" Hello World"); + fixture.Output + .NormalizeLineEndings() + .ShouldBe("Hello\n Worl\nd"); } - [Fact] - public void Should_Center_Text_To_Parent() + public sealed class TheStylizeMethod { - // Given - var console = new PlainConsole(width: 15); + [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 Appearance(style: Styles.Underline)); - // When - console.Render(new Text("Hello World", justify: Justify.Center)); + // When + fixture.Console.Render(text); - // Then - console.Output.ShouldBe(" Hello World "); - } + // Then + fixture.Output + .NormalizeLineEndings() + .ShouldBe("Hello World"); + } - [Fact] - public void Should_Split_Text_To_Multiple_Lines_If_It_Does_Not_Fit() - { - // Given - var console = new PlainConsole(width: 5); + [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 Appearance(style: Styles.Underline)); - // When - console.Render(new Text("Hello World")); + // When + fixture.Console.Render(text); - // Then - console.Output.ShouldBe("Hello\n Worl\nd"); + // Then + fixture.Output + .NormalizeLineEndings() + .ShouldBe("Hello\n Worl\nd"); + } } } } diff --git a/src/Spectre.Console/Appearance.cs b/src/Spectre.Console/Appearance.cs index eb1b986..8ec4fb7 100644 --- a/src/Spectre.Console/Appearance.cs +++ b/src/Spectre.Console/Appearance.cs @@ -26,12 +26,7 @@ namespace Spectre.Console /// Gets an with the /// default color and without style. /// - public static Appearance Plain { get; } - - static Appearance() - { - Plain = new Appearance(); - } + public static Appearance Plain { get; } = new Appearance(); private Appearance() : this(null, null, null) @@ -51,6 +46,33 @@ namespace Spectre.Console Style = style ?? Styles.None; } + /// + /// Combines this appearance with another one. + /// + /// The item to combine with this. + /// A new appearance representing a combination of this and the other one. + 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); + } + /// public override int GetHashCode() { diff --git a/src/Spectre.Console/Renderables/Panel.cs b/src/Spectre.Console/Composition/Panel.cs similarity index 54% rename from src/Spectre.Console/Renderables/Panel.cs rename to src/Spectre.Console/Composition/Panel.cs index b1b8229..f6e7a62 100644 --- a/src/Spectre.Console/Renderables/Panel.cs +++ b/src/Spectre.Console/Composition/Panel.cs @@ -12,16 +12,19 @@ namespace Spectre.Console { private readonly IRenderable _child; private readonly bool _fit; + private readonly Justify _content; /// /// Initializes a new instance of the class. /// /// The child. /// Whether or not to fit the panel to it's parent. - public Panel(IRenderable child, bool fit = false) + /// The justification of the panel content. + public Panel(IRenderable child, bool fit = false, Justify content = Justify.Left) { _child = child; _fit = fit; + _content = content; } /// @@ -48,23 +51,59 @@ namespace Spectre.Console result.Add(new Segment("┐")); result.Add(new Segment("\n")); + // Render the child. 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("β”‚ ")); - foreach (var segment in line) - { - result.Add(segment.StripLineEndings()); - } + var content = new List(); var length = line.Sum(segment => segment.CellLength(encoding)); if (length < childWidth) { - var diff = childWidth - length; - result.Add(new Segment(new string(' ', diff))); + if (_content == Justify.Right) + { + 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("\n")); } diff --git a/src/Spectre.Console/Composition/Segment.cs b/src/Spectre.Console/Composition/Segment.cs index f9216b1..b01cffa 100644 --- a/src/Spectre.Console/Composition/Segment.cs +++ b/src/Spectre.Console/Composition/Segment.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using Spectre.Console.Internal; @@ -9,13 +10,20 @@ namespace Spectre.Console.Composition /// /// Represents a renderable segment. /// - public sealed class Segment + [DebuggerDisplay("{Text,nq}")] + public class Segment { /// /// Gets the segment text. /// public string Text { get; } + /// + /// Gets a value indicating whether or not this is an expicit line break + /// that should be preserved. + /// + public bool IsLineBreak { get; } + /// /// Gets the appearance of the segment. /// @@ -36,9 +44,24 @@ namespace Spectre.Console.Composition /// The segment text. /// The segment 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)); Appearance = appearance; + IsLineBreak = lineBreak; + } + + /// + /// Creates a segment that represents an implicit line break. + /// + /// A segment that represents an implicit line break. + public static Segment LineBreak() + { + return new Segment("\n", Appearance.Plain, true); } /// @@ -61,12 +84,45 @@ namespace Spectre.Console.Composition return new Segment(Text.TrimEnd('\n'), Appearance); } + /// + /// Splits the segment at the offset. + /// + /// The offset where to split the segment. + /// One or two new segments representing the split. + 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)); + } + /// /// Splits the provided segments into lines. /// /// The segments to split. /// A collection of lines. - public static List Split(IEnumerable segments) + public static List SplitLines(IEnumerable segments) + { + return SplitLines(segments, int.MaxValue); + } + + /// + /// Splits the provided segments into lines with a maximum width. + /// + /// The segments to split into lines. + /// The maximum width. + /// A list of lines. + public static List SplitLines(IEnumerable segments, int maxWidth) { if (segments is null) { @@ -76,14 +132,41 @@ namespace Spectre.Console.Composition var lines = new List(); var line = new SegmentLine(); - foreach (var segment in segments) + var stack = new Stack(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 == "\n") { - lines.Add(line); - line = new SegmentLine(); + if (line.Length > 0 || segment.IsLineBreak) + { + lines.Add(line); + line = new SegmentLine(); + } + continue; } @@ -93,19 +176,21 @@ namespace Spectre.Console.Composition var parts = text.SplitLines(); 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) { - lines.Add(line); - line = new SegmentLine(); + if (line.Length > 0) + { + lines.Add(line); + line = new SegmentLine(); + } text = string.Concat(parts.Skip(1).Take(parts.Length - 1)); - if (string.IsNullOrWhiteSpace(text)) - { - text = null; - } } else { diff --git a/src/Spectre.Console/Composition/SegmentLine.cs b/src/Spectre.Console/Composition/SegmentLine.cs index ebc0003..5888be8 100644 --- a/src/Spectre.Console/Composition/SegmentLine.cs +++ b/src/Spectre.Console/Composition/SegmentLine.cs @@ -1,5 +1,6 @@ -ο»Ώusing System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Spectre.Console.Composition { @@ -9,5 +10,9 @@ namespace Spectre.Console.Composition [SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix")] public sealed class SegmentLine : List { + /// + /// Gets the length of the line. + /// + public int Length => this.Sum(line => line.Text.Length); } } diff --git a/src/Spectre.Console/Composition/Text.cs b/src/Spectre.Console/Composition/Text.cs new file mode 100644 index 0000000..fcac206 --- /dev/null +++ b/src/Spectre.Console/Composition/Text.cs @@ -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 +{ + /// + /// Represents text with color and style. + /// + [SuppressMessage("Naming", "CA1724:Type names should not match namespaces")] + public sealed class Text : IRenderable + { + private readonly List _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; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The text. + internal Text(string text) + { + _text = text ?? throw new ArgumentNullException(nameof(text)); + _spans = new List(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The text. + /// The foreground. + /// The background. + /// The style. + /// A instance. + 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; + } + + /// + /// Appends some text with a style. + /// + /// The text to append. + /// The appearance of the text. + 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); + } + + /// + /// Stylizes a part of the text. + /// + /// The start position. + /// The end position. + /// The color and style to apply. + 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)); + } + + /// + public int Measure(Encoding encoding, int maxWidth) + { + var lines = _text.SplitLines(); + return lines.Max(x => x.CellLength(encoding)); + } + + /// + public IEnumerable Render(Encoding encoding, int width) + { + var result = new List(); + + 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 SplitLineBreaks(IEnumerable segments) + { + // Creates individual segments of line breaks. + var result = new List(); + var queue = new Queue(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 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(); + + // Now build the segments. + var result = new List(); + 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)); + } + } +} diff --git a/src/Spectre.Console/ConsoleExtensions.Markup.cs b/src/Spectre.Console/ConsoleExtensions.Markup.cs index f13e16a..369ba18 100644 --- a/src/Spectre.Console/ConsoleExtensions.Markup.cs +++ b/src/Spectre.Console/ConsoleExtensions.Markup.cs @@ -29,8 +29,7 @@ namespace Spectre.Console /// An array of objects to write. public static void Markup(this IAnsiConsole console, IFormatProvider provider, string format, params object[] args) { - var result = MarkupParser.Parse(string.Format(provider, format, args)); - result.Render(console); + console.Render(MarkupParser.Parse(string.Format(provider, format, args))); } /// diff --git a/src/Spectre.Console/Internal/Colors/ColorPalette.cs b/src/Spectre.Console/Internal/Colors/ColorPalette.cs index 91a644f..69d7081 100644 --- a/src/Spectre.Console/Internal/Colors/ColorPalette.cs +++ b/src/Spectre.Console/Internal/Colors/ColorPalette.cs @@ -92,12 +92,7 @@ namespace Spectre.Console.Internal internal static Color ExactOrClosest(ColorSystem system, Color color) { var exact = Exact(system, color); - if (exact != null) - { - return exact.Value; - } - - return Closest(system, color); + return exact ?? Closest(system, color); } private static Color? Exact(ColorSystem system, Color color) diff --git a/src/Spectre.Console/Internal/Extensions/AppearanceExtensions.cs b/src/Spectre.Console/Internal/Extensions/AppearanceExtensions.cs new file mode 100644 index 0000000..421ac93 --- /dev/null +++ b/src/Spectre.Console/Internal/Extensions/AppearanceExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Spectre.Console.Internal +{ + internal static class AppearanceExtensions + { + public static Appearance Combine(this Appearance appearance, IEnumerable source) + { + var current = appearance; + foreach (var item in source) + { + current = current.Combine(item); + } + + return current; + } + } +} diff --git a/src/Spectre.Console/Internal/Extensions/CharExtensions.cs b/src/Spectre.Console/Internal/Extensions/CharExtensions.cs deleted file mode 100644 index ac393f3..0000000 --- a/src/Spectre.Console/Internal/Extensions/CharExtensions.cs +++ /dev/null @@ -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); - } - } -} diff --git a/src/Spectre.Console/Internal/Extensions/ConsoleExtensions.cs b/src/Spectre.Console/Internal/Extensions/ConsoleExtensions.cs index 4ecfd87..daf4e1d 100644 --- a/src/Spectre.Console/Internal/Extensions/ConsoleExtensions.cs +++ b/src/Spectre.Console/Internal/Extensions/ConsoleExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using Spectre.Console.Composition; namespace Spectre.Console.Internal { diff --git a/src/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs b/src/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..48b9da0 --- /dev/null +++ b/src/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs @@ -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(this IEnumerable 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(this IEnumerator 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 SelectIndex(this IEnumerable source, Func func) + { + return source.Select((value, index) => func(value, index)); + } + } +} diff --git a/src/Spectre.Console/Internal/Text/Markup/Ast/MarkupBlockNode.cs b/src/Spectre.Console/Internal/Text/Markup/Ast/MarkupBlockNode.cs deleted file mode 100644 index 0a8ff99..0000000 --- a/src/Spectre.Console/Internal/Text/Markup/Ast/MarkupBlockNode.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; - -namespace Spectre.Console.Internal -{ - internal sealed class MarkupBlockNode : IMarkupNode - { - private readonly List _elements; - - public MarkupBlockNode() - { - _elements = new List(); - } - - public void Append(IMarkupNode element) - { - if (element != null) - { - _elements.Add(element); - } - } - - public void Render(IAnsiConsole renderer) - { - foreach (var element in _elements) - { - element.Render(renderer); - } - } - } -} diff --git a/src/Spectre.Console/Internal/Text/Markup/Ast/MarkupStyleNode.cs b/src/Spectre.Console/Internal/Text/Markup/Ast/MarkupStyleNode.cs deleted file mode 100644 index b4aa4a4..0000000 --- a/src/Spectre.Console/Internal/Text/Markup/Ast/MarkupStyleNode.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/src/Spectre.Console/Internal/Text/Markup/Ast/MarkupTextNode.cs b/src/Spectre.Console/Internal/Text/Markup/Ast/MarkupTextNode.cs deleted file mode 100644 index f7505d7..0000000 --- a/src/Spectre.Console/Internal/Text/Markup/Ast/MarkupTextNode.cs +++ /dev/null @@ -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); - } - } -} diff --git a/src/Spectre.Console/Internal/Text/Markup/IMarkupNode.cs b/src/Spectre.Console/Internal/Text/Markup/IMarkupNode.cs deleted file mode 100644 index 8136e00..0000000 --- a/src/Spectre.Console/Internal/Text/Markup/IMarkupNode.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Spectre.Console.Internal -{ - /// - /// Represents a parsed markup node. - /// - internal interface IMarkupNode - { - /// - /// Renders the node using the specified renderer. - /// - /// The renderer to use. - void Render(IAnsiConsole renderer); - } -} diff --git a/src/Spectre.Console/Internal/Text/Markup/MarkupParser.cs b/src/Spectre.Console/Internal/Text/Markup/MarkupParser.cs index 53cf68c..f132def 100644 --- a/src/Spectre.Console/Internal/Text/Markup/MarkupParser.cs +++ b/src/Spectre.Console/Internal/Text/Markup/MarkupParser.cs @@ -5,37 +5,23 @@ namespace Spectre.Console.Internal { 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); - var root = new MarkupBlockNode(); - var stack = new Stack(); - var current = root; + var stack = new Stack(); - while (true) + while (tokenizer.MoveNext()) { - var token = tokenizer.GetNext(); - if (token == null) - { - break; - } + var token = tokenizer.Current; - if (token.Kind == MarkupTokenKind.Text) - { - current.Append(new MarkupTextNode(token.Value)); - continue; - } - else if (token.Kind == MarkupTokenKind.Open) + if (token.Kind == MarkupTokenKind.Open) { var (style, foreground, background) = MarkupStyleParser.Parse(token.Value); - var content = new MarkupBlockNode(); - current.Append(new MarkupStyleNode(style, foreground, background, content)); - - current = content; - stack.Push(current); - - continue; + stack.Push(new Appearance(foreground, background, style)); } else if (token.Kind == MarkupTokenKind.Close) { @@ -45,20 +31,17 @@ namespace Spectre.Console.Internal } stack.Pop(); - - if (stack.Count == 0) - { - current = root; - } - else - { - current = stack.Peek(); - } - - continue; } - - throw new InvalidOperationException("Encountered unkown markup token."); + else if (token.Kind == MarkupTokenKind.Text) + { + // 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) @@ -66,7 +49,7 @@ namespace Spectre.Console.Internal throw new InvalidOperationException("Unbalanced markup stack. Did you forget to close a tag?"); } - return root; + return result; } } } diff --git a/src/Spectre.Console/Internal/Text/Markup/MarkupTokenizer.cs b/src/Spectre.Console/Internal/Text/Markup/MarkupTokenizer.cs index dd4902a..bce67c8 100644 --- a/src/Spectre.Console/Internal/Text/Markup/MarkupTokenizer.cs +++ b/src/Spectre.Console/Internal/Text/Markup/MarkupTokenizer.cs @@ -7,6 +7,8 @@ namespace Spectre.Console.Internal { private readonly StringBuffer _reader; + public MarkupToken Current { get; private set; } + public MarkupTokenizer(string text) { _reader = new StringBuffer(text ?? throw new ArgumentNullException(nameof(text))); @@ -17,11 +19,11 @@ namespace Spectre.Console.Internal _reader.Dispose(); } - public MarkupToken GetNext() + public bool MoveNext() { if (_reader.Eof) { - return null; + return false; } var current = _reader.Peek(); @@ -40,7 +42,8 @@ namespace Spectre.Console.Internal if (current == '[') { _reader.Read(); - return new MarkupToken(MarkupTokenKind.Text, "[", position); + Current = new MarkupToken(MarkupTokenKind.Text, "[", position); + return true; } if (current == '/') @@ -59,7 +62,8 @@ namespace Spectre.Console.Internal } _reader.Read(); - return new MarkupToken(MarkupTokenKind.Close, string.Empty, position); + Current = new MarkupToken(MarkupTokenKind.Close, string.Empty, position); + return true; } var builder = new StringBuilder(); @@ -80,7 +84,8 @@ namespace Spectre.Console.Internal } _reader.Read(); - return new MarkupToken(MarkupTokenKind.Open, builder.ToString(), position); + Current = new MarkupToken(MarkupTokenKind.Open, builder.ToString(), position); + return true; } else { @@ -97,7 +102,8 @@ namespace Spectre.Console.Internal builder.Append(_reader.Read()); } - return new MarkupToken(MarkupTokenKind.Text, builder.ToString(), position); + Current = new MarkupToken(MarkupTokenKind.Text, builder.ToString(), position); + return true; } } } diff --git a/src/Spectre.Console/Renderables/Text.cs b/src/Spectre.Console/Renderables/Text.cs deleted file mode 100644 index 8d9c6ca..0000000 --- a/src/Spectre.Console/Renderables/Text.cs +++ /dev/null @@ -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 -{ - /// - /// Represents text with color and style. - /// - [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; - - /// - /// Initializes a new instance of the class. - /// - /// The text. - /// The appearance. - /// The justification. - public Text(string text, Appearance appearance = null, Justify justify = Justify.Left) - { - _text = text ?? throw new ArgumentNullException(nameof(text)); - _appearance = appearance ?? Appearance.Plain; - _justify = justify; - } - - /// - /// Initializes a new instance of the class. - /// - /// The text. - /// The foreground. - /// The background. - /// The style. - /// The justification. - /// A instance. - 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); - } - - /// - public int Measure(Encoding encoding, int maxWidth) - { - return _text.SplitLines().Max(x => x.CellLength(encoding)); - } - - /// - public IEnumerable Render(Encoding encoding, int width) - { - var result = new List(); - - foreach (var line in Partition(encoding, _text, width)) - { - result.Add(new Segment(line, _appearance)); - } - - return result; - } - - private IEnumerable Partition(Encoding encoding, string text, int width) - { - var lines = new List(); - 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; - } - } -}