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 = "![image](\"onclick=\"alert&#40;'click'&#41;\"://)"; + string expected = "

\"image\"

"; + + TestParser.TestSpec(input, expected); + } + + [Test] + public void TestAltTextIsCorrectlyEscaped() + { + TestParser.TestSpec( + @"![This is image alt text with quotation ' and double quotation ""hello"" world](girl.png)", + @"

"); + } + + [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, @"
    +
  1. In the :

    +
    Id                                   DisplayName         Description
    +--                                   -----------         -----------
    +62375ab9-6b52-47ed-826b-58e47e0e304b Group.Unified       ...
    +
  2. +
"); + } + + [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 = @"
+
+

Note

+

Also a note.

+
+
+"; + 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
  • +
+
    +
  1. Ordered 1 QL2 level 1 +Continued 1
  2. +
+
+

Not continued QL3

+
+
    +
  1. Ordered 2 QL2 level 1 +Continued 2a

    +

    Continued 2b

    +

    Continued 2c

    +
      +
    1. Ordered 3 QL2 level 2 +Continued 3a

      +

      Continued 3b

      +
    2. +
    +
  2. +
+
+
"; + + 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; + } +}