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 9ef11985d..58669639f 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,32 @@ 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 originalCrossLinkUrl = obj.GetData("originalCrossLinkUrl") as string; + var isInternalMarkdownLink = !isCrossLink && hasTargetNavigationRoot; + var isCrossLinkToMarkdown = isCrossLink && originalCrossLinkUrl is not 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, use absolute URL with .md extension + var absoluteUrl = LlmRenderingHelpers.MakeAbsoluteUrl(renderer, url); + var urlWithMdExtension = EnsureMarkdownExtension(absoluteUrl); + renderer.Writer.Write(urlWithMdExtension ?? string.Empty); + } + else + { + // For external links and non-markdown cross-links, make absolute + var absoluteUrl = LlmRenderingHelpers.MakeAbsoluteUrl(renderer, url); + renderer.Writer.Write(absoluteUrl ?? string.Empty); + } } if (!string.IsNullOrEmpty(obj.Title)) { @@ -43,6 +68,43 @@ 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"; + } + + /// + /// 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; + } } public class LlmEmphasisInlineRenderer : MarkdownObjectRenderer diff --git a/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs b/tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs index 7daec9c86..21c18b47b 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](docs-content:/solutions/observability/apps/apm-server-binary.md). +""" + 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-v3-preview.elastic.dev/elastic/docs-content/tree/main/solutions/observability/apps/apm-server-binary.md). +"""