From 55f770cc0724aff99b1de593ad4fba080645b500 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Wed, 9 Apr 2025 20:55:54 +0200 Subject: [PATCH] feat: infer pipe table column widths from separator row Adds support for calculating column widths in pipe tables based on the number of dashes in the header separator row. Enabled via the InferColumnWidthsFromSeparator option in PipeTableOptions. --- src/Markdig.Tests/TestPipeTable.cs | 39 +++++++++++++++- .../Extensions/Tables/GridTableParser.cs | 2 +- .../Extensions/Tables/PipeTableOptions.cs | 7 +++ .../Extensions/Tables/PipeTableParser.cs | 45 ++++++++++++++----- src/Markdig/Extensions/Tables/TableHelper.cs | 14 +++--- 5 files changed, 88 insertions(+), 19 deletions(-) diff --git a/src/Markdig.Tests/TestPipeTable.cs b/src/Markdig.Tests/TestPipeTable.cs index cffd479c..b891c722 100644 --- a/src/Markdig.Tests/TestPipeTable.cs +++ b/src/Markdig.Tests/TestPipeTable.cs @@ -12,10 +12,47 @@ public sealed class TestPipeTable [TestCase("| S | \r\n|---|\r\n| G |\r\n\r\n| D | D |\r\n| ---| ---| \r\n| V | V |", 2)] public void TestTableBug(string markdown, int tableCount = 1) { - MarkdownDocument document = Markdown.Parse(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build()); + MarkdownDocument document = + Markdown.Parse(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build()); Table[] tables = document.Descendants().OfType().ToArray(); Assert.AreEqual(tableCount, tables.Length); } + + [TestCase("A | B\r\n---|---", new[] {50.0f, 50.0f})] + [TestCase("A | B\r\n-|---", new[] {25.0f, 75.0f})] + [TestCase("A | B\r\n-|---\r\nA | B\r\n---|---", new[] {25.0f, 75.0f})] + [TestCase("A | B\r\n---|---|---", new[] {33.33f, 33.33f, 33.33f})] + [TestCase("A | B\r\n---|---|---|", new[] {33.33f, 33.33f, 33.33f})] + public void TestColumnWidthByHeaderLines(string markdown, float[] expectedWidth) + { + var pipeline = new MarkdownPipelineBuilder() + .UsePipeTables(new PipeTableOptions() {InferColumnWidthsFromSeparator = true}) + .Build(); + var document = Markdown.Parse(markdown, pipeline); + var table = document.Descendants().OfType
().FirstOrDefault(); + Assert.IsNotNull(table); + var actualWidths = table.ColumnDefinitions.Select(x => x.Width).ToList(); + Assert.AreEqual(actualWidths.Count, expectedWidth.Length); + for (int i = 0; i < expectedWidth.Length; i++) + { + Assert.AreEqual(actualWidths[i], expectedWidth[i], 0.01); + } + } + + [Test] + public void TestColumnWidthIsNotSetWithoutConfigurationFlag() + { + var pipeline = new MarkdownPipelineBuilder() + .UsePipeTables(new PipeTableOptions() {InferColumnWidthsFromSeparator = false}) + .Build(); + var document = Markdown.Parse("| A | B | C |\r\n|---|---|---|", pipeline); + var table = document.Descendants().OfType
().FirstOrDefault(); + Assert.IsNotNull(table); + foreach (var column in table.ColumnDefinitions) + { + Assert.AreEqual(0, column.Width); + } + } } diff --git a/src/Markdig/Extensions/Tables/GridTableParser.cs b/src/Markdig/Extensions/Tables/GridTableParser.cs index 6e6d562a..847bd94c 100644 --- a/src/Markdig/Extensions/Tables/GridTableParser.cs +++ b/src/Markdig/Extensions/Tables/GridTableParser.cs @@ -43,7 +43,7 @@ public class GridTableParser : BlockParser } // Parse a column alignment - if (!TableHelper.ParseColumnHeader(ref line, '-', out TableColumnAlign? columnAlign)) + if (!TableHelper.ParseColumnHeader(ref line, '-', out TableColumnAlign? columnAlign, out _)) { return BlockState.None; } diff --git a/src/Markdig/Extensions/Tables/PipeTableOptions.cs b/src/Markdig/Extensions/Tables/PipeTableOptions.cs index 05181ec2..bbeafac9 100644 --- a/src/Markdig/Extensions/Tables/PipeTableOptions.cs +++ b/src/Markdig/Extensions/Tables/PipeTableOptions.cs @@ -33,4 +33,11 @@ public class PipeTableOptions /// in all other rows (default behavior). /// public bool UseHeaderForColumnCount { get; set; } + + + /// + /// Gets or sets a value indicating whether column widths should be inferred based on the number of dashes + /// in the header separator row. Each column's width will be proportional to the dash count in its respective column. + /// + public bool InferColumnWidthsFromSeparator { get; set; } } \ No newline at end of file diff --git a/src/Markdig/Extensions/Tables/PipeTableParser.cs b/src/Markdig/Extensions/Tables/PipeTableParser.cs index 71c14538..f4d17c41 100644 --- a/src/Markdig/Extensions/Tables/PipeTableParser.cs +++ b/src/Markdig/Extensions/Tables/PipeTableParser.cs @@ -481,9 +481,10 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor return false; } - private static bool ParseHeaderString(Inline? inline, out TableColumnAlign? align) + private static bool ParseHeaderString(Inline? inline, out TableColumnAlign? align, out int delimiterCount) { align = 0; + delimiterCount = 0; var literal = inline as LiteralInline; if (literal is null) { @@ -492,7 +493,7 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor // Work on a copy of the slice var line = literal.Content; - if (TableHelper.ParseColumnHeader(ref line, '-', out align)) + if (TableHelper.ParseColumnHeader(ref line, '-', out align, out delimiterCount)) { if (line.CurrentChar != '\0') { @@ -507,7 +508,8 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor private List? FindHeaderRow(List delimiters) { bool isValidRow = false; - List? aligns = null; + int totalDelimiterCount = 0; + List? columnDefinitions = null; for (int i = 0; i < delimiters.Count; i++) { if (!IsLine(delimiters[i])) @@ -529,18 +531,19 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor // Check the left side of a `|` delimiter TableColumnAlign? align = null; + int delimiterCount = 0; if (delimiter.PreviousSibling != null && !(delimiter.PreviousSibling is LiteralInline li && li.Content.IsEmptyOrWhitespace()) && // ignore parsed whitespace - !ParseHeaderString(delimiter.PreviousSibling, out align)) + !ParseHeaderString(delimiter.PreviousSibling, out align, out delimiterCount)) { break; } // Create aligns until we may have a header row - aligns ??= new List(); - - aligns.Add(new TableColumnDefinition() { Alignment = align }); + columnDefinitions ??= new List(); + totalDelimiterCount += delimiterCount; + columnDefinitions.Add(new TableColumnDefinition() { Alignment = align, Width = delimiterCount}); // If this is the last delimiter, we need to check the right side of the `|` delimiter if (nextDelimiter is null) @@ -556,13 +559,13 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor break; } - if (!ParseHeaderString(nextSibling, out align)) + if (!ParseHeaderString(nextSibling, out align, out delimiterCount)) { break; } - + totalDelimiterCount += delimiterCount; isValidRow = true; - aligns.Add(new TableColumnDefinition() { Alignment = align }); + columnDefinitions.Add(new TableColumnDefinition() { Alignment = align, Width = delimiterCount}); break; } @@ -576,7 +579,27 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor break; } - return isValidRow ? aligns : null; + // calculate the width of the columns in percent based on the delimiter count + if (!isValidRow || columnDefinitions == null) + { + return null; + } + + if (Options.InferColumnWidthsFromSeparator) + { + foreach (var columnDefinition in columnDefinitions) + { + columnDefinition.Width = (columnDefinition.Width * 100) / totalDelimiterCount; + } + } + else + { + foreach (var columnDefinition in columnDefinitions) + { + columnDefinition.Width = 0; + } + } + return columnDefinitions; } private static bool IsLine(Inline inline) diff --git a/src/Markdig/Extensions/Tables/TableHelper.cs b/src/Markdig/Extensions/Tables/TableHelper.cs index bbbda511..f75090a5 100644 --- a/src/Markdig/Extensions/Tables/TableHelper.cs +++ b/src/Markdig/Extensions/Tables/TableHelper.cs @@ -17,12 +17,13 @@ public static class TableHelper /// The text slice. /// The delimiter character (either `-` or `=`). /// The alignment of the column. + /// The number of delimiters. /// /// true if parsing was successful /// - public static bool ParseColumnHeader(ref StringSlice slice, char delimiterChar, out TableColumnAlign? align) + public static bool ParseColumnHeader(ref StringSlice slice, char delimiterChar, out TableColumnAlign? align, out int delimiterCount) { - return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align); + return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align, out delimiterCount); } /// @@ -37,7 +38,7 @@ public static class TableHelper public static bool ParseColumnHeaderAuto(ref StringSlice slice, out char delimiterChar, out TableColumnAlign? align) { delimiterChar = '\0'; - return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align); + return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align, out _); } /// @@ -49,10 +50,10 @@ public static class TableHelper /// /// true if parsing was successful /// - public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign? align) + public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign? align, out int delimiterCount) { align = null; - + delimiterCount = 0; slice.TrimStart(); var c = slice.CurrentChar; bool hasLeft = false; @@ -80,7 +81,8 @@ public static class TableHelper } // We expect at least one `-` delimiter char - if (slice.CountAndSkipChar(delimiterChar) == 0) + delimiterCount = slice.CountAndSkipChar(delimiterChar); + if (delimiterCount == 0) { return false; }