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 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/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 - + diff --git a/tests/Routing/ContextAwareUrlTransformerTests.cs b/tests/Routing/ContextAwareUrlTransformerTests.cs index 7abf9b14..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_WithFileBasedTopicAndExistingAsset_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"); @@ -402,7 +438,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 +457,42 @@ public void TryTransformUrl_WithFileBasedTopicAndExistingAsset_ReturnsAssetUrl() } } + [Test] + public void TryTransformUrl_WithFileBasedTopicAndExistingAsset_UnrelatedFolders_ReturnsAssetUrl() + { + var directory = Path.GetTempPath(); + 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)!); + 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("../../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/assets/legals/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 +502,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 +529,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 +557,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")); } }