std/path/windows/relative.ts

129 lines
4.2 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
import { CHAR_BACKWARD_SLASH } from "../_common/constants.ts";
import { resolve } from "./resolve.ts";
import { assertArgs } from "../_common/relative.ts";
/**
* Return the relative path from `from` to `to` based on current working directory.
*
* An example in windws, for instance:
* from = 'C:\\orandea\\test\\aaa'
* to = 'C:\\orandea\\impl\\bbb'
* The output of the function should be: '..\\..\\impl\\bbb'
*
* @example Usage
* ```ts
* import { relative } from "@std/path/windows/relative";
* import { assertEquals } from "@std/assert";
*
* const relativePath = relative("C:\\foobar\\test\\aaa", "C:\\foobar\\impl\\bbb");
* assertEquals(relativePath, "..\\..\\impl\\bbb");
* ```
*
* @param from The path from which to calculate the relative path
* @param to The path to which to calculate the relative path
* @returns The relative path from `from` to `to`
*/
export function relative(from: string, to: string): string {
assertArgs(from, to);
const fromOrig = resolve(from);
const toOrig = resolve(to);
if (fromOrig === toOrig) return "";
from = fromOrig.toLowerCase();
to = toOrig.toLowerCase();
if (from === to) return "";
// Trim any leading backslashes
let fromStart = 0;
let fromEnd = from.length;
for (; fromStart < fromEnd; ++fromStart) {
if (from.charCodeAt(fromStart) !== CHAR_BACKWARD_SLASH) break;
}
// Trim trailing backslashes (applicable to UNC paths only)
for (; fromEnd - 1 > fromStart; --fromEnd) {
if (from.charCodeAt(fromEnd - 1) !== CHAR_BACKWARD_SLASH) break;
}
const fromLen = fromEnd - fromStart;
// Trim any leading backslashes
let toStart = 0;
let toEnd = to.length;
for (; toStart < toEnd; ++toStart) {
if (to.charCodeAt(toStart) !== CHAR_BACKWARD_SLASH) break;
}
// Trim trailing backslashes (applicable to UNC paths only)
for (; toEnd - 1 > toStart; --toEnd) {
if (to.charCodeAt(toEnd - 1) !== CHAR_BACKWARD_SLASH) break;
}
const toLen = toEnd - toStart;
// Compare paths to find the longest common path from root
const length = fromLen < toLen ? fromLen : toLen;
let lastCommonSep = -1;
let i = 0;
for (; i <= length; ++i) {
if (i === length) {
if (toLen > length) {
if (to.charCodeAt(toStart + i) === CHAR_BACKWARD_SLASH) {
// We get here if `from` is the exact base path for `to`.
// For example: from='C:\\foo\\bar'; to='C:\\foo\\bar\\baz'
return toOrig.slice(toStart + i + 1);
} else if (i === 2) {
// We get here if `from` is the device root.
// For example: from='C:\\'; to='C:\\foo'
return toOrig.slice(toStart + i);
}
}
if (fromLen > length) {
if (from.charCodeAt(fromStart + i) === CHAR_BACKWARD_SLASH) {
// We get here if `to` is the exact base path for `from`.
// For example: from='C:\\foo\\bar'; to='C:\\foo'
lastCommonSep = i;
} else if (i === 2) {
// We get here if `to` is the device root.
// For example: from='C:\\foo\\bar'; to='C:\\'
lastCommonSep = 3;
}
}
break;
}
const fromCode = from.charCodeAt(fromStart + i);
const toCode = to.charCodeAt(toStart + i);
if (fromCode !== toCode) break;
else if (fromCode === CHAR_BACKWARD_SLASH) lastCommonSep = i;
}
// We found a mismatch before the first common path separator was seen, so
// return the original `to`.
if (i !== length && lastCommonSep === -1) {
return toOrig;
}
let out = "";
if (lastCommonSep === -1) lastCommonSep = 0;
// Generate the relative path based on the path difference between `to` and
// `from`
for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
if (i === fromEnd || from.charCodeAt(i) === CHAR_BACKWARD_SLASH) {
if (out.length === 0) out += "..";
else out += "\\..";
}
}
// Lastly, append the rest of the destination (`to`) path that comes after
// the common path parts
if (out.length > 0) {
return out + toOrig.slice(toStart + lastCommonSep, toEnd);
} else {
toStart += lastCommonSep;
if (toOrig.charCodeAt(toStart) === CHAR_BACKWARD_SLASH) ++toStart;
return toOrig.slice(toStart, toEnd);
}
}