diff --git a/MfGames.Markdown.Gemtext.sln.DotSettings b/MfGames.Markdown.Gemtext.sln.DotSettings index 06b6404..3fc229d 100644 --- a/MfGames.Markdown.Gemtext.sln.DotSettings +++ b/MfGames.Markdown.Gemtext.sln.DotSettings @@ -1366,5 +1366,6 @@ using(DataAccessAdapter dataAccessAdapter = new DataAccessAdapter(ConnectionStri True 2.0 InCSharpStatement + True True diff --git a/src/MfGames.Markdown.Gemtext.Tests/TableTests.cs b/src/MfGames.Markdown.Gemtext.Tests/TableTests.cs index b470aac..0c82a21 100644 --- a/src/MfGames.Markdown.Gemtext.Tests/TableTests.cs +++ b/src/MfGames.Markdown.Gemtext.Tests/TableTests.cs @@ -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); } diff --git a/src/MfGames.Markdown.Gemtext/Extensions/GemtextPipeTableExtension.cs b/src/MfGames.Markdown.Gemtext/Extensions/GemtextPipeTableExtension.cs new file mode 100644 index 0000000..7b34954 --- /dev/null +++ b/src/MfGames.Markdown.Gemtext/Extensions/GemtextPipeTableExtension.cs @@ -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 +{ + /// + /// Extension method to control how links are processed inside blocks. + /// + /// + public class GemtextPipeTableExtension : IMarkdownExtension + { + /// + /// Initializes a new instance of the + /// class. + /// + /// The options. + public GemtextPipeTableExtension( + GemtextPipeTableOptions? options = null) + { + this.Options = options ?? new GemtextPipeTableOptions(); + } + + /// + /// Gets the options. + /// + public GemtextPipeTableOptions Options { get; } + + /// + public void Setup(MarkdownPipelineBuilder pipeline) + { + pipeline.PreciseSourceLocation = true; + + if (!pipeline.BlockParsers.Contains()) + { + pipeline.BlockParsers.Insert(0, new PipeTableBlockParser()); + } + + LineBreakInlineParser? lineBreakParser = + pipeline.InlineParsers.FindExact(); + + if (!pipeline.InlineParsers.Contains()) + { + pipeline.InlineParsers.InsertBefore( + new PipeTableParser(lineBreakParser!, this.Options)); + } + } + + /// + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is not GemtextRenderer gemtext) + { + return; + } + + gemtext.ObjectRenderers.Add( + new TableRenderer(this.Options.ConfigureTableBuilder)); + } + } +} diff --git a/src/MfGames.Markdown.Gemtext/Extensions/GemtextPipeTableOptions.cs b/src/MfGames.Markdown.Gemtext/Extensions/GemtextPipeTableOptions.cs new file mode 100644 index 0000000..3de1d18 --- /dev/null +++ b/src/MfGames.Markdown.Gemtext/Extensions/GemtextPipeTableOptions.cs @@ -0,0 +1,16 @@ +using System; + +using ConsoleTableExt; + +using Markdig.Extensions.Tables; + +namespace MfGames.Markdown.Gemtext.Extensions +{ + public class GemtextPipeTableOptions : PipeTableOptions + { + /// + /// Gets or sets the table builder to control formatting. + /// + public Action? ConfigureTableBuilder { get; set; } + } +} diff --git a/src/MfGames.Markdown.Gemtext/MfGames.Markdown.Gemtext.csproj b/src/MfGames.Markdown.Gemtext/MfGames.Markdown.Gemtext.csproj index 1073790..479b8a7 100644 --- a/src/MfGames.Markdown.Gemtext/MfGames.Markdown.Gemtext.csproj +++ b/src/MfGames.Markdown.Gemtext/MfGames.Markdown.Gemtext.csproj @@ -18,7 +18,8 @@ - + + diff --git a/src/MfGames.Markdown.Gemtext/Renderers/Gemtext/Blocks/TableRenderer.cs b/src/MfGames.Markdown.Gemtext/Renderers/Gemtext/Blocks/TableRenderer.cs new file mode 100644 index 0000000..8e63c6a --- /dev/null +++ b/src/MfGames.Markdown.Gemtext/Renderers/Gemtext/Blocks/TableRenderer.cs @@ -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 + { + private readonly Action? configureTableBuilder; + + public TableRenderer(Action? 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 header = new(); + List> data = new(); + Dictionary align = new(); + + foreach (TableRow row in table.OfType()) + { + // 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 cells = GetCellValues(row); + + data.Add(cells); + } + + // Set up the table. + ConsoleTableBuilder builder = ConsoleTableBuilder + .From(data) + .WithColumn(header.OfType().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 GetCellValues(TableRow row) + { + List cells = new(); + + foreach (TableCell cell in row.OfType()) + { + // 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 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, + }; + } + } + } + } +}