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"));
}
}