diff --git a/site/docs/extensions/auto-identifiers.md b/site/docs/extensions/auto-identifiers.md index f3aaea7..888fda5 100644 --- a/site/docs/extensions/auto-identifiers.md +++ b/site/docs/extensions/auto-identifiers.md @@ -28,6 +28,12 @@ You can then link to it: See [Getting Started](#getting-started). ``` +With the default `AutoLink` option, `[Getting Started]` also links to the heading. +These implicit heading references do not resolve inside another still-open link or +image bracket. For example, `[Read [Getting Started]](/guide)` links the entire +label `Read [Getting Started]` to `/guide`. Explicit reference definitions such as +`[Getting Started]: /guide` retain normal CommonMark reference-link behavior. + ## ID generation rules 1. Convert to lowercase diff --git a/src/Markdig.Tests/TestAutoLinks.cs b/src/Markdig.Tests/TestAutoLinks.cs index 005c3d1..ac5c2bd 100644 --- a/src/Markdig.Tests/TestAutoLinks.cs +++ b/src/Markdig.Tests/TestAutoLinks.cs @@ -21,4 +21,60 @@ public class TestAutoLinks Assert.That(html, Is.EqualTo(expected).IgnoreWhiteSpace); } + + // https://github.com/xoofx/markdig/issues/668 + // A heading's implicit reference must not resolve inside another still-open + // link bracket, which would break the outer 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); + } + + [TestCase("[Header]", "Header")] + [TestCase("[Header][]", "Header")] + [TestCase("[label][Header]", "label")] + [TestCase("[Testing [Header]](/target)", "Testing [Header]")] + [TestCase("[Testing [Header][]](/target)", "Testing [Header][]")] + [TestCase("[Testing [label][Header]](/target)", "Testing [label][Header]")] + [TestCase("![Testing [Header]](/image.png)", "\"Testing")] + public void TestAutoIdentifierReferenceResolution(string markdown, string expected) + { + foreach (var trackTrivia in new[] { false, true }) + { + var builder = new MarkdownPipelineBuilder().UseAutoLinks().UseAutoIdentifiers(); + if (trackTrivia) + { + builder.EnableTrackTrivia(); + } + + var html = Markdown.ToHtml("# Header\n\n" + markdown, builder.Build()); + + Assert.That(html, Is.EqualTo("

Header

\n

" + expected + "

").IgnoreWhiteSpace, $"TrackTrivia: {trackTrivia}"); + } + } + + [Test] + public void TestExplicitReferenceStillResolvesInsideOpenLink() + { + var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().UseAutoIdentifiers().Build(); + var html = Markdown.ToHtml("[Header]: /explicit\n\n# Header\n\n[Testing [Header]](/target)", pipeline); + + Assert.That(html, Is.EqualTo("

Header

\n

[Testing Header](/target)

").IgnoreWhiteSpace); + } } diff --git a/src/Markdig/Extensions/AutoIdentifiers/HeadingLinkReferenceDefinition.cs b/src/Markdig/Extensions/AutoIdentifiers/HeadingLinkReferenceDefinition.cs index 32562cc..13a2566 100644 --- a/src/Markdig/Extensions/AutoIdentifiers/HeadingLinkReferenceDefinition.cs +++ b/src/Markdig/Extensions/AutoIdentifiers/HeadingLinkReferenceDefinition.cs @@ -18,6 +18,9 @@ public class HeadingLinkReferenceDefinition : LinkReferenceDefinition public HeadingLinkReferenceDefinition(HeadingBlock headling) { Heading = headling; + // Created implicitly, so it must not resolve inside another still-open + // link bracket, e.g. [Some text [Heading]](url). + AllowResolutionInsideOpenLink = false; } /// diff --git a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs index 83ac5db..0701ae4 100644 --- a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs @@ -150,6 +150,13 @@ public class LinkInlineParser : InlineParser return false; } + // An implicit definition must not hijack a bracket nested inside a + // still-open link, which would break the outer one. + 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 +437,24 @@ public class LinkInlineParser : InlineParser } } + /// + /// Determines whether the delimiter is nested inside another link/image + /// delimiter that may still resolve. + /// + 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/Polyfills/IsExternalInit.cs b/src/Markdig/Polyfills/IsExternalInit.cs new file mode 100644 index 0000000..ac7213e --- /dev/null +++ b/src/Markdig/Polyfills/IsExternalInit.cs @@ -0,0 +1,12 @@ +// 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. + +#if !NET5_0_OR_GREATER +namespace System.Runtime.CompilerServices; + +// Required by the compiler for init-only properties on older target frameworks. +internal static class IsExternalInit +{ +} +#endif diff --git a/src/Markdig/Syntax/LinkReferenceDefinition.cs b/src/Markdig/Syntax/LinkReferenceDefinition.cs index 056d91d..64c84d9 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinition.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinition.cs @@ -34,6 +34,7 @@ public class LinkReferenceDefinition : LeafBlock public LinkReferenceDefinition() : base(null) { IsOpen = false; + AllowResolutionInsideOpenLink = true; } /// @@ -136,6 +137,16 @@ public class LinkReferenceDefinition : LeafBlock /// public CreateLinkInlineDelegate? CreateLinkInline { get; set; } + /// + /// Gets whether this definition may resolve into a link while its label is + /// nested inside another still-open link or image bracket. + /// + /// + /// true for user-authored definitions, preserving CommonMark's "links + /// cannot contain links". Implicitly generated ones can initialize it to false. + /// + internal bool AllowResolutionInsideOpenLink { get; private protected init; } + /// /// Tries to the parse the specified text into a definition. /// @@ -241,4 +252,4 @@ public class LinkReferenceDefinition : LeafBlock public StringSlice UnescapedTitle; public char TitleEnclosingCharacter; } -} \ No newline at end of file +}