Skip to content

Commit 4e4ccea

Browse files
committed
VT100 terminal rendering based on Powershell code
1 parent 7d39e93 commit 4e4ccea

17 files changed

Lines changed: 936 additions & 5 deletions

Source/Bookgen.Lib/Markdown/MarkdownConverter.cs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@
33
// This code is licensed under MIT license (see LICENSE for details)
44
//-----------------------------------------------------------------------------
55

6-
using Bookgen.Lib.ImageService;
6+
using Bookgen.Lib.Markdown.Renderers.Terminal;
77
using Bookgen.Lib.Markdown.TableOfContents;
8+
using Bookgen.Lib.Pipeline;
89

910
using Markdig;
1011

12+
using Microsoft.AspNetCore.Components;
13+
1114
namespace Bookgen.Lib.Markdown;
1215

1316
public sealed class MarkdownConverter : IDisposable
1417
{
15-
private readonly MarkdownPipeline _pipeline;
18+
private readonly MarkdownPipeline _htmlPipeLine;
19+
private readonly MarkdownPipeline _terminalPipeLine;
1620

1721
public MarkdownConverter(RenderSettings settings)
1822
{
@@ -31,12 +35,16 @@ public MarkdownConverter(RenderSettings settings)
3135
}
3236
}
3337

34-
_pipeline = configuration.Build();
38+
_htmlPipeLine = configuration.Build();
39+
40+
_terminalPipeLine = new MarkdownPipelineBuilder()
41+
.UseYamlFrontMatter()
42+
.Build();
3543
}
3644

3745
public void Dispose()
3846
{
39-
foreach (IMarkdownExtension? extension in _pipeline.Extensions)
47+
foreach (IMarkdownExtension? extension in _htmlPipeLine.Extensions)
4048
{
4149
if (extension is IDisposable disposable)
4250
{
@@ -46,5 +54,15 @@ public void Dispose()
4654
}
4755

4856
public string RenderMarkdownToHtml(string markdown)
49-
=> Markdig.Markdown.ToHtml(markdown, _pipeline);
57+
=> Markdig.Markdown.ToHtml(markdown, _htmlPipeLine);
58+
59+
public string RenderMarkdownToTerminal(string markdown)
60+
{
61+
PSMarkdownOptionInfo optionInfo = new();
62+
63+
using var writer = new StringWriter();
64+
var renderer = new VT100Renderer(writer, optionInfo);
65+
66+
return Markdig.Markdown.Convert(markdown, renderer, _terminalPipeLine).ToString() ?? "";
67+
}
5068
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Markdig.Syntax.Inlines;
5+
6+
namespace Bookgen.Lib.Markdown.Renderers.Terminal;
7+
8+
/// <summary>
9+
/// Renderer for adding VT100 escape sequences for inline code elements.
10+
/// </summary>
11+
internal class CodeInlineRenderer : VT100ObjectRenderer<CodeInline>
12+
{
13+
protected override void Write(VT100Renderer renderer, CodeInline obj)
14+
{
15+
renderer.Write(renderer.EscapeSequences.FormatCode(obj.Content, isInline: true));
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Markdig.Syntax.Inlines;
5+
6+
namespace Bookgen.Lib.Markdown.Renderers.Terminal;
7+
8+
/// <summary>
9+
/// Renderer for adding VT100 escape sequences for bold and italics elements.
10+
/// </summary>
11+
internal class EmphasisInlineRenderer : VT100ObjectRenderer<EmphasisInline>
12+
{
13+
protected override void Write(VT100Renderer renderer, EmphasisInline obj)
14+
{
15+
renderer.Write(renderer.EscapeSequences.FormatEmphasis(obj.FirstChild?.ToString() ?? "", isBold: obj.DelimiterCount == 2));
16+
}
17+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Markdig.Helpers;
5+
using Markdig.Syntax;
6+
7+
namespace Bookgen.Lib.Markdown.Renderers.Terminal;
8+
9+
/// <summary>
10+
/// Renderer for adding VT100 escape sequences for code blocks with language type.
11+
/// </summary>
12+
internal class FencedCodeBlockRenderer : VT100ObjectRenderer<FencedCodeBlock>
13+
{
14+
protected override void Write(VT100Renderer renderer, FencedCodeBlock obj)
15+
{
16+
if (obj?.Lines.Lines != null)
17+
{
18+
foreach (StringLine codeLine in obj.Lines.Lines)
19+
{
20+
if (!string.IsNullOrWhiteSpace(codeLine.ToString()))
21+
{
22+
// If the code block is of type YAML, then tab to right to improve readability.
23+
// This specifically helps for parameters help content.
24+
if (string.Equals(obj.Info, "yaml", StringComparison.OrdinalIgnoreCase))
25+
{
26+
renderer.Write("\t").WriteLine(codeLine.ToString());
27+
}
28+
else
29+
{
30+
renderer.WriteLine(renderer.EscapeSequences.FormatCode(codeLine.ToString(), isInline: false));
31+
}
32+
}
33+
}
34+
35+
// Add a blank line after the code block for better readability.
36+
renderer.WriteLine();
37+
}
38+
}
39+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Markdig.Syntax;
5+
6+
namespace Bookgen.Lib.Markdown.Renderers.Terminal;
7+
8+
/// <summary>
9+
/// Renderer for adding VT100 escape sequences for headings.
10+
/// </summary>
11+
internal class HeaderBlockRenderer : VT100ObjectRenderer<HeadingBlock>
12+
{
13+
protected override void Write(VT100Renderer renderer, HeadingBlock obj)
14+
{
15+
string? headerText = obj.Inline?.FirstChild?.ToString();
16+
17+
if (!string.IsNullOrEmpty(headerText))
18+
{
19+
// Format header and then add blank line to improve readability.
20+
switch (obj.Level)
21+
{
22+
case 1:
23+
renderer.WriteLine(renderer.EscapeSequences.FormatHeader1(headerText));
24+
renderer.WriteLine();
25+
break;
26+
27+
case 2:
28+
renderer.WriteLine(renderer.EscapeSequences.FormatHeader2(headerText));
29+
renderer.WriteLine();
30+
break;
31+
32+
case 3:
33+
renderer.WriteLine(renderer.EscapeSequences.FormatHeader3(headerText));
34+
renderer.WriteLine();
35+
break;
36+
37+
case 4:
38+
renderer.WriteLine(renderer.EscapeSequences.FormatHeader4(headerText));
39+
renderer.WriteLine();
40+
break;
41+
42+
case 5:
43+
renderer.WriteLine(renderer.EscapeSequences.FormatHeader5(headerText));
44+
renderer.WriteLine();
45+
break;
46+
47+
case 6:
48+
renderer.WriteLine(renderer.EscapeSequences.FormatHeader6(headerText));
49+
renderer.WriteLine();
50+
break;
51+
}
52+
}
53+
}
54+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Markdig.Syntax.Inlines;
5+
6+
namespace Bookgen.Lib.Markdown.Renderers.Terminal;
7+
8+
/// <summary>
9+
/// Renderer for adding VT100 escape sequences for leaf elements like plain text in paragraphs.
10+
/// </summary>
11+
internal class LeafInlineRenderer : VT100ObjectRenderer<LeafInline>
12+
{
13+
protected override void Write(VT100Renderer renderer, LeafInline obj)
14+
{
15+
// If the next sibling is null, then this is the last line in the paragraph.
16+
// Add new line character at the end.
17+
// Else just write without newline at the end.
18+
if (obj.NextSibling == null)
19+
{
20+
renderer.WriteLine(obj.ToString() ?? "");
21+
}
22+
else
23+
{
24+
renderer.Write(obj.ToString());
25+
}
26+
}
27+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Markdig.Syntax.Inlines;
5+
6+
namespace Bookgen.Lib.Markdown.Renderers.Terminal;
7+
8+
/// <summary>
9+
/// Renderer for adding VT100 escape sequences for line breaks.
10+
/// </summary>
11+
internal class LineBreakRenderer : VT100ObjectRenderer<LineBreakInline>
12+
{
13+
protected override void Write(VT100Renderer renderer, LineBreakInline obj)
14+
{
15+
// If it is a hard line break add new line at the end.
16+
// Else, add a space for after the last character to improve readability.
17+
if (obj.IsHard)
18+
{
19+
renderer.WriteLine();
20+
}
21+
else
22+
{
23+
renderer.Write(" ");
24+
}
25+
}
26+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Markdig.Syntax.Inlines;
5+
6+
namespace Bookgen.Lib.Markdown.Renderers.Terminal;
7+
8+
/// <summary>
9+
/// Renderer for adding VT100 escape sequences for links.
10+
/// </summary>
11+
internal class LinkInlineRenderer : VT100ObjectRenderer<LinkInline>
12+
{
13+
protected override void Write(VT100Renderer renderer, LinkInline obj)
14+
{
15+
string? text = obj.FirstChild?.ToString();
16+
17+
// Format link as image or link.
18+
if (obj.IsImage)
19+
{
20+
renderer.Write(renderer.EscapeSequences.FormatImage(text));
21+
}
22+
else
23+
{
24+
renderer.Write(renderer.EscapeSequences.FormatLink(text ?? "", obj.Url ?? ""));
25+
}
26+
}
27+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Markdig.Syntax;
5+
6+
namespace Bookgen.Lib.Markdown.Renderers.Terminal;
7+
8+
/// <summary>
9+
/// Renderer for adding VT100 escape sequences for list blocks.
10+
/// </summary>
11+
internal class ListBlockRenderer : VT100ObjectRenderer<ListBlock>
12+
{
13+
protected override void Write(VT100Renderer renderer, ListBlock obj)
14+
{
15+
// start index of a numbered block.
16+
int index = 1;
17+
18+
foreach (var item in obj)
19+
{
20+
if (item is ListItemBlock listItem)
21+
{
22+
if (obj.IsOrdered)
23+
{
24+
RenderNumberedList(renderer, listItem, index++);
25+
}
26+
else
27+
{
28+
renderer.Write(listItem);
29+
}
30+
}
31+
}
32+
33+
renderer.WriteLine();
34+
}
35+
36+
private static void RenderNumberedList(VT100Renderer renderer, ListItemBlock block, int index)
37+
{
38+
// For a numbered list, we need to make sure the index is incremented.
39+
foreach (var line in block)
40+
{
41+
if (line is ParagraphBlock paragraphBlock && paragraphBlock.Inline != null)
42+
{
43+
renderer.Write(index.ToString()).Write(". ").Write(paragraphBlock.Inline);
44+
}
45+
}
46+
}
47+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Markdig.Syntax;
5+
6+
namespace Bookgen.Lib.Markdown.Renderers.Terminal;
7+
8+
/// <summary>
9+
/// Renderer for adding VT100 escape sequences for items in a list block.
10+
/// </summary>
11+
internal class ListItemBlockRenderer : VT100ObjectRenderer<ListItemBlock>
12+
{
13+
protected override void Write(VT100Renderer renderer, ListItemBlock obj)
14+
{
15+
if (obj.Parent is ListBlock parent)
16+
{
17+
if (!parent.IsOrdered)
18+
{
19+
foreach (var line in obj)
20+
{
21+
RenderWithIndent(renderer, line, parent.BulletType, 0);
22+
}
23+
}
24+
}
25+
}
26+
27+
private static void RenderWithIndent(VT100Renderer renderer, MarkdownObject block, char listBullet, int indentLevel)
28+
{
29+
// Indent left by 2 for each level on list.
30+
string indent = Padding(indentLevel * 2);
31+
32+
if (block is ParagraphBlock paragraphBlock && paragraphBlock.Inline != null)
33+
{
34+
renderer.Write(indent).Write(listBullet).Write(" ").Write(paragraphBlock.Inline);
35+
}
36+
else
37+
{
38+
// If there is a sublist, the block is a ListBlock instead of ParagraphBlock.
39+
if (block is ListBlock subList)
40+
{
41+
foreach (var subListItem in subList)
42+
{
43+
if (subListItem is ListItemBlock subListItemBlock)
44+
{
45+
foreach (var line in subListItemBlock)
46+
{
47+
// Increment indent level for sub list.
48+
RenderWithIndent(renderer, line, listBullet, indentLevel + 1);
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
56+
// Typical padding is at most a screen's width, any more than that and we won't bother caching.
57+
private const int IndentCacheMax = 120;
58+
59+
private static readonly string[] IndentCache = new string[IndentCacheMax];
60+
61+
internal static string Padding(int countOfSpaces)
62+
{
63+
if (countOfSpaces >= IndentCacheMax)
64+
{
65+
return new string(' ', countOfSpaces);
66+
}
67+
68+
var result = IndentCache[countOfSpaces];
69+
70+
if (result == null)
71+
{
72+
Interlocked.CompareExchange(ref IndentCache[countOfSpaces], new string(' ', countOfSpaces), comparand: null);
73+
result = IndentCache[countOfSpaces];
74+
}
75+
76+
return result;
77+
}
78+
}

0 commit comments

Comments
 (0)