-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
relative.ts
166 lines (142 loc) · 4.51 KB
/
relative.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/**
* @file Library - relative
* @module pathe/lib/relative
*/
import ensurePosix from '#src/internal/ensure-posix'
import isSep from '#src/internal/is-sep'
import validateString from '#src/internal/validate-string'
import { DOT, at, ifelse, lowercase } from '@flex-development/tutils'
import resolve from './resolve'
import sep from './sep'
/**
* Returns the relative path from `from` to `to` based on the current working
* directory.
*
* If `from` and `to` resolve to the same path (after calling [`resolve`][1] on
* each), a zero-length string will be returned.
*
* If a zero-length string is passed as `from` or `to`, the current working
* directory will be used instead of the zero-length strings.
*
* [1]: {@link ./resolve.ts}
*
* @param {string} from - Start path
* @param {string} to - Destination path
* @return {string} Relative path from `from` to `to`
* @throws {TypeError} If either `from` or `to` is not a string
*/
const relative = (from: string, to: string): string => {
validateString(from, 'from')
validateString(to, 'to')
// exit early if from and to are the same path
if (from === to) return ''
// ensure paths meet posix standards + resolve paths
from = resolve((from = ensurePosix(from)))
to = resolve((to = ensurePosix(to)))
// exit early if from and to are the same resolved path
if (lowercase(from) === lowercase(to)) return ''
/**
* Measures the given `path`.
*
* The path will have leading and trailing separators removed. The function
* will return the length, start index, and end index of the trimmed path.
*
* @param {string} path - Path to measure
* @return {[number, number, number]} Length and indices of trimmed path
*/
const measure = (path: string): [number, number, number] => {
/**
* Start index of trimmed path.
*
* @var {number} start
*/
let start: number = 0
/**
* End index of trimmed path.
*
* @var {number} end
*/
let end: number = path.length
// remove leading separators
while (start < path.length && isSep(at(path, start))) start++
// remove trailing separators
while (end - 1 > start && isSep(at(path, end - 1))) end--
return [end - start, start, end]
}
// measure paths
const [from_length, from_start, from_end] = measure(from)
const [to_length, to_start, to_end] = measure(to)
/**
* Length of longest common path from root.
*
* @const {number} length
*/
const length: number = from_length < to_length ? from_length : to_length
/**
* Index of last common separator.
*
* @var {number} sepidx
*/
let sepidx: number = -1
/**
* Current index.
*
* @var {number} i
*/
let i: number = 0
// get index of last common separator
for (; i < length; i++) {
/**
* Character at {@linkcode from_start} + {@linkcode i} in {@linkcode from}.
*
* @const {string} char
*/
const char: string = lowercase(at(from, from_start + i)!)
if (char !== lowercase(at(to, to_start + i)!)) break
else if (isSep(char)) sepidx = i
}
if (i === length) {
// from is an exact base path, device root, or posix root
if (to_length > length) {
// from is an exact base path
if (isSep(at(to, to_start + i))) return to.slice(to_start + i + 1)
// from is a device root or posix root
if (i === 0 || i === 2) return to.slice(to_start + i)
}
// to is an exact base path, device root, or posix root
if (from_length > length) {
// to is an exact base path
if (isSep(at(from, from_start + i))) sepidx = i
// to is a device root
/* c8 ignore next */ else if (i === 2) sepidx = 3
// to is posix root
else if (i === 0) sepidx = 0
}
} else {
// mismatch before first common path separator was seen
if (sepidx === -1 && !(from.startsWith(sep) && to.startsWith(sep))) {
return to
}
}
/**
* Index of relative path between {@linkcode from} and {@linkcode to}.
*
* @const {number} offset
*/
const offset: number = to_start + sepidx
/**
* Relative path between {@linkcode from} and {@linkcode to}.
*
* @var {string} rel
*/
let rel: string = ''
// generate relative path based on path difference between to and from
for (i = from_start + sepidx + 1; i <= from_end; ++i) {
if (i === from_end || isSep(at(from, i))) {
rel += `${ifelse(rel, sep, '')}${DOT.repeat(2)}`
}
}
// append rest of destination path that comes after common path components
return rel + to.slice(offset, to_end)
}
export default relative