From 4769fc0283923ebe58b62497fb5837f5c61967d2 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 20 Nov 2025 10:37:58 -0300 Subject: [PATCH 1/2] Handle index page scenarios --- .../Myst/InlineParsers/DiagnosticLinkInlineParser.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 From 0d50bb1320e66f6b25162731207a38d9cd3ce71e Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 20 Nov 2025 10:40:56 -0300 Subject: [PATCH 2/2] Adjust tests to check extra situations --- .../Inline/ImagePathResolutionTests.cs | 84 +++++++++++-------- 1 file changed, 51 insertions(+), 33 deletions(-) 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 + + ![Alt](setup/{relativeAssetPath}) + """ + ), ["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('/'));