diff --git a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs
index ec6fa2167..3d41d599c 100644
--- a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs
+++ b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs
@@ -404,7 +404,12 @@ public static string UpdateRelativeUrl(ParserContext context, string url)
// Acquire navigation-aware path
if (context.NavigationTraversable.NavigationDocumentationFileLookup.TryGetValue(currentMarkdown, out var currentNavigation))
{
- var uri = new Uri(new UriBuilder("http", "localhost", 80, currentNavigation.Url).Uri, url);
+ // Check if we're handling relative to an index file, which has an unique URL resolution rule
+ var baseUrl = currentMarkdown.FileName.Equals("index.md", StringComparison.OrdinalIgnoreCase)
+ ? $"{currentNavigation.Url.TrimEnd('/')}/"
+ : currentNavigation.Url;
+
+ var uri = new Uri(new UriBuilder("http", "localhost", 80, baseUrl).Uri, url);
newUrl = uri.AbsolutePath;
}
else
diff --git a/tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs b/tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs
index f234116a7..a2074cb22 100644
--- a/tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs
+++ b/tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs
@@ -27,8 +27,8 @@ public async Task UpdateRelativeUrlUsesNavigationPathWhenAssemblerBuildEnabled()
var nonAssemblerResult = await ResolveUrlForBuildMode(relativeAssetPath, assemblerBuild: false, pathPrefix: "this-is-not-relevant");
var assemblerResult = await ResolveUrlForBuildMode(relativeAssetPath, assemblerBuild: true, pathPrefix: "platform");
- nonAssemblerResult.Should().Be("/docs/setup/images/pic.png");
- assemblerResult.Should().Be("/docs/platform/setup/images/pic.png");
+ nonAssemblerResult.Should().AllBe("/docs/setup/images/pic.png");
+ assemblerResult.Should().AllBe("/docs/platform/setup/images/pic.png");
}
[Fact]
@@ -37,7 +37,7 @@ public async Task UpdateRelativeUrlWithoutPathPrefixKeepsGlobalPrefix()
var relativeAssetPath = "images/funny-image.png";
var assemblerResult = await ResolveUrlForBuildMode(relativeAssetPath, assemblerBuild: true, pathPrefix: null);
- assemblerResult.Should().Be("/docs/setup/images/funny-image.png");
+ assemblerResult.Should().AllBe("/docs/setup/images/funny-image.png");
}
[Fact]
@@ -46,16 +46,15 @@ public async Task UpdateRelativeUrlAppliesCustomPathPrefix()
var relativeAssetPath = "images/image.png";
var assemblerResult = await ResolveUrlForBuildMode(relativeAssetPath, assemblerBuild: true, pathPrefix: "custom");
- assemblerResult.Should().Be("/docs/custom/setup/images/image.png");
+ assemblerResult.Should().AllBe("/docs/custom/setup/images/image.png");
}
///
/// Resolves a relative asset URL the same way the assembler would for a single markdown file, using the provided navigation path prefix.
///
- private async Task ResolveUrlForBuildMode(string relativeAssetPath, bool assemblerBuild, string? pathPrefix)
+ private async Task ResolveUrlForBuildMode(string relativeAssetPath, bool assemblerBuild, string? pathPrefix)
{
const string guideRelativePath = "setup/guide.md";
- var navigationUrl = BuildNavigationUrl(pathPrefix, guideRelativePath);
var files = new Dictionary
{
["docs/docset.yml"] = new(
@@ -66,7 +65,13 @@ private async Task ResolveUrlForBuildMode(string relativeAssetPath, bool
- file: {guideRelativePath}
"""
),
- ["docs/index.md"] = new("# Home"),
+ ["docs/index.md"] = new(
+ $"""
+ # Home
+
+ 
+ """
+ ),
["docs/" + guideRelativePath] = new(
$"""
# Guide
@@ -97,38 +102,45 @@ private async Task ResolveUrlForBuildMode(string relativeAssetPath, bool
await documentationSet.ResolveDirectoryTree(TestContext.Current.CancellationToken);
// Normalize path for cross-platform compatibility (Windows uses backslashes)
- var normalizedPath = guideRelativePath.Replace('/', Path.DirectorySeparatorChar);
- if (documentationSet.TryFindDocumentByRelativePath(normalizedPath) is not MarkdownFile markdownFile)
- throw new InvalidOperationException($"Failed to resolve markdown file for test. Tried path: {normalizedPath}");
-
- // For assembler builds DocumentationSetNavigation seeds MarkdownNavigationLookup with navigation items whose Url already
- // includes the computed path_prefix. To exercise the same branch in isolation, inject a stub navigation entry with the
- // expected Url (and minimal metadata for the surrounding API contract).
- _ = documentationSet.NavigationDocumentationFileLookup.Remove(markdownFile);
- documentationSet.NavigationDocumentationFileLookup.Add(markdownFile, new NavigationItemStub(navigationUrl));
- documentationSet.NavigationDocumentationFileLookup.TryGetValue(markdownFile, out var navigation).Should()
- .BeTrue("navigation lookup should contain current page");
- navigation?.Url.Should().Be(navigationUrl);
-
- var parserState = new ParserState(buildContext)
+ (string, string)[] pathsToTest = [(guideRelativePath.Replace('/', Path.DirectorySeparatorChar), relativeAssetPath), ("index.md", $"setup{Path.DirectorySeparatorChar}{relativeAssetPath}")];
+ List toReturn = [];
+
+ foreach (var normalizedPath in pathsToTest)
{
- MarkdownSourcePath = markdownFile.SourceFile,
- YamlFrontMatter = null,
- CrossLinkResolver = documentationSet.CrossLinkResolver,
- TryFindDocument = file => documentationSet.TryFindDocument(file),
- TryFindDocumentByRelativePath = path => documentationSet.TryFindDocumentByRelativePath(path),
- NavigationTraversable = documentationSet
- };
+ if (documentationSet.TryFindDocumentByRelativePath(normalizedPath.Item1) is not MarkdownFile markdownFile)
+ throw new InvalidOperationException($"Failed to resolve markdown file for test. Tried path: {normalizedPath}");
+
+ var navigationUrl = BuildNavigationUrl(pathPrefix, normalizedPath.Item1);
+ // For assembler builds DocumentationSetNavigation seeds MarkdownNavigationLookup with navigation items whose Url already
+ // includes the computed path_prefix. To exercise the same branch in isolation, inject a stub navigation entry with the
+ // expected Url (and minimal metadata for the surrounding API contract).
+ _ = documentationSet.NavigationDocumentationFileLookup.Remove(markdownFile);
+ documentationSet.NavigationDocumentationFileLookup.Add(markdownFile, new NavigationItemStub(navigationUrl));
+ documentationSet.NavigationDocumentationFileLookup.TryGetValue(markdownFile, out var navigation).Should()
+ .BeTrue("navigation lookup should contain current page");
+ navigation?.Url.Should().Be(navigationUrl);
+
+ var parserState = new ParserState(buildContext)
+ {
+ MarkdownSourcePath = markdownFile.SourceFile,
+ YamlFrontMatter = null,
+ CrossLinkResolver = documentationSet.CrossLinkResolver,
+ TryFindDocument = file => documentationSet.TryFindDocument(file),
+ TryFindDocumentByRelativePath = path => documentationSet.TryFindDocumentByRelativePath(path),
+ NavigationTraversable = documentationSet
+ };
+
+ var context = new ParserContext(parserState);
+ context.TryFindDocument(context.MarkdownSourcePath).Should().BeSameAs(markdownFile);
+ context.Build.AssemblerBuild.Should().Be(assemblerBuild);
- var context = new ParserContext(parserState);
- context.TryFindDocument(context.MarkdownSourcePath).Should().BeSameAs(markdownFile);
- context.Build.AssemblerBuild.Should().Be(assemblerBuild);
+ toReturn.Add(DiagnosticLinkInlineParser.UpdateRelativeUrl(context, normalizedPath.Item2));
- var resolved = DiagnosticLinkInlineParser.UpdateRelativeUrl(context, relativeAssetPath);
+ }
await collector.StopAsync(TestContext.Current.CancellationToken);
- return resolved;
+ return toReturn.ToArray();
}
///
@@ -142,6 +154,12 @@ private static string BuildNavigationUrl(string? pathPrefix, string docRelativeP
if (docPath.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
docPath = docPath[..^3];
+ // Handle index.md
+ if (docPath.EndsWith("/index", StringComparison.OrdinalIgnoreCase))
+ docPath = docPath[..^6];
+ else if (docPath.Equals("index", StringComparison.OrdinalIgnoreCase))
+ docPath = string.Empty;
+
var segments = new List();
if (!string.IsNullOrWhiteSpace(pathPrefix))
segments.Add(pathPrefix.Trim('/'));