From 5806f924e44a956512d8572eed47445f4716e3d5 Mon Sep 17 00:00:00 2001 From: Kambiz Khojasteh Date: Mon, 26 Jan 2026 23:29:44 +0800 Subject: [PATCH 1/4] Fix asset resolution logic in ContextAwareUrlTransformer Updated ResolveRelativeUrl to correctly combine the documentation scope's RootUrl with relative asset paths, ensuring proper resolution regardless of whether RootUrl is absolute or relative. Increased test coverage. --- src/Routing/ContextAwareUrlTransformer.cs | 26 ++++++---- .../ContextAwareUrlTransformerTests.cs | 50 ++++++++++++++++--- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/Routing/ContextAwareUrlTransformer.cs b/src/Routing/ContextAwareUrlTransformer.cs index 5e7619a0..d665bd0d 100644 --- a/src/Routing/ContextAwareUrlTransformer.cs +++ b/src/Routing/ContextAwareUrlTransformer.cs @@ -96,16 +96,24 @@ public virtual bool TryTransformUrl(string urlString, [NotNullWhen(true)] out Ur var scope = Context.AddressProvider.ActiveScope; // Asset resolution - if - ( - scope.Model is TopicModel currentTopic && - currentTopic.Source is IFileBasedTopic sourceTopic && - PathHelper.TryNormalizePath(Path.Combine(sourceTopic.FilePath, "..", urlPath), out var filePath) && - File.Exists(filePath) - ) + if (scope.Model is TopicModel currentTopic && currentTopic.Source is IFileBasedTopic sourceTopic) { - transformedUrl = currentTopic.Url.Combine("../" + urlString); - return true; + var topicDirectory = Path.GetDirectoryName(sourceTopic.FilePath) ?? string.Empty; + if (PathHelper.TryNormalizePath(Path.Combine(topicDirectory, urlPath), out var filePath) && File.Exists(filePath)) + { + if (scope.RootUrl.IsAbsoluteUri) + { + transformedUrl = scope.RootUrl.Combine(urlString); + } + else + { + var relativePath = "../" + scope.RootUrl + urlString; + transformedUrl = currentTopic.Url.IsAbsoluteUri + ? new Uri(currentTopic.Url, relativePath) + : currentTopic.Url.Combine(relativePath); + } + return true; + } } // Site-relative resolution diff --git a/tests/Routing/ContextAwareUrlTransformerTests.cs b/tests/Routing/ContextAwareUrlTransformerTests.cs index 7abf9b14..b30527d9 100644 --- a/tests/Routing/ContextAwareUrlTransformerTests.cs +++ b/tests/Routing/ContextAwareUrlTransformerTests.cs @@ -386,7 +386,7 @@ public void TryTransformUrl_WithNestedTopicFromDifferentBranch_ResolvesToCorrect } [Test] - public void TryTransformUrl_WithFileBasedTopicAndExistingAsset_ReturnsAssetUrl() + public void TryTransformUrl_WithFileBasedTopicAndExistingAssetInDifferentFolders_ReturnsAssetUrl() { var directory = Path.GetTempPath(); var topicFile = Path.Combine(directory, "guides", "test-topic.md"); @@ -402,7 +402,7 @@ public void TryTransformUrl_WithFileBasedTopicAndExistingAsset_ReturnsAssetUrl() context.Topics.TryGetById("TestTopic", out var contextualTopic); - using var _ = context.AddressProvider.BeginScope("test", contextualTopic); + using var _ = context.AddressProvider.BeginScope("test/topics", contextualTopic); var result = transformer.TryTransformUrl("../assets/license.txt", out var transformedUrl); @@ -421,6 +421,42 @@ public void TryTransformUrl_WithFileBasedTopicAndExistingAsset_ReturnsAssetUrl() } } + [Test] + public void TryTransformUrl_WithFileBasedTopicAndExistingAssetInSameFolder_ReturnsAssetUrl() + { + var directory = Path.GetTempPath(); + var topicFile = Path.Combine(directory, "docs", "test-topic.md"); + var assetFile = Path.Combine(directory, "docs", "license.txt"); + try + { + Directory.CreateDirectory(Path.GetDirectoryName(assetFile)!); + File.WriteAllText(assetFile, "fake license content"); + + var fileBasedTopic = new MarkdownFileTopic("TestTopic", topicFile); + using var context = MockHelper.CreateDocumentationContext([fileBasedTopic]); + var transformer = new ContextAwareUrlTransformer(context); + + context.Topics.TryGetById("TestTopic", out var contextualTopic); + + using var _ = context.AddressProvider.BeginScope("test/topics", contextualTopic); + + var result = transformer.TryTransformUrl("license.txt", out var transformedUrl); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Is.True); + Assert.That(transformedUrl, Is.EqualTo(new Uri("https://example.com/license.txt"))); + } + } + finally + { + if (File.Exists(assetFile)) + File.Delete(assetFile); + if (Directory.Exists(Path.GetDirectoryName(assetFile)!)) + Directory.Delete(Path.GetDirectoryName(assetFile)!); + } + } + [Test] public void TryTransformUrl_WithFileBasedTopicAndBeyondRootRelativeAsset_ReturnsFalse() { @@ -430,7 +466,7 @@ public void TryTransformUrl_WithFileBasedTopicAndBeyondRootRelativeAsset_Returns context.Topics.TryGetById("TestTopic", out var contextualTopic); - using var _ = context.AddressProvider.BeginScope("test", contextualTopic); + using var _ = context.AddressProvider.BeginScope("test/topics", contextualTopic); var result = transformer.TryTransformUrl("../../../license.txt", out var transformedUrl); @@ -457,14 +493,14 @@ public void TryTransformUrl_WithNonFileBasedTopicAndExistingAsset_FallsBackToSit context.Topics.TryGetById("NonFileBasedTopic", out var contextualTopic); - using var _ = context.AddressProvider.BeginScope("test", contextualTopic); + using var _ = context.AddressProvider.BeginScope("test/topics", contextualTopic); var result = transformer.TryTransformUrl("license.txt", out var transformedUrl); using (Assert.EnterMultipleScope()) { Assert.That(result, Is.True); - Assert.That(transformedUrl?.ToString(), Is.EqualTo("../license.txt")); + Assert.That(transformedUrl?.ToString(), Is.EqualTo("../../license.txt")); } } finally @@ -485,14 +521,14 @@ public void TryTransformUrl_WithFileBasedTopicAndNonExistingAsset_FallsBackToSit context.Topics.TryGetById("TestTopic", out var contextualTopic); - using var _ = context.AddressProvider.BeginScope("test", contextualTopic); + using var _ = context.AddressProvider.BeginScope("test/topics", contextualTopic); var result = transformer.TryTransformUrl("nonexistent.png", out var transformedUrl); using (Assert.EnterMultipleScope()) { Assert.That(result, Is.True); - Assert.That(transformedUrl?.ToString(), Is.EqualTo("../nonexistent.png")); + Assert.That(transformedUrl?.ToString(), Is.EqualTo("../../nonexistent.png")); } } From ed6555a4923129f3d7f2298b6b6a9f97bfac60d6 Mon Sep 17 00:00:00 2001 From: Kambiz Khojasteh Date: Mon, 26 Jan 2026 23:33:59 +0800 Subject: [PATCH 2/4] Update NUnit3TestAdapter to version 6.1.0 --- tests/Kampute.DocToolkit.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Kampute.DocToolkit.Test.csproj b/tests/Kampute.DocToolkit.Test.csproj index ce94d7d8..cf70db01 100644 --- a/tests/Kampute.DocToolkit.Test.csproj +++ b/tests/Kampute.DocToolkit.Test.csproj @@ -23,7 +23,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From c5b8d80c338ec9a19b009440950bdeb9ccdd33cf Mon Sep 17 00:00:00 2001 From: Kambiz Khojasteh Date: Mon, 26 Jan 2026 23:34:46 +0800 Subject: [PATCH 3/4] Bump version to 2.2.1 in project file --- src/Kampute.DocToolkit.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kampute.DocToolkit.csproj b/src/Kampute.DocToolkit.csproj index cff9ff64..ae21b1be 100644 --- a/src/Kampute.DocToolkit.csproj +++ b/src/Kampute.DocToolkit.csproj @@ -4,7 +4,7 @@ netstandard2.1 Kampute.DocDotLib Provides extensible pipeline for generating .NET API documentation by transforming assembly metadata and XML documentation into structured models, with automatic cross-reference resolution, support for multiple output formats (HTML, Markdown), and integration of conceptual topics. - 2.2.0 + 2.2.1 Kampute Kambiz Khojasteh Copyright (C) Kampute From a2a1f96abfadf319a6833a904166f60fdb13aa26 Mon Sep 17 00:00:00 2001 From: Kambiz Khojasteh Date: Tue, 27 Jan 2026 15:19:43 +0800 Subject: [PATCH 4/4] Refactor asset URL transformation tests for improved clarity and coverage --- .../ContextAwareUrlTransformerTests.cs | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/tests/Routing/ContextAwareUrlTransformerTests.cs b/tests/Routing/ContextAwareUrlTransformerTests.cs index b30527d9..c09b77bb 100644 --- a/tests/Routing/ContextAwareUrlTransformerTests.cs +++ b/tests/Routing/ContextAwareUrlTransformerTests.cs @@ -386,7 +386,43 @@ public void TryTransformUrl_WithNestedTopicFromDifferentBranch_ResolvesToCorrect } [Test] - public void TryTransformUrl_WithFileBasedTopicAndExistingAssetInDifferentFolders_ReturnsAssetUrl() + public void TryTransformUrl_WithFileBasedTopicAndExistingAsset_SameFolder_ReturnsAssetUrl() + { + var directory = Path.GetTempPath(); + var topicFile = Path.Combine(directory, "docs", "test-topic.md"); + var assetFile = Path.Combine(directory, "docs", "license.txt"); + try + { + Directory.CreateDirectory(Path.GetDirectoryName(assetFile)!); + File.WriteAllText(assetFile, "fake license content"); + + var fileBasedTopic = new MarkdownFileTopic("TestTopic", topicFile); + using var context = MockHelper.CreateDocumentationContext([fileBasedTopic]); + var transformer = new ContextAwareUrlTransformer(context); + + context.Topics.TryGetById("TestTopic", out var contextualTopic); + + using var _ = context.AddressProvider.BeginScope("test/topics", contextualTopic); + + var result = transformer.TryTransformUrl("license.txt", out var transformedUrl); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Is.True); + Assert.That(transformedUrl, Is.EqualTo(new Uri("https://example.com/license.txt"))); + } + } + finally + { + if (File.Exists(assetFile)) + File.Delete(assetFile); + if (Directory.Exists(Path.GetDirectoryName(assetFile)!)) + Directory.Delete(Path.GetDirectoryName(assetFile)!); + } + } + + [Test] + public void TryTransformUrl_WithFileBasedTopicAndExistingAsset_SiblingFolders_ReturnsAssetUrl() { var directory = Path.GetTempPath(); var topicFile = Path.Combine(directory, "guides", "test-topic.md"); @@ -422,11 +458,11 @@ public void TryTransformUrl_WithFileBasedTopicAndExistingAssetInDifferentFolders } [Test] - public void TryTransformUrl_WithFileBasedTopicAndExistingAssetInSameFolder_ReturnsAssetUrl() + public void TryTransformUrl_WithFileBasedTopicAndExistingAsset_UnrelatedFolders_ReturnsAssetUrl() { var directory = Path.GetTempPath(); - var topicFile = Path.Combine(directory, "docs", "test-topic.md"); - var assetFile = Path.Combine(directory, "docs", "license.txt"); + var topicFile = Path.Combine(directory, "topics/guides", "test-topic.md"); + var assetFile = Path.Combine(directory, "assets/legals", "license.txt"); try { Directory.CreateDirectory(Path.GetDirectoryName(assetFile)!); @@ -440,12 +476,12 @@ public void TryTransformUrl_WithFileBasedTopicAndExistingAssetInSameFolder_Retur using var _ = context.AddressProvider.BeginScope("test/topics", contextualTopic); - var result = transformer.TryTransformUrl("license.txt", out var transformedUrl); + var result = transformer.TryTransformUrl("../../assets/legals/license.txt", out var transformedUrl); using (Assert.EnterMultipleScope()) { Assert.That(result, Is.True); - Assert.That(transformedUrl, Is.EqualTo(new Uri("https://example.com/license.txt"))); + Assert.That(transformedUrl, Is.EqualTo(new Uri("https://example.com/assets/legals/license.txt"))); } } finally