Skip to content

Commit 92be47f

Browse files
authored
Fix image path resolution on index pages (#2242)
* Handle index page scenarios * Adjust tests to check extra situations
1 parent e71bd65 commit 92be47f

File tree

2 files changed

+57
-34
lines changed

2 files changed

+57
-34
lines changed

src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,12 @@ public static string UpdateRelativeUrl(ParserContext context, string url)
404404
// Acquire navigation-aware path
405405
if (context.NavigationTraversable.NavigationDocumentationFileLookup.TryGetValue(currentMarkdown, out var currentNavigation))
406406
{
407-
var uri = new Uri(new UriBuilder("http", "localhost", 80, currentNavigation.Url).Uri, url);
407+
// Check if we're handling relative to an index file, which has an unique URL resolution rule
408+
var baseUrl = currentMarkdown.FileName.Equals("index.md", StringComparison.OrdinalIgnoreCase)
409+
? $"{currentNavigation.Url.TrimEnd('/')}/"
410+
: currentNavigation.Url;
411+
412+
var uri = new Uri(new UriBuilder("http", "localhost", 80, baseUrl).Uri, url);
408413
newUrl = uri.AbsolutePath;
409414
}
410415
else

tests/Elastic.Markdown.Tests/Inline/ImagePathResolutionTests.cs

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ public async Task UpdateRelativeUrlUsesNavigationPathWhenAssemblerBuildEnabled()
2727
var nonAssemblerResult = await ResolveUrlForBuildMode(relativeAssetPath, assemblerBuild: false, pathPrefix: "this-is-not-relevant");
2828
var assemblerResult = await ResolveUrlForBuildMode(relativeAssetPath, assemblerBuild: true, pathPrefix: "platform");
2929

30-
nonAssemblerResult.Should().Be("/docs/setup/images/pic.png");
31-
assemblerResult.Should().Be("/docs/platform/setup/images/pic.png");
30+
nonAssemblerResult.Should().AllBe("/docs/setup/images/pic.png");
31+
assemblerResult.Should().AllBe("/docs/platform/setup/images/pic.png");
3232
}
3333

3434
[Fact]
@@ -37,7 +37,7 @@ public async Task UpdateRelativeUrlWithoutPathPrefixKeepsGlobalPrefix()
3737
var relativeAssetPath = "images/funny-image.png";
3838
var assemblerResult = await ResolveUrlForBuildMode(relativeAssetPath, assemblerBuild: true, pathPrefix: null);
3939

40-
assemblerResult.Should().Be("/docs/setup/images/funny-image.png");
40+
assemblerResult.Should().AllBe("/docs/setup/images/funny-image.png");
4141
}
4242

4343
[Fact]
@@ -46,16 +46,15 @@ public async Task UpdateRelativeUrlAppliesCustomPathPrefix()
4646
var relativeAssetPath = "images/image.png";
4747
var assemblerResult = await ResolveUrlForBuildMode(relativeAssetPath, assemblerBuild: true, pathPrefix: "custom");
4848

49-
assemblerResult.Should().Be("/docs/custom/setup/images/image.png");
49+
assemblerResult.Should().AllBe("/docs/custom/setup/images/image.png");
5050
}
5151

5252
/// <summary>
5353
/// Resolves a relative asset URL the same way the assembler would for a single markdown file, using the provided navigation path prefix.
5454
/// </summary>
55-
private async Task<string> ResolveUrlForBuildMode(string relativeAssetPath, bool assemblerBuild, string? pathPrefix)
55+
private async Task<string[]> ResolveUrlForBuildMode(string relativeAssetPath, bool assemblerBuild, string? pathPrefix)
5656
{
5757
const string guideRelativePath = "setup/guide.md";
58-
var navigationUrl = BuildNavigationUrl(pathPrefix, guideRelativePath);
5958
var files = new Dictionary<string, MockFileData>
6059
{
6160
["docs/docset.yml"] = new(
@@ -66,7 +65,13 @@ private async Task<string> ResolveUrlForBuildMode(string relativeAssetPath, bool
6665
- file: {guideRelativePath}
6766
"""
6867
),
69-
["docs/index.md"] = new("# Home"),
68+
["docs/index.md"] = new(
69+
$"""
70+
# Home
71+
72+
![Alt](setup/{relativeAssetPath})
73+
"""
74+
),
7075
["docs/" + guideRelativePath] = new(
7176
$"""
7277
# Guide
@@ -97,38 +102,45 @@ private async Task<string> ResolveUrlForBuildMode(string relativeAssetPath, bool
97102
await documentationSet.ResolveDirectoryTree(TestContext.Current.CancellationToken);
98103

99104
// Normalize path for cross-platform compatibility (Windows uses backslashes)
100-
var normalizedPath = guideRelativePath.Replace('/', Path.DirectorySeparatorChar);
101-
if (documentationSet.TryFindDocumentByRelativePath(normalizedPath) is not MarkdownFile markdownFile)
102-
throw new InvalidOperationException($"Failed to resolve markdown file for test. Tried path: {normalizedPath}");
103-
104-
// For assembler builds DocumentationSetNavigation seeds MarkdownNavigationLookup with navigation items whose Url already
105-
// includes the computed path_prefix. To exercise the same branch in isolation, inject a stub navigation entry with the
106-
// expected Url (and minimal metadata for the surrounding API contract).
107-
_ = documentationSet.NavigationDocumentationFileLookup.Remove(markdownFile);
108-
documentationSet.NavigationDocumentationFileLookup.Add(markdownFile, new NavigationItemStub(navigationUrl));
109-
documentationSet.NavigationDocumentationFileLookup.TryGetValue(markdownFile, out var navigation).Should()
110-
.BeTrue("navigation lookup should contain current page");
111-
navigation?.Url.Should().Be(navigationUrl);
112-
113-
var parserState = new ParserState(buildContext)
105+
(string, string)[] pathsToTest = [(guideRelativePath.Replace('/', Path.DirectorySeparatorChar), relativeAssetPath), ("index.md", $"setup{Path.DirectorySeparatorChar}{relativeAssetPath}")];
106+
List<string> toReturn = [];
107+
108+
foreach (var normalizedPath in pathsToTest)
114109
{
115-
MarkdownSourcePath = markdownFile.SourceFile,
116-
YamlFrontMatter = null,
117-
CrossLinkResolver = documentationSet.CrossLinkResolver,
118-
TryFindDocument = file => documentationSet.TryFindDocument(file),
119-
TryFindDocumentByRelativePath = path => documentationSet.TryFindDocumentByRelativePath(path),
120-
NavigationTraversable = documentationSet
121-
};
110+
if (documentationSet.TryFindDocumentByRelativePath(normalizedPath.Item1) is not MarkdownFile markdownFile)
111+
throw new InvalidOperationException($"Failed to resolve markdown file for test. Tried path: {normalizedPath}");
112+
113+
var navigationUrl = BuildNavigationUrl(pathPrefix, normalizedPath.Item1);
114+
// For assembler builds DocumentationSetNavigation seeds MarkdownNavigationLookup with navigation items whose Url already
115+
// includes the computed path_prefix. To exercise the same branch in isolation, inject a stub navigation entry with the
116+
// expected Url (and minimal metadata for the surrounding API contract).
117+
_ = documentationSet.NavigationDocumentationFileLookup.Remove(markdownFile);
118+
documentationSet.NavigationDocumentationFileLookup.Add(markdownFile, new NavigationItemStub(navigationUrl));
119+
documentationSet.NavigationDocumentationFileLookup.TryGetValue(markdownFile, out var navigation).Should()
120+
.BeTrue("navigation lookup should contain current page");
121+
navigation?.Url.Should().Be(navigationUrl);
122+
123+
var parserState = new ParserState(buildContext)
124+
{
125+
MarkdownSourcePath = markdownFile.SourceFile,
126+
YamlFrontMatter = null,
127+
CrossLinkResolver = documentationSet.CrossLinkResolver,
128+
TryFindDocument = file => documentationSet.TryFindDocument(file),
129+
TryFindDocumentByRelativePath = path => documentationSet.TryFindDocumentByRelativePath(path),
130+
NavigationTraversable = documentationSet
131+
};
132+
133+
var context = new ParserContext(parserState);
134+
context.TryFindDocument(context.MarkdownSourcePath).Should().BeSameAs(markdownFile);
135+
context.Build.AssemblerBuild.Should().Be(assemblerBuild);
122136

123-
var context = new ParserContext(parserState);
124-
context.TryFindDocument(context.MarkdownSourcePath).Should().BeSameAs(markdownFile);
125-
context.Build.AssemblerBuild.Should().Be(assemblerBuild);
137+
toReturn.Add(DiagnosticLinkInlineParser.UpdateRelativeUrl(context, normalizedPath.Item2));
126138

127-
var resolved = DiagnosticLinkInlineParser.UpdateRelativeUrl(context, relativeAssetPath);
139+
}
128140

129141
await collector.StopAsync(TestContext.Current.CancellationToken);
130142

131-
return resolved;
143+
return toReturn.ToArray();
132144
}
133145

134146
/// <summary>
@@ -142,6 +154,12 @@ private static string BuildNavigationUrl(string? pathPrefix, string docRelativeP
142154
if (docPath.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
143155
docPath = docPath[..^3];
144156

157+
// Handle index.md
158+
if (docPath.EndsWith("/index", StringComparison.OrdinalIgnoreCase))
159+
docPath = docPath[..^6];
160+
else if (docPath.Equals("index", StringComparison.OrdinalIgnoreCase))
161+
docPath = string.Empty;
162+
145163
var segments = new List<string>();
146164
if (!string.IsNullOrWhiteSpace(pathPrefix))
147165
segments.Add(pathPrefix.Trim('/'));

0 commit comments

Comments
 (0)