mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-10-31 09:09:25 +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:
		 Patrik Svensson
					Patrik Svensson
				
			
				
					committed by
					
						 Patrik Svensson
						Patrik Svensson
					
				
			
			
				
	
			
			
			 Patrik Svensson
						Patrik Svensson
					
				
			
						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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user