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: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/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
|
||||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F87CBA43E9CDCC41A45B39A2A2A25764/Scope/=2C285F182AC98D44B0B4F29D4D2149EC/Type/@EntryValue">InCSharpStatement</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>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tocks/@EntryIndexedValue">True</s:Boolean>
|
||||||
</wpf:ResourceDictionary>
|
</wpf:ResourceDictionary>
|
||||||
|
|
|
@ -1,13 +1,60 @@
|
||||||
|
using ConsoleTableExt;
|
||||||
|
|
||||||
|
using Markdig;
|
||||||
|
|
||||||
using MfGames.Markdown.Gemtext;
|
using MfGames.Markdown.Gemtext;
|
||||||
|
using MfGames.Markdown.Gemtext.Extensions;
|
||||||
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace MfGames.Markdown.Gemini.Tests
|
namespace MfGames.Markdown.Gemini.Tests
|
||||||
{
|
{
|
||||||
public class TableTests
|
public class TableTests
|
||||||
{
|
{
|
||||||
[Fact(Skip = "Tables are out of scope at this point")]
|
[Fact]
|
||||||
public void SimpleImageLink()
|
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(
|
string input = string.Join(
|
||||||
"\n",
|
"\n",
|
||||||
"a|b|c",
|
"a|b|c",
|
||||||
|
@ -16,14 +63,12 @@ namespace MfGames.Markdown.Gemini.Tests
|
||||||
"4|5|6");
|
"4|5|6");
|
||||||
string expected = string.Join(
|
string expected = string.Join(
|
||||||
"\n",
|
"\n",
|
||||||
"┌───┬───┬───┐",
|
"| a | b | c |",
|
||||||
"│ a │ b │ c │",
|
"|---|---|---|",
|
||||||
"╞═══╪═══╪═══╡",
|
"| 1 | 2 | 3 |",
|
||||||
"│ 1 │ 2 │ 3 │",
|
"| 4 | 5 | 6 |",
|
||||||
"├───┼───┼───┤",
|
"");
|
||||||
"│ 4 │ 5 │ 6 │",
|
string actual = MarkdownGemtext.ToGemtext(input, pipeline);
|
||||||
"└───┴───┴───┘");
|
|
||||||
string actual = MarkdownGemtext.ToGemtext(input);
|
|
||||||
|
|
||||||
Assert.Equal(expected, actual);
|
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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ConsoleTableExt" Version="3.1.9" />
|
||||||
<PackageReference Include="Markdig" Version="0.25.0" />
|
<PackageReference Include="Markdig" Version="0.25.0" />
|
||||||
</ItemGroup>
|
</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