feat: added table rendering
This commit is contained in:
parent
d0267b9428
commit
6fac646f18
6 changed files with 259 additions and 11 deletions
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ConsoleTableExt" Version="3.1.9" />
|
||||
<PackageReference Include="Markdig" Version="0.25.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in a new issue