diff --git a/src/Docfx.Common/Path/PathUtility.cs b/src/Docfx.Common/Path/PathUtility.cs index 0a18c25a1db..0b7435f999b 100644 --- a/src/Docfx.Common/Path/PathUtility.cs +++ b/src/Docfx.Common/Path/PathUtility.cs @@ -69,14 +69,14 @@ public static string MakeRelativePath(string basePath, string absolutePath) return absolutePath; } - Uri relativeUri = fromUri.MakeRelativeUri(toUri); - string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); - - if (string.Equals(toUri.Scheme, "FILE", StringComparison.InvariantCultureIgnoreCase)) + if (toUri.IsFile && !toUri.OriginalString.StartsWith("file://", StringComparison.InvariantCultureIgnoreCase)) { - relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + return Path.GetRelativePath(basePath, absolutePath).BackSlashToForwardSlash(); } + Uri relativeUri = fromUri.MakeRelativeUri(toUri); + string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); + return relativePath.BackSlashToForwardSlash(); } diff --git a/test/Docfx.Common.Tests/PathUtilityTest.cs b/test/Docfx.Common.Tests/PathUtilityTest.cs new file mode 100644 index 00000000000..17a6f05c8fe --- /dev/null +++ b/test/Docfx.Common.Tests/PathUtilityTest.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Xunit; + +namespace Docfx.Common.Tests; + +public class PathUtilityTest +{ + [Theory] + [MemberData(nameof(TestData.AdditionalTests), MemberType = typeof(TestData))] + public void TestMakeRelativePath(string basePath, string targetPath, string expected) + { + // Act + var result = PathUtility.MakeRelativePath(basePath, targetPath); + + // Assert + result.Should().Be(expected); + } + + [Theory] + [MemberData(nameof(TestData.EscapedPaths), MemberType = typeof(TestData))] + public void TestMakeRelativePathWithEncodedPath(string inputPath) + { + // Arrange + string basePath = "./"; + var expected = inputPath; + + // Act + var result = PathUtility.MakeRelativePath(basePath, inputPath); + + // Assert + result.Should().Be(expected); + } + + private static class TestData + { + public static TheoryData AdditionalTests = new() + { + { @"/a/b/d", @"/a/b/file.md", @"../file.md"}, // root relative path + { @"~/a/b/d", @"~/a/b/file.md", @"../file.md"}, // user home directory relative path + { @"./", @"\\UNCPath\file.md", @"//UNCPath/file.md"}, // UNC path + { @"./", @"file:///C:/temp/test.md", @"file:/C:/temp/test.md"}, // `file:` Uri path + { @"file:///C:/temp", @"file:///C:/temp/test.md", @"test.md"}, // `file:` Uri relative path + { @"/temp/dir", @"/temp/dir/subdir/", @"subdir/"}, // If target path endsWith directory separator char. resolved path should contain directory separator. + }; + + public static TheoryData EscapedPaths = new() + { + "EscapedHypen(%2D).md", // Contains escaped hypen char + "EscapedSpace(%20)_with_NonAsciiChar(α).md", // Contains escaped space char and non-unicode char + }; + } +}