diff --git a/site/docs/extensions/emphasis-extras.md b/site/docs/extensions/emphasis-extras.md index 8c0ee64..6f4a5ce 100644 --- a/site/docs/extensions/emphasis-extras.md +++ b/site/docs/extensions/emphasis-extras.md @@ -60,6 +60,11 @@ This is ==highlighted== text. This is ==highlighted== text. +An unmatched remainder of a closing delimiter run must not pull subsequent text +into the emphasis. For example, `===a===== tail` renders as +`=a=== tail`. This example also round-trips unchanged with +`EnableTrackTrivia()` and `RoundtripRenderer`. + ## HTML output {.table} diff --git a/src/Markdig.Tests/TestEmphasisRoundtrip.cs b/src/Markdig.Tests/TestEmphasisRoundtrip.cs new file mode 100644 index 0000000..295df0c --- /dev/null +++ b/src/Markdig.Tests/TestEmphasisRoundtrip.cs @@ -0,0 +1,83 @@ +// 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(); + } + + // Leftover delimiter characters below the minimum count used to stay nested + // inside the emphasis they closed, moving the following text to the end of + // the block. 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)); + } + + // Same defect, minimal: leftovers (15 = 7 * 2 + 1) below the "==" minimum. + [Test] + public void UnbalancedMarkedRunsRoundtrip() + { + string markdown = "+===============+===============+\n"; + + var pipeline = new MarkdownPipelineBuilder() + .UseEmphasisExtras() + .EnableTrackTrivia() + .Build(); + + Assert.That(RoundTrip(markdown, pipeline), Is.EqualTo(markdown)); + } + + [TestCase("===a===== tail\n", "

=a=== tail

")] + [TestCase("===a==== tail\n", "

=a== tail

")] + [TestCase("=====a======= tail\n", "

=a=== tail

")] + [TestCase("+++a+++++ tail\n", "

+a+++ tail

")] + [TestCase("*===a===== tail*\n", "

=a=== tail

")] + [TestCase("===a===== tail ==next==\n", "

=a=== tail next

")] + public void UnmatchedCloserPreservesTrailingContent(string markdown, string expectedHtml) + { + var pipeline = new MarkdownPipelineBuilder().UseEmphasisExtras().EnableTrackTrivia().Build(); + + Assert.That(RoundTrip(markdown, pipeline), Is.EqualTo(markdown)); + Assert.That(Markdown.ToHtml(markdown, pipeline).Trim(), Is.EqualTo(expectedHtml)); + Assert.That(Markdown.ToHtml(markdown, new MarkdownPipelineBuilder().UseEmphasisExtras().Build()).Trim(), Is.EqualTo(expectedHtml)); + } + + // 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..bc0b08e 100644 --- a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs @@ -407,18 +407,33 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor i--; } } - else if ((closeDelimiter.Type & DelimiterType.Open) == 0) - { - closeDelimiter.ReplaceBy(closeDelimiter.AsLiteralInline()); - delimiters.RemoveAt(i); - i--; - break; - } else { + // Keep unmatched closers attached until their trailing content + // has been moved out below. break; } } + + // Leftover delimiter characters keep the following inlines nested inside + // the emphasis; move them out so they keep their original position. + if (closeDelimiter.DelimiterCount > 0 && closeDelimiter.Parent is EmphasisInline closerParentEmphasis) + { + var outermostEmphasis = closerParentEmphasis; + while (outermostEmphasis.Parent is EmphasisInline ancestorEmphasis) + { + outermostEmphasis = ancestorEmphasis; + } + + closeDelimiter.MoveChildrenAfter(outermostEmphasis); + } + + if (closeDelimiter.DelimiterCount > 0 && (closeDelimiter.Type & DelimiterType.Open) == 0) + { + closeDelimiter.ReplaceBy(closeDelimiter.AsLiteralInline()); + delimiters.RemoveAt(i); + i--; + } } }