From a735446df6c113c9e891fde9087eb3c09a83be5d Mon Sep 17 00:00:00 2001 From: shainaraskas Date: Wed, 13 May 2026 17:16:11 -0400 Subject: [PATCH 1/3] stop autolinking links that are markdown link titles --- .../InlineParsers/AutoLinkInlineParser.cs | 15 ++++++++ .../Inline/AutoLinkTests.cs | 35 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/Elastic.Markdown/Myst/InlineParsers/AutoLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/AutoLinkInlineParser.cs index 16cb3fc04d..7f1ce78dc7 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/AutoLinkInlineParser.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/AutoLinkInlineParser.cs @@ -52,6 +52,10 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) if (!span.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) return false; + // Skip when inside open `[...]` link text to avoid nested (elastic/docs-builder#3317). + if (IsInsideOpenLinkDelimiter(processor.Inline)) + return false; + // Find the end of the URL var urlLength = FindUrlEnd(span); if (urlLength <= "https://".Length) @@ -212,6 +216,17 @@ private static bool ShouldExcludeUrl(string url) return false; } + private static bool IsInsideOpenLinkDelimiter(Inline? current) + { + while (current != null) + { + if (current is LinkDelimiterInline) + return true; + current = current.PreviousSibling; + } + return false; + } + private static bool IsAllDigits(ReadOnlySpan span) { foreach (var c in span) diff --git a/tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs index cf327efefd..56de31b7ba 100644 --- a/tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs @@ -245,6 +245,41 @@ public void BothLinksWork() => public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); } +// Regression test for elastic/docs-builder#3317: no nested when a URL is the link text. +public class AutoLinkInsideLinkTextTests(ITestOutputHelper output) : AutoLinkTestBase(output, +""" +Upload to a service like [https://gist.github.com](https://gist.github.com). +""" +) +{ + [Fact] + public void DoesNotCreateNestedAnchor() => + Html.Should().Contain( + """https://gist.github.com""" + ).And.NotContain( + """ Collector.Diagnostics.Should().HaveCount(0); +} + +public class AutoLinkInsideLinkTextWithSurroundingTextTests(ITestOutputHelper output) : AutoLinkTestBase(output, +""" +See [the page at https://example.test.io for details](https://docs.test.io). +""" +) +{ + [Fact] + public void DoesNotAutolinkUrlInsideLinkText() => + Html.Should().Contain( + """the page at https://example.test.io for details""" + ); + + [Fact] + public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); +} + public class MultipleAutoLinksTests(ITestOutputHelper output) : AutoLinkTestBase(output, """ First https://first.com then https://second.com and finally https://third.com are all linked. From 5f534236e0a814cd364ffc4ff1062f78655e8ea8 Mon Sep 17 00:00:00 2001 From: shainaraskas Date: Wed, 13 May 2026 18:04:28 -0400 Subject: [PATCH 2/3] move nested-link guard to the renderer The parser-level check only caught the case where the autolink URL appeared immediately after `[`. Once any literal text intervened, the LinkDelimiterInline walk-back missed it. Detecting nested links in the renderer instead works for every parse shape. Co-authored-by: Cursor --- .../InlineParsers/AutoLinkInlineParser.cs | 15 --------------- .../Myst/Renderers/HtmxLinkInlineRenderer.cs | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Elastic.Markdown/Myst/InlineParsers/AutoLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/AutoLinkInlineParser.cs index 7f1ce78dc7..16cb3fc04d 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/AutoLinkInlineParser.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/AutoLinkInlineParser.cs @@ -52,10 +52,6 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) if (!span.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) return false; - // Skip when inside open `[...]` link text to avoid nested (elastic/docs-builder#3317). - if (IsInsideOpenLinkDelimiter(processor.Inline)) - return false; - // Find the end of the URL var urlLength = FindUrlEnd(span); if (urlLength <= "https://".Length) @@ -216,17 +212,6 @@ private static bool ShouldExcludeUrl(string url) return false; } - private static bool IsInsideOpenLinkDelimiter(Inline? current) - { - while (current != null) - { - if (current is LinkDelimiterInline) - return true; - current = current.PreviousSibling; - } - return false; - } - private static bool IsAllDigits(ReadOnlySpan span) { foreach (var c in span) diff --git a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs index a33e8de5d9..e4ce331fb9 100644 --- a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs @@ -18,6 +18,13 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) { if (renderer.EnableHtmlForInline && !link.IsImage) { + // Avoid nested tags when a URL inside link text was autolinked (elastic/docs-builder#3317). + if (IsNestedInsideLink(link)) + { + renderer.WriteChildren(link); + return; + } + if (link.GetData(nameof(ParserContext.CurrentUrlPath)) is not string) { base.Write(renderer, link); @@ -107,6 +114,18 @@ private static void WriteImage(HtmlRenderer renderer, LinkInline link) private static IHtmxAttributeProvider? GetHtmxProvider(LinkInline link) => link.GetData(nameof(IHtmxAttributeProvider)) as IHtmxAttributeProvider; + + private static bool IsNestedInsideLink(LinkInline link) + { + var parent = link.Parent; + while (parent != null) + { + if (parent is LinkInline) + return true; + parent = parent.Parent; + } + return false; + } } public static class CustomLinkInlineRendererExtensions From a3450166d38ef753358c4f1d06d4c0ea6296f8de Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Thu, 21 May 2026 09:21:59 +0200 Subject: [PATCH 3/3] Strengthen nested-anchor regression tests for #3317 Replace fragile exact-string NotContain with NotMatchRegex to catch nested regardless of attribute ordering. Add ImageInsideLinkTests to confirm the IsImage branch is unaffected by the IsNestedInsideLink guard. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../Inline/AutoLinkTests.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs index 56de31b7ba..5f31a380fd 100644 --- a/tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs @@ -256,9 +256,7 @@ public class AutoLinkInsideLinkTextTests(ITestOutputHelper output) : AutoLinkTes public void DoesNotCreateNestedAnchor() => Html.Should().Contain( """https://gist.github.com""" - ).And.NotContain( - """]*> Collector.Diagnostics.Should().HaveCount(0); @@ -280,6 +278,25 @@ public void DoesNotAutolinkUrlInsideLinkText() => public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); } +// Verify that image-inside-link is unaffected by the IsNestedInsideLink guard (images bypass it via the IsImage branch). +public class ImageInsideLinkTests(ITestOutputHelper output) : InlineTest(output, +""" +[![alt text](https://example.com/image.png)](https://example.com) +""" +) +{ + [Fact] + public void RendersOuterAnchor() => + Html.Should().Contain(""""""); + + [Fact] + public void RendersImage() => + Html.Should().Contain(" Collector.Diagnostics.Should().HaveCount(0); +} + public class MultipleAutoLinksTests(ITestOutputHelper output) : AutoLinkTestBase(output, """ First https://first.com then https://second.com and finally https://third.com are all linked.