UNPKG

18 kBJavaScriptView Raw
1/*! Universal Router | MIT License | https://www.kriasoft.com/universal-router/ */
2
3(function (global, factory) {
4 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
5 typeof define === 'function' && define.amd ? define(factory) :
6 (global.UniversalRouter = factory());
7}(this, (function () { 'use strict';
8
9var index$1 = Array.isArray || function (arr) {
10 return Object.prototype.toString.call(arr) == '[object Array]';
11};
12
13/**
14 * Expose `pathToRegexp`.
15 */
16var index = pathToRegexp;
17var parse_1 = parse;
18var compile_1 = compile;
19var tokensToFunction_1 = tokensToFunction;
20var tokensToRegExp_1 = tokensToRegExp;
21
22/**
23 * The main path matching regexp utility.
24 *
25 * @type {RegExp}
26 */
27var PATH_REGEXP = new RegExp([
28 // Match escaped characters that would otherwise appear in future matches.
29 // This allows the user to escape special characters that won't transform.
30 '(\\\\.)',
31 // Match Express-style parameters and un-named parameters with a prefix
32 // and optional suffixes. Matches appear as:
33 //
34 // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined]
35 // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined]
36 // "/*" => ["/", undefined, undefined, undefined, undefined, "*"]
37 '([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))'
38].join('|'), 'g');
39
40/**
41 * Parse a string for the raw tokens.
42 *
43 * @param {string} str
44 * @param {Object=} options
45 * @return {!Array}
46 */
47function parse (str, options) {
48 var tokens = [];
49 var key = 0;
50 var index = 0;
51 var path = '';
52 var defaultDelimiter = options && options.delimiter || '/';
53 var res;
54
55 while ((res = PATH_REGEXP.exec(str)) != null) {
56 var m = res[0];
57 var escaped = res[1];
58 var offset = res.index;
59 path += str.slice(index, offset);
60 index = offset + m.length;
61
62 // Ignore already escaped sequences.
63 if (escaped) {
64 path += escaped[1];
65 continue
66 }
67
68 var next = str[index];
69 var prefix = res[2];
70 var name = res[3];
71 var capture = res[4];
72 var group = res[5];
73 var modifier = res[6];
74 var asterisk = res[7];
75
76 // Push the current path onto the tokens.
77 if (path) {
78 tokens.push(path);
79 path = '';
80 }
81
82 var partial = prefix != null && next != null && next !== prefix;
83 var repeat = modifier === '+' || modifier === '*';
84 var optional = modifier === '?' || modifier === '*';
85 var delimiter = res[2] || defaultDelimiter;
86 var pattern = capture || group;
87
88 tokens.push({
89 name: name || key++,
90 prefix: prefix || '',
91 delimiter: delimiter,
92 optional: optional,
93 repeat: repeat,
94 partial: partial,
95 asterisk: !!asterisk,
96 pattern: pattern ? escapeGroup(pattern) : (asterisk ? '.*' : '[^' + escapeString(delimiter) + ']+?')
97 });
98 }
99
100 // Match any characters still remaining.
101 if (index < str.length) {
102 path += str.substr(index);
103 }
104
105 // If the path exists, push it onto the end.
106 if (path) {
107 tokens.push(path);
108 }
109
110 return tokens
111}
112
113/**
114 * Compile a string to a template function for the path.
115 *
116 * @param {string} str
117 * @param {Object=} options
118 * @return {!function(Object=, Object=)}
119 */
120function compile (str, options) {
121 return tokensToFunction(parse(str, options))
122}
123
124/**
125 * Prettier encoding of URI path segments.
126 *
127 * @param {string}
128 * @return {string}
129 */
130function encodeURIComponentPretty (str) {
131 return encodeURI(str).replace(/[\/?#]/g, function (c) {
132 return '%' + c.charCodeAt(0).toString(16).toUpperCase()
133 })
134}
135
136/**
137 * Encode the asterisk parameter. Similar to `pretty`, but allows slashes.
138 *
139 * @param {string}
140 * @return {string}
141 */
142function encodeAsterisk (str) {
143 return encodeURI(str).replace(/[?#]/g, function (c) {
144 return '%' + c.charCodeAt(0).toString(16).toUpperCase()
145 })
146}
147
148/**
149 * Expose a method for transforming tokens into the path function.
150 */
151function tokensToFunction (tokens) {
152 // Compile all the tokens into regexps.
153 var matches = new Array(tokens.length);
154
155 // Compile all the patterns before compilation.
156 for (var i = 0; i < tokens.length; i++) {
157 if (typeof tokens[i] === 'object') {
158 matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$');
159 }
160 }
161
162 return function (obj, opts) {
163 var path = '';
164 var data = obj || {};
165 var options = opts || {};
166 var encode = options.pretty ? encodeURIComponentPretty : encodeURIComponent;
167
168 for (var i = 0; i < tokens.length; i++) {
169 var token = tokens[i];
170
171 if (typeof token === 'string') {
172 path += token;
173
174 continue
175 }
176
177 var value = data[token.name];
178 var segment;
179
180 if (value == null) {
181 if (token.optional) {
182 // Prepend partial segment prefixes.
183 if (token.partial) {
184 path += token.prefix;
185 }
186
187 continue
188 } else {
189 throw new TypeError('Expected "' + token.name + '" to be defined')
190 }
191 }
192
193 if (index$1(value)) {
194 if (!token.repeat) {
195 throw new TypeError('Expected "' + token.name + '" to not repeat, but received `' + JSON.stringify(value) + '`')
196 }
197
198 if (value.length === 0) {
199 if (token.optional) {
200 continue
201 } else {
202 throw new TypeError('Expected "' + token.name + '" to not be empty')
203 }
204 }
205
206 for (var j = 0; j < value.length; j++) {
207 segment = encode(value[j]);
208
209 if (!matches[i].test(segment)) {
210 throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '", but received `' + JSON.stringify(segment) + '`')
211 }
212
213 path += (j === 0 ? token.prefix : token.delimiter) + segment;
214 }
215
216 continue
217 }
218
219 segment = token.asterisk ? encodeAsterisk(value) : encode(value);
220
221 if (!matches[i].test(segment)) {
222 throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"')
223 }
224
225 path += token.prefix + segment;
226 }
227
228 return path
229 }
230}
231
232/**
233 * Escape a regular expression string.
234 *
235 * @param {string} str
236 * @return {string}
237 */
238function escapeString (str) {
239 return str.replace(/([.+*?=^!:${}()[\]|\/\\])/g, '\\$1')
240}
241
242/**
243 * Escape the capturing group by escaping special characters and meaning.
244 *
245 * @param {string} group
246 * @return {string}
247 */
248function escapeGroup (group) {
249 return group.replace(/([=!:$\/()])/g, '\\$1')
250}
251
252/**
253 * Attach the keys as a property of the regexp.
254 *
255 * @param {!RegExp} re
256 * @param {Array} keys
257 * @return {!RegExp}
258 */
259function attachKeys (re, keys) {
260 re.keys = keys;
261 return re
262}
263
264/**
265 * Get the flags for a regexp from the options.
266 *
267 * @param {Object} options
268 * @return {string}
269 */
270function flags (options) {
271 return options.sensitive ? '' : 'i'
272}
273
274/**
275 * Pull out keys from a regexp.
276 *
277 * @param {!RegExp} path
278 * @param {!Array} keys
279 * @return {!RegExp}
280 */
281function regexpToRegexp (path, keys) {
282 // Use a negative lookahead to match only capturing groups.
283 var groups = path.source.match(/\((?!\?)/g);
284
285 if (groups) {
286 for (var i = 0; i < groups.length; i++) {
287 keys.push({
288 name: i,
289 prefix: null,
290 delimiter: null,
291 optional: false,
292 repeat: false,
293 partial: false,
294 asterisk: false,
295 pattern: null
296 });
297 }
298 }
299
300 return attachKeys(path, keys)
301}
302
303/**
304 * Transform an array into a regexp.
305 *
306 * @param {!Array} path
307 * @param {Array} keys
308 * @param {!Object} options
309 * @return {!RegExp}
310 */
311function arrayToRegexp (path, keys, options) {
312 var parts = [];
313
314 for (var i = 0; i < path.length; i++) {
315 parts.push(pathToRegexp(path[i], keys, options).source);
316 }
317
318 var regexp = new RegExp('(?:' + parts.join('|') + ')', flags(options));
319
320 return attachKeys(regexp, keys)
321}
322
323/**
324 * Create a path regexp from string input.
325 *
326 * @param {string} path
327 * @param {!Array} keys
328 * @param {!Object} options
329 * @return {!RegExp}
330 */
331function stringToRegexp (path, keys, options) {
332 return tokensToRegExp(parse(path, options), keys, options)
333}
334
335/**
336 * Expose a function for taking tokens and returning a RegExp.
337 *
338 * @param {!Array} tokens
339 * @param {(Array|Object)=} keys
340 * @param {Object=} options
341 * @return {!RegExp}
342 */
343function tokensToRegExp (tokens, keys, options) {
344 if (!index$1(keys)) {
345 options = /** @type {!Object} */ (keys || options);
346 keys = [];
347 }
348
349 options = options || {};
350
351 var strict = options.strict;
352 var end = options.end !== false;
353 var route = '';
354
355 // Iterate over the tokens and create our regexp string.
356 for (var i = 0; i < tokens.length; i++) {
357 var token = tokens[i];
358
359 if (typeof token === 'string') {
360 route += escapeString(token);
361 } else {
362 var prefix = escapeString(token.prefix);
363 var capture = '(?:' + token.pattern + ')';
364
365 keys.push(token);
366
367 if (token.repeat) {
368 capture += '(?:' + prefix + capture + ')*';
369 }
370
371 if (token.optional) {
372 if (!token.partial) {
373 capture = '(?:' + prefix + '(' + capture + '))?';
374 } else {
375 capture = prefix + '(' + capture + ')?';
376 }
377 } else {
378 capture = prefix + '(' + capture + ')';
379 }
380
381 route += capture;
382 }
383 }
384
385 var delimiter = escapeString(options.delimiter || '/');
386 var endsWithDelimiter = route.slice(-delimiter.length) === delimiter;
387
388 // In non-strict mode we allow a slash at the end of match. If the path to
389 // match already ends with a slash, we remove it for consistency. The slash
390 // is valid at the end of a path match, not in the middle. This is important
391 // in non-ending mode, where "/test/" shouldn't match "/test//route".
392 if (!strict) {
393 route = (endsWithDelimiter ? route.slice(0, -delimiter.length) : route) + '(?:' + delimiter + '(?=$))?';
394 }
395
396 if (end) {
397 route += '$';
398 } else {
399 // In non-ending mode, we need the capturing groups to match as much as
400 // possible by using a positive lookahead to the end or next path segment.
401 route += strict && endsWithDelimiter ? '' : '(?=' + delimiter + '|$)';
402 }
403
404 return attachKeys(new RegExp('^' + route, flags(options)), keys)
405}
406
407/**
408 * Normalize the given path string, returning a regular expression.
409 *
410 * An empty array can be passed in for the keys, which will hold the
411 * placeholder key descriptions. For example, using `/user/:id`, `keys` will
412 * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
413 *
414 * @param {(string|RegExp|Array)} path
415 * @param {(Array|Object)=} keys
416 * @param {Object=} options
417 * @return {!RegExp}
418 */
419function pathToRegexp (path, keys, options) {
420 if (!index$1(keys)) {
421 options = /** @type {!Object} */ (keys || options);
422 keys = [];
423 }
424
425 options = options || {};
426
427 if (path instanceof RegExp) {
428 return regexpToRegexp(path, /** @type {!Array} */ (keys))
429 }
430
431 if (index$1(path)) {
432 return arrayToRegexp(/** @type {!Array} */ (path), /** @type {!Array} */ (keys), options)
433 }
434
435 return stringToRegexp(/** @type {string} */ (path), /** @type {!Array} */ (keys), options)
436}
437
438index.parse = parse_1;
439index.compile = compile_1;
440index.tokensToFunction = tokensToFunction_1;
441index.tokensToRegExp = tokensToRegExp_1;
442
443/**
444 * Universal Router (https://www.kriasoft.com/universal-router/)
445 *
446 * Copyright © 2015-present Kriasoft, LLC. All rights reserved.
447 *
448 * This source code is licensed under the Apache 2.0 license found in the
449 * LICENSE.txt file in the root directory of this source tree.
450 */
451
452var cache = new Map();
453
454function decodeParam(val) {
455 try {
456 return decodeURIComponent(val);
457 } catch (err) {
458 return val;
459 }
460}
461
462function matchPath(routePath, urlPath, end, parentParams) {
463 var key = routePath + '|' + end;
464 var regexp = cache.get(key);
465
466 if (!regexp) {
467 var keys = [];
468 regexp = { pattern: index(routePath, keys, { end: end }), keys: keys };
469 cache.set(key, regexp);
470 }
471
472 var m = regexp.pattern.exec(urlPath);
473 if (!m) {
474 return null;
475 }
476
477 var path = m[0];
478 var params = Object.create(null);
479
480 if (parentParams) {
481 Object.assign(params, parentParams);
482 }
483
484 for (var i = 1; i < m.length; i += 1) {
485 params[regexp.keys[i - 1].name] = m[i] && decodeParam(m[i]);
486 }
487
488 return { path: path === '' ? '/' : path, keys: regexp.keys.slice(), params: params };
489}
490
491/**
492 * Universal Router (https://www.kriasoft.com/universal-router/)
493 *
494 * Copyright © 2015-present Kriasoft, LLC. All rights reserved.
495 *
496 * This source code is licensed under the Apache 2.0 license found in the
497 * LICENSE.txt file in the root directory of this source tree.
498 */
499
500function matchRoute(route, baseUrl, path, parentParams) {
501 var match = void 0;
502 var childMatches = void 0;
503 var childIndex = 0;
504
505 return {
506 next: function next() {
507 if (!match) {
508 match = matchPath(route.path, path, !route.children, parentParams);
509
510 if (match) {
511 return {
512 done: false,
513 value: {
514 route: route,
515 baseUrl: baseUrl,
516 path: match.path,
517 keys: match.keys,
518 params: match.params
519 }
520 };
521 }
522 }
523
524 if (match && route.children) {
525 while (childIndex < route.children.length) {
526 if (!childMatches) {
527 var newPath = path.substr(match.path.length);
528 var childRoute = route.children[childIndex];
529 childRoute.parent = route;
530
531 childMatches = matchRoute(childRoute, baseUrl + (match.path === '/' ? '' : match.path), newPath.charAt(0) === '/' ? newPath : '/' + newPath, match.params);
532 }
533
534 var childMatch = childMatches.next();
535 if (!childMatch.done) {
536 return {
537 done: false,
538 value: childMatch.value
539 };
540 }
541
542 childMatches = null;
543 childIndex += 1;
544 }
545 }
546
547 return { done: true };
548 }
549 };
550}
551
552/**
553 * Universal Router (https://www.kriasoft.com/universal-router/)
554 *
555 * Copyright © 2015-present Kriasoft, LLC. All rights reserved.
556 *
557 * This source code is licensed under the Apache 2.0 license found in the
558 * LICENSE.txt file in the root directory of this source tree.
559 */
560
561function resolveRoute(context, params) {
562 if (typeof context.route.action === 'function') {
563 return context.route.action(context, params);
564 }
565
566 return null;
567}
568
569var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
570
571function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
572
573/**
574 * Universal Router (https://www.kriasoft.com/universal-router/)
575 *
576 * Copyright © 2015-present Kriasoft, LLC. All rights reserved.
577 *
578 * This source code is licensed under the Apache 2.0 license found in the
579 * LICENSE.txt file in the root directory of this source tree.
580 */
581
582function isChildRoute(parentRoute, childRoute) {
583 var route = childRoute;
584 while (route) {
585 route = route.parent;
586 if (route === parentRoute) {
587 return true;
588 }
589 }
590 return false;
591}
592
593var Router = function () {
594 function Router(routes) {
595 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
596
597 _classCallCheck(this, Router);
598
599 if (Object(routes) !== routes) {
600 throw new TypeError('Invalid routes');
601 }
602
603 this.baseUrl = options.baseUrl || '';
604 this.resolveRoute = options.resolveRoute || resolveRoute;
605 this.context = Object.assign({ router: this }, options.context);
606 this.root = Array.isArray(routes) ? { path: '/', children: routes, parent: null } : routes;
607 this.root.parent = null;
608 }
609
610 _createClass(Router, [{
611 key: 'resolve',
612 value: function resolve(pathOrContext) {
613 var context = Object.assign({}, this.context, typeof pathOrContext === 'string' ? { path: pathOrContext } : pathOrContext);
614 var match = matchRoute(this.root, this.baseUrl, context.path.substr(this.baseUrl.length));
615 var resolve = this.resolveRoute;
616 var matches = null;
617 var nextMatches = null;
618
619 function next(resume) {
620 var parent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : matches.value.route;
621
622 matches = nextMatches || match.next();
623 nextMatches = null;
624
625 if (!resume) {
626 if (matches.done || !isChildRoute(parent, matches.value.route)) {
627 nextMatches = matches;
628 return Promise.resolve(null);
629 }
630 }
631
632 if (matches.done) {
633 return Promise.reject(Object.assign(new Error('Page not found'), { context: context, status: 404, statusCode: 404 }));
634 }
635
636 return Promise.resolve(resolve(Object.assign({}, context, matches.value), matches.value.params)).then(function (result) {
637 if (result !== null && result !== undefined) {
638 return result;
639 }
640
641 return next(resume, parent);
642 });
643 }
644
645 context.url = context.path;
646 context.next = next;
647
648 return next(true, this.root);
649 }
650 }]);
651
652 return Router;
653}();
654
655Router.pathToRegexp = index;
656Router.matchPath = matchPath;
657Router.matchRoute = matchRoute;
658Router.resolveRoute = resolveRoute;
659
660return Router;
661
662})));
663//# sourceMappingURL=universal-router.js.map