Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Kampute.DocToolkit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>netstandard2.1</TargetFramework>
<Title>Kampute.DocDotLib</Title>
<Description>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.</Description>
<Version>2.2.0</Version>
<Version>2.2.1</Version>
<Company>Kampute</Company>
<Authors>Kambiz Khojasteh</Authors>
<Copyright>Copyright (C) Kampute</Copyright>
Expand Down
26 changes: 17 additions & 9 deletions src/Routing/ContextAwareUrlTransformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/Kampute.DocToolkit.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="6.0.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
</ItemGroup>

<ItemGroup>
Expand Down
86 changes: 79 additions & 7 deletions tests/Routing/ContextAwareUrlTransformerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HtmlFormat>([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");
Expand All @@ -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);

Expand All @@ -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<HtmlFormat>([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()
{
Expand All @@ -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);

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

Expand Down