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("Hel[4mlo Wo[0mrld");
+ }
- [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("Hel[4mlo[0m\n[4m Wo[0mrl\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;
- }
- }
-}