mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-14 16:02:50 +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
|
||||
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.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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
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 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()
|
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 Xunit;
|
||||
|
||||
namespace Spectre.Console.Tests
|
||||
namespace Spectre.Console.Tests.Unit
|
||||
{
|
||||
public partial class AnsiConsoleTests
|
||||
{
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -1,7 +1,7 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace Spectre.Console.Tests
|
||||
namespace Spectre.Console.Tests.Unit
|
||||
{
|
||||
public partial class AnsiConsoleTests
|
||||
{
|
||||
|
@ -3,7 +3,7 @@ using System.Globalization;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace Spectre.Console.Tests
|
||||
namespace Spectre.Console.Tests.Unit
|
||||
{
|
||||
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 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);
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,12 +26,7 @@ namespace Spectre.Console
|
||||
/// Gets an <see cref="Appearance"/> with the
|
||||
/// default color and without style.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
|
@ -12,16 +12,19 @@ namespace Spectre.Console
|
||||
{
|
||||
private readonly IRenderable _child;
|
||||
private readonly bool _fit;
|
||||
private readonly Justify _content;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Panel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="child">The child.</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;
|
||||
_fit = fit;
|
||||
_content = content;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@ -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<Segment>();
|
||||
|
||||
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"));
|
||||
}
|
@ -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
|
||||
/// <summary>
|
||||
/// Represents a renderable segment.
|
||||
/// </summary>
|
||||
public sealed class Segment
|
||||
[DebuggerDisplay("{Text,nq}")]
|
||||
public class Segment
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the segment text.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Gets the appearance of the segment.
|
||||
/// </summary>
|
||||
@ -36,9 +44,24 @@ namespace Spectre.Console.Composition
|
||||
/// <param name="text">The segment text.</param>
|
||||
/// <param name="appearance">The segment appearance.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@ -61,12 +84,45 @@ namespace Spectre.Console.Composition
|
||||
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>
|
||||
/// Splits the provided segments into lines.
|
||||
/// </summary>
|
||||
/// <param name="segments">The segments to split.</param>
|
||||
/// <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)
|
||||
{
|
||||
@ -76,14 +132,41 @@ namespace Spectre.Console.Composition
|
||||
var lines = new List<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 == "\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
|
||||
{
|
||||
|
@ -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<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>
|
||||
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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -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)
|
||||
|
@ -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.Diagnostics.CodeAnalysis;
|
||||
using Spectre.Console.Composition;
|
||||
|
||||
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
|
||||
{
|
||||
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<MarkupBlockNode>();
|
||||
var current = root;
|
||||
var stack = new Stack<Appearance>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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