-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathurl.js
106 lines (92 loc) · 3.73 KB
/
url.js
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
const urlSymbol = Symbol('url')
class WrappedUrl {
constructor(raw, symbol) {
if (typeof raw !== 'string')
throw new TypeError('first argument of WrappedUrl constructor should be of type "string"')
if (symbol !== urlSymbol) throw new TypeError('Use url`` to construct urls') // Just a double-check against misuse
this[urlSymbol] = raw
}
toString() {
return this[urlSymbol]
}
valueOf() {
return this[urlSymbol]
}
}
const urlUnwrap = (url) => {
if (url instanceof WrappedUrl) return url[urlSymbol]
throw new TypeError('Input is not a safe wrapped URL string')
}
const encodeComponent = (raw) => {
if (raw instanceof WrappedUrl) return urlUnwrap(raw)
if (typeof raw === 'string' || typeof raw === 'number') {
const arg = String(raw)
if (arg === '..') throw new Error('Unexpected .. in path')
return encodeURIComponent(arg)
}
throw new TypeError('Unexpected non-primitive component type!')
}
// URI-escape all components of the string
function urlComponent(strings, ...args) {
if (!strings.raw) throw new TypeError('urlComponent`` should be only used as a template literal')
const escaped = args.map((arg) => encodeComponent(arg))
const raw = [strings[0], ...escaped.flatMap((arg, i) => [arg, strings[i + 1]])].join('')
return new WrappedUrl(raw, urlSymbol)
}
// Returns a template constructor
function urlBase(base) {
if (typeof base !== 'string') throw new TypeError('Expected a string as base URL')
return (...args) => {
const res = urlUnwrap(urlComponent(...args))
if (!base.endsWith('/') && !res.startsWith('/')) throw new Error('Missing / after base URL')
return new WrappedUrl(base + res, urlSymbol)
}
}
function validateBase(url, res) {
if (typeof url !== 'string' || typeof res !== 'string') throw new TypeError('Unexpected types')
if (res === url) return
if (res.startsWith(url) && (url.endsWith('/') || url.endsWith('?') || url.endsWith('&'))) return
if (res.startsWith(`${url}/`) || res.startsWith(`${url}?`) || res.startsWith(`${url}&`)) return
throw new Error('Result url does not start with the base url!')
}
function subquery(params) {
let entries
if (Object.getPrototypeOf(params) === Map.prototype) {
entries = [...params]
} else if (typeof params === 'object' && Object.getPrototypeOf(params) === Object.prototype) {
entries = Object.entries(params)
} else throw new TypeError('query can be only a Map or a plain object')
return entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&')
}
// Returns an URL() object based on the logic above
function url(strings, ...args) {
if (!strings.raw) throw new TypeError('url`` should be only used as a template literal')
let base
const escaped = args.map((raw, i) => {
if (raw instanceof URL) {
if (i === 0 && strings[0] === '') {
base = String(raw)
return base
}
throw new Error('URL typed argument should always be first')
} else if (
Object.getPrototypeOf(raw) === Map.prototype ||
(typeof raw === 'object' && Object.getPrototypeOf(raw) === Object.prototype)
) {
if (i === args.length - 1 && strings[i + 1] === '') {
if (!/^[&?]$/.test(strings[i].slice(-1))) {
throw new Error('Missing & or ? before object params!')
}
return subquery(raw)
}
throw new Error('Object/map params could come only at the end of the URL')
}
return encodeComponent(raw)
})
const res = [strings[0], ...escaped.flatMap((arg, i) => [arg, strings[i + 1]])].join('')
if (base) validateBase(base, res)
const url = new URL(res)
if (String(url) !== res) throw new Error('Unexpected URL produced!') // e.g. .. which get resolved
return url
}
module.exports = { url, urlComponent, urlBase, urlUnwrap }