* Fix heading auto-link hijacking nested link labels (#668) * Refactor heading reference resolution to init-only state (#949) --------- Co-authored-by: Alexandre Mutel <alexandre_mutel@live.com>
This commit is contained in:
1 parent
40eebc0cd8
commit
9006f0b4a2
6 files changed
+114
-1
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 = "<h1 id=\"testing-markdown\">Testing Markdown</h1>\n" +
|
||||
"<h3 id=\"header\">Header</h3>\n" +
|
||||
"<p><a href=\"https://www.bing.com\">Testing [Header]</a></p>\n" +
|
||||
"<p><a href=\"https://www.google.com\">Testing [test]</a></p>";
|
||||
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAutoLinks()
|
||||
.UseAutoIdentifiers()
|
||||
.Build();
|
||||
var html = Markdown.ToHtml(markdown, pipeline);
|
||||
|
||||
Assert.That(html, Is.EqualTo(expected).IgnoreWhiteSpace);
|
||||
}
|
||||
|
||||
[TestCase("[Header]", "<a href=\"#header\">Header</a>")]
|
||||
[TestCase("[Header][]", "<a href=\"#header\">Header</a>")]
|
||||
[TestCase("[label][Header]", "<a href=\"#header\">label</a>")]
|
||||
[TestCase("[Testing [Header]](/target)", "<a href=\"/target\">Testing [Header]</a>")]
|
||||
[TestCase("[Testing [Header][]](/target)", "<a href=\"/target\">Testing [Header][]</a>")]
|
||||
[TestCase("[Testing [label][Header]](/target)", "<a href=\"/target\">Testing [label][Header]</a>")]
|
||||
[TestCase("![Testing [Header]](/image.png)", "<img src=\"/image.png\" alt=\"Testing [Header]\" />")]
|
||||
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("<h1 id=\"header\">Header</h1>\n<p>" + expected + "</p>").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("<h1 id=\"header\">Header</h1>\n<p>[Testing <a href=\"/explicit\">Header</a>](/target)</p>").IgnoreWhiteSpace);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the delimiter is nested inside another link/image
|
||||
/// delimiter that may still resolve.
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
@@ -34,6 +34,7 @@ public class LinkReferenceDefinition : LeafBlock
|
||||
public LinkReferenceDefinition() : base(null)
|
||||
{
|
||||
IsOpen = false;
|
||||
AllowResolutionInsideOpenLink = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,6 +137,16 @@ public class LinkReferenceDefinition : LeafBlock
|
||||
/// </remarks>
|
||||
public CreateLinkInlineDelegate? CreateLinkInline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this definition may resolve into a link while its label is
|
||||
/// nested inside another still-open link or image bracket.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>true</c> for user-authored definitions, preserving CommonMark's "links
|
||||
/// cannot contain links". Implicitly generated ones can initialize it to <c>false</c>.
|
||||
/// </remarks>
|
||||
internal bool AllowResolutionInsideOpenLink { get; private protected init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tries to the parse the specified text into a definition.
|
||||
/// </summary>
|
||||
@@ -241,4 +252,4 @@ public class LinkReferenceDefinition : LeafBlock
|
||||
public StringSlice UnescapedTitle;
|
||||
public char TitleEnclosingCharacter;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user