find the path to_path, relative to from_path.

TODO: the claim below is wrong, because joinSlash cannot do "./" traversal correctly. for instance joinSlash("a/b/c.txt", "./") === "a/b/c.txt/", but you'd expect it to be "a/b/" if we had correctly resolved the path. which is why the output from this function will not work with joinSlash, but it will work with resolveAsUrl or URL.parse (when you provide from_path as the base path, and from_path is NOT a relative path, otherwise the URL constructor will fail).

if we call the result rel_path, then joining the from_path with rel_path and normalizing it should give you back to_path.

note that both from_path and to_path must have a common root folder in their path strings. for instance, if both paths begin with a relative segment "./", then it will be assumed that both paths are referring to the same common root ancestral directory. however, if for instance, from_path begins with a "C:/" segment, while to_path begins with either "./" or "D:/" or http:// segment, then this function will fail, as it will not be possible for it to navigate/transcend from one point of reference to a completely different point of reference.

so, to be safe, wherever you are certain that both paths are of a certain common type: before passing them here, you should either apply the ensureStartDotSlash function for relative paths, or apply the ensureStartSlash for absolute local paths, or write a custom "ensure" function for your situation. (for example, you could write an "ensureHttp" function that ensures that your path begins with "http").

Error an error will be thrown if there isn't any common ancestral directory between the two provided paths.

import { assertEquals, assertThrows } from "jsr:@std/assert"

// aliasing our functions for brevity
const eq = assertEquals, err = assertThrows, fn = relativePath

eq(fn(
"././hello/world/a/b/c/d/g/../e.txt",
"././hello/world/a/b/x/y/w/../z/",
), "../../x/y/z/")
eq(fn(
"././hello/world/a/b/c/d/g/../e.txt",
"././hello/world/a/b/x/y/w/../z/e.md",
), "../../x/y/z/e.md")
eq(fn(
".\\./hello\\world\\a/b\\c/d/g/../",
"././hello/world/a/b/x/y/w/../z/e.md",
), "../../x/y/z/e.md")
eq(fn(
"././hello/world/a/b/c/d/",
"././hello/world/a/b/x/y/w/../z/e.md",
), "../../x/y/z/e.md")
eq(fn(
"././hello/world/a/b/c/d/g/../",
"././hello/world/a/b/x/y/w/../z/e.md",
), "../../x/y/z/e.md")
eq(fn(
"././hello/world/a/b/c/d/",
"././hello/world/a/b/x/y/w/../z/",
), "../../x/y/z/")
eq(fn(
"./././e.txt",
"./e.md",
), "./e.md")
eq(fn(
"/e.txt",
"/e.md",
), "./e.md")
eq(fn(
"C:/e.txt",
"C:/e.md",
), "./e.md")
eq(fn(
"././hello/world/a/b/c/d/g/../e.txt",
"././hello/world/a/k/../b/q/../c/d/e.md",
), "./e.md")
eq(fn(
"./",
"./",
), "./")
eq(fn(
"/",
"/",
), "./")

// there is no common ancestral root between the two paths (since one is absolute, while the other is relative)
err(() => fn(
"/e.txt",
"./e.md",
))
// there is no common ancestral root between the two paths
err(() => fn(
"C:/e.txt",
"D:/e.md",
))
// there is no common ancestral root between the two paths
err(() => fn(
"http://e.txt",
"./e.md",
))
// there is no common ancestral root between the two paths
err(() => fn(
"file:///C:/e.txt",
"C:/e.md",
))