diff --git a/src/Markdig.Tests/TestAutoLinks.cs b/src/Markdig.Tests/TestAutoLinks.cs index 005c3d1..e3bb239 100644 --- a/src/Markdig.Tests/TestAutoLinks.cs +++ b/src/Markdig.Tests/TestAutoLinks.cs @@ -21,4 +21,29 @@ public class TestAutoLinks Assert.That(html, Is.EqualTo(expected).IgnoreWhiteSpace); } + + // https://github.com/xoofx/markdig/issues/668 + // A heading's implicit "AutoLink" reference (registered by the AutoIdentifiers extension) must not + // resolve when its label is nested inside another still-open link bracket, otherwise it silently + // breaks the outer, explicitly authored link. + [Test] + public void TestAutoIdentifierHeadingLinkDoesNotHijackNestedLinkLabel() + { + var markdown = "# Testing Markdown\n\n" + + "### Header\n\n" + + "[Testing [Header]](https://www.bing.com)\n\n" + + "[Testing [test]](https://www.google.com)\n"; + var expected = "

Testing Markdown

\n" + + "

Header

\n" + + "

Testing [Header]

\n" + + "

Testing [test]

"; + + var pipeline = new MarkdownPipelineBuilder() + .UseAutoLinks() + .UseAutoIdentifiers() + .Build(); + var html = Markdown.ToHtml(markdown, pipeline); + + Assert.That(html, Is.EqualTo(expected).IgnoreWhiteSpace); + } } diff --git a/src/Markdig/Extensions/AutoIdentifiers/HeadingLinkReferenceDefinition.cs b/src/Markdig/Extensions/AutoIdentifiers/HeadingLinkReferenceDefinition.cs index 32562cc..a8b6958 100644 --- a/src/Markdig/Extensions/AutoIdentifiers/HeadingLinkReferenceDefinition.cs +++ b/src/Markdig/Extensions/AutoIdentifiers/HeadingLinkReferenceDefinition.cs @@ -24,4 +24,12 @@ public class HeadingLinkReferenceDefinition : LinkReferenceDefinition /// Gets or sets the heading related to this link reference definition. /// public HeadingBlock Heading { get; set; } + + /// + /// + /// This reference definition is created implicitly for every heading, so it must not resolve when its + /// label is nested inside another still-open link bracket, e.g. [Some text [HeadingText]](url). + /// Otherwise it would silently break the outer, explicitly authored link. + /// + internal override bool AllowResolutionInsideOpenLink => false; } diff --git a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs index 83ac5db..fcd1e93 100644 --- a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs @@ -150,6 +150,14 @@ public class LinkInlineParser : InlineParser return false; } + // Implicitly generated reference definitions (e.g. automatic heading anchors from the auto-identifier + // extension) must not hijack a bracket pair nested inside another still-open link, otherwise they would + // silently break the outer, explicitly authored link (see https://github.com/xoofx/markdig/issues/668). + if (!linkRef.AllowResolutionInsideOpenLink && HasActiveAncestorLink(parent)) + { + return false; + } + Inline? link = null; // Try to use a callback directly defined on the LinkReferenceDefinition if (linkRef.CreateLinkInline != null) @@ -430,6 +438,24 @@ public class LinkInlineParser : InlineParser } } + /// + /// Determines whether the given link delimiter is nested inside another link/image delimiter that is + /// still active, i.e. it may still be resolved into a real link/image once fully parsed. + /// + private static bool HasActiveAncestorLink(LinkDelimiterInline delimiter) + { + var inline = delimiter.Parent; + while (inline != null) + { + if (inline is LinkDelimiterInline { IsActive: true }) + { + return true; + } + inline = inline.Parent; + } + return false; + } + private static void MarkParentAsInactive(Inline? inline) { while (inline != null) diff --git a/src/Markdig/Syntax/LinkReferenceDefinition.cs b/src/Markdig/Syntax/LinkReferenceDefinition.cs index 056d91d..c44ee06 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinition.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinition.cs @@ -136,6 +136,21 @@ public class LinkReferenceDefinition : LeafBlock /// public CreateLinkInlineDelegate? CreateLinkInline { get; set; } + /// + /// Gets a value indicating whether this reference definition is allowed to resolve into a link when its + /// label is nested inside another link or image bracket that is still open (and may still become a valid + /// link/image once fully parsed). + /// + /// + /// This is true for regular, user-authored reference definitions so that CommonMark's "links cannot + /// contain links" resolution order is preserved. Implicitly generated reference definitions (e.g. the + /// automatic heading anchors created by the auto-identifier extension) override this to false so + /// they don't silently hijack a bracket pair that is part of a larger, still-open link label, such as + /// [Some text [HeadingText]](https://example.com) (see + /// https://github.com/xoofx/markdig/issues/668). + /// + internal virtual bool AllowResolutionInsideOpenLink => true; + /// /// Tries to the parse the specified text into a definition. ///