From a87db0b3cea13a63ffd9dd616918295dc74172b7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 29 Sep 2025 10:43:07 +0000
Subject: [PATCH 1/4] Initial plan
From 5ad52af7e429f5a1dc16c05e822cf7a99ed783c6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 29 Sep 2025 11:06:10 +0000
Subject: [PATCH 2/4] Implement .md link preservation for internal markdown
links in LLM output
- Modified LlmLinkInlineRenderer to detect internal markdown links
- Internal markdown links now preserve .md extension
- External links and cross-links continue to use absolute URLs
- Added comprehensive test coverage for link handling
- Verified functionality with CLI tool output
Co-authored-by: reakaleek <16325797+reakaleek@users.noreply.github.com>
---
.../LlmMarkdown/LlmInlineRenderers.cs | 39 ++++++++++++++++++-
.../LlmMarkdown/LlmMarkdownOutput.fs | 24 ++++++++++++
2 files changed, 61 insertions(+), 2 deletions(-)
diff --git a/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmInlineRenderers.cs b/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmInlineRenderers.cs
index 9ef11985d..ae9f7a4b2 100644
--- a/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmInlineRenderers.cs
+++ b/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmInlineRenderers.cs
@@ -2,6 +2,7 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
+using Elastic.Markdown.IO;
using Elastic.Markdown.Myst.InlineParsers.Substitution;
using Elastic.Markdown.Myst.Roles;
using Elastic.Markdown.Myst.Roles.Kbd;
@@ -32,8 +33,23 @@ protected override void Write(LlmMarkdownRenderer renderer, LinkInline obj)
renderer.WriteChildren(obj);
renderer.Writer.Write("](");
var url = obj.GetDynamicUrl?.Invoke() ?? obj.Url;
- var absoluteUrl = LlmRenderingHelpers.MakeAbsoluteUrl(renderer, url);
- renderer.Writer.Write(absoluteUrl ?? string.Empty);
+
+ // Check if this is an internal link to a markdown page
+ var isCrossLink = (obj.GetData("isCrossLink") as bool?) == true;
+ var hasTargetNavigationRoot = obj.GetData($"Target{nameof(MarkdownFile.NavigationRoot)}") != null;
+ var isInternalMarkdownLink = !isCrossLink && hasTargetNavigationRoot;
+
+ if (isInternalMarkdownLink)
+ {
+ // For internal markdown links, preserve the .md extension
+ renderer.Writer.Write(EnsureMarkdownExtension(url) ?? string.Empty);
+ }
+ else
+ {
+ // For external links and cross-links, make absolute
+ var absoluteUrl = LlmRenderingHelpers.MakeAbsoluteUrl(renderer, url);
+ renderer.Writer.Write(absoluteUrl ?? string.Empty);
+ }
}
if (!string.IsNullOrEmpty(obj.Title))
{
@@ -43,6 +59,25 @@ protected override void Write(LlmMarkdownRenderer renderer, LinkInline obj)
}
renderer.Writer.Write(")");
}
+
+ ///
+ /// Ensures the URL ends with .md extension for markdown links
+ ///
+ private static string? EnsureMarkdownExtension(string? url)
+ {
+ if (string.IsNullOrEmpty(url))
+ return url;
+
+ // If it already has .md extension, return as-is
+ if (url.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
+ return url;
+
+ // Convert absolute paths to relative paths for markdown links
+ var processedUrl = url.StartsWith('/') ? url.TrimStart('/') : url;
+
+ // Add .md extension to internal markdown links
+ return processedUrl + ".md";
+ }
}
public class LlmEmphasisInlineRenderer : MarkdownObjectRenderer
diff --git a/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs b/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs
index 7daec9c86..cbbf8875e 100644
--- a/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs
+++ b/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs
@@ -538,3 +538,27 @@ sub:
markdown |> convertsToNewLLM """
## Hello, World!
"""
+
+type ``links`` () =
+ static let generator = Setup.Generate [
+ Index """
+This is a [link to another page](another-page.md).
+
+This is an [external link](https://example.com).
+
+This is a [cross-link](https://docs.elastic.co/some-page).
+"""
+ Markdown "another-page.md" """
+# Another Page
+
+This is another page for testing internal links.
+"""
+ ]
+
+ []
+ let ``internal markdown links preserve .md extension while external links become absolute`` () =
+ generator |> convertsToNewLLM """
+This is a [link to another page](another-page.md).
+This is an [external link](https://example.com).
+This is a [cross-link](https://docs.elastic.co/some-page).
+"""
From fdd97d2fd165a57f8ba9f493384e6135ff61400f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 29 Sep 2025 11:23:34 +0000
Subject: [PATCH 3/4] Fix cross-links to markdown files to preserve .md
extensions
- Store original cross-link URL before resolution
- Detect cross-links pointing to .md files
- Extract relative path with .md extension for LLM output
- Update test to validate cross-link behavior
- Maintain absolute URLs for non-markdown cross-links
Co-authored-by: reakaleek <16325797+reakaleek@users.noreply.github.com>
---
.../DiagnosticLinkInlineParser.cs | 3 ++
.../LlmMarkdown/LlmInlineRenderers.cs | 48 ++++++++++++++++++-
.../LlmMarkdown/LlmMarkdownOutput.fs | 4 +-
3 files changed, 52 insertions(+), 3 deletions(-)
diff --git a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs
index 0f2f1b614..1296d26f4 100644
--- a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs
+++ b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs
@@ -179,6 +179,9 @@ private static void ProcessCrossLink(LinkInline link, InlineProcessor processor,
if (url != null)
context.Build.Collector.EmitCrossLink(url);
+ // Store the original cross-link URL for LLM rendering
+ link.SetData("originalCrossLinkUrl", uri.ToString());
+
if (context.CrossLinkResolver.TryResolve(
s => processor.EmitError(link, s),
uri, out var resolvedUri)
diff --git a/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmInlineRenderers.cs b/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmInlineRenderers.cs
index ae9f7a4b2..7ef8dfb47 100644
--- a/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmInlineRenderers.cs
+++ b/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmInlineRenderers.cs
@@ -37,16 +37,24 @@ protected override void Write(LlmMarkdownRenderer renderer, LinkInline obj)
// Check if this is an internal link to a markdown page
var isCrossLink = (obj.GetData("isCrossLink") as bool?) == true;
var hasTargetNavigationRoot = obj.GetData($"Target{nameof(MarkdownFile.NavigationRoot)}") != null;
+ var originalCrossLinkUrl = obj.GetData("originalCrossLinkUrl") as string;
var isInternalMarkdownLink = !isCrossLink && hasTargetNavigationRoot;
+ var isCrossLinkToMarkdown = isCrossLink && originalCrossLinkUrl != null && IsCrossLinkToMarkdown(originalCrossLinkUrl);
if (isInternalMarkdownLink)
{
// For internal markdown links, preserve the .md extension
renderer.Writer.Write(EnsureMarkdownExtension(url) ?? string.Empty);
}
+ else if (isCrossLinkToMarkdown)
+ {
+ // For cross-links to markdown files, extract relative path with .md extension
+ var markdownPath = ExtractMarkdownPath(originalCrossLinkUrl!);
+ renderer.Writer.Write(markdownPath ?? string.Empty);
+ }
else
{
- // For external links and cross-links, make absolute
+ // For external links and non-markdown cross-links, make absolute
var absoluteUrl = LlmRenderingHelpers.MakeAbsoluteUrl(renderer, url);
renderer.Writer.Write(absoluteUrl ?? string.Empty);
}
@@ -78,6 +86,44 @@ protected override void Write(LlmMarkdownRenderer renderer, LinkInline obj)
// Add .md extension to internal markdown links
return processedUrl + ".md";
}
+
+ ///
+ /// Checks if a cross-link URL points to a markdown file
+ ///
+ private static bool IsCrossLinkToMarkdown(string originalCrossLinkUrl)
+ {
+ if (string.IsNullOrEmpty(originalCrossLinkUrl))
+ return false;
+
+ // Parse the cross-link URI to extract the path
+ if (Uri.TryCreate(originalCrossLinkUrl, UriKind.Absolute, out var uri))
+ {
+ var path = uri.AbsolutePath;
+ return path.EndsWith(".md", StringComparison.OrdinalIgnoreCase);
+ }
+
+ return false;
+ }
+
+ ///
+ /// Extracts the relative markdown path from a cross-link URL
+ ///
+ private static string? ExtractMarkdownPath(string originalCrossLinkUrl)
+ {
+ if (string.IsNullOrEmpty(originalCrossLinkUrl))
+ return null;
+
+ // Parse the cross-link URI to extract the path
+ if (Uri.TryCreate(originalCrossLinkUrl, UriKind.Absolute, out var uri))
+ {
+ var path = uri.AbsolutePath;
+ if (path.StartsWith('/'))
+ path = path.TrimStart('/');
+ return path;
+ }
+
+ return null;
+ }
}
public class LlmEmphasisInlineRenderer : MarkdownObjectRenderer
diff --git a/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs b/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs
index cbbf8875e..c0c90deb4 100644
--- a/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs
+++ b/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs
@@ -546,7 +546,7 @@ This is a [link to another page](another-page.md).
This is an [external link](https://example.com).
-This is a [cross-link](https://docs.elastic.co/some-page).
+This is a [cross-link](docs-content:/solutions/observability/apps/apm-server-binary.md).
"""
Markdown "another-page.md" """
# Another Page
@@ -560,5 +560,5 @@ This is another page for testing internal links.
generator |> convertsToNewLLM """
This is a [link to another page](another-page.md).
This is an [external link](https://example.com).
-This is a [cross-link](https://docs.elastic.co/some-page).
+This is a [cross-link](solutions/observability/apps/apm-server-binary.md).
"""
From 8824094690f0978e2fb599199ac6ded4fee3f8b6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 29 Sep 2025 11:33:37 +0000
Subject: [PATCH 4/4] Fix cross-links to use absolute URLs with .md extensions
- Cross-links to markdown now become full URLs with .md extensions
- Update logic to use MakeAbsoluteUrl + EnsureMarkdownExtension for cross-links
- Remove unused ExtractMarkdownPath method
- Update test expectations to match corrected behavior
- Use pattern matching to fix code analysis warning
Co-authored-by: reakaleek <16325797+reakaleek@users.noreply.github.com>
---
.../LlmMarkdown/LlmInlineRenderers.cs | 29 ++++---------------
.../LlmMarkdown/LlmMarkdownOutput.fs | 2 +-
2 files changed, 6 insertions(+), 25 deletions(-)
diff --git a/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmInlineRenderers.cs b/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmInlineRenderers.cs
index 7ef8dfb47..58669639f 100644
--- a/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmInlineRenderers.cs
+++ b/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmInlineRenderers.cs
@@ -39,7 +39,7 @@ protected override void Write(LlmMarkdownRenderer renderer, LinkInline obj)
var hasTargetNavigationRoot = obj.GetData($"Target{nameof(MarkdownFile.NavigationRoot)}") != null;
var originalCrossLinkUrl = obj.GetData("originalCrossLinkUrl") as string;
var isInternalMarkdownLink = !isCrossLink && hasTargetNavigationRoot;
- var isCrossLinkToMarkdown = isCrossLink && originalCrossLinkUrl != null && IsCrossLinkToMarkdown(originalCrossLinkUrl);
+ var isCrossLinkToMarkdown = isCrossLink && originalCrossLinkUrl is not null && IsCrossLinkToMarkdown(originalCrossLinkUrl);
if (isInternalMarkdownLink)
{
@@ -48,9 +48,10 @@ protected override void Write(LlmMarkdownRenderer renderer, LinkInline obj)
}
else if (isCrossLinkToMarkdown)
{
- // For cross-links to markdown files, extract relative path with .md extension
- var markdownPath = ExtractMarkdownPath(originalCrossLinkUrl!);
- renderer.Writer.Write(markdownPath ?? string.Empty);
+ // For cross-links to markdown files, use absolute URL with .md extension
+ var absoluteUrl = LlmRenderingHelpers.MakeAbsoluteUrl(renderer, url);
+ var urlWithMdExtension = EnsureMarkdownExtension(absoluteUrl);
+ renderer.Writer.Write(urlWithMdExtension ?? string.Empty);
}
else
{
@@ -104,26 +105,6 @@ private static bool IsCrossLinkToMarkdown(string originalCrossLinkUrl)
return false;
}
-
- ///
- /// Extracts the relative markdown path from a cross-link URL
- ///
- private static string? ExtractMarkdownPath(string originalCrossLinkUrl)
- {
- if (string.IsNullOrEmpty(originalCrossLinkUrl))
- return null;
-
- // Parse the cross-link URI to extract the path
- if (Uri.TryCreate(originalCrossLinkUrl, UriKind.Absolute, out var uri))
- {
- var path = uri.AbsolutePath;
- if (path.StartsWith('/'))
- path = path.TrimStart('/');
- return path;
- }
-
- return null;
- }
}
public class LlmEmphasisInlineRenderer : MarkdownObjectRenderer
diff --git a/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs b/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs
index c0c90deb4..21c18b47b 100644
--- a/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs
+++ b/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs
@@ -560,5 +560,5 @@ This is another page for testing internal links.
generator |> convertsToNewLLM """
This is a [link to another page](another-page.md).
This is an [external link](https://example.com).
-This is a [cross-link](solutions/observability/apps/apm-server-binary.md).
+This is a [cross-link](https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/solutions/observability/apps/apm-server-binary.md).
"""