Skip to content

Commit 7959e3b

Browse files
committed
Reject pipe-only rows as pipe table separators (Fixes #927)
1 parent 391e03b commit 7959e3b

3 files changed

Lines changed: 91 additions & 21 deletions

File tree

src/Markdig.Tests/Specs/PipeTableSpecs.generated.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,5 +1010,25 @@ public void ExtensionsPipeTable_Example027()
10101010

10111011
TestParser.TestSpec("Bullet list\n* Table 1\n\n | Header 1 | Header 2 |\n |----------------|----------------|\n | Row 1 Column 1 | Row 1 Column 2 |\n\n* Table 2\n | Header 1 | Header 2 |\n |----------------|----------------|\n | Row 1 Column 1 | Row 1 Column 2 |\n\n* Table 3\n Lorem ipsum ...\n Lorem ipsum ...\n | Header 1 | Header 2 |\n |----------------|----------------|\n | Row 1 Column 1 | Row 1 Column 2 |\n\n\nOrdered list\n1. Table 1\n\n | Header 1 | Header 2 |\n |----------------|----------------|\n | Row 1 Column 1 | Row 1 Column 2 |\n\n2. Table 2\n | Header 1 | Header 2 |\n |----------------|----------------|\n | Row 1 Column 1 | Row 1 Column 2 |\n\n3. Table 3\n Lorem ipsum ...\n Lorem ipsum ...\n | Header 1 | Header 2 |\n |----------------|----------------|\n | Row 1 Column 1 | Row 1 Column 2 |", "<p>Bullet list</p>\n<ul>\n<li><p>Table 1</p>\n<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Row 1 Column 1</td>\n<td>Row 1 Column 2</td>\n</tr>\n</tbody>\n</table></li>\n<li><p>Table 2</p>\n<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Row 1 Column 1</td>\n<td>Row 1 Column 2</td>\n</tr>\n</tbody>\n</table></li>\n<li><p>Table 3\nLorem ipsum ...\nLorem ipsum ...</p>\n<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Row 1 Column 1</td>\n<td>Row 1 Column 2</td>\n</tr>\n</tbody>\n</table></li>\n</ul>\n<p>Ordered list</p>\n<ol>\n<li><p>Table 1</p>\n<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Row 1 Column 1</td>\n<td>Row 1 Column 2</td>\n</tr>\n</tbody>\n</table></li>\n<li><p>Table 2</p>\n<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Row 1 Column 1</td>\n<td>Row 1 Column 2</td>\n</tr>\n</tbody>\n</table></li>\n<li><p>Table 3\nLorem ipsum ...\nLorem ipsum ...</p>\n<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Row 1 Column 1</td>\n<td>Row 1 Column 2</td>\n</tr>\n</tbody>\n</table></li>\n</ol>", "pipetables|advanced", context: "Example 27\nSection Extensions / Pipe Table\n");
10121012
}
1013+
1014+
// A sequence of rows containing only pipes is not a table separator row and must remain a paragraph:
1015+
[Test]
1016+
public void ExtensionsPipeTable_Example028()
1017+
{
1018+
// Example 28
1019+
// Section: Extensions / Pipe Table
1020+
//
1021+
// The following Markdown:
1022+
// |||
1023+
// |||
1024+
// |||
1025+
//
1026+
// Should be rendered as:
1027+
// <p>|||
1028+
// |||
1029+
// |||</p>
1030+
1031+
TestParser.TestSpec("|||\n|||\n|||", "<p>|||\n|||\n|||</p>", "pipetables|advanced", context: "Example 28\nSection Extensions / Pipe Table\n");
1032+
}
10131033
}
10141034
}

src/Markdig.Tests/Specs/PipeTableSpecs.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -781,4 +781,16 @@ Lorem ipsum ...</p>
781781
</tbody>
782782
</table></li>
783783
</ol>
784-
````````````````````````````````
784+
````````````````````````````````
785+
786+
A sequence of rows containing only pipes is not a table separator row and must remain a paragraph:
787+
788+
```````````````````````````````` example
789+
|||
790+
|||
791+
|||
792+
.
793+
<p>|||
794+
|||
795+
|||</p>
796+
````````````````````````````````

src/Markdig/Extensions/Tables/PipeTableParser.cs

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -545,23 +545,40 @@ private static bool ParseHeaderString(Inline? inline, out TableColumnAlign? alig
545545
continue;
546546
}
547547

548+
if (IsLine(delimiter))
549+
{
550+
var previousSignificantSibling = SkipTrailingWhitespace(delimiter.PreviousSibling);
551+
552+
// Trailing pipe at end-of-line (`| --- |`) is valid and does not add a new column.
553+
if (previousSignificantSibling is PipeTableDelimiterInline)
554+
{
555+
isValidRow = columnDefinitions is { Count: > 0 };
556+
break;
557+
}
558+
559+
if (!TryParseHeaderColumn(previousSignificantSibling, out var align, out var delimiterCount))
560+
{
561+
break;
562+
}
563+
564+
columnDefinitions ??= [];
565+
totalDelimiterCount += delimiterCount;
566+
columnDefinitions.Add(new TableColumnDefinition() { Alignment = align, Width = delimiterCount });
567+
isValidRow = true;
568+
break;
569+
}
570+
548571
// Parse the content before this delimiter as a column definition (e.g., `:---`, `---:`, `:---:`)
549-
// Skip if previous sibling is a pipe (empty cell) or whitespace
550-
TableColumnAlign? align = null;
551-
int delimiterCount = 0;
552-
if (delimiter.PreviousSibling != null &&
553-
!(delimiter.PreviousSibling is PipeTableDelimiterInline) &&
554-
!(delimiter.PreviousSibling is LiteralInline li && li.Content.IsEmptyOrWhitespace()) &&
555-
!ParseHeaderString(delimiter.PreviousSibling, out align, out delimiterCount))
572+
if (!TryParseHeaderColumn(delimiter.PreviousSibling, out var pipeAlign, out var pipeDelimiterCount))
556573
{
557574
break;
558575
}
559576

560577
// Create aligns until we may have a header row
561578

562579
columnDefinitions ??= new List<TableColumnDefinition>();
563-
totalDelimiterCount += delimiterCount;
564-
columnDefinitions.Add(new TableColumnDefinition() { Alignment = align, Width = delimiterCount});
580+
totalDelimiterCount += pipeDelimiterCount;
581+
columnDefinitions.Add(new TableColumnDefinition() { Alignment = pipeAlign, Width = pipeDelimiterCount});
565582

566583
// If this is the last pipe, check for a trailing column definition (row without trailing pipe)
567584
// e.g., `| :--- | ---:` has content after the last pipe
@@ -576,28 +593,21 @@ private static bool ParseHeaderString(Inline? inline, out TableColumnAlign? alig
576593
break;
577594
}
578595

579-
if (!ParseHeaderString(nextSibling, out align, out delimiterCount))
596+
if (!TryParseHeaderColumn(nextSibling, out var trailingAlign, out var trailingDelimiterCount))
580597
{
581598
break;
582599
}
583-
totalDelimiterCount += delimiterCount;
584-
isValidRow = true;
585-
columnDefinitions.Add(new TableColumnDefinition() { Alignment = align, Width = delimiterCount});
586-
break;
587-
}
588-
589-
// If we are on a Line delimiter, exit
590-
if (IsLine(delimiter))
591-
{
600+
totalDelimiterCount += trailingDelimiterCount;
592601
isValidRow = true;
602+
columnDefinitions.Add(new TableColumnDefinition() { Alignment = trailingAlign, Width = trailingDelimiterCount });
593603
break;
594604
}
595605
}
596606
break;
597607
}
598608

599609
// calculate the width of the columns in percent based on the delimiter count
600-
if (!isValidRow || columnDefinitions == null)
610+
if (!isValidRow || columnDefinitions == null || totalDelimiterCount == 0)
601611
{
602612
return null;
603613
}
@@ -619,6 +629,34 @@ private static bool ParseHeaderString(Inline? inline, out TableColumnAlign? alig
619629
return columnDefinitions;
620630
}
621631

632+
private static bool TryParseHeaderColumn(Inline? inline, out TableColumnAlign? align, out int delimiterCount)
633+
{
634+
align = null;
635+
delimiterCount = 0;
636+
637+
if (inline is null || inline is PipeTableDelimiterInline)
638+
{
639+
return false;
640+
}
641+
642+
if (inline is LiteralInline literal && literal.Content.IsEmptyOrWhitespace())
643+
{
644+
return false;
645+
}
646+
647+
return ParseHeaderString(inline, out align, out delimiterCount);
648+
}
649+
650+
private static Inline? SkipTrailingWhitespace(Inline? inline)
651+
{
652+
while (inline is LiteralInline literal && literal.Content.IsEmptyOrWhitespace())
653+
{
654+
inline = inline.PreviousSibling;
655+
}
656+
657+
return inline;
658+
}
659+
622660
private static bool IsLine(Inline inline)
623661
{
624662
return inline is LineBreakInline;

0 commit comments

Comments
 (0)