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.
This commit is contained in:
Manuel Amstutz
2025-04-09 20:55:54 +02:00
parent 8b84542527
commit 55f770cc07
5 changed files with 88 additions and 19 deletions

View File

@@ -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<Table>().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<Table>().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<Table>().FirstOrDefault();
Assert.IsNotNull(table);
foreach (var column in table.ColumnDefinitions)
{
Assert.AreEqual(0, column.Width);
}
}
}

View File

@@ -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;
}

View File

@@ -33,4 +33,11 @@ public class PipeTableOptions
/// in all other rows (default behavior).
/// </summary>
public bool UseHeaderForColumnCount { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool InferColumnWidthsFromSeparator { get; set; }
}

View File

@@ -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<TableColumnDefinition>? FindHeaderRow(List<Inline> delimiters)
{
bool isValidRow = false;
List<TableColumnDefinition>? aligns = null;
int totalDelimiterCount = 0;
List<TableColumnDefinition>? 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<TableColumnDefinition>();
aligns.Add(new TableColumnDefinition() { Alignment = align });
columnDefinitions ??= new List<TableColumnDefinition>();
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)

View File

@@ -17,12 +17,13 @@ public static class TableHelper
/// <param name="slice">The text slice.</param>
/// <param name="delimiterChar">The delimiter character (either `-` or `=`).</param>
/// <param name="align">The alignment of the column.</param>
/// <param name="delimiterCount">The number of delimiters.</param>
/// <returns>
/// <c>true</c> if parsing was successful
/// </returns>
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);
}
/// <summary>
@@ -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 _);
}
/// <summary>
@@ -49,10 +50,10 @@ public static class TableHelper
/// <returns>
/// <c>true</c> if parsing was successful
/// </returns>
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;
}