diff --git a/src/Markdig.Tests/MiscTests.cs b/src/Markdig.Tests/MiscTests.cs
new file mode 100644
index 0000000..ca5ad4c
--- /dev/null
+++ b/src/Markdig.Tests/MiscTests.cs
@@ -0,0 +1,560 @@
+using System.Text.RegularExpressions;
+
+using Markdig.Extensions.AutoLinks;
+using Markdig.Extensions.Tables;
+using Markdig.Syntax;
+using NUnit.Framework;
+
+namespace Markdig.Tests;
+
+public class MiscTests
+{
+ [Test]
+ public void LinkWithInvalidNonAsciiDomainNameIsIgnored()
+ {
+ // Url from https://github.com/lunet-io/markdig/issues/438
+ _ = Markdown.ToHtml("[minulém díle](http://V%20minulém%20díle%20jsme%20nainstalovali%20SQL%20Server,%20který%20je%20nutný%20pro%20běh%20Configuration%20Manageru.%20Dnes%20nás%20čeká%20instalace%20WSUS,%20což%20je%20produkt,%20jež%20je%20možné%20používat%20i%20jako%20samostatnou%20funkci%20ve%20Windows%20Serveru,%20který%20se%20stará%20o%20stažení%20a%20instalaci%20aktualizací%20z%20Microsoft%20Update%20na%20klientské%20počítače.%20Stejně%20jako%20v%20předchozích%20dílech,%20tak%20i%20v%20tomto%20si%20ukážeme%20obě%20varianty%20instalace%20–%20a%20to%20jak%20instalaci%20z%20PowerShellu,%20tak%20instalaci%20pomocí%20GUI.) ");
+
+ // Valid IDN
+ TestParser.TestSpec("[foo](http://ünicode.com)", "
foo
");
+ TestParser.TestSpec("[foo](http://ünicode.ünicode.com)", "foo
");
+
+ // Invalid IDN
+ TestParser.TestSpec("[foo](http://ünicode..com)", "foo
");
+ }
+
+ [TestCase("link [foo [bar]]")] // https://spec.commonmark.org/0.29/#example-508
+ [TestCase("link [foo][bar]")]
+ [TestCase("link [][foo][bar][]")]
+ [TestCase("link [][foo][bar][[]]")]
+ [TestCase("link [foo] [bar]")]
+ [TestCase("link [[foo] [] [bar] [[abc]def]]")]
+ [TestCase("[]")]
+ [TestCase("[ ]")]
+ [TestCase("[bar][]")]
+ [TestCase("[bar][ foo]")]
+ [TestCase("[bar][foo ][]")]
+ [TestCase("[bar][fo[ ]o ][][]")]
+ [TestCase("[a]b[c[d[e]f]g]h")]
+ [TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o")]
+ [TestCase("a[b[c[d]e]f[g]h]i[] [][foo][bar][] foo [j]k[l[m]n]o")]
+ [TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o[][]")]
+ public void LinkTextMayContainBalancedBrackets(string linkText)
+ {
+ string markdown = $"[{linkText}](/uri)";
+ string expected = $@"{linkText}
";
+
+ TestParser.TestSpec(markdown, expected);
+
+ // Make the link text unbalanced
+ foreach (var bracketIndex in linkText
+ .Select((c, i) => new Tuple(c, i))
+ .Where(t => t.Item1 == '[' || t.Item1 == ']')
+ .Select(t => t.Item2))
+ {
+ string brokenLinkText = linkText.Remove(bracketIndex, 1);
+
+ markdown = $"[{brokenLinkText}](/uri)";
+ expected = $@"{brokenLinkText}
";
+
+ string actual = Markdown.ToHtml(markdown);
+ Assert.AreNotEqual(expected, actual);
+ }
+ }
+
+ [Theory]
+ [TestCase('[', 9 * 1024, true, false)]
+ [TestCase('[', 11 * 1024, true, true)]
+ [TestCase('[', 100, false, false)]
+ [TestCase('[', 150, false, true)]
+ [TestCase('>', 100, true, false)]
+ [TestCase('>', 150, true, true)]
+ public void GuardsAgainstHighlyNestedNodes(char c, int count, bool parseOnly, bool shouldThrow)
+ {
+ var markdown = new string(c, count);
+ Action test = parseOnly ? () => Markdown.Parse(markdown) : () => Markdown.ToHtml(markdown);
+
+ if (shouldThrow)
+ {
+ Exception e = Assert.Throws(test);
+ Assert.True(e.Message.Contains("depth limit"));
+ }
+ else
+ {
+ test();
+ }
+ }
+
+ [Test]
+ public void MaximumNestingDepthCanBeRaisedForDeepListExtras()
+ {
+ var markdown = "Krankenhaus\nD. " + string.Join(" ", Enumerable.Repeat("M.", 160));
+ var pipeline = new MarkdownPipelineBuilder
+ {
+ MaximumNestingDepth = 512,
+ }.UseListExtras().Build();
+
+ Assert.DoesNotThrow(() => Markdown.ToHtml(markdown, pipeline));
+ }
+
+ [Test]
+ public void MaximumNestingDepthCanBeLowered()
+ {
+ var pipeline = new MarkdownPipelineBuilder
+ {
+ MaximumNestingDepth = 16,
+ }.Build();
+
+ Exception e = Assert.Throws(() => Markdown.ToHtml(new string('>', 20), pipeline));
+ Assert.True(e.Message.Contains("depth limit"));
+ }
+
+ [Test]
+ public void IsIssue356Corrected()
+ {
+ string input = @"https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97";
+ string expected = @"https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97
";
+
+ TestParser.TestSpec($"<{input}>", expected);
+ TestParser.TestSpec(input, expected, "autolinks|advanced");
+ }
+
+ [Test]
+ public void IsIssue365Corrected()
+ {
+ // The scheme must be escaped too...
+ string input = "\"://)";
+ string expected = "
";
+
+ TestParser.TestSpec(input, expected);
+ }
+
+ [Test]
+ public void TestAltTextIsCorrectlyEscaped()
+ {
+ TestParser.TestSpec(
+ @"",
+ @"
");
+ }
+
+ [Test]
+ public void TestFixHang()
+ {
+ var input = File.ReadAllText(Path.Combine(TestParser.TestsDirectory, "hang.md"));
+ _ = Markdown.ToHtml(input);
+ }
+
+ [Test]
+ public void TestInvalidHtmlEntity()
+ {
+ var input = "9&ddr;&*&ddr;&de��__";
+ TestParser.TestSpec(input, "9&ddr;&*&ddr;&de��__
");
+ }
+
+ [Test]
+ public void TestInvalidCharacterHandling()
+ {
+ var input = File.ReadAllText(Path.Combine(TestParser.TestsDirectory, "ArgumentOutOfRangeException.md"));
+ _ = Markdown.ToHtml(input);
+ }
+
+ [Test]
+ public void TestInvalidCodeEscape()
+ {
+ var input = "```**Header** ";
+ _ = Markdown.ToHtml(input);
+ }
+
+ [Test]
+ public void TestEmphasisAndHtmlEntity()
+ {
+ var markdownText = "*Unlimited-Fun®*®";
+ TestParser.TestSpec(markdownText, "Unlimited-Fun® ®
");
+ }
+
+ [Test]
+ public void TestThematicInsideCodeBlockInsideList()
+ {
+ var input = @"1. In the :
+
+ ```
+ Id DisplayName Description
+ -- ----------- -----------
+ 62375ab9-6b52-47ed-826b-58e47e0e304b Group.Unified ...
+ ```";
+ TestParser.TestSpec(input, @"
+In the :
+Id DisplayName Description
+-- ----------- -----------
+62375ab9-6b52-47ed-826b-58e47e0e304b Group.Unified ...
+
+ ");
+ }
+
+ [Test]
+ public void VisualizeMathExpressions()
+ {
+ string math = @"Math expressions
+
+$\frac{n!}{k!(n-k)!} = \binom{n}{k}$
+
+$$\frac{n!}{k!(n-k)!} = \binom{n}{k}$$
+
+$$
+\frac{n!}{k!(n-k)!} = \binom{n}{k}
+$$
+
+
+\begin{align}
+\sqrt{37} & = \sqrt{\frac{73^2-1}{12^2}} \\
+ & = \sqrt{\frac{73^2}{12^2}\cdot\frac{73^2-1}{73^2}} \\
+ & = \sqrt{\frac{73^2}{12^2}}\sqrt{\frac{73^2-1}{73^2}} \\
+ & = \frac{73}{12}\sqrt{1 - \frac{1}{73^2}} \\
+ & \approx \frac{73}{12}\left(1 - \frac{1}{2\cdot73^2}\right)
+\end{align}
+
+";
+ //Console.WriteLine("Math Expressions:\n");
+ var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()
+ var html = Markdown.ToHtml(math, pl);
+ //Console.WriteLine(html);
+ }
+
+ [Test]
+ public void InlineMathExpression()
+ {
+ string math = @"Math expressions
+
+$\frac{n!}{k!(n-k)!} = \binom{n}{k}$
+";
+ var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()
+
+ var html = Markdown.ToHtml(math, pl);
+
+ var test1 = html.Contains("\\(");
+ var test2 = html.Contains("\\)
");
+ if (!test1 || !test2)
+ {
+ Console.WriteLine(html);
+ }
+
+ Assert.IsTrue(test1, "Leading bracket missing");
+ Assert.IsTrue(test2, "Trailing bracket missing");
+ }
+
+ [Test]
+ public void BlockMathExpression()
+ {
+ string math = @"Math expressions
+
+$$
+\frac{n!}{k!(n-k)!} = \binom{n}{k}
+$$
+";
+ var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()
+
+ var html = Markdown.ToHtml(math, pl);
+ var test1 = html.Contains("\n\\[");
+ var test2 = html.Contains("\\]
");
+ if (!test1 || !test2)
+ {
+ Console.WriteLine(html);
+ }
+
+ Assert.IsTrue(test1, "Leading bracket missing");
+ Assert.IsTrue(test2, "Trailing bracket missing");
+ }
+
+ [Test]
+ public void CanDisableParsingHeadings()
+ {
+ var noHeadingsPipeline = new MarkdownPipelineBuilder().DisableHeadings().Build();
+
+ TestParser.TestSpec("Foo\n===", "Foo ");
+ TestParser.TestSpec("Foo\n===", "Foo\n===
", noHeadingsPipeline);
+
+ TestParser.TestSpec("# Heading 1", "Heading 1 ");
+ TestParser.TestSpec("# Heading 1", "# Heading 1
", noHeadingsPipeline);
+
+ // Does not also disable link reference definitions
+ TestParser.TestSpec("[Foo]\n\n[Foo]: bar", "Foo
");
+ TestParser.TestSpec("[Foo]\n\n[Foo]: bar", "Foo
", noHeadingsPipeline);
+ }
+
+ [Test]
+ public void CanOpenAutoLinksInNewWindow()
+ {
+ var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().Build();
+ var newWindowPipeline = new MarkdownPipelineBuilder().UseAutoLinks(new AutoLinkOptions() { OpenInNewWindow = true }).Build();
+
+ TestParser.TestSpec("www.foo.bar", "www.foo.bar
", pipeline);
+ TestParser.TestSpec("www.foo.bar", "www.foo.bar
", newWindowPipeline);
+ }
+
+ [Test]
+ public void CanUseHttpsPrefixForWWWAutoLinks()
+ {
+ var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().Build();
+ var httpsPipeline = new MarkdownPipelineBuilder().UseAutoLinks(new AutoLinkOptions() { UseHttpsForWWWLinks = true }).Build();
+
+ TestParser.TestSpec("www.foo.bar", "www.foo.bar
", pipeline);
+ TestParser.TestSpec("www.foo.bar", "www.foo.bar
", httpsPipeline);
+ }
+
+ [Test]
+ public void RootInlineHasCorrectSourceSpan()
+ {
+ var pipeline = new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build();
+ pipeline.TrackTrivia = true;
+
+ var document = Markdown.Parse("0123456789\n", pipeline);
+
+ var expectedSourceSpan = new SourceSpan(0, 10);
+ Assert.That(((LeafBlock)document.LastChild).Inline.Span == expectedSourceSpan);
+ }
+
+ [Test]
+ public void RootInlineInTableCellHasCorrectSourceSpan()
+ {
+ var pipeline = new MarkdownPipelineBuilder().UsePreciseSourceLocation().UseAdvancedExtensions().Build();
+ pipeline.TrackTrivia = true;
+
+ var document = Markdown.Parse("| a | b |\n| --- | --- |\n| *dest* | \\[in\\] The address of the result of the operation. |", pipeline);
+
+ var paragraph = (ParagraphBlock)((TableCell)((TableRow)((Table)document.LastChild).LastChild).First()).LastChild;
+ Assert.That(paragraph.Inline.Span.Start == paragraph.Inline.FirstChild.Span.Start);
+ Assert.That(paragraph.Inline.Span.End == paragraph.Inline.LastChild.Span.End);
+ }
+
+ [Test]
+ public void TestGridTableShortLine()
+ {
+ var input = @"
++--+
+| |
++-";
+
+ var expected = @"
+";
+ TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseGridTables().Build());
+ }
+
+ [Test]
+ public void TestDefinitionListInListItemWithBlankLine()
+ {
+ var input = @"
+-
+
+ term
+ : definition
+";
+
+ var expected = @"
+
+
+term
+definition
+
+
+
+";
+ TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseDefinitionLists().Build());
+ }
+
+ [Test]
+ public void TestAlertWithinAlertOrNestedBlock()
+ {
+ var input = @"
+>[!NOTE]
+[!NOTE]
+The second one is not a note.
+
+>>[!NOTE]
+Also not a note.
+";
+
+ var expected = @"
+
Note
+
[!NOTE]
+The second one is not a note.
+
+
+
+[!NOTE]
+Also not a note.
+
+
+";
+ TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseAlertBlocks().Build());
+ }
+
+ [Test]
+ public void TestNestedAlertInsideBlockquote()
+ {
+ // >>[!NOTE] should become a blockquote wrapping an alert when AllowNestedAlerts is enabled
+ var input = @">>[!NOTE]
+Also a note.
+";
+
+ var expected = @"
+
+
+";
+ TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseAlertBlocks(allowNestedAlerts: true).Build());
+ }
+
+ [Test]
+ public void TestNestedAlertInsideAlert()
+ {
+ // An alert inside another alert is never recognized, even with AllowNestedAlerts
+ var input = @">[!NOTE]
+> >[!TIP]
+> > A tip inside a note
+";
+
+ var expected = @"
+
Note
+
+
+[!TIP]
+A tip inside a note
+
+
+";
+ TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseAlertBlocks(allowNestedAlerts: true).Build());
+ }
+
+ [Test]
+ public void TestAlertInsideListItem()
+ {
+ // Alerts inside list items require AllowNestedAlerts
+ var input = @"- > [!NOTE]
+ > A note inside a list item
+";
+
+ var expected = @"
+
+
+
Note
+
A note inside a list item
+
+
+
+";
+ TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseAlertBlocks(allowNestedAlerts: true).Build());
+ }
+
+ [Test]
+ public void TestAlertInsideNestedListItem()
+ {
+ // Alert inside a nested list item (list item indented under another list item) requires AllowNestedAlerts
+ var input = @"- list item 1
+- list item 2
+ - > [!NOTE]
+ > A note inside a nested list item
+";
+
+ var expected = @"
+list item 1
+list item 2
+
+
+
+
Note
+
A note inside a nested list item
+
+
+
+
+
+";
+ TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseAlertBlocks(allowNestedAlerts: true).Build());
+ }
+
+ [Test]
+ public void TestIssue887ListAfterNestedBlockQuote()
+ {
+ var input = @"> >* _Unordered 1_ QL2 level 1 no space sep
+> > Continued 1
+> > * Unordered 2 level 1
+> > 1. **Ordered 1** QL2 level 1
+> > Continued 1
+> > > Not continued QL3
+> > 2. Ordered 2 QL2 level 1
+> > Continued 2a
+> >
+> > Continued 2b
+> >
+> > Continued 2c
+> >
+> > 3. Ordered 3 QL2 level 2
+> > Continued 3a
+> >
+> > Continued 3b
+";
+
+ var expected = @"
+
+
+Unordered 1 QL2 level 1 no space sep
+Continued 1
+Unordered 2 level 1
+
+
+Ordered 1 QL2 level 1
+Continued 1
+
+
+Not continued QL3
+
+
+Ordered 2 QL2 level 1
+Continued 2a
+Continued 2b
+Continued 2c
+
+Ordered 3 QL2 level 2
+Continued 3a
+Continued 3b
+
+
+
+
+
+ ";
+
+ TestParser.TestSpec(input, expected);
+ }
+
+ [Test]
+ public void TestIssue845ListItemBlankLine()
+ {
+ TestParser.TestSpec("-\n\n foo",@"
+
+foo
");
+ TestParser.TestSpec("-\n-\n\n foo",@"
+
+foo
");
+ TestParser.TestSpec("-\n\n-\n\n foo",@"
+
+foo
");
+ }
+}
diff --git a/src/Markdig/Extensions/Tables/GridTableParser.cs b/src/Markdig/Extensions/Tables/GridTableParser.cs
new file mode 100644
index 0000000..35de6f1
--- /dev/null
+++ b/src/Markdig/Extensions/Tables/GridTableParser.cs
@@ -0,0 +1,396 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using Markdig.Helpers;
+using Markdig.Parsers;
+using Markdig.Syntax;
+using System.Linq;
+
+namespace Markdig.Extensions.Tables;
+
+///
+/// Represents the GridTableParser type.
+///
+public class GridTableParser : BlockParser
+{
+ ///
+ /// Initializes a new instance of the GridTableParser class.
+ ///
+ public GridTableParser()
+ {
+ OpeningCharacters = ['+'];
+ }
+
+ ///
+ /// Attempts to open a block at the current parser position.
+ ///
+ public override BlockState TryOpen(BlockProcessor processor)
+ {
+ // A grid table cannot start more than an indent
+ if (processor.IsCodeIndent)
+ {
+ return BlockState.None;
+ }
+
+ var line = processor.Line;
+ GridTableState? tableState = null;
+
+ // Match the first row that should be of the minimal form: +---------------
+ var c = line.CurrentChar;
+ var lineStart = line.Start;
+ while (c == '+')
+ {
+ var columnStart = line.Start;
+ line.SkipChar();
+ line.TrimStart();
+
+ // if we have reached the end of the line, exit
+ c = line.CurrentChar;
+ if (c == 0)
+ {
+ break;
+ }
+
+ // Parse a column alignment
+ if (!TableHelper.ParseColumnHeader(ref line, '-', out TableColumnAlign? columnAlign, out _))
+ {
+ return BlockState.None;
+ }
+
+ tableState ??= new GridTableState(start: processor.Start, expectRow: true);
+ tableState.AddColumn(columnStart - lineStart, line.Start - lineStart, columnAlign);
+
+ c = line.CurrentChar;
+ }
+
+ if (c != 0 || tableState is null)
+ {
+ return BlockState.None;
+ }
+ // Store the line (if we need later to build a ParagraphBlock because the GridTable was in fact invalid)
+ tableState.AddLine(ref processor.Line);
+ var table = new Table(this)
+ {
+ Line = processor.LineIndex,
+ Column = processor.Column,
+ Span = { Start = lineStart }
+ };
+ table.SetData(typeof(GridTableState), tableState);
+
+ // Calculate the total width of all columns
+ int totalWidth = 0;
+ foreach (var columnSlice in tableState.ColumnSlices!)
+ {
+ totalWidth += columnSlice.End - columnSlice.Start - 1;
+ }
+
+ // Store the column width and alignment
+ foreach (var columnSlice in tableState.ColumnSlices)
+ {
+ var columnDefinition = new TableColumnDefinition
+ {
+ // Column width proportional to the total width
+ Width = (float)(columnSlice.End - columnSlice.Start - 1) * 100.0f / totalWidth,
+ Alignment = columnSlice.Align
+ };
+ table.ColumnDefinitions.Add(columnDefinition);
+ }
+
+ processor.NewBlocks.Push(table);
+
+ return BlockState.ContinueDiscard;
+ }
+
+ ///
+ /// Attempts to continue parsing the specified block.
+ ///
+ public override BlockState TryContinue(BlockProcessor processor, Block block)
+ {
+ var gridTable = (Table)block;
+ var tableState = (GridTableState)block.GetData(typeof(GridTableState))!;
+ tableState.AddLine(ref processor.Line);
+ if (processor.CurrentChar == '+')
+ {
+ gridTable.UpdateSpanEnd(processor.Line.End);
+ return HandleNewRow(processor, tableState, gridTable);
+ }
+ if (processor.CurrentChar == '|')
+ {
+ gridTable.UpdateSpanEnd(processor.Line.End);
+ return HandleContents(processor, tableState, gridTable);
+ }
+ TerminateCurrentRow(processor, tableState, gridTable, true);
+ // If the table is not valid we need to remove the grid table,
+ // and create a ParagraphBlock with the slices
+ if (!gridTable.IsValid())
+ {
+ Undo(processor, tableState, gridTable);
+ }
+ return BlockState.Break;
+ }
+
+ private BlockState HandleNewRow(BlockProcessor processor, GridTableState tableState, Table gridTable)
+ {
+ var columns = tableState.ColumnSlices!;
+ SetRowSpanState(columns, processor.Line, out bool isHeaderRow, out bool hasRowSpan);
+ SetColumnSpanState(columns, processor.Line);
+ TerminateCurrentRow(processor, tableState, gridTable, false);
+ if (isHeaderRow)
+ {
+ for (int i = 0; i < gridTable.Count; i++)
+ {
+ var row = (TableRow)gridTable[i];
+ row.IsHeader = true;
+ }
+ }
+ tableState.StartRowGroup = gridTable.Count;
+ if (hasRowSpan)
+ {
+ HandleContents(processor, tableState, gridTable);
+ }
+ return BlockState.ContinueDiscard;
+ }
+
+ private static void SetRowSpanState(List columns, StringSlice line, out bool isHeaderRow, out bool hasRowSpan)
+ {
+ var lineStart = line.Start;
+ var lineEnd = line.End;
+ isHeaderRow = line.PeekChar() == '=' || line.PeekChar(2) == '=';
+ hasRowSpan = false;
+ foreach (var columnSlice in columns)
+ {
+ if (columnSlice.CurrentCell != null)
+ {
+ line.Start = lineStart + columnSlice.Start + 1;
+ line.End = Math.Min(lineStart + columnSlice.End - 1, lineEnd);
+ line.Trim();
+ if (line.IsEmptyOrWhitespace() || !IsRowSeparator(line))
+ {
+ hasRowSpan = true;
+ columnSlice.CurrentCell.RowSpan++;
+ columnSlice.CurrentCell.AllowClose = false;
+ }
+ else
+ {
+ columnSlice.CurrentCell.AllowClose = true;
+ }
+ }
+ }
+ }
+
+ private static bool IsRowSeparator(StringSlice slice)
+ {
+ char c = slice.CurrentChar;
+ do
+ {
+ if (c != '-' && c != '=' && c != ':')
+ {
+ return c == '\0';
+ }
+ c = slice.NextChar();
+ }
+ while (true);
+ }
+
+ private static void TerminateCurrentRow(BlockProcessor processor, GridTableState tableState, Table gridTable, bool isLastRow)
+ {
+ var columns = tableState.ColumnSlices;
+ TableRow? currentRow = null;
+ for (int i = 0; i < columns!.Count; i++)
+ {
+ var columnSlice = columns[i];
+ if (columnSlice.CurrentCell != null)
+ {
+ if (currentRow == null)
+ {
+ TableCell firstCell = columns.First(c => c.CurrentCell != null).CurrentCell!;
+ TableCell lastCell = columns.Last(c => c.CurrentCell != null).CurrentCell!;
+
+ currentRow ??= new TableRow()
+ {
+ Span = new SourceSpan(firstCell.Span.Start, lastCell.Span.End),
+ Line = firstCell.Line
+ };
+ }
+
+ // If this cell does not already belong to a row
+ if (columnSlice.CurrentCell.Parent is null)
+ {
+ currentRow.Add(columnSlice.CurrentCell);
+ }
+ // If the cell is not going to span through to the next row
+ if (columnSlice.CurrentCell.AllowClose)
+ {
+ columnSlice.BlockProcessor!.Close(columnSlice.CurrentCell);
+ }
+ }
+
+ // Renew the block parser processor (or reset it for the last row)
+ if (columnSlice.BlockProcessor != null && (columnSlice.CurrentCell is null || columnSlice.CurrentCell.AllowClose))
+ {
+ columnSlice.BlockProcessor.ReleaseChild();
+ columnSlice.BlockProcessor = isLastRow ? null : processor.CreateChild();
+ }
+
+ // Create or erase the cell
+ if (isLastRow || columnSlice.CurrentColumnSpan == 0 || (columnSlice.CurrentCell != null && columnSlice.CurrentCell.AllowClose))
+ {
+ // We don't need the cell anymore if we have a last row
+ // Or the cell has a columnspan == 0
+ // And the cell does not have to be kept open to span rows
+ columnSlice.CurrentCell = null;
+ }
+ }
+
+ if (currentRow is { Count: > 0 })
+ {
+ gridTable.Add(currentRow);
+ }
+ }
+
+ private BlockState HandleContents(BlockProcessor processor, GridTableState tableState, Table gridTable)
+ {
+ var isRowLine = processor.CurrentChar == '+';
+ var columns = tableState.ColumnSlices!;
+ var line = processor.Line;
+ SetColumnSpanState(columns, line);
+ if (!isRowLine && !CanContinueRow(columns))
+ {
+ TerminateCurrentRow(processor, tableState, gridTable, false);
+ }
+ for (int i = 0; i < columns.Count;)
+ {
+ var columnSlice = columns[i];
+ var nextColumnIndex = i + columnSlice.CurrentColumnSpan;
+ // If the span is 0, we exit
+ if (nextColumnIndex == i)
+ {
+ break;
+ }
+ var nextColumn = nextColumnIndex < columns.Count ? columns[nextColumnIndex] : null;
+
+ var sliceForCell = line;
+ sliceForCell.Start = line.Start + columnSlice.Start + 1;
+ if (nextColumn != null)
+ {
+ sliceForCell.End = line.Start + nextColumn.Start - 1;
+ }
+ else
+ {
+ var columnEnd = columns[columns.Count - 1].End;
+ var columnEndChar = line.PeekCharExtra(columnEnd);
+ // If there is a `|` (or a `+` in the case that we are dealing with a row line
+ // with spanned contents) exactly at the expected end of the table row, we cut the line
+ // otherwise we allow to have the last cell of a row to be open for longer cell content
+ if (columnEndChar == '|' || (isRowLine && columnEndChar == '+'))
+ {
+ sliceForCell.End = line.Start + columnEnd - 1;
+ }
+ else if (line.PeekCharExtra(line.End - line.Start) == '|')
+ {
+ sliceForCell.End = line.End - 1;
+ }
+ }
+ sliceForCell.TrimEnd();
+
+ if (!isRowLine || !IsRowSeparator(sliceForCell))
+ {
+ if (columnSlice.CurrentCell is null)
+ {
+ columnSlice.CurrentCell = new TableCell(this)
+ {
+ ColumnSpan = columnSlice.CurrentColumnSpan,
+ ColumnIndex = i,
+ Column = columnSlice.Start,
+ Line = processor.LineIndex,
+ Span = new SourceSpan(line.Start + columnSlice.Start, line.Start + columnSlice.End)
+ };
+
+ columnSlice.BlockProcessor ??= processor.CreateChild();
+
+ // Ensure that the BlockParser is aware that the TableCell is the top-level container
+ columnSlice.BlockProcessor.Open(columnSlice.CurrentCell);
+ }
+ // Process the content of the cell
+ columnSlice.BlockProcessor!.LineIndex = processor.LineIndex;
+
+ columnSlice.BlockProcessor.ProcessLinePart(sliceForCell, sliceForCell.Start - line.Start);
+ }
+
+ // Go to next column
+ i = nextColumnIndex;
+ }
+ return BlockState.ContinueDiscard;
+ }
+
+ private static void SetColumnSpanState(List columns, StringSlice line)
+ {
+ foreach (var columnSlice in columns)
+ {
+ columnSlice.PreviousColumnSpan = columnSlice.CurrentColumnSpan;
+ columnSlice.CurrentColumnSpan = 0;
+ }
+ // | ------------- | ------------ | ---------------------------------------- |
+ // Calculate the colspan for the new row
+ int columnIndex = -1;
+ for (int i = 0; i < columns.Count; i++)
+ {
+ var columnSlice = columns[i];
+ var peek = line.PeekChar(columnSlice.Start);
+ if (peek == '|' || peek == '+')
+ {
+ columnIndex = i;
+ }
+ if (columnIndex >= 0)
+ {
+ columns[columnIndex].CurrentColumnSpan++;
+ }
+ }
+ }
+
+ private static bool CanContinueRow(List columns)
+ {
+ foreach (var columnSlice in columns)
+ {
+ if (columnSlice.PreviousColumnSpan != columnSlice.CurrentColumnSpan)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static void Undo(BlockProcessor processor, GridTableState tableState, Table gridTable)
+ {
+ var parser = processor.Parsers.FindExact();
+ // Discard the grid table
+ var parent = gridTable.Parent!;
+ processor.Discard(gridTable);
+ var paragraphBlock = new ParagraphBlock(parser)
+ {
+ Lines = tableState.Lines,
+ };
+ parent.Add(paragraphBlock);
+ processor.Open(paragraphBlock);
+ }
+
+ ///
+ /// Performs the close operation.
+ ///
+ public override bool Close(BlockProcessor processor, Block block)
+ {
+ // Work only on Table, not on TableCell
+ var gridTable = block as Table;
+ if (gridTable != null)
+ {
+ var tableState = (GridTableState)block.GetData(typeof(GridTableState))!;
+ TerminateCurrentRow(processor, tableState, gridTable, true);
+ if (!gridTable.IsValid())
+ {
+ Undo(processor, tableState, gridTable);
+ }
+ }
+ return true;
+ }
+}