-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
location.ts
231 lines (205 loc) · 6.23 KB
/
location.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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
import { LocationQuery, LocationQueryRaw } from './query'
import {
RouteLocation,
RouteLocationNormalized,
RouteParamValue,
} from './types'
import { RouteRecord } from './matcher/types'
import { warn } from './warning'
/**
* Location object returned by {@link `parseURL`}.
* @internal
*/
interface LocationNormalized {
path: string
fullPath: string
hash: string
query: LocationQuery
}
/**
* Location object accepted by {@link `stringifyURL`}.
* @internal
*/
interface LocationPartial {
path: string
query?: LocationQueryRaw
hash?: string
}
const TRAILING_SLASH_RE = /\/$/
export const removeTrailingSlash = (path: string) =>
path.replace(TRAILING_SLASH_RE, '')
/**
* Transforms an URI into a normalized history location
*
* @param parseQuery
* @param location - URI to normalize
* @param currentLocation - current absolute location. Allows resolving relative
* paths. Must start with `/`. Defaults to `/`
* @returns a normalized history location
*/
export function parseURL(
parseQuery: (search: string) => LocationQuery,
location: string,
currentLocation: string = '/'
): LocationNormalized {
let path: string | undefined,
query: LocationQuery = {},
searchString = '',
hash = ''
// Could use URL and URLSearchParams but IE 11 doesn't support it
const searchPos = location.indexOf('?')
const hashPos = location.indexOf('#', searchPos > -1 ? searchPos : 0)
if (searchPos > -1) {
path = location.slice(0, searchPos)
searchString = location.slice(
searchPos + 1,
hashPos > -1 ? hashPos : location.length
)
query = parseQuery(searchString)
}
if (hashPos > -1) {
path = path || location.slice(0, hashPos)
// keep the # character
hash = location.slice(hashPos, location.length)
}
// no search and no query
path = resolveRelativePath(path != null ? path : location, currentLocation)
// empty path means a relative query or hash `?foo=f`, `#thing`
return {
fullPath: path + (searchString && '?') + searchString + hash,
path,
query,
hash,
}
}
/**
* Stringifies a URL object
*
* @param stringifyQuery
* @param location
*/
export function stringifyURL(
stringifyQuery: (query: LocationQueryRaw) => string,
location: LocationPartial
): string {
const query: string = location.query ? stringifyQuery(location.query) : ''
return location.path + (query && '?') + query + (location.hash || '')
}
/**
* Strips off the base from the beginning of a location.pathname in a non
* case-sensitive way.
*
* @param pathname - location.pathname
* @param base - base to strip off
*/
export function stripBase(pathname: string, base: string): string {
// no base or base is not found at the beginning
if (!base || !pathname.toLowerCase().startsWith(base.toLowerCase()))
return pathname
return pathname.slice(base.length) || '/'
}
/**
* Checks if two RouteLocation are equal. This means that both locations are
* pointing towards the same {@link RouteRecord} and that all `params`, `query`
* parameters and `hash` are the same
*
* @param a - first {@link RouteLocation}
* @param b - second {@link RouteLocation}
*/
export function isSameRouteLocation(
stringifyQuery: (query: LocationQueryRaw) => string,
a: RouteLocation,
b: RouteLocation
): boolean {
const aLastIndex = a.matched.length - 1
const bLastIndex = b.matched.length - 1
return (
aLastIndex > -1 &&
aLastIndex === bLastIndex &&
isSameRouteRecord(a.matched[aLastIndex], b.matched[bLastIndex]) &&
isSameRouteLocationParams(a.params, b.params) &&
stringifyQuery(a.query) === stringifyQuery(b.query) &&
a.hash === b.hash
)
}
/**
* Check if two `RouteRecords` are equal. Takes into account aliases: they are
* considered equal to the `RouteRecord` they are aliasing.
*
* @param a - first {@link RouteRecord}
* @param b - second {@link RouteRecord}
*/
export function isSameRouteRecord(a: RouteRecord, b: RouteRecord): boolean {
// since the original record has an undefined value for aliasOf
// but all aliases point to the original record, this will always compare
// the original record
return (a.aliasOf || a) === (b.aliasOf || b)
}
export function isSameRouteLocationParams(
a: RouteLocationNormalized['params'],
b: RouteLocationNormalized['params']
): boolean {
if (Object.keys(a).length !== Object.keys(b).length) return false
for (const key in a) {
if (!isSameRouteLocationParamsValue(a[key], b[key])) return false
}
return true
}
function isSameRouteLocationParamsValue(
a: RouteParamValue | RouteParamValue[],
b: RouteParamValue | RouteParamValue[]
): boolean {
return Array.isArray(a)
? isEquivalentArray(a, b)
: Array.isArray(b)
? isEquivalentArray(b, a)
: a === b
}
/**
* Check if two arrays are the same or if an array with one single entry is the
* same as another primitive value. Used to check query and parameters
*
* @param a - array of values
* @param b - array of values or a single value
*/
function isEquivalentArray<T>(a: T[], b: T[] | T): boolean {
return Array.isArray(b)
? a.length === b.length && a.every((value, i) => value === b[i])
: a.length === 1 && a[0] === b
}
/**
* Resolves a relative path that starts with `.`.
*
* @param to - path location we are resolving
* @param from - currentLocation.path, should start with `/`
*/
export function resolveRelativePath(to: string, from: string): string {
if (to.startsWith('/')) return to
if (__DEV__ && !from.startsWith('/')) {
warn(
`Cannot resolve a relative location without an absolute path. Trying to resolve "${to}" from "${from}". It should look like "/${from}".`
)
return to
}
if (!to) return from
const fromSegments = from.split('/')
const toSegments = to.split('/')
let position = fromSegments.length - 1
let toPosition: number
let segment: string
for (toPosition = 0; toPosition < toSegments.length; toPosition++) {
segment = toSegments[toPosition]
// can't go below zero
if (position === 1 || segment === '.') continue
if (segment === '..') position--
// found something that is not relative path
else break
}
return (
fromSegments.slice(0, position).join('/') +
'/' +
toSegments
.slice(toPosition - (toPosition === toSegments.length ? 1 : 0))
.join('/')
)
}