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). """