* 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).
|
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
|
## ID generation rules
|
||||||
|
|
||||||
1. Convert to lowercase
|
1. Convert to lowercase
|
||||||
|
|||||||
@@ -21,4 +21,60 @@ public class TestAutoLinks
|
|||||||
|
|
||||||
Assert.That(html, Is.EqualTo(expected).IgnoreWhiteSpace);
|
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)
|
public HeadingLinkReferenceDefinition(HeadingBlock headling)
|
||||||
{
|
{
|
||||||
Heading = 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>
|
/// <summary>
|
||||||
|
|||||||
@@ -150,6 +150,13 @@ public class LinkInlineParser : InlineParser
|
|||||||
return false;
|
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;
|
Inline? link = null;
|
||||||
// Try to use a callback directly defined on the LinkReferenceDefinition
|
// Try to use a callback directly defined on the LinkReferenceDefinition
|
||||||
if (linkRef.CreateLinkInline != null)
|
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)
|
private static void MarkParentAsInactive(Inline? inline)
|
||||||
{
|
{
|
||||||
while (inline != null)
|
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)
|
public LinkReferenceDefinition() : base(null)
|
||||||
{
|
{
|
||||||
IsOpen = false;
|
IsOpen = false;
|
||||||
|
AllowResolutionInsideOpenLink = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -136,6 +137,16 @@ public class LinkReferenceDefinition : LeafBlock
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public CreateLinkInlineDelegate? CreateLinkInline { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Tries to the parse the specified text into a definition.
|
/// Tries to the parse the specified text into a definition.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -241,4 +252,4 @@ public class LinkReferenceDefinition : LeafBlock
|
|||||||
public StringSlice UnescapedTitle;
|
public StringSlice UnescapedTitle;
|
||||||
public char TitleEnclosingCharacter;
|
public char TitleEnclosingCharacter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user