1 | var strictUriEncode = str => encodeURIComponent(str).replace(/[!'()*]/g, x => `%${x.charCodeAt(0).toString(16).toUpperCase()}`);
|
2 |
|
3 | var token = '%[a-f0-9]{2}';
|
4 | var singleMatcher = new RegExp(token, 'gi');
|
5 | var multiMatcher = new RegExp('(' + token + ')+', 'gi');
|
6 |
|
7 | function decodeComponents(components, split) {
|
8 | try {
|
9 |
|
10 | return decodeURIComponent(components.join(''));
|
11 | } catch (err) {
|
12 |
|
13 | }
|
14 |
|
15 | if (components.length === 1) {
|
16 | return components;
|
17 | }
|
18 |
|
19 | split = split || 1;
|
20 |
|
21 |
|
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 |
|
28 | function 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 |
|
44 | function customDecodeURIComponent(input) {
|
45 |
|
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 |
|
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 |
|
68 | replaceMap['%C2'] = '\uFFFD';
|
69 |
|
70 | var entries = Object.keys(replaceMap);
|
71 |
|
72 | for (var i = 0; i < entries.length; i++) {
|
73 |
|
74 | var key = entries[i];
|
75 | input = input.replace(new RegExp(key, 'g'), replaceMap[key]);
|
76 | }
|
77 |
|
78 | return input;
|
79 | }
|
80 |
|
81 | var 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 |
|
90 | return decodeURIComponent(encodedURI);
|
91 | } catch (err) {
|
92 |
|
93 | return customDecodeURIComponent(encodedURI);
|
94 | }
|
95 | };
|
96 |
|
97 | var 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 |
|
118 | function 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 |
|
178 | function 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 |
|
237 | function encode(value, options) {
|
238 | if (options.encode) {
|
239 | return options.strict ? strictUriEncode(value) : encodeURIComponent(value);
|
240 | }
|
241 |
|
242 | return value;
|
243 | }
|
244 |
|
245 | function decode$1(value, options) {
|
246 | if (options.decode) {
|
247 | return decodeUriComponent(value);
|
248 | }
|
249 |
|
250 | return value;
|
251 | }
|
252 |
|
253 | function 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 |
|
267 | function 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 |
|
277 | function 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 |
|
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 |
|
305 |
|
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 |
|
329 | result[key] = keysSorter(value);
|
330 | } else {
|
331 | result[key] = value;
|
332 | }
|
333 |
|
334 | return result;
|
335 | }, Object.create(null));
|
336 | }
|
337 | var parse_1 = parse;
|
338 |
|
339 | var 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 |
|
378 | var defaultExport = (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 |
|
394 | function 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 | }
|
432 | var 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 |
|
454 | PathMatcher.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 |
|
472 | PathMatcher.sort = function sort (root) {
|
473 | root.keys.sort(function (a, b) {
|
474 | return root[a].pattern._depth - root[b].pattern._depth;
|
475 | });
|
476 | };
|
477 |
|
478 | function merge(path, parent) {
|
479 | return ("" + (parent && parent !== '/' ? parent : '') + (path || ''));
|
480 | }
|
481 | function 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 | }
|
503 | function 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);
|
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 | }
|
564 | function 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 | }
|
582 | function 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 | }
|
607 | function 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 | }
|
646 |
|
647 |
|
648 | if (root.route === leaf.route && (!root.info || root.info.key === leaf.info.key)) { delete leaf.info; }
|
649 | }
|
650 |
|
651 | var 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 |
|
687 | Router.matches = function matches (uri, path) {
|
688 | return buildMatcher(uri, path).regex.test(path);
|
689 | };
|
690 |
|
691 | export { Router, parse_1 as parse, stringify };
|