diff --git a/src/Markdig.Tests/TestEmphasisRoundtrip.cs b/src/Markdig.Tests/TestEmphasisRoundtrip.cs new file mode 100644 index 0000000..dc6648a --- /dev/null +++ b/src/Markdig.Tests/TestEmphasisRoundtrip.cs @@ -0,0 +1,70 @@ +// 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.Renderers.Roundtrip; +using Markdig.Syntax; + +namespace Markdig.Tests; + +[TestFixture] +public class TestEmphasisRoundtrip +{ + private static string RoundTrip(string markdown, MarkdownPipeline pipeline) + { + MarkdownDocument document = Markdown.Parse(markdown, pipeline); + var writer = new StringWriter(); + var renderer = new RoundtripRenderer(writer); + pipeline.Setup(renderer); + renderer.Write(document); + return writer.ToString(); + } + + // A closer with leftover delimiter characters that dropped below the minimum count + // used to stay nested inside the emphasis it closed, together with the content that + // followed it. The round-tripped text then emitted the leftover run at the end of the + // block. Reproduces https://github.com/xoofx/markdig/issues/743. + [Test] + public void GridTableSeparatorRoundtripsWithTrackTrivia() + { + string markdown = + "+---------------+---------------+--------------------+\n" + + "| Fruit | Price | Advantages |\n" + + "+===============+===============+====================+\n" + + "| Bananas | first line | first line |\n" + + "| | next line | next line |\n" + + "+---------------+---------------+--------------------+\n"; + + var pipeline = new MarkdownPipelineBuilder() + .UseAutoLinks() + .UseEmphasisExtras() + .UseListExtras() + .EnableTrackTrivia() + .Build(); + + Assert.That(RoundTrip(markdown, pipeline), Is.EqualTo(markdown)); + } + + // Minimal reduction of the same defect: two '=' runs whose leftover characters + // (15 = 7 * 2 + 1) fall below the "marked" (`==`) minimum count of 2. + [Test] + public void UnbalancedMarkedRunsRoundtrip() + { + string markdown = "+===============+===============+\n"; + + var pipeline = new MarkdownPipelineBuilder() + .UseEmphasisExtras() + .EnableTrackTrivia() + .Build(); + + Assert.That(RoundTrip(markdown, pipeline), Is.EqualTo(markdown)); + } + + // The fix must not prevent balanced "marked" emphasis from being detected. + [Test] + public void BalancedMarkedEmphasisStillParses() + { + var pipeline = new MarkdownPipelineBuilder().UseEmphasisExtras().Build(); + Assert.That(Markdown.ToHtml("==bold==", pipeline).Trim(), Is.EqualTo("
bold
")); + } +} diff --git a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs index 10fb4e3..32566af 100644 --- a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs @@ -419,6 +419,27 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor break; } } + + // The closer could not be fully consumed and still holds leftover delimiter + // characters that cannot match any opener. When the closer was embraced as the + // closing side of an emphasis, the inlines that follow the closer belong after + // that emphasis, not nested inside it. This is already handled when the opener is + // fully consumed, but not when the opener also keeps leftover characters. Move the + // trailing content out to the outermost enclosing emphasis so it keeps its original + // position; otherwise it is rendered before the closing delimiters, which corrupts + // round-tripped output when EnableTrackTrivia() is used (for example a grid table + // separator, #743). The closer itself keeps its position, matching the behaviour + // for a leftover closer that has no trailing content. + if (closeDelimiter.DelimiterCount > 0 && closeDelimiter.Parent is EmphasisInline closerParentEmphasis) + { + var outermostEmphasis = closerParentEmphasis; + while (outermostEmphasis.Parent is EmphasisInline ancestorEmphasis) + { + outermostEmphasis = ancestorEmphasis; + } + + closeDelimiter.MoveChildrenAfter(outermostEmphasis); + } } }