diff --git a/src/FencedCodeBlockRenderer.cs b/src/FencedCodeBlockRenderer.cs index d878ae9..cee551d 100644 --- a/src/FencedCodeBlockRenderer.cs +++ b/src/FencedCodeBlockRenderer.cs @@ -16,25 +16,26 @@ protected override void Write(VT100Renderer renderer, FencedCodeBlock obj) { if (obj?.Lines.Lines != null) { + bool f = true; foreach (StringLine codeLine in obj.Lines.Lines) { if (!string.IsNullOrWhiteSpace(codeLine.ToString())) { + if (f) f = false; + else renderer.WriteLine(); + // If the code block is of type YAML, then tab to right to improve readability. // This specifically helps for parameters help content. if (string.Equals(obj.Info, "yaml", StringComparison.OrdinalIgnoreCase)) { - renderer.Write("\t").WriteLine(codeLine.ToString()); + renderer.Write("\t").Write(codeLine.ToString()); } else { - renderer.WriteLine(renderer.EscapeSequences.FormatCode(codeLine.ToString(), isInline: false)); + renderer.Write(renderer.EscapeSequences.FormatCode(codeLine.ToString(), isInline: false)); } } } - - // Add a blank line after the code block for better readability. - renderer.WriteLine(); } } } diff --git a/src/HeaderBlockRenderer.cs b/src/HeaderBlockRenderer.cs index 454000a..46a5b0b 100644 --- a/src/HeaderBlockRenderer.cs +++ b/src/HeaderBlockRenderer.cs @@ -20,33 +20,27 @@ protected override void Write(VT100Renderer renderer, HeadingBlock obj) switch (obj.Level) { case 1: - renderer.WriteLine(renderer.EscapeSequences.FormatHeader1(headerText)); - renderer.WriteLine(); + renderer.Write(renderer.EscapeSequences.FormatHeader1(headerText)); break; case 2: - renderer.WriteLine(renderer.EscapeSequences.FormatHeader2(headerText)); - renderer.WriteLine(); + renderer.Write(renderer.EscapeSequences.FormatHeader2(headerText)); break; case 3: - renderer.WriteLine(renderer.EscapeSequences.FormatHeader3(headerText)); - renderer.WriteLine(); + renderer.Write(renderer.EscapeSequences.FormatHeader3(headerText)); break; case 4: - renderer.WriteLine(renderer.EscapeSequences.FormatHeader4(headerText)); - renderer.WriteLine(); + renderer.Write(renderer.EscapeSequences.FormatHeader4(headerText)); break; case 5: - renderer.WriteLine(renderer.EscapeSequences.FormatHeader5(headerText)); - renderer.WriteLine(); + renderer.Write(renderer.EscapeSequences.FormatHeader5(headerText)); break; case 6: - renderer.WriteLine(renderer.EscapeSequences.FormatHeader6(headerText)); - renderer.WriteLine(); + renderer.Write(renderer.EscapeSequences.FormatHeader6(headerText)); break; } } diff --git a/src/LeafInlineRenderer.cs b/src/LeafInlineRenderer.cs index d1af034..780cbd3 100644 --- a/src/LeafInlineRenderer.cs +++ b/src/LeafInlineRenderer.cs @@ -12,17 +12,7 @@ internal class LeafInlineRenderer : VT100ObjectRenderer { protected override void Write(VT100Renderer renderer, LeafInline obj) { - // If the next sibling is null, then this is the last line in the paragraph. - // Add new line character at the end. - // Else just write without newline at the end. - if (obj.NextSibling == null) - { - renderer.WriteLine(obj.ToString()); - } - else - { - renderer.Write(obj.ToString()); - } + renderer.Write(obj.ToString()); } } } diff --git a/src/ListBlockRenderer.cs b/src/ListBlockRenderer.cs index 100565b..65277a7 100644 --- a/src/ListBlockRenderer.cs +++ b/src/ListBlockRenderer.cs @@ -12,36 +12,23 @@ internal class ListBlockRenderer : VT100ObjectRenderer { protected override void Write(VT100Renderer renderer, ListBlock obj) { - // start index of a numbered block. - int index = 1; + for (int idx = 0; idx < obj.Count; idx++) + { + if (idx > 0) renderer.WriteLine(); + + Block block = obj[idx]; + + if (obj.IsOrdered) + { + renderer.Write((idx + 1).ToString()).Write(". "); + } + else + { + renderer.Write(obj.BulletType).Write(" "); + } + + renderer.Write(block); - foreach (var item in obj) - { - if (item is ListItemBlock listItem) - { - if (obj.IsOrdered) - { - RenderNumberedList(renderer, listItem, index++); - } - else - { - renderer.Write(listItem); - } - } - } - - renderer.WriteLine(); - } - - private static void RenderNumberedList(VT100Renderer renderer, ListItemBlock block, int index) - { - // For a numbered list, we need to make sure the index is incremented. - foreach (var line in block) - { - if (line is ParagraphBlock paragraphBlock) - { - renderer.Write(index.ToString()).Write(". ").Write(paragraphBlock.Inline); - } } } } diff --git a/src/ListItemBlockRenderer.cs b/src/ListItemBlockRenderer.cs index 54cdeab..5edcb3e 100644 --- a/src/ListItemBlockRenderer.cs +++ b/src/ListItemBlockRenderer.cs @@ -13,68 +13,10 @@ internal class ListItemBlockRenderer : VT100ObjectRenderer { protected override void Write(VT100Renderer renderer, ListItemBlock obj) { - if (obj.Parent is ListBlock parent) - { - if (!parent.IsOrdered) - { - foreach (var line in obj) - { - RenderWithIndent(renderer, line, parent.BulletType, 0); - } - } - } - } - - private static void RenderWithIndent(VT100Renderer renderer, MarkdownObject block, char listBullet, int indentLevel) - { - // Indent left by 2 for each level on list. - string indent = Padding(indentLevel * 2); - - if (block is ParagraphBlock paragraphBlock) - { - renderer.Write(indent).Write(listBullet).Write(" ").Write(paragraphBlock.Inline); - } - else - { - // If there is a sublist, the block is a ListBlock instead of ParagraphBlock. - if (block is ListBlock subList) - { - foreach (var subListItem in subList) - { - if (subListItem is ListItemBlock subListItemBlock) - { - foreach (var line in subListItemBlock) - { - // Increment indent level for sub list. - RenderWithIndent(renderer, line, listBullet, indentLevel + 1); - } - } - } - } - } - } - - // Typical padding is at most a screen's width, any more than that and we won't bother caching. - private const int IndentCacheMax = 120; - - private static readonly string[] IndentCache = new string[IndentCacheMax]; - - internal static string Padding(int countOfSpaces) - { - if (countOfSpaces >= IndentCacheMax) - { - return new string(' ', countOfSpaces); - } - - var result = IndentCache[countOfSpaces]; - - if (result == null) - { - Interlocked.CompareExchange(ref IndentCache[countOfSpaces], new string(' ', countOfSpaces), comparand: null); - result = IndentCache[countOfSpaces]; - } - - return result; + // 2 spaces for indentation + renderer.PushIndent(" "); + renderer.WriteChildrenJoinNewLine(obj); + renderer.PopIndent(); } } } diff --git a/src/MarkdownConverter.cs b/src/MarkdownConverter.cs index bb4a3e8..1cb32f5 100644 --- a/src/MarkdownConverter.cs +++ b/src/MarkdownConverter.cs @@ -73,7 +73,7 @@ public static MarkdownInfo Convert(string markdownString, MarkdownConversionType if (conversionType.HasFlag(MarkdownConversionType.VT100)) { - pipeline = new MarkdownPipelineBuilder().Build(); + pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); // Use the VT100 renderer. var renderer = new VT100Renderer(writer, optionInfo); diff --git a/src/MarkdownDocumentRenderer.cs b/src/MarkdownDocumentRenderer.cs new file mode 100644 index 0000000..c03e78b --- /dev/null +++ b/src/MarkdownDocumentRenderer.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Markdig.Syntax; +using Markdig.Syntax.Inlines; + +namespace Microsoft.PowerShell.MarkdownRender +{ + /// + /// Renderer for adding VT100 escape sequences for paragraphs. + /// + internal class MarkdownDocumentRenderer : VT100ObjectRenderer + { + protected override void Write(VT100Renderer renderer, MarkdownDocument obj) + { + bool f = true; + foreach (Block item in obj) + { + if (item.Span.IsEmpty) continue; + + if (f) f = false; + else renderer.WriteLine(); + + renderer.Write(item); + renderer.WriteLine(); + } + } + } +} diff --git a/src/ParagraphBlockRenderer.cs b/src/ParagraphBlockRenderer.cs index 3b854cb..0040707 100644 --- a/src/ParagraphBlockRenderer.cs +++ b/src/ParagraphBlockRenderer.cs @@ -1,22 +1,22 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Syntax; - -namespace Microsoft.PowerShell.MarkdownRender -{ - /// - /// Renderer for adding VT100 escape sequences for paragraphs. - /// - internal class ParagraphBlockRenderer : VT100ObjectRenderer - { - protected override void Write(VT100Renderer renderer, ParagraphBlock obj) - { - // Call the renderer for children, leaf inline or line breaks. - renderer.WriteChildren(obj.Inline); - - // Add new line at the end of the paragraph. - renderer.WriteLine(); - } - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Markdig.Syntax; + +namespace Microsoft.PowerShell.MarkdownRender +{ + /// + /// Renderer for adding VT100 escape sequences for paragraphs. + /// + internal class ParagraphBlockRenderer : VT100ObjectRenderer + { + /** + * Does not append newline after rendering the paragraph. + */ + protected override void Write(VT100Renderer renderer, ParagraphBlock obj) + { + // Call the renderer for children, leaf inline or line breaks. + renderer.WriteChildren(obj.Inline); + } + } +} diff --git a/src/QuoteBlockRenderer.cs b/src/QuoteBlockRenderer.cs index 7e0bda6..806f486 100644 --- a/src/QuoteBlockRenderer.cs +++ b/src/QuoteBlockRenderer.cs @@ -12,14 +12,9 @@ internal class QuoteBlockRenderer : VT100ObjectRenderer { protected override void Write(VT100Renderer renderer, QuoteBlock obj) { - // Iterate through each item and add the quote character before the content. - foreach (var item in obj) - { - renderer.Write(obj.QuoteChar).Write(" ").Write(item); - } - - // Add blank line after the quote block. - renderer.WriteLine(); + renderer.PushIndent(obj.QuoteChar + " "); + renderer.WriteChildrenJoinNewLine(obj); + renderer.PopIndent(); } } } diff --git a/src/TableRenderer.cs b/src/TableRenderer.cs new file mode 100644 index 0000000..a9fa18e --- /dev/null +++ b/src/TableRenderer.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Markdig.Extensions.Tables; +using Markdig.Syntax; + +namespace Microsoft.PowerShell.MarkdownRender +{ + /// + /// Renderer for adding VT100 escape sequences for quote blocks. + /// + internal class TableRenderer : VT100ObjectRenderer + { + protected override void Write(VT100Renderer renderer, Table table) + { + // TODO: improve table rendering + // Probably have to create a new renderer, render the content as string, + // and then process the string to table in order to align the columns + // Additionally, the new rendered content will not have proper length because of + // VT100 escape sequences, therefore a sanitization step is needed + // The sanitization regex is currently in VT100Tests.cs:VT100Tests.MarkdownDocument + // Console column width can be exposed by renderer in order + // to prevent tables from being wider than console + + for (int i = 0; i < table.Count; i++) + { + Block b = table[i]; + if (b.Span.IsEmpty) continue; + + if (i > 0) renderer.WriteLine(); + if (i == 1) + { + if (table[0] is TableRow head) + { + foreach (var cell in head) + { + renderer.Write("|-"); + renderer.Write(new string('-', cell.Span.End - cell.Span.Start)); + } + renderer.Write("|"); + } + renderer.WriteLine(); + } + + if (b is ContainerBlock cb) + { + foreach (var item in cb) + { + renderer.Write("| "); + renderer.Write(item); + renderer.Write(" "); + } + + renderer.Write("|"); + } else + { + renderer.Write(b); + } + } + } + } +} diff --git a/src/VT100Renderer.cs b/src/VT100Renderer.cs index 1f5da62..b4efbf0 100644 --- a/src/VT100Renderer.cs +++ b/src/VT100Renderer.cs @@ -3,6 +3,7 @@ using System.IO; using Markdig.Renderers; +using Markdig.Syntax; namespace Microsoft.PowerShell.MarkdownRender { @@ -20,6 +21,9 @@ public VT100Renderer(TextWriter writer, PSMarkdownOptionInfo optionInfo) : base( { EscapeSequences = new VT100EscapeSequences(optionInfo); + // Root Renderer to handle element spacing + ObjectRenderers.Add(new MarkdownDocumentRenderer()); + // Add the various element renderers. ObjectRenderers.Add(new HeaderBlockRenderer()); ObjectRenderers.Add(new LineBreakRenderer()); @@ -32,11 +36,33 @@ public VT100Renderer(TextWriter writer, PSMarkdownOptionInfo optionInfo) : base( ObjectRenderers.Add(new ListBlockRenderer()); ObjectRenderers.Add(new ListItemBlockRenderer()); ObjectRenderers.Add(new QuoteBlockRenderer()); + + // Table + ObjectRenderers.Add(new TableRenderer()); } /// /// Gets the current escape sequences. /// public VT100EscapeSequences EscapeSequences { get; } + + /// + /// Modify the container block to remove empty children and write the children + /// + /// Then write children excluding last line break + /// + public void WriteChildrenJoinNewLine(ContainerBlock obj) + { + bool f = true; + foreach (Block b in obj) + { + if (b.Span.IsEmpty) continue; + + if (f) f = false; + else WriteLine(); + + Write(b); + } + } } } diff --git a/test/BasicTests.cs b/test/BasicTests.cs index 5b11ffd..a1a2195 100644 --- a/test/BasicTests.cs +++ b/test/BasicTests.cs @@ -12,7 +12,7 @@ public class BasicTests public void VT100Renderer() { var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert("# Heading1", MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); - string expected = $"{Esc}[7mHeading1{Esc}[0m\n\n"; + string expected = $"{Esc}[7mHeading1{Esc}[0m\n"; Assert.Equal(expected, m.VT100EncodedString); } diff --git a/test/VT100Tests.cs b/test/VT100Tests.cs index 6f7a51d..42bb0f5 100644 --- a/test/VT100Tests.cs +++ b/test/VT100Tests.cs @@ -2,6 +2,7 @@ using System.Runtime.InteropServices; using Microsoft.PowerShell.MarkdownRender; using Xunit; +using System.Text.RegularExpressions; namespace Microsoft.PowerShell.MarkdownRender.Tests { @@ -38,7 +39,7 @@ public void FencedCodeBlock() { string inputString = "```PowerShell\n$a = 1\n```"; var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert(inputString, MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); - string expected = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{Esc}[107;95m$a = 1{Esc}[500@{Esc}[0m\n\n" : $"{Esc}[48;2;155;155;155;38;2;30;30;30m$a = 1{Esc}[500@{Esc}[0m\n\n"; + string expected = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{Esc}[107;95m$a = 1{Esc}[500@{Esc}[0m\n\n" : $"{Esc}[48;2;155;155;155;38;2;30;30;30m$a = 1{Esc}[500@{Esc}[0m\n"; Assert.Equal(expected, m.VT100EncodedString); } @@ -46,7 +47,7 @@ public void FencedCodeBlock() public void Header1() { var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert("# Heading1", MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); - string expected = $"{Esc}[7mHeading1{Esc}[0m\n\n"; + string expected = $"{Esc}[7mHeading1{Esc}[0m\n"; Assert.Equal(expected, m.VT100EncodedString); } @@ -54,7 +55,7 @@ public void Header1() public void Header2() { var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert("## Heading2", MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); - string expected = $"{Esc}[4;93mHeading2{Esc}[0m\n\n"; + string expected = $"{Esc}[4;93mHeading2{Esc}[0m\n"; Assert.Equal(expected, m.VT100EncodedString); } @@ -62,7 +63,7 @@ public void Header2() public void Header3() { var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert("### Heading3", MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); - string expected = $"{Esc}[4;94mHeading3{Esc}[0m\n\n"; + string expected = $"{Esc}[4;94mHeading3{Esc}[0m\n"; Assert.Equal(expected, m.VT100EncodedString); } @@ -70,7 +71,7 @@ public void Header3() public void Header4() { var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert("#### Heading4", MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); - string expected = $"{Esc}[4;95mHeading4{Esc}[0m\n\n"; + string expected = $"{Esc}[4;95mHeading4{Esc}[0m\n"; Assert.Equal(expected, m.VT100EncodedString); } @@ -78,7 +79,7 @@ public void Header4() public void Header5() { var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert("##### Heading5", MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); - string expected = $"{Esc}[4;96mHeading5{Esc}[0m\n\n"; + string expected = $"{Esc}[4;96mHeading5{Esc}[0m\n"; Assert.Equal(expected, m.VT100EncodedString); } @@ -86,7 +87,15 @@ public void Header5() public void Header6() { var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert("###### Heading6", MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); - string expected = $"{Esc}[4;97mHeading6{Esc}[0m\n\n"; + string expected = $"{Esc}[4;97mHeading6{Esc}[0m\n"; + Assert.Equal(expected, m.VT100EncodedString); + } + + [Fact] + public void Inline() + { + var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert("Hello\n\nWorld", MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); + string expected = "Hello\n\nWorld\n"; Assert.Equal(expected, m.VT100EncodedString); } @@ -109,20 +118,20 @@ public void LinkImage() [Fact] public void OrderedList() { - string inputString = "1. A\n2. B\n3. C\n"; + string inputString = "1. `A`\n2. B\n3. C\n"; var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert(inputString, MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); - string expected = $"1. A\n2. B\n3. C\n\n"; + string expected = $"1. {Esc}[48;2;155;155;155;38;2;30;30;30mA{Esc}[0m\n2. B\n3. C\n"; Assert.Equal(expected, m.VT100EncodedString); } [Fact] public void UnorderedList() { - string inputString = "* A\n* B\n* C\n"; + string inputString = "* `A`\n* B\n* C\n"; var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert(inputString, MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); - string expected = $"* A\n* B\n* C\n\n"; + string expected = $"* {Esc}[48;2;155;155;155;38;2;30;30;30mA{Esc}[0m\n* B\n* C\n"; Assert.Equal(expected, m.VT100EncodedString); } @@ -132,9 +141,118 @@ public void QuoteBlock() string inputString = "> Hello"; var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert(inputString, MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); - string expected = $"> Hello\n\n\n"; + string expected = $"> Hello\n"; Assert.Equal(expected, m.VT100EncodedString); } + + [Fact] + public void MarkdownDocument() + { + string md = @" + +# Heading1 + +- li +- `hello` +- [link](linker) + +`solo inline` + +code `code inline` should work + +```ps1 +$a = 1 +$b = 2 +``` + +> > double quote + +> quote +> > double quote +> > > triple quote +> > +> > back a quote +> > +> still quote + +1. `a` +2. [b](l) +3. c + + +- l1.0 + 1. l2.0 + - l3.0\ + l3.0.line1\ + l3.0.line2 + 3. l2.1 + - l3.1 + 3. l2.2 +- l1.1 + + +| a | bc | +|---|---| +| 1 | 2 | +| 3 | 4 | + +--- + +end +".Replace("\r\n", "\n"); + string expected = @"Heading1 + +- li +- hello +- ""link"" + +solo inline + +code code inline should work + +$a = 1 +$b = 2 + +> > double quote + +> quote +> > double quote +> > > triple quote +> > back a quote +> still quote + +1. a +2. ""b"" +3. c + +- l1.0 + 1. l2.0 + - l3.0 + l3.0.line1 + l3.0.line2 + 2. l2.1 + - l3.1 + 3. l2.2 +- l1.1 + +| a | bc | +|---|----| +| 1 | 2 | +| 3 | 4 | + + + +end +".Replace("\r\n", "\n"); + + var m = Microsoft.PowerShell.MarkdownRender.MarkdownConverter.Convert(md, MarkdownConversionType.VT100, new PSMarkdownOptionInfo() ); + string pattern = @"\x1B\[[0-?]*[@-~]"; + + // Replace all occurrences of VT100 escape sequences with an empty string + string actual = Regex.Replace(m.VT100EncodedString, pattern, ""); + + Assert.Equal(expected, actual); + } } public class PSMarkdownOptionInfoTests