diff --git a/README.md b/README.md index 2800d44..cac2a38 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,16 @@ A .NET Standard 2.0 library that makes it easier to create beautiful console app It is heavily inspired by the excellent [Rich library](https://github.com/willmcgugan/rich) for Python. +## Table of Contents + +1. [Features](#features) +2. [Example](#example) +3. [Usage](#usage) + 3.1. [Using the static API](#using-the-static-api) + 3.2. [Creating a console](#creating-a-console) +4. [Available styles](#available-styles) +5. [Predefiend colors](#predefined-colors) + ## Features * Written with unit testing in mind. @@ -44,7 +54,7 @@ AnsiConsole.Style = Styles.Underline | Styles.Bold; AnsiConsole.WriteLine("Hello World!"); AnsiConsole.Reset(); -AnsiConsole.WriteLine("Good bye!"); +AnsiConsole.MarkupLine("[yellow]{0}[/] [underline]world[/]!", "Goodbye"); ``` If you want to get a reference to the default `IAnsiConsole`, @@ -70,3 +80,280 @@ _NOTE: Even if you can specify a specific color system to use when manually creating a console, remember that the user's terminal might not be able to use it, so unless you're creating an IAnsiConsole for testing, always use `ColorSystemSupport.Detect` and `AnsiSupport.Detect`._ + +## Available styles + +_NOTE: Not all styles are supported in every terminal._ + +Name | Description +--- | --- +`bold` | Bold text +`dim` | Dim or faint text +`italic` | Italic text +`underline` | Underlined text +`invert` | Swaps the foreground and background colors +`conceal` | Hides the text +`slowblink` | Makes text blink slowly +`rapidblink` | Makes text blink +`strikethrough` | Shows text with a horizontal line through the center + +## Predefined colors + +Number | Name | RGB | Hex +--- | --- | --- | --- +`0` | `black` | `0,0,0` | `#000000` +`1` | `maroon` | `128,0,0` | `#800000` +`2` | `green` | `0,128,0` | `#008000` +`3` | `olive` | `128,128,0` | `#808000` +`4` | `navy` | `0,0,128` | `#000080` +`5` | `purple` | `128,0,128` | `#800080` +`6` | `teal` | `0,128,128` | `#008080` +`7` | `silver` | `192,192,192` | `#c0c0c0` +`8` | `grey` | `128,128,128` | `#808080` +`9` | `red` | `255,0,0` | `#ff0000` +`10` | `lime` | `0,255,0` | `#00ff00` +`11` | `yellow` | `255,255,0` | `#ffff00` +`12` | `blue` | `0,0,255` | `#0000ff` +`13` | `fuchsia` | `255,0,255` | `#ff00ff` +`14` | `aqua` | `0,255,255` | `#00ffff` +`15` | `white` | `255,255,255` | `#ffffff` +`16` | `grey0` | `0,0,0` | `#000000` +`17` | `navyblue` | `0,0,95` | `#00005f` +`18` | `darkblue` | `0,0,135` | `#000087` +`19` | `blue3` | `0,0,175` | `#0000af` +`20` | `blue3_1` | `0,0,215` | `#0000d7` +`21` | `blue1` | `0,0,255` | `#0000ff` +`22` | `darkgreen` | `0,95,0` | `#005f00` +`23` | `deepskyblue4` | `0,95,95` | `#005f5f` +`24` | `deepskyblue4_1` | `0,95,135` | `#005f87` +`25` | `deepskyblue4_2` | `0,95,175` | `#005faf` +`26` | `dodgerblue3` | `0,95,215` | `#005fd7` +`27` | `dodgerblue2` | `0,95,255` | `#005fff` +`28` | `green4` | `0,135,0` | `#008700` +`29` | `springgreen4` | `0,135,95` | `#00875f` +`30` | `turquoise4` | `0,135,135` | `#008787` +`31` | `deepskyblue3` | `0,135,175` | `#0087af` +`32` | `deepskyblue3_1` | `0,135,215` | `#0087d7` +`33` | `dodgerblue1` | `0,135,255` | `#0087ff` +`34` | `green3` | `0,175,0` | `#00af00` +`35` | `springgreen3` | `0,175,95` | `#00af5f` +`36` | `darkcyan` | `0,175,135` | `#00af87` +`37` | `lightseagreen` | `0,175,175` | `#00afaf` +`38` | `deepskyblue2` | `0,175,215` | `#00afd7` +`39` | `deepskyblue1` | `0,175,255` | `#00afff` +`40` | `green3_1` | `0,215,0` | `#00d700` +`41` | `springgreen3_1` | `0,215,95` | `#00d75f` +`42` | `springgreen2` | `0,215,135` | `#00d787` +`43` | `cyan3` | `0,215,175` | `#00d7af` +`44` | `darkturquoise` | `0,215,215` | `#00d7d7` +`45` | `turquoise2` | `0,215,255` | `#00d7ff` +`46` | `green1` | `0,255,0` | `#00ff00` +`47` | `springgreen2_1` | `0,255,95` | `#00ff5f` +`48` | `springgreen1` | `0,255,135` | `#00ff87` +`49` | `mediumspringgreen` | `0,255,175` | `#00ffaf` +`50` | `cyan2` | `0,255,215` | `#00ffd7` +`51` | `cyan1` | `0,255,255` | `#00ffff` +`52` | `darkred` | `95,0,0` | `#5f0000` +`53` | `deeppink4` | `95,0,95` | `#5f005f` +`54` | `purple4` | `95,0,135` | `#5f0087` +`55` | `purple4_1` | `95,0,175` | `#5f00af` +`56` | `purple3` | `95,0,215` | `#5f00d7` +`57` | `blueviolet` | `95,0,255` | `#5f00ff` +`58` | `orange4` | `95,95,0` | `#5f5f00` +`59` | `grey37` | `95,95,95` | `#5f5f5f` +`60` | `mediumpurple4` | `95,95,135` | `#5f5f87` +`61` | `slateblue3` | `95,95,175` | `#5f5faf` +`62` | `slateblue3_1` | `95,95,215` | `#5f5fd7` +`63` | `royalblue1` | `95,95,255` | `#5f5fff` +`64` | `chartreuse4` | `95,135,0` | `#5f8700` +`65` | `darkseagreen4` | `95,135,95` | `#5f875f` +`66` | `paleturquoise4` | `95,135,135` | `#5f8787` +`67` | `steelblue` | `95,135,175` | `#5f87af` +`68` | `steelblue3` | `95,135,215` | `#5f87d7` +`69` | `cornflowerblue` | `95,135,255` | `#5f87ff` +`70` | `chartreuse3` | `95,175,0` | `#5faf00` +`71` | `darkseagreen4_1` | `95,175,95` | `#5faf5f` +`72` | `cadetblue` | `95,175,135` | `#5faf87` +`73` | `cadetblue_1` | `95,175,175` | `#5fafaf` +`74` | `skyblue3` | `95,175,215` | `#5fafd7` +`75` | `steelblue1` | `95,175,255` | `#5fafff` +`76` | `chartreuse3_1` | `95,215,0` | `#5fd700` +`77` | `palegreen3` | `95,215,95` | `#5fd75f` +`78` | `seagreen3` | `95,215,135` | `#5fd787` +`79` | `aquamarine3` | `95,215,175` | `#5fd7af` +`80` | `mediumturquoise` | `95,215,215` | `#5fd7d7` +`81` | `steelblue1_1` | `95,215,255` | `#5fd7ff` +`82` | `chartreuse2` | `95,255,0` | `#5fff00` +`83` | `seagreen2` | `95,255,95` | `#5fff5f` +`84` | `seagreen1` | `95,255,135` | `#5fff87` +`85` | `seagreen1_1` | `95,255,175` | `#5fffaf` +`86` | `aquamarine1` | `95,255,215` | `#5fffd7` +`87` | `darkslategray2` | `95,255,255` | `#5fffff` +`88` | `darkred_1` | `135,0,0` | `#870000` +`89` | `deeppink4_1` | `135,0,95` | `#87005f` +`90` | `darkmagenta` | `135,0,135` | `#870087` +`91` | `darkmagenta_1` | `135,0,175` | `#8700af` +`92` | `darkviolet` | `135,0,215` | `#8700d7` +`93` | `purple_1` | `135,0,255` | `#8700ff` +`94` | `orange4_1` | `135,95,0` | `#875f00` +`95` | `lightpink4` | `135,95,95` | `#875f5f` +`96` | `plum4` | `135,95,135` | `#875f87` +`97` | `mediumpurple3` | `135,95,175` | `#875faf` +`98` | `mediumpurple3_1` | `135,95,215` | `#875fd7` +`99` | `slateblue1` | `135,95,255` | `#875fff` +`100` | `yellow4` | `135,135,0` | `#878700` +`101` | `wheat4` | `135,135,95` | `#87875f` +`102` | `grey53` | `135,135,135` | `#878787` +`103` | `lightslategrey` | `135,135,175` | `#8787af` +`104` | `mediumpurple` | `135,135,215` | `#8787d7` +`105` | `lightslateblue` | `135,135,255` | `#8787ff` +`106` | `yellow4_1` | `135,175,0` | `#87af00` +`107` | `darkolivegreen3` | `135,175,95` | `#87af5f` +`108` | `darkseagreen` | `135,175,135` | `#87af87` +`109` | `lightskyblue3` | `135,175,175` | `#87afaf` +`110` | `lightskyblue3_1` | `135,175,215` | `#87afd7` +`111` | `skyblue2` | `135,175,255` | `#87afff` +`112` | `chartreuse2_1` | `135,215,0` | `#87d700` +`113` | `darkolivegreen3_1` | `135,215,95` | `#87d75f` +`114` | `palegreen3_1` | `135,215,135` | `#87d787` +`115` | `darkseagreen3` | `135,215,175` | `#87d7af` +`116` | `darkslategray3` | `135,215,215` | `#87d7d7` +`117` | `skyblue1` | `135,215,255` | `#87d7ff` +`118` | `chartreuse1` | `135,255,0` | `#87ff00` +`119` | `lightgreen` | `135,255,95` | `#87ff5f` +`120` | `lightgreen_1` | `135,255,135` | `#87ff87` +`121` | `palegreen1` | `135,255,175` | `#87ffaf` +`122` | `aquamarine1_1` | `135,255,215` | `#87ffd7` +`123` | `darkslategray1` | `135,255,255` | `#87ffff` +`124` | `red3` | `175,0,0` | `#af0000` +`125` | `deeppink4_2` | `175,0,95` | `#af005f` +`126` | `mediumvioletred` | `175,0,135` | `#af0087` +`127` | `magenta3` | `175,0,175` | `#af00af` +`128` | `darkviolet_1` | `175,0,215` | `#af00d7` +`129` | `purple_2` | `175,0,255` | `#af00ff` +`130` | `darkorange3` | `175,95,0` | `#af5f00` +`131` | `indianred` | `175,95,95` | `#af5f5f` +`132` | `hotpink3` | `175,95,135` | `#af5f87` +`133` | `mediumorchid3` | `175,95,175` | `#af5faf` +`134` | `mediumorchid` | `175,95,215` | `#af5fd7` +`135` | `mediumpurple2` | `175,95,255` | `#af5fff` +`136` | `darkgoldenrod` | `175,135,0` | `#af8700` +`137` | `lightsalmon3` | `175,135,95` | `#af875f` +`138` | `rosybrown` | `175,135,135` | `#af8787` +`139` | `grey63` | `175,135,175` | `#af87af` +`140` | `mediumpurple2_1` | `175,135,215` | `#af87d7` +`141` | `mediumpurple1` | `175,135,255` | `#af87ff` +`142` | `gold3` | `175,175,0` | `#afaf00` +`143` | `darkkhaki` | `175,175,95` | `#afaf5f` +`144` | `navajowhite3` | `175,175,135` | `#afaf87` +`145` | `grey69` | `175,175,175` | `#afafaf` +`146` | `lightsteelblue3` | `175,175,215` | `#afafd7` +`147` | `lightsteelblue` | `175,175,255` | `#afafff` +`148` | `yellow3` | `175,215,0` | `#afd700` +`149` | `darkolivegreen3_2` | `175,215,95` | `#afd75f` +`150` | `darkseagreen3_1` | `175,215,135` | `#afd787` +`151` | `darkseagreen2` | `175,215,175` | `#afd7af` +`152` | `lightcyan3` | `175,215,215` | `#afd7d7` +`153` | `lightskyblue1` | `175,215,255` | `#afd7ff` +`154` | `greenyellow` | `175,255,0` | `#afff00` +`155` | `darkolivegreen2` | `175,255,95` | `#afff5f` +`156` | `palegreen1_1` | `175,255,135` | `#afff87` +`157` | `darkseagreen2_1` | `175,255,175` | `#afffaf` +`158` | `darkseagreen1` | `175,255,215` | `#afffd7` +`159` | `paleturquoise1` | `175,255,255` | `#afffff` +`160` | `red3_1` | `215,0,0` | `#d70000` +`161` | `deeppink3` | `215,0,95` | `#d7005f` +`162` | `deeppink3_1` | `215,0,135` | `#d70087` +`163` | `magenta3_1` | `215,0,175` | `#d700af` +`164` | `magenta3_2` | `215,0,215` | `#d700d7` +`165` | `magenta2` | `215,0,255` | `#d700ff` +`166` | `darkorange3_1` | `215,95,0` | `#d75f00` +`167` | `indianred_1` | `215,95,95` | `#d75f5f` +`168` | `hotpink3_1` | `215,95,135` | `#d75f87` +`169` | `hotpink2` | `215,95,175` | `#d75faf` +`170` | `orchid` | `215,95,215` | `#d75fd7` +`171` | `mediumorchid1` | `215,95,255` | `#d75fff` +`172` | `orange3` | `215,135,0` | `#d78700` +`173` | `lightsalmon3_1` | `215,135,95` | `#d7875f` +`174` | `lightpink3` | `215,135,135` | `#d78787` +`175` | `pink3` | `215,135,175` | `#d787af` +`176` | `plum3` | `215,135,215` | `#d787d7` +`177` | `violet` | `215,135,255` | `#d787ff` +`178` | `gold3_1` | `215,175,0` | `#d7af00` +`179` | `lightgoldenrod3` | `215,175,95` | `#d7af5f` +`180` | `tan` | `215,175,135` | `#d7af87` +`181` | `mistyrose3` | `215,175,175` | `#d7afaf` +`182` | `thistle3` | `215,175,215` | `#d7afd7` +`183` | `plum2` | `215,175,255` | `#d7afff` +`184` | `yellow3_1` | `215,215,0` | `#d7d700` +`185` | `khaki3` | `215,215,95` | `#d7d75f` +`186` | `lightgoldenrod2` | `215,215,135` | `#d7d787` +`187` | `lightyellow3` | `215,215,175` | `#d7d7af` +`188` | `grey84` | `215,215,215` | `#d7d7d7` +`189` | `lightsteelblue1` | `215,215,255` | `#d7d7ff` +`190` | `yellow2` | `215,255,0` | `#d7ff00` +`191` | `darkolivegreen1` | `215,255,95` | `#d7ff5f` +`192` | `darkolivegreen1_1` | `215,255,135` | `#d7ff87` +`193` | `darkseagreen1_1` | `215,255,175` | `#d7ffaf` +`194` | `honeydew2` | `215,255,215` | `#d7ffd7` +`195` | `lightcyan1` | `215,255,255` | `#d7ffff` +`196` | `red1` | `255,0,0` | `#ff0000` +`197` | `deeppink2` | `255,0,95` | `#ff005f` +`198` | `deeppink1` | `255,0,135` | `#ff0087` +`199` | `deeppink1_1` | `255,0,175` | `#ff00af` +`200` | `magenta2_1` | `255,0,215` | `#ff00d7` +`201` | `magenta1` | `255,0,255` | `#ff00ff` +`202` | `orangered1` | `255,95,0` | `#ff5f00` +`203` | `indianred1` | `255,95,95` | `#ff5f5f` +`204` | `indianred1_1` | `255,95,135` | `#ff5f87` +`205` | `hotpink` | `255,95,175` | `#ff5faf` +`206` | `hotpink_1` | `255,95,215` | `#ff5fd7` +`207` | `mediumorchid1_1` | `255,95,255` | `#ff5fff` +`208` | `darkorange` | `255,135,0` | `#ff8700` +`209` | `salmon1` | `255,135,95` | `#ff875f` +`210` | `lightcoral` | `255,135,135` | `#ff8787` +`211` | `palevioletred1` | `255,135,175` | `#ff87af` +`212` | `orchid2` | `255,135,215` | `#ff87d7` +`213` | `orchid1` | `255,135,255` | `#ff87ff` +`214` | `orange1` | `255,175,0` | `#ffaf00` +`215` | `sandybrown` | `255,175,95` | `#ffaf5f` +`216` | `lightsalmon1` | `255,175,135` | `#ffaf87` +`217` | `lightpink1` | `255,175,175` | `#ffafaf` +`218` | `pink1` | `255,175,215` | `#ffafd7` +`219` | `plum1` | `255,175,255` | `#ffafff` +`220` | `gold1` | `255,215,0` | `#ffd700` +`221` | `lightgoldenrod2_1` | `255,215,95` | `#ffd75f` +`222` | `lightgoldenrod2_2` | `255,215,135` | `#ffd787` +`223` | `navajowhite1` | `255,215,175` | `#ffd7af` +`224` | `mistyrose1` | `255,215,215` | `#ffd7d7` +`225` | `thistle1` | `255,215,255` | `#ffd7ff` +`226` | `yellow1` | `255,255,0` | `#ffff00` +`227` | `lightgoldenrod1` | `255,255,95` | `#ffff5f` +`228` | `khaki1` | `255,255,135` | `#ffff87` +`229` | `wheat1` | `255,255,175` | `#ffffaf` +`230` | `cornsilk1` | `255,255,215` | `#ffffd7` +`231` | `grey100` | `255,255,255` | `#ffffff` +`232` | `grey3` | `8,8,8` | `#080808` +`233` | `grey7` | `18,18,18` | `#121212` +`234` | `grey11` | `28,28,28` | `#1c1c1c` +`235` | `grey15` | `38,38,38` | `#262626` +`236` | `grey19` | `48,48,48` | `#303030` +`237` | `grey23` | `58,58,58` | `#3a3a3a` +`238` | `grey27` | `68,68,68` | `#444444` +`239` | `grey30` | `78,78,78` | `#4e4e4e` +`240` | `grey35` | `88,88,88` | `#585858` +`241` | `grey39` | `98,98,98` | `#626262` +`242` | `grey42` | `108,108,108` | `#6c6c6c` +`243` | `grey46` | `118,118,118` | `#767676` +`244` | `grey50` | `128,128,128` | `#808080` +`245` | `grey54` | `138,138,138` | `#8a8a8a` +`246` | `grey58` | `148,148,148` | `#949494` +`247` | `grey62` | `158,158,158` | `#9e9e9e` +`248` | `grey66` | `168,168,168` | `#a8a8a8` +`249` | `grey70` | `178,178,178` | `#b2b2b2` +`250` | `grey74` | `188,188,188` | `#bcbcbc` +`251` | `grey78` | `198,198,198` | `#c6c6c6` +`252` | `grey82` | `208,208,208` | `#d0d0d0` +`253` | `grey85` | `218,218,218` | `#dadada` +`254` | `grey89` | `228,228,228` | `#e4e4e4` +`255` | `grey93` | `238,238,238` | `#eeeeee` diff --git a/src/Sample/Program.cs b/src/Sample/Program.cs index f9d9f76..c524d9f 100644 --- a/src/Sample/Program.cs +++ b/src/Sample/Program.cs @@ -12,9 +12,9 @@ namespace Sample AnsiConsole.Style = Styles.Underline | Styles.Bold; AnsiConsole.WriteLine("Hello World!"); AnsiConsole.Reset(); - AnsiConsole.WriteLine("Capabilities: {0}", AnsiConsole.Capabilities); + AnsiConsole.MarkupLine("Capabilities: [yellow underline]{0}[/]", AnsiConsole.Capabilities); AnsiConsole.WriteLine($"Width={AnsiConsole.Width}, Height={AnsiConsole.Height}"); - AnsiConsole.WriteLine("Good bye!"); + AnsiConsole.MarkupLine("[white on red]Good[/] [red]bye[/]!"); AnsiConsole.WriteLine(); // We can get the default console via the static API. @@ -37,7 +37,7 @@ namespace Sample console.ResetColors(); console.ResetStyle(); console.WriteLine("Capabilities: {0}", AnsiConsole.Capabilities); - console.WriteLine($"Width={AnsiConsole.Width}, Height={AnsiConsole.Height}"); + console.MarkupLine("Width=[yellow]{0}[/], Height=[yellow]{1}[/]", AnsiConsole.Width, AnsiConsole.Height); console.WriteLine("Good bye!"); console.WriteLine(); } diff --git a/src/Spectre.Console.Tests/AnsiConsoleTests.Markup.cs b/src/Spectre.Console.Tests/AnsiConsoleTests.Markup.cs new file mode 100644 index 0000000..4633647 --- /dev/null +++ b/src/Spectre.Console.Tests/AnsiConsoleTests.Markup.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Shouldly; +using Xunit; + +namespace Spectre.Console.Tests +{ + public partial class AnsiConsoleTests + { + [SuppressMessage("Naming", "CA1724:Type names should not match namespaces")] + public sealed class Markup + { + [Theory] + [InlineData("[yellow]Hello[/]", "Hello")] + [InlineData("[yellow]Hello [italic]World[/]![/]", "Hello World!")] + public void Should_Output_Expected_Ansi_For_Markup(string markup, string expected) + { + // Given + var fixture = new AnsiConsoleFixture(ColorSystem.Standard, AnsiSupport.Yes); + + // When + fixture.Console.Markup(markup); + + // Then + fixture.Output.ShouldBe(expected); + } + + [Theory] + [InlineData("[yellow]Hello [[ World[/]", "Hello [ World")] + public void Should_Be_Able_To_Escape_Tags(string markup, string expected) + { + // Given + var fixture = new AnsiConsoleFixture(ColorSystem.Standard, AnsiSupport.Yes); + + // When + fixture.Console.Markup(markup); + + // Then + fixture.Output.ShouldBe(expected); + } + + [Theory] + [InlineData("[yellow]Hello[", "Encountered malformed markup tag at position 14.")] + [InlineData("[yellow]Hello[/", "Encountered malformed markup tag at position 15.")] + [InlineData("[yellow]Hello[/foo", "Encountered malformed markup tag at position 15.")] + [InlineData("[yellow Hello", "Encountered malformed markup tag at position 13.")] + public void Should_Throw_If_Encounters_Malformed_Tag(string markup, string expected) + { + // Given + var fixture = new AnsiConsoleFixture(ColorSystem.Standard, AnsiSupport.Yes); + + // When + var result = Record.Exception(() => fixture.Console.Markup(markup)); + + // Then + result.ShouldBeOfType() + .Message.ShouldBe(expected); + } + + [Fact] + public void Should_Throw_If_Tags_Are_Unbalanced() + { + // Given + var fixture = new AnsiConsoleFixture(ColorSystem.Standard, AnsiSupport.Yes); + + // When + var result = Record.Exception(() => fixture.Console.Markup("[yellow][blue]Hello[/]")); + + // Then + result.ShouldBeOfType() + .Message.ShouldBe("Unbalanced markup stack. Did you forget to close a tag?"); + } + + [Fact] + public void Should_Throw_If_Encounters_Closing_Tag() + { + // Given + var fixture = new AnsiConsoleFixture(ColorSystem.Standard, AnsiSupport.Yes); + + // When + var result = Record.Exception(() => fixture.Console.Markup("Hello[/]World")); + + // Then + result.ShouldBeOfType() + .Message.ShouldBe("Encountered closing tag when none was expected near position 5."); + } + } + } +} diff --git a/src/Spectre.Console/AnsiConsole.Markup.cs b/src/Spectre.Console/AnsiConsole.Markup.cs new file mode 100644 index 0000000..9f89d37 --- /dev/null +++ b/src/Spectre.Console/AnsiConsole.Markup.cs @@ -0,0 +1,52 @@ +using System; + +namespace Spectre.Console +{ + /// + /// A console capable of writing ANSI escape sequences. + /// + public static partial class AnsiConsole + { + /// + /// Writes the specified markup to the console. + /// + /// A composite format string. + /// An array of objects to write. + public static void Markup(string format, params object[] args) + { + Console.Markup(format, args); + } + + /// + /// Writes the specified markup to the console. + /// + /// An object that supplies culture-specific formatting information. + /// A composite format string. + /// An array of objects to write. + public static void Markup(IFormatProvider provider, string format, params object[] args) + { + Console.Markup(provider, format, args); + } + + /// + /// Writes the specified markup, followed by the current line terminator, to the console. + /// + /// A composite format string. + /// An array of objects to write. + public static void MarkupLine(string format, params object[] args) + { + Console.MarkupLine(format, args); + } + + /// + /// Writes the specified markup, followed by the current line terminator, to the console. + /// + /// An object that supplies culture-specific formatting information. + /// A composite format string. + /// An array of objects to write. + public static void MarkupLine(IFormatProvider provider, string format, params object[] args) + { + Console.MarkupLine(provider, format, args); + } + } +} diff --git a/src/Spectre.Console/AnsiConsoleSettings.cs b/src/Spectre.Console/AnsiConsoleSettings.cs index ccb34f8..fe951e9 100644 --- a/src/Spectre.Console/AnsiConsoleSettings.cs +++ b/src/Spectre.Console/AnsiConsoleSettings.cs @@ -3,8 +3,7 @@ using System.IO; namespace Spectre.Console { /// - /// Settings used by - /// when building a . + /// Settings used when building a . /// public sealed class AnsiConsoleSettings { diff --git a/src/Spectre.Console/ConsoleExtensions.Markup.cs b/src/Spectre.Console/ConsoleExtensions.Markup.cs new file mode 100644 index 0000000..f13e16a --- /dev/null +++ b/src/Spectre.Console/ConsoleExtensions.Markup.cs @@ -0,0 +1,60 @@ +using System; +using System.Globalization; +using Spectre.Console.Internal; + +namespace Spectre.Console +{ + /// + /// Contains extension methods for . + /// + public static partial class ConsoleExtensions + { + /// + /// Writes the specified markup to the console. + /// + /// The console to write to. + /// A composite format string. + /// An array of objects to write. + public static void Markup(this IAnsiConsole console, string format, params object[] args) + { + Markup(console, CultureInfo.CurrentCulture, format, args); + } + + /// + /// Writes the specified markup to the console. + /// + /// The console to write to. + /// An object that supplies culture-specific formatting information. + /// A composite format string. + /// An array of objects to write. + public static void Markup(this IAnsiConsole console, IFormatProvider provider, string format, params object[] args) + { + var result = MarkupParser.Parse(string.Format(provider, format, args)); + result.Render(console); + } + + /// + /// Writes the specified markup, followed by the current line terminator, to the console. + /// + /// The console to write to. + /// A composite format string. + /// An array of objects to write. + public static void MarkupLine(this IAnsiConsole console, string format, params object[] args) + { + MarkupLine(console, CultureInfo.CurrentCulture, format, args); + } + + /// + /// Writes the specified markup, followed by the current line terminator, to the console. + /// + /// The console to write to. + /// An object that supplies culture-specific formatting information. + /// A composite format string. + /// An array of objects to write. + public static void MarkupLine(this IAnsiConsole console, IFormatProvider provider, string format, params object[] args) + { + Markup(console, provider, format, args); + console.WriteLine(); + } + } +} diff --git a/src/Spectre.Console/Internal/Composition/Composer.cs b/src/Spectre.Console/Internal/Composition/Composer.cs deleted file mode 100644 index a63d4b6..0000000 --- a/src/Spectre.Console/Internal/Composition/Composer.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; - -namespace Spectre.Console.Internal -{ - internal sealed class Composer : IRenderable - { - private readonly BlockElement _root; - - /// - public int Length => _root.Length; - - public Composer() - { - _root = new BlockElement(); - } - - public static Composer New() - { - return new Composer(); - } - - public Composer Text(string text) - { - _root.Append(new TextElement(text)); - return this; - } - - public Composer Foreground(Color color, Action action) - { - if (action is null) - { - return this; - } - - var content = new Composer(); - action(content); - _root.Append(new ForegroundElement(color, content)); - return this; - } - - public Composer Background(Color color, Action action) - { - if (action is null) - { - return this; - } - - var content = new Composer(); - action(content); - _root.Append(new BackgroundElement(color, content)); - return this; - } - - public Composer Style(Styles style, Action action) - { - if (action is null) - { - return this; - } - - var content = new Composer(); - action(content); - _root.Append(new StyleElement(style, content)); - return this; - } - - /// - public void Render(IAnsiConsole renderer) - { - _root.Render(renderer); - } - } -} diff --git a/src/Spectre.Console/Internal/Composition/Elements/BackgroundElement.cs b/src/Spectre.Console/Internal/Composition/Elements/BackgroundElement.cs deleted file mode 100644 index 764ba29..0000000 --- a/src/Spectre.Console/Internal/Composition/Elements/BackgroundElement.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Spectre.Console.Internal -{ - [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Not used (yet)")] - internal sealed class BackgroundElement : IRenderable - { - private readonly Color _color; - private readonly IRenderable _element; - - /// - public int Length => _element.Length; - - public BackgroundElement(Color color, IRenderable element) - { - _color = color; - _element = element ?? throw new ArgumentNullException(nameof(element)); - } - - /// - public void Render(IAnsiConsole renderer) - { - if (renderer is null) - { - throw new ArgumentNullException(nameof(renderer)); - } - - using (renderer.PushColor(_color, foreground: false)) - { - _element.Render(renderer); - } - } - } -} diff --git a/src/Spectre.Console/Internal/Composition/Elements/BlockElement.cs b/src/Spectre.Console/Internal/Composition/Elements/BlockElement.cs deleted file mode 100644 index f38c3e5..0000000 --- a/src/Spectre.Console/Internal/Composition/Elements/BlockElement.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Spectre.Console.Internal -{ - [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Not used (yet)")] - internal sealed class BlockElement : IRenderable - { - private readonly List _elements; - - /// - public int Length { get; private set; } - - public IReadOnlyList Elements => _elements; - - public BlockElement() - { - _elements = new List(); - } - - public BlockElement Append(IRenderable element) - { - if (element != null) - { - _elements.Add(element); - Length += element.Length; - } - - return this; - } - - /// - public void Render(IAnsiConsole renderer) - { - foreach (var element in _elements) - { - element.Render(renderer); - } - } - } -} diff --git a/src/Spectre.Console/Internal/Composition/Elements/ForegroundElement.cs b/src/Spectre.Console/Internal/Composition/Elements/ForegroundElement.cs deleted file mode 100644 index 70080eb..0000000 --- a/src/Spectre.Console/Internal/Composition/Elements/ForegroundElement.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Spectre.Console.Internal -{ - [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Not used (yet)")] - internal sealed class ForegroundElement : IRenderable - { - private readonly Color _color; - private readonly IRenderable _element; - - /// - public int Length => _element.Length; - - public ForegroundElement(Color color, IRenderable element) - { - _color = color; - _element = element ?? throw new ArgumentNullException(nameof(element)); - } - - /// - public void Render(IAnsiConsole renderer) - { - if (renderer is null) - { - throw new ArgumentNullException(nameof(renderer)); - } - - using (renderer.PushColor(_color, foreground: true)) - { - _element.Render(renderer); - } - } - } -} diff --git a/src/Spectre.Console/Internal/Composition/Elements/LineBreakElement.cs b/src/Spectre.Console/Internal/Composition/Elements/LineBreakElement.cs deleted file mode 100644 index a6b4ec9..0000000 --- a/src/Spectre.Console/Internal/Composition/Elements/LineBreakElement.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Spectre.Console.Internal -{ - [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Not used (yet)")] - internal sealed class LineBreakElement : IRenderable - { - /// - public int Length => 0; - - /// - public void Render(IAnsiConsole renderer) - { - renderer.Write(Environment.NewLine); - } - } -} diff --git a/src/Spectre.Console/Internal/Composition/Elements/StyleElement.cs b/src/Spectre.Console/Internal/Composition/Elements/StyleElement.cs deleted file mode 100644 index 804f993..0000000 --- a/src/Spectre.Console/Internal/Composition/Elements/StyleElement.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Spectre.Console.Internal -{ - [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Not used (yet)")] - internal sealed class StyleElement : IRenderable - { - private readonly Styles _style; - private readonly IRenderable _element; - - /// - public int Length => _element.Length; - - public StyleElement(Styles style, IRenderable element) - { - _style = style; - _element = element ?? throw new ArgumentNullException(nameof(element)); - } - - /// - public void Render(IAnsiConsole renderer) - { - if (renderer is null) - { - throw new ArgumentNullException(nameof(renderer)); - } - - using (renderer.PushStyle(_style)) - { - _element.Render(renderer); - } - } - } -} diff --git a/src/Spectre.Console/Internal/Composition/Elements/TextElement.cs b/src/Spectre.Console/Internal/Composition/Elements/TextElement.cs deleted file mode 100644 index dbb0eeb..0000000 --- a/src/Spectre.Console/Internal/Composition/Elements/TextElement.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Spectre.Console.Internal -{ - [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Not used (yet)")] - internal sealed class TextElement : IRenderable - { - private readonly string _text; - - /// - public int Length => _text.Length; - - public TextElement(string text) - { - _text = text ?? throw new System.ArgumentNullException(nameof(text)); - } - - /// - public void Render(IAnsiConsole renderer) - { - renderer.Write(_text); - } - } -} diff --git a/src/Spectre.Console/Internal/Composition/IRenderable.cs b/src/Spectre.Console/Internal/Composition/IRenderable.cs deleted file mode 100644 index 8f85b78..0000000 --- a/src/Spectre.Console/Internal/Composition/IRenderable.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Spectre.Console.Internal -{ - /// - /// Represents something that can be rendered to a console. - /// - internal interface IRenderable - { - /// - /// Gets the length of the element. - /// - int Length { get; } - - /// - /// Renders the element using the specified renderer. - /// - /// The renderer to use. - void Render(IAnsiConsole console); - } -} diff --git a/src/Spectre.Console/Internal/ConsoleBuilder.cs b/src/Spectre.Console/Internal/ConsoleBuilder.cs index 2d21f98..1e257ed 100644 --- a/src/Spectre.Console/Internal/ConsoleBuilder.cs +++ b/src/Spectre.Console/Internal/ConsoleBuilder.cs @@ -1,7 +1,6 @@ using System; -using Spectre.Console.Internal; -namespace Spectre.Console +namespace Spectre.Console.Internal { internal static class ConsoleBuilder { diff --git a/src/Spectre.Console/Internal/Extensions/ConsoleExtensions.cs b/src/Spectre.Console/Internal/Extensions/ConsoleExtensions.cs index ae01a5d..8fd0771 100644 --- a/src/Spectre.Console/Internal/Extensions/ConsoleExtensions.cs +++ b/src/Spectre.Console/Internal/Extensions/ConsoleExtensions.cs @@ -12,7 +12,7 @@ namespace Spectre.Console.Internal throw new ArgumentNullException(nameof(console)); } - var current = console.Foreground; + var current = foreground ? console.Foreground : console.Background; console.SetColor(color, foreground); return new ColorScope(console, current, foreground); } diff --git a/src/Spectre.Console/Internal/Lookup.cs b/src/Spectre.Console/Internal/Lookup.cs new file mode 100644 index 0000000..c228d8f --- /dev/null +++ b/src/Spectre.Console/Internal/Lookup.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; + +namespace Spectre.Console.Internal +{ + internal sealed class Lookup + { + private readonly Dictionary _styles; + private readonly Dictionary _colors; + + private static readonly Lazy _lazy = new Lazy(() => new Lookup()); + public static Lookup Instance => _lazy.Value; + + private Lookup() + { + _styles = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "bold", Styles.Bold }, + { "dim", Styles.Dim }, + { "italic", Styles.Italic }, + { "underline", Styles.Underline }, + { "invert", Styles.Invert }, + { "conceal", Styles.Conceal }, + { "slowblink", Styles.SlowBlink }, + { "rapidblink", Styles.RapidBlink }, + { "strikethrough", Styles.Strikethrough }, + }; + + _colors = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var color in ColorPalette.EightBit) + { + _colors.Add(color.Name, color); + } + } + + public Styles? GetStyle(string name) + { + _styles.TryGetValue(name, out var style); + return style; + } + + public Color? GetColor(string name) + { + _colors.TryGetValue(name, out var color); + return color; + } + } +} diff --git a/src/Spectre.Console/Internal/Markup/Ast/MarkupBlockNode.cs b/src/Spectre.Console/Internal/Markup/Ast/MarkupBlockNode.cs new file mode 100644 index 0000000..0a8ff99 --- /dev/null +++ b/src/Spectre.Console/Internal/Markup/Ast/MarkupBlockNode.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace Spectre.Console.Internal +{ + internal sealed class MarkupBlockNode : IMarkupNode + { + private readonly List _elements; + + public MarkupBlockNode() + { + _elements = new List(); + } + + public void Append(IMarkupNode element) + { + if (element != null) + { + _elements.Add(element); + } + } + + public void Render(IAnsiConsole renderer) + { + foreach (var element in _elements) + { + element.Render(renderer); + } + } + } +} diff --git a/src/Spectre.Console/Internal/Markup/Ast/MarkupStyleNode.cs b/src/Spectre.Console/Internal/Markup/Ast/MarkupStyleNode.cs new file mode 100644 index 0000000..b4aa4a4 --- /dev/null +++ b/src/Spectre.Console/Internal/Markup/Ast/MarkupStyleNode.cs @@ -0,0 +1,52 @@ +using System; + +namespace Spectre.Console.Internal +{ + internal sealed class MarkupStyleNode : IMarkupNode + { + private readonly Styles? _style; + private readonly Color? _foreground; + private readonly Color? _background; + private readonly IMarkupNode _element; + + public MarkupStyleNode( + Styles? style, + Color? foreground, + Color? background, + IMarkupNode element) + { + _style = style; + _foreground = foreground; + _background = background; + _element = element ?? throw new ArgumentNullException(nameof(element)); + } + + public void Render(IAnsiConsole renderer) + { + var style = (IDisposable)null; + var foreground = (IDisposable)null; + var background = (IDisposable)null; + + if (_style != null) + { + style = renderer.PushStyle(_style.Value); + } + + if (_foreground != null) + { + foreground = renderer.PushColor(_foreground.Value, foreground: true); + } + + if (_background != null) + { + background = renderer.PushColor(_background.Value, foreground: false); + } + + _element.Render(renderer); + + background?.Dispose(); + foreground?.Dispose(); + style?.Dispose(); + } + } +} diff --git a/src/Spectre.Console/Internal/Markup/Ast/MarkupTextNode.cs b/src/Spectre.Console/Internal/Markup/Ast/MarkupTextNode.cs new file mode 100644 index 0000000..f7505d7 --- /dev/null +++ b/src/Spectre.Console/Internal/Markup/Ast/MarkupTextNode.cs @@ -0,0 +1,19 @@ +using System; + +namespace Spectre.Console.Internal +{ + internal sealed class MarkupTextNode : IMarkupNode + { + public string Text { get; } + + public MarkupTextNode(string text) + { + Text = text ?? throw new ArgumentNullException(nameof(text)); + } + + public void Render(IAnsiConsole renderer) + { + renderer.Write(Text); + } + } +} diff --git a/src/Spectre.Console/Internal/Markup/IMarkupNode.cs b/src/Spectre.Console/Internal/Markup/IMarkupNode.cs new file mode 100644 index 0000000..8136e00 --- /dev/null +++ b/src/Spectre.Console/Internal/Markup/IMarkupNode.cs @@ -0,0 +1,14 @@ +namespace Spectre.Console.Internal +{ + /// + /// Represents a parsed markup node. + /// + internal interface IMarkupNode + { + /// + /// Renders the node using the specified renderer. + /// + /// The renderer to use. + void Render(IAnsiConsole renderer); + } +} diff --git a/src/Spectre.Console/Internal/Markup/MarkupParser.cs b/src/Spectre.Console/Internal/Markup/MarkupParser.cs new file mode 100644 index 0000000..53cf68c --- /dev/null +++ b/src/Spectre.Console/Internal/Markup/MarkupParser.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; + +namespace Spectre.Console.Internal +{ + internal static class MarkupParser + { + public static IMarkupNode Parse(string text) + { + using var tokenizer = new MarkupTokenizer(text); + var root = new MarkupBlockNode(); + + var stack = new Stack(); + var current = root; + + while (true) + { + var token = tokenizer.GetNext(); + if (token == null) + { + break; + } + + if (token.Kind == MarkupTokenKind.Text) + { + current.Append(new MarkupTextNode(token.Value)); + continue; + } + else 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; + } + else if (token.Kind == MarkupTokenKind.Close) + { + if (stack.Count == 0) + { + throw new InvalidOperationException($"Encountered closing tag when none was expected near position {token.Position}."); + } + + stack.Pop(); + + if (stack.Count == 0) + { + current = root; + } + else + { + current = stack.Peek(); + } + + continue; + } + + throw new InvalidOperationException("Encountered unkown markup token."); + } + + if (stack.Count > 0) + { + throw new InvalidOperationException("Unbalanced markup stack. Did you forget to close a tag?"); + } + + return root; + } + } +} diff --git a/src/Spectre.Console/Internal/Markup/MarkupStyleParser.cs b/src/Spectre.Console/Internal/Markup/MarkupStyleParser.cs new file mode 100644 index 0000000..cbd3c68 --- /dev/null +++ b/src/Spectre.Console/Internal/Markup/MarkupStyleParser.cs @@ -0,0 +1,65 @@ +using System; + +namespace Spectre.Console.Internal +{ + internal static class MarkupStyleParser + { + public static (Styles? Style, Color? Foreground, Color? Background) Parse(string text) + { + var effectiveStyle = (Styles?)null; + var effectiveForeground = (Color?)null; + var effectiveBackground = (Color?)null; + + var parts = text.Split(new[] { ' ' }); + var foreground = true; + foreach (var part in parts) + { + if (part.Equals("on", StringComparison.OrdinalIgnoreCase)) + { + foreground = false; + continue; + } + + var style = Lookup.Instance.GetStyle(part); + if (style != null) + { + if (effectiveStyle == null) + { + effectiveStyle = Styles.None; + } + + effectiveStyle |= style.Value; + } + else + { + var color = Lookup.Instance.GetColor(part); + if (color == null) + { + throw new InvalidOperationException("Could not find color.."); + } + + if (foreground) + { + if (effectiveForeground != null) + { + throw new InvalidOperationException("A foreground has already been set."); + } + + effectiveForeground = color; + } + else + { + if (effectiveBackground != null) + { + throw new InvalidOperationException("A background has already been set."); + } + + effectiveBackground = color; + } + } + } + + return (effectiveStyle, effectiveForeground, effectiveBackground); + } + } +} diff --git a/src/Spectre.Console/Internal/Markup/MarkupToken.cs b/src/Spectre.Console/Internal/Markup/MarkupToken.cs new file mode 100644 index 0000000..181289f --- /dev/null +++ b/src/Spectre.Console/Internal/Markup/MarkupToken.cs @@ -0,0 +1,18 @@ +using System; + +namespace Spectre.Console.Internal +{ + internal sealed class MarkupToken + { + public MarkupTokenKind Kind { get; } + public string Value { get; } + public int Position { get; set; } + + public MarkupToken(MarkupTokenKind kind, string value, int position) + { + Kind = kind; + Value = value ?? throw new ArgumentNullException(nameof(value)); + Position = position; + } + } +} diff --git a/src/Spectre.Console/Internal/Markup/MarkupTokenKind.cs b/src/Spectre.Console/Internal/Markup/MarkupTokenKind.cs new file mode 100644 index 0000000..8bb044c --- /dev/null +++ b/src/Spectre.Console/Internal/Markup/MarkupTokenKind.cs @@ -0,0 +1,9 @@ +namespace Spectre.Console.Internal +{ + internal enum MarkupTokenKind + { + Text = 0, + Open, + Close, + } +} diff --git a/src/Spectre.Console/Internal/Markup/MarkupTokenizer.cs b/src/Spectre.Console/Internal/Markup/MarkupTokenizer.cs new file mode 100644 index 0000000..dd4902a --- /dev/null +++ b/src/Spectre.Console/Internal/Markup/MarkupTokenizer.cs @@ -0,0 +1,104 @@ +using System; +using System.Text; + +namespace Spectre.Console.Internal +{ + internal sealed class MarkupTokenizer : IDisposable + { + private readonly StringBuffer _reader; + + public MarkupTokenizer(string text) + { + _reader = new StringBuffer(text ?? throw new ArgumentNullException(nameof(text))); + } + + public void Dispose() + { + _reader.Dispose(); + } + + public MarkupToken GetNext() + { + if (_reader.Eof) + { + return null; + } + + var current = _reader.Peek(); + if (current == '[') + { + var position = _reader.Position; + + _reader.Read(); + + if (_reader.Eof) + { + throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}."); + } + + current = _reader.Peek(); + if (current == '[') + { + _reader.Read(); + return new MarkupToken(MarkupTokenKind.Text, "[", position); + } + + if (current == '/') + { + _reader.Read(); + + if (_reader.Eof) + { + throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}."); + } + + current = _reader.Peek(); + if (current != ']') + { + throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}."); + } + + _reader.Read(); + return new MarkupToken(MarkupTokenKind.Close, string.Empty, position); + } + + var builder = new StringBuilder(); + while (!_reader.Eof) + { + current = _reader.Peek(); + if (current == ']') + { + break; + } + + builder.Append(_reader.Read()); + } + + if (_reader.Eof) + { + throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}."); + } + + _reader.Read(); + return new MarkupToken(MarkupTokenKind.Open, builder.ToString(), position); + } + else + { + var position = _reader.Position; + var builder = new StringBuilder(); + while (!_reader.Eof) + { + current = _reader.Peek(); + if (current == '[') + { + break; + } + + builder.Append(_reader.Read()); + } + + return new MarkupToken(MarkupTokenKind.Text, builder.ToString(), position); + } + } + } +} diff --git a/src/Spectre.Console/Internal/Markup/StringBuffer.cs b/src/Spectre.Console/Internal/Markup/StringBuffer.cs new file mode 100644 index 0000000..d941465 --- /dev/null +++ b/src/Spectre.Console/Internal/Markup/StringBuffer.cs @@ -0,0 +1,50 @@ +using System; +using System.IO; + +namespace Spectre.Console.Internal +{ + internal sealed class StringBuffer : IDisposable + { + private readonly StringReader _reader; + private readonly int _length; + + public int Position { get; private set; } + public bool Eof => Position >= _length; + + public StringBuffer(string text) + { + text ??= string.Empty; + + _reader = new StringReader(text); + _length = text.Length; + + Position = 0; + } + + public void Dispose() + { + _reader.Dispose(); + } + + public char Peek() + { + if (Eof) + { + throw new InvalidOperationException("Tried to peek past the end of the text."); + } + + return (char)_reader.Peek(); + } + + public char Read() + { + if (Eof) + { + throw new InvalidOperationException("Tried to read past the end of the text."); + } + + Position++; + return (char)_reader.Read(); + } + } +}