Skip to content

Commit ee19a02

Browse files
shainaraskascursoragenttheletterfclaude
authored
Don't autolink URLs used as markdown link text (#3319)
* stop autolinking links that are markdown link titles * 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 <cursoragent@cursor.com> * Strengthen nested-anchor regression tests for #3317 Replace fragile exact-string NotContain with NotMatchRegex to catch nested <a> 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) <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Fabrizio Ferri Benedetti <fabri.ferribenedetti@elastic.co> Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent e614b91 commit ee19a02

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ protected override void Write(HtmlRenderer renderer, LinkInline link)
1818
{
1919
if (renderer.EnableHtmlForInline && !link.IsImage)
2020
{
21+
// Avoid nested <a> tags when a URL inside link text was autolinked (elastic/docs-builder#3317).
22+
if (IsNestedInsideLink(link))
23+
{
24+
renderer.WriteChildren(link);
25+
return;
26+
}
27+
2128
if (link.GetData(nameof(ParserContext.CurrentUrlPath)) is not string)
2229
{
2330
base.Write(renderer, link);
@@ -107,6 +114,18 @@ private static void WriteImage(HtmlRenderer renderer, LinkInline link)
107114

108115
private static IHtmxAttributeProvider? GetHtmxProvider(LinkInline link) =>
109116
link.GetData(nameof(IHtmxAttributeProvider)) as IHtmxAttributeProvider;
117+
118+
private static bool IsNestedInsideLink(LinkInline link)
119+
{
120+
var parent = link.Parent;
121+
while (parent != null)
122+
{
123+
if (parent is LinkInline)
124+
return true;
125+
parent = parent.Parent;
126+
}
127+
return false;
128+
}
110129
}
111130

112131
public static class CustomLinkInlineRendererExtensions

tests/Elastic.Markdown.Tests/Inline/AutoLinkTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,58 @@ public void BothLinksWork() =>
245245
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
246246
}
247247

248+
// Regression test for elastic/docs-builder#3317: no nested <a> when a URL is the link text.
249+
public class AutoLinkInsideLinkTextTests(ITestOutputHelper output) : AutoLinkTestBase(output,
250+
"""
251+
Upload to a service like [https://gist.github.com](https://gist.github.com).
252+
"""
253+
)
254+
{
255+
[Fact]
256+
public void DoesNotCreateNestedAnchor() =>
257+
Html.Should().Contain(
258+
"""<a href="https://gist.github.com" target="_blank" rel="noopener noreferrer">https://gist.github.com</a>"""
259+
).And.NotMatchRegex(@"<a\b[^>]*><a\b");
260+
261+
[Fact]
262+
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
263+
}
264+
265+
public class AutoLinkInsideLinkTextWithSurroundingTextTests(ITestOutputHelper output) : AutoLinkTestBase(output,
266+
"""
267+
See [the page at https://example.test.io for details](https://docs.test.io).
268+
"""
269+
)
270+
{
271+
[Fact]
272+
public void DoesNotAutolinkUrlInsideLinkText() =>
273+
Html.Should().Contain(
274+
"""<a href="https://docs.test.io" target="_blank" rel="noopener noreferrer">the page at https://example.test.io for details</a>"""
275+
);
276+
277+
[Fact]
278+
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
279+
}
280+
281+
// Verify that image-inside-link is unaffected by the IsNestedInsideLink guard (images bypass it via the IsImage branch).
282+
public class ImageInsideLinkTests(ITestOutputHelper output) : InlineTest<LinkInline>(output,
283+
"""
284+
[![alt text](https://example.com/image.png)](https://example.com)
285+
"""
286+
)
287+
{
288+
[Fact]
289+
public void RendersOuterAnchor() =>
290+
Html.Should().Contain("""<a href="https://example.com" target="_blank" rel="noopener noreferrer">""");
291+
292+
[Fact]
293+
public void RendersImage() =>
294+
Html.Should().Contain("<img src=\"https://example.com/image.png\"");
295+
296+
[Fact]
297+
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
298+
}
299+
248300
public class MultipleAutoLinksTests(ITestOutputHelper output) : AutoLinkTestBase(output,
249301
"""
250302
First https://first.com then https://second.com and finally https://third.com are all linked.

0 commit comments

Comments
 (0)