UNPKG

15.9 kBJavaScriptView Raw
1var strictUriEncode = str => encodeURIComponent(str).replace(/[!'()*]/g, x => `%${x.charCodeAt(0).toString(16).toUpperCase()}`);
2
3var token = '%[a-f0-9]{2}';
4var singleMatcher = new RegExp(token, 'gi');
5var multiMatcher = new RegExp('(' + token + ')+', 'gi');
6
7function decodeComponents(components, split) {
8 try {
9 // Try to decode the entire string first
10 return decodeURIComponent(components.join(''));
11 } catch (err) {
12 // Do nothing
13 }
14
15 if (components.length === 1) {
16 return components;
17 }
18
19 split = split || 1;
20
21 // Split the array in 2 parts
22 var left = components.slice(0, split);
23 var right = components.slice(split);
24
25 return Array.prototype.concat.call([], decodeComponents(left), decodeComponents(right));
26}
27
28function decode(input) {
29 try {
30 return decodeURIComponent(input);
31 } catch (err) {
32 var tokens = input.match(singleMatcher);
33
34 for (var i = 1; i < tokens.length; i++) {
35 input = decodeComponents(tokens, i).join('');
36
37 tokens = input.match(singleMatcher);
38 }
39
40 return input;
41 }
42}
43
44function customDecodeURIComponent(input) {
45 // Keep track of all the replacements and prefill the map with the `BOM`
46 var replaceMap = {
47 '%FE%FF': '\uFFFD\uFFFD',
48 '%FF%FE': '\uFFFD\uFFFD'
49 };
50
51 var match = multiMatcher.exec(input);
52 while (match) {
53 try {
54 // Decode as big chunks as possible
55 replaceMap[match[0]] = decodeURIComponent(match[0]);
56 } catch (err) {
57 var result = decode(match[0]);
58
59 if (result !== match[0]) {
60 replaceMap[match[0]] = result;
61 }
62 }
63
64 match = multiMatcher.exec(input);
65 }
66
67 // Add `%C2` at the end of the map to make sure it does not replace the combinator before everything else
68 replaceMap['%C2'] = '\uFFFD';
69
70 var entries = Object.keys(replaceMap);
71
72 for (var i = 0; i < entries.length; i++) {
73 // Replace all decoded components
74 var key = entries[i];
75 input = input.replace(new RegExp(key, 'g'), replaceMap[key]);
76 }
77
78 return input;
79}
80
81var decodeUriComponent = function (encodedURI) {
82 if (typeof encodedURI !== 'string') {
83 throw new TypeError('Expected `encodedURI` to be of type `string`, got `' + typeof encodedURI + '`');
84 }
85
86 try {
87 encodedURI = encodedURI.replace(/\+/g, ' ');
88
89 // Try the built in decoder first
90 return decodeURIComponent(encodedURI);
91 } catch (err) {
92 // Fallback to a more advanced decoder
93 return customDecodeURIComponent(encodedURI);
94 }
95};
96
97var splitOnFirst = (string, separator) => {
98 if (!(typeof string === 'string' && typeof separator === 'string')) {
99 throw new TypeError('Expected the arguments to be of type `string`');
100 }
101
102 if (separator === '') {
103 return [string];
104 }
105
106 const separatorIndex = string.indexOf(separator);
107
108 if (separatorIndex === -1) {
109 return [string];
110 }
111
112 return [
113 string.slice(0, separatorIndex),
114 string.slice(separatorIndex + separator.length)
115 ];
116};
117
118function encoderForArrayFormat(options) {
119 switch (options.arrayFormat) {
120 case 'index':
121 return key => (result, value) => {
122 const index = result.length;
123 if (value === undefined) {
124 return result;
125 }
126
127 if (value === null) {
128 return [...result, [encode(key, options), '[', index, ']'].join('')];
129 }
130
131 return [
132 ...result,
133 [encode(key, options), '[', encode(index, options), ']=', encode(value, options)].join('')
134 ];
135 };
136
137 case 'bracket':
138 return key => (result, value) => {
139 if (value === undefined) {
140 return result;
141 }
142
143 if (value === null) {
144 return [...result, [encode(key, options), '[]'].join('')];
145 }
146
147 return [...result, [encode(key, options), '[]=', encode(value, options)].join('')];
148 };
149
150 case 'comma':
151 return key => (result, value, index) => {
152 if (value === null || value === undefined || value.length === 0) {
153 return result;
154 }
155
156 if (index === 0) {
157 return [[encode(key, options), '=', encode(value, options)].join('')];
158 }
159
160 return [[result, encode(value, options)].join(',')];
161 };
162
163 default:
164 return key => (result, value) => {
165 if (value === undefined) {
166 return result;
167 }
168
169 if (value === null) {
170 return [...result, encode(key, options)];
171 }
172
173 return [...result, [encode(key, options), '=', encode(value, options)].join('')];
174 };
175 }
176}
177
178function parserForArrayFormat(options) {
179 let result;
180
181 switch (options.arrayFormat) {
182 case 'index':
183 return (key, value, accumulator) => {
184 result = /\[(\d*)\]$/.exec(key);
185
186 key = key.replace(/\[\d*\]$/, '');
187
188 if (!result) {
189 accumulator[key] = value;
190 return;
191 }
192
193 if (accumulator[key] === undefined) {
194 accumulator[key] = {};
195 }
196
197 accumulator[key][result[1]] = value;
198 };
199
200 case 'bracket':
201 return (key, value, accumulator) => {
202 result = /(\[\])$/.exec(key);
203 key = key.replace(/\[\]$/, '');
204
205 if (!result) {
206 accumulator[key] = value;
207 return;
208 }
209
210 if (accumulator[key] === undefined) {
211 accumulator[key] = [value];
212 return;
213 }
214
215 accumulator[key] = [].concat(accumulator[key], value);
216 };
217
218 case 'comma':
219 return (key, value, accumulator) => {
220 const isArray = typeof value === 'string' && value.split('').indexOf(',') > -1;
221 const newValue = isArray ? value.split(',') : value;
222 accumulator[key] = newValue;
223 };
224
225 default:
226 return (key, value, accumulator) => {
227 if (accumulator[key] === undefined) {
228 accumulator[key] = value;
229 return;
230 }
231
232 accumulator[key] = [].concat(accumulator[key], value);
233 };
234 }
235}
236
237function encode(value, options) {
238 if (options.encode) {
239 return options.strict ? strictUriEncode(value) : encodeURIComponent(value);
240 }
241
242 return value;
243}
244
245function decode$1(value, options) {
246 if (options.decode) {
247 return decodeUriComponent(value);
248 }
249
250 return value;
251}
252
253function keysSorter(input) {
254 if (Array.isArray(input)) {
255 return input.sort();
256 }
257
258 if (typeof input === 'object') {
259 return keysSorter(Object.keys(input))
260 .sort((a, b) => Number(a) - Number(b))
261 .map(key => input[key]);
262 }
263
264 return input;
265}
266
267function parseValue(value, options) {
268 if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
269 value = Number(value);
270 } else if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
271 value = value.toLowerCase() === 'true';
272 }
273
274 return value;
275}
276
277function parse(input, options) {
278 options = Object.assign({
279 decode: true,
280 sort: true,
281 arrayFormat: 'none',
282 parseNumbers: false,
283 parseBooleans: false
284 }, options);
285
286 const formatter = parserForArrayFormat(options);
287
288 // Create an object with no prototype
289 const ret = Object.create(null);
290
291 if (typeof input !== 'string') {
292 return ret;
293 }
294
295 input = input.trim().replace(/^[?#&]/, '');
296
297 if (!input) {
298 return ret;
299 }
300
301 for (const param of input.split('&')) {
302 let [key, value] = splitOnFirst(param.replace(/\+/g, ' '), '=');
303
304 // Missing `=` should be `null`:
305 // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
306 value = value === undefined ? null : decode$1(value, options);
307 formatter(decode$1(key, options), value, ret);
308 }
309
310 for (const key of Object.keys(ret)) {
311 const value = ret[key];
312 if (typeof value === 'object' && value !== null) {
313 for (const k of Object.keys(value)) {
314 value[k] = parseValue(value[k], options);
315 }
316 } else {
317 ret[key] = parseValue(value, options);
318 }
319 }
320
321 if (options.sort === false) {
322 return ret;
323 }
324
325 return (options.sort === true ? Object.keys(ret).sort() : Object.keys(ret).sort(options.sort)).reduce((result, key) => {
326 const value = ret[key];
327 if (Boolean(value) && typeof value === 'object' && !Array.isArray(value)) {
328 // Sort object keys, not values
329 result[key] = keysSorter(value);
330 } else {
331 result[key] = value;
332 }
333
334 return result;
335 }, Object.create(null));
336}
337var parse_1 = parse;
338
339var stringify = (object, options) => {
340 if (!object) {
341 return '';
342 }
343
344 options = Object.assign({
345 encode: true,
346 strict: true,
347 arrayFormat: 'none'
348 }, options);
349
350 const formatter = encoderForArrayFormat(options);
351 const keys = Object.keys(object);
352
353 if (options.sort !== false) {
354 keys.sort(options.sort);
355 }
356
357 return keys.map(key => {
358 const value = object[key];
359
360 if (value === undefined) {
361 return '';
362 }
363
364 if (value === null) {
365 return encode(key, options);
366 }
367
368 if (Array.isArray(value)) {
369 return value
370 .reduce(formatter(key), [])
371 .join('&');
372 }
373
374 return encode(key, options) + '=' + encode(value, options);
375 }).filter(x => x.length > 0).join('&');
376};
377
378var defaultExport = /*@__PURE__*/(function (Error) {
379 function defaultExport(route, path) {
380 var message = "Unreachable '" + (route !== '/' ? route.replace(/\/$/, '') : route) + "', segment '" + path + "' is not defined";
381 Error.call(this, message);
382 this.message = message;
383 this.route = route;
384 this.path = path;
385 }
386
387 if ( Error ) defaultExport.__proto__ = Error;
388 defaultExport.prototype = Object.create( Error && Error.prototype );
389 defaultExport.prototype.constructor = defaultExport;
390
391 return defaultExport;
392}(Error));
393
394function buildMatcher(path, parent) {
395 var regex;
396
397 var _isSplat;
398
399 var _priority = -100;
400
401 var keys = [];
402 regex = path.replace(/[-$.]/g, '\\$&').replace(/\(/g, '(?:').replace(/\)/g, ')?').replace(/([:*]\w+)(?:<([^<>]+?)>)?/g, function (_, key, expr) {
403 keys.push(key.substr(1));
404
405 if (key.charAt() === ':') {
406 _priority += 100;
407 return ("((?!#)" + (expr || '[^#/]+?') + ")");
408 }
409
410 _isSplat = true;
411 _priority += 500;
412 return ("((?!#)" + (expr || '[^#]+?') + ")");
413 });
414
415 try {
416 regex = new RegExp(("^" + regex + "$"));
417 } catch (e) {
418 throw new TypeError(("Invalid route expression, given '" + parent + "'"));
419 }
420
421 var _hashed = path.includes('#') ? 0.5 : 1;
422
423 var _depth = path.length * _priority * _hashed;
424
425 return {
426 keys: keys,
427 regex: regex,
428 _depth: _depth,
429 _isSplat: _isSplat
430 };
431}
432var PathMatcher = function PathMatcher(path, parent) {
433 var ref = buildMatcher(path, parent);
434 var keys = ref.keys;
435 var regex = ref.regex;
436 var _depth = ref._depth;
437 var _isSplat = ref._isSplat;
438 return {
439 _isSplat: _isSplat,
440 _depth: _depth,
441 match: function (value) {
442 var matches = value.match(regex);
443
444 if (matches) {
445 return keys.reduce(function (prev, cur, i) {
446 prev[cur] = typeof matches[i + 1] === 'string' ? decodeURIComponent(matches[i + 1]) : null;
447 return prev;
448 }, {});
449 }
450 }
451 };
452};
453
454PathMatcher.push = function push (key, prev, leaf, parent) {
455 var root = prev[key] || (prev[key] = {});
456
457 if (!root.pattern) {
458 root.pattern = new PathMatcher(key, parent);
459 root.route = (leaf || '').replace(/\/$/, '') || '/';
460 }
461
462 prev.keys = prev.keys || [];
463
464 if (!prev.keys.includes(key)) {
465 prev.keys.push(key);
466 PathMatcher.sort(prev);
467 }
468
469 return root;
470};
471
472PathMatcher.sort = function sort (root) {
473 root.keys.sort(function (a, b) {
474 return root[a].pattern._depth - root[b].pattern._depth;
475 });
476};
477
478function merge(path, parent) {
479 return ("" + (parent && parent !== '/' ? parent : '') + (path || ''));
480}
481function walk(path, cb) {
482 var matches = path.match(/<[^<>]*\/[^<>]*>/);
483
484 if (matches) {
485 throw new TypeError(("RegExp cannot contain slashes, given '" + matches + "'"));
486 }
487
488 var parts = path.split(/(?=\/|#)/);
489 var root = [];
490
491 if (parts[0] !== '/') {
492 parts.unshift('/');
493 }
494
495 parts.some(function (x, i) {
496 var parent = root.slice(1).concat(x).join('') || null;
497 var segment = parts.slice(i + 1).join('') || null;
498 var retval = cb(x, parent, segment ? ("" + (x !== '/' ? x : '') + segment) : null);
499 root.push(x);
500 return retval;
501 });
502}
503function reduce(key, root, _seen) {
504 var params = {};
505 var out = [];
506 var splat;
507 walk(key, function (x, leaf, extra) {
508 var found;
509
510 if (!root.keys) {
511 throw new defaultExport(key, x);
512 }
513
514 root.keys.some(function (k) {
515 if (_seen.includes(k)) { return false; }
516 var ref = root[k].pattern;
517 var match = ref.match;
518 var _isSplat = ref._isSplat;
519 var matches = match(_isSplat ? extra || x : x);
520
521 if (matches) {
522 Object.assign(params, matches);
523
524 if (root[k].route) {
525 var routeInfo = Object.assign({}, root[k].info); // properly handle exact-routes!
526
527 var hasMatch = false;
528
529 if (routeInfo.exact) {
530 hasMatch = extra === null;
531 } else {
532 hasMatch = !(x && leaf === null) || x === leaf || _isSplat || !extra;
533 }
534
535 routeInfo.matches = hasMatch;
536 routeInfo.params = Object.assign({}, params);
537 routeInfo.route = root[k].route;
538 routeInfo.path = _isSplat && extra || leaf || x;
539 out.push(routeInfo);
540 }
541
542 if (extra === null && !root[k].keys) {
543 return true;
544 }
545
546 if (k !== '/') { _seen.push(k); }
547 splat = _isSplat;
548 root = root[k];
549 found = true;
550 return true;
551 }
552
553 return false;
554 });
555
556 if (!(found || root.keys.some(function (k) { return root[k].pattern.match(x); }))) {
557 throw new defaultExport(key, x);
558 }
559
560 return splat || !found;
561 });
562 return out;
563}
564function find(path, routes, retries) {
565 var get = reduce.bind(null, path, routes);
566 var set = [];
567
568 while (retries > 0) {
569 retries -= 1;
570
571 try {
572 return get(set);
573 } catch (e) {
574 if (retries > 0) {
575 return get(set);
576 }
577
578 throw e;
579 }
580 }
581}
582function add(path, routes, parent, routeInfo) {
583 var fullpath = merge(path, parent);
584 var root = routes;
585 var key;
586
587 if (routeInfo && routeInfo.nested !== true) {
588 key = routeInfo.key;
589 delete routeInfo.key;
590 }
591
592 walk(fullpath, function (x, leaf) {
593 root = PathMatcher.push(x, root, leaf, fullpath);
594
595 if (x !== '/') {
596 root.info = root.info || Object.assign({}, routeInfo);
597 }
598 });
599 root.info = root.info || Object.assign({}, routeInfo);
600
601 if (key) {
602 root.info.key = key;
603 }
604
605 return fullpath;
606}
607function rm(path, routes, parent) {
608 var fullpath = merge(path, parent);
609 var root = routes;
610 var leaf = null;
611 var key = null;
612 walk(fullpath, function (x) {
613 if (!root) {
614 leaf = null;
615 return true;
616 }
617
618 if (!root.keys) {
619 throw new defaultExport(path, x);
620 }
621
622 key = x;
623 leaf = root;
624 root = root[key];
625 });
626
627 if (!(leaf && key)) {
628 throw new defaultExport(path, key);
629 }
630
631 if (leaf === routes) {
632 leaf = routes['/'];
633 }
634
635 if (leaf.route !== key) {
636 var offset = leaf.keys.indexOf(key);
637
638 if (offset === -1) {
639 throw new defaultExport(path, key);
640 }
641
642 leaf.keys.splice(offset, 1);
643 PathMatcher.sort(leaf);
644 delete leaf[key];
645 } // nested routes are upgradeable, so keep original info...
646
647
648 if (root.route === leaf.route && (!root.info || root.info.key === leaf.info.key)) { delete leaf.info; }
649}
650
651var Router = function Router() {
652 var routes = {};
653 var stack = [];
654 return {
655 resolve: function (path, cb) {
656 var url = path.split('?')[0];
657 var seen = [];
658 walk(url, function (x, leaf, extra) {
659 try {
660 cb(null, find(leaf, routes, 1).filter(function (r) {
661 if (!seen.includes(r.path)) {
662 seen.push(r.path);
663 return true;
664 }
665
666 return false;
667 }));
668 } catch (e) {
669 cb(e, []);
670 }
671 });
672 },
673 mount: function (path, cb) {
674 if (path !== '/') {
675 stack.push(path);
676 }
677
678 cb();
679 stack.pop();
680 },
681 find: function (path, retries) { return find(path, routes, retries === true ? 2 : retries || 1); },
682 add: function (path, routeInfo) { return add(path, routes, stack.join(''), routeInfo); },
683 rm: function (path) { return rm(path, routes, stack.join('')); }
684 };
685};
686
687Router.matches = function matches (uri, path) {
688 return buildMatcher(uri, path).regex.test(path);
689};
690
691export { Router, parse_1 as parse, stringify };