mirror of
https://github.com/xoofx/markdig.git
synced 2026-02-04 05:44:50 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user