feat: added table rendering

This commit is contained in:
Dylan R. E. Moonfire 2022-02-15 23:23:44 -06:00
parent d0267b9428
commit 6fac646f18
6 changed files with 259 additions and 11 deletions

View file

@ -1366,5 +1366,6 @@ using(DataAccessAdapter dataAccessAdapter = new DataAccessAdapter(ConnectionStri
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F87CBA43E9CDCC41A45B39A2A2A25764/Scope/=2C285F182AC98D44B0B4F29D4D2149EC/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F87CBA43E9CDCC41A45B39A2A2A25764/Scope/=2C285F182AC98D44B0B4F29D4D2149EC/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F87CBA43E9CDCC41A45B39A2A2A25764/Scope/=2C285F182AC98D44B0B4F29D4D2149EC/Type/@EntryValue">InCSharpStatement</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gemtext/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tocks/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>

View file

@ -1,13 +1,60 @@
using ConsoleTableExt;
using Markdig;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests
{
public class TableTests
{
[Fact(Skip = "Tables are out of scope at this point")]
public void SimpleImageLink()
[Fact]
public void AlignedTable()
{
MarkdownPipeline pipeline = new MarkdownPipelineBuilder()
.Use(
new GemtextPipeTableExtension(
new GemtextPipeTableOptions()
{
ConfigureTableBuilder = (x) =>
x.WithFormat(
ConsoleTableBuilderFormat.MarkDown),
}))
.Build();
string input = string.Join(
"\n",
"aaa|bbb|ccc",
"--:|---|:-:",
"1|2|3",
"4|5|6");
string expected = string.Join(
"\n",
"| aaa | bbb | ccc |",
"|-----|-----|-----|",
"| 1 | 2 | 3 |",
"| 4 | 5 | 6 |",
"");
string actual = MarkdownGemtext.ToGemtext(input, pipeline);
Assert.Equal(expected, actual);
}
[Fact]
public void SimpleTable()
{
MarkdownPipeline pipeline = new MarkdownPipelineBuilder()
.Use(
new GemtextPipeTableExtension(
new GemtextPipeTableOptions()
{
ConfigureTableBuilder = (x) =>
x.WithFormat(
ConsoleTableBuilderFormat.MarkDown),
}))
.Build();
string input = string.Join(
"\n",
"a|b|c",
@ -16,14 +63,12 @@ namespace MfGames.Markdown.Gemini.Tests
"4|5|6");
string expected = string.Join(
"\n",
"┌───┬───┬───┐",
"│ a │ b │ c │",
"╞═══╪═══╪═══╡",
"│ 1 │ 2 │ 3 │",
"├───┼───┼───┤",
"│ 4 │ 5 │ 6 │",
"└───┴───┴───┘");
string actual = MarkdownGemtext.ToGemtext(input);
"| a | b | c |",
"|---|---|---|",
"| 1 | 2 | 3 |",
"| 4 | 5 | 6 |",
"");
string actual = MarkdownGemtext.ToGemtext(input, pipeline);
Assert.Equal(expected, actual);
}

View file

@ -0,0 +1,65 @@
using Markdig;
using Markdig.Extensions.Tables;
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
namespace MfGames.Markdown.Gemtext.Extensions
{
/// <summary>
/// Extension method to control how links are processed inside blocks.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class GemtextPipeTableExtension : IMarkdownExtension
{
/// <summary>
/// Initializes a new instance of the <see cref="GemtextPipeTableExtension" />
/// class.
/// </summary>
/// <param name="options">The options.</param>
public GemtextPipeTableExtension(
GemtextPipeTableOptions? options = null)
{
this.Options = options ?? new GemtextPipeTableOptions();
}
/// <summary>
/// Gets the options.
/// </summary>
public GemtextPipeTableOptions Options { get; }
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
pipeline.PreciseSourceLocation = true;
if (!pipeline.BlockParsers.Contains<PipeTableBlockParser>())
{
pipeline.BlockParsers.Insert(0, new PipeTableBlockParser());
}
LineBreakInlineParser? lineBreakParser =
pipeline.InlineParsers.FindExact<LineBreakInlineParser>();
if (!pipeline.InlineParsers.Contains<PipeTableParser>())
{
pipeline.InlineParsers.InsertBefore<EmphasisInlineParser>(
new PipeTableParser(lineBreakParser!, this.Options));
}
}
/// <inheritdoc />
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not GemtextRenderer gemtext)
{
return;
}
gemtext.ObjectRenderers.Add(
new TableRenderer(this.Options.ConfigureTableBuilder));
}
}
}

View file

@ -0,0 +1,16 @@
using System;
using ConsoleTableExt;
using Markdig.Extensions.Tables;
namespace MfGames.Markdown.Gemtext.Extensions
{
public class GemtextPipeTableOptions : PipeTableOptions
{
/// <summary>
/// Gets or sets the table builder to control formatting.
/// </summary>
public Action<ConsoleTableBuilder>? ConfigureTableBuilder { get; set; }
}
}

View file

@ -18,7 +18,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Markdig" Version="0.25.0"/>
<PackageReference Include="ConsoleTableExt" Version="3.1.9" />
<PackageReference Include="Markdig" Version="0.25.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ConsoleTableExt;
using Markdig.Extensions.Tables;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks
{
public class TableRenderer : GemtextObjectRenderer<Table>
{
private readonly Action<ConsoleTableBuilder>? configureTableBuilder;
public TableRenderer(Action<ConsoleTableBuilder>? configureTableBuilder)
{
this.configureTableBuilder = configureTableBuilder;
}
protected override void Write(GemtextRenderer renderer, Table table)
{
// Make sure we have plenty of space above us.
renderer.EnsureTwoLines();
// Since Gemtext doesn't have a table format per-se, we are going
// to use ConsoleTableEx to make a nicely-formatted table and emit
// the lines directly. That should produce the desired result.
// Gather up information about the data since that is where the
// builder starts with.
bool hasHeader = false;
List<object> header = new();
List<List<object>> data = new();
Dictionary<int, TextAligntment> align = new();
foreach (TableRow row in table.OfType<TableRow>())
{
// If we haven't seen a header, then we include that.
if (!hasHeader && row.IsHeader)
{
header = GetCellValues(row);
SetAlignments(table, align, row);
continue;
}
// Otherwise, we treat it as a row and go through the columns.
List<object> cells = GetCellValues(row);
data.Add(cells);
}
// Set up the table.
ConsoleTableBuilder builder = ConsoleTableBuilder
.From(data)
.WithColumn(header.OfType<string>().ToArray())
.WithHeaderTextAlignment(align)
.WithTextAlignment(align);
this.configureTableBuilder?.Invoke(builder);
// Format the final table.
string formatted = builder.Export().ToString().TrimEnd();
renderer.WriteLine(formatted);
}
private static List<object> GetCellValues(TableRow row)
{
List<object> cells = new();
foreach (TableCell cell in row.OfType<TableCell>())
{
// Write out to a text since we can't have a callback while
// rendering the table cells.
using var writer = new StringWriter();
var innerRenderer = new GemtextRenderer(writer);
innerRenderer.Render(cell);
cells.Add(writer.ToString());
}
return cells;
}
private static void SetAlignments(
Table table,
Dictionary<int, TextAligntment> align,
TableRow row)
{
for (int i = 0; i < row.Count; i++)
{
// Copied from Markdig's version.
var cell = (TableCell)row[i];
int columnIndex = cell.ColumnIndex < 0
|| cell.ColumnIndex >= table.ColumnDefinitions.Count
? i
: cell.ColumnIndex;
columnIndex =
columnIndex >= table.ColumnDefinitions.Count
? table.ColumnDefinitions.Count - 1
: columnIndex;
TableColumnAlign? alignment = table
.ColumnDefinitions[columnIndex]
.Alignment;
if (alignment.HasValue)
{
align[columnIndex] = alignment.Value switch
{
TableColumnAlign.Center => TextAligntment.Center,
TableColumnAlign.Left => TextAligntment.Left,
TableColumnAlign.Right => TextAligntment.Right,
_ => TextAligntment.Left,
};
}
}
}
}
}