1 | 'use strict';
|
2 |
|
3 | const URLParser = typeof URL === 'undefined' ? require('url').URL : URL;
|
4 |
|
5 | const testParameter = (name, filters) => {
|
6 | return filters.some(filter => filter instanceof RegExp ? filter.test(name) : filter === name);
|
7 | };
|
8 |
|
9 | module.exports = (urlString, opts) => {
|
10 | opts = Object.assign({
|
11 | defaultProtocol: 'http:',
|
12 | normalizeProtocol: true,
|
13 | forceHttp: false,
|
14 | forceHttps: false,
|
15 | stripHash: true,
|
16 | stripWWW: true,
|
17 | removeQueryParameters: [/^utm_\w+/i],
|
18 | removeTrailingSlash: true,
|
19 | removeDirectoryIndex: false,
|
20 | sortQueryParameters: true
|
21 | }, opts);
|
22 |
|
23 |
|
24 | if (Reflect.has(opts, 'normalizeHttps')) {
|
25 | opts.forceHttp = opts.normalizeHttps;
|
26 | }
|
27 |
|
28 | if (Reflect.has(opts, 'normalizeHttp')) {
|
29 | opts.forceHttps = opts.normalizeHttp;
|
30 | }
|
31 |
|
32 | if (Reflect.has(opts, 'stripFragment')) {
|
33 | opts.stripHash = opts.stripFragment;
|
34 | }
|
35 |
|
36 | urlString = urlString.trim();
|
37 |
|
38 | const hasRelativeProtocol = urlString.startsWith('//');
|
39 | const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString);
|
40 |
|
41 |
|
42 | if (!isRelativeUrl) {
|
43 | urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, opts.defaultProtocol);
|
44 | }
|
45 |
|
46 | const urlObj = new URLParser(urlString);
|
47 |
|
48 | if (opts.forceHttp && opts.forceHttps) {
|
49 | throw new Error('The `forceHttp` and `forceHttps` options cannot be used together');
|
50 | }
|
51 |
|
52 | if (opts.forceHttp && urlObj.protocol === 'https:') {
|
53 | urlObj.protocol = 'http:';
|
54 | }
|
55 |
|
56 | if (opts.forceHttps && urlObj.protocol === 'http:') {
|
57 | urlObj.protocol = 'https:';
|
58 | }
|
59 |
|
60 |
|
61 | if (opts.stripHash) {
|
62 | urlObj.hash = '';
|
63 | }
|
64 |
|
65 |
|
66 | if (urlObj.pathname) {
|
67 |
|
68 |
|
69 | urlObj.pathname = urlObj.pathname.replace(/((?![https?:]).)\/{2,}/g, (_, p1) => {
|
70 | if (/^(?!\/)/g.test(p1)) {
|
71 | return `${p1}/`;
|
72 | }
|
73 | return '/';
|
74 | });
|
75 | }
|
76 |
|
77 |
|
78 | if (urlObj.pathname) {
|
79 | urlObj.pathname = decodeURI(urlObj.pathname);
|
80 | }
|
81 |
|
82 |
|
83 | if (opts.removeDirectoryIndex === true) {
|
84 | opts.removeDirectoryIndex = [/^index\.[a-z]+$/];
|
85 | }
|
86 |
|
87 | if (Array.isArray(opts.removeDirectoryIndex) && opts.removeDirectoryIndex.length > 0) {
|
88 | let pathComponents = urlObj.pathname.split('/');
|
89 | const lastComponent = pathComponents[pathComponents.length - 1];
|
90 |
|
91 | if (testParameter(lastComponent, opts.removeDirectoryIndex)) {
|
92 | pathComponents = pathComponents.slice(0, pathComponents.length - 1);
|
93 | urlObj.pathname = pathComponents.slice(1).join('/') + '/';
|
94 | }
|
95 | }
|
96 |
|
97 | if (urlObj.hostname) {
|
98 |
|
99 | urlObj.hostname = urlObj.hostname.replace(/\.$/, '');
|
100 |
|
101 |
|
102 |
|
103 | if (opts.stripWWW && /^www\.([a-z\-\d]{2,63})\.([a-z\.]{2,5})$/.test(urlObj.hostname)) {
|
104 |
|
105 |
|
106 |
|
107 | urlObj.hostname = urlObj.hostname.replace(/^www\./, '');
|
108 | }
|
109 | }
|
110 |
|
111 |
|
112 | if (Array.isArray(opts.removeQueryParameters)) {
|
113 | for (const key of [...urlObj.searchParams.keys()]) {
|
114 | if (testParameter(key, opts.removeQueryParameters)) {
|
115 | urlObj.searchParams.delete(key);
|
116 | }
|
117 | }
|
118 | }
|
119 |
|
120 |
|
121 | if (opts.sortQueryParameters) {
|
122 | urlObj.searchParams.sort();
|
123 | }
|
124 |
|
125 |
|
126 | urlString = urlObj.toString();
|
127 |
|
128 |
|
129 | if (opts.removeTrailingSlash || urlObj.pathname === '/') {
|
130 | urlString = urlString.replace(/\/$/, '');
|
131 | }
|
132 |
|
133 |
|
134 | if (hasRelativeProtocol && !opts.normalizeProtocol) {
|
135 | urlString = urlString.replace(/^http:\/\//, '//');
|
136 | }
|
137 |
|
138 | return urlString;
|
139 | };
|