universal-router
Version:
Isomorphic router for JavaScript web applications
626 lines (521 loc) • 17.3 kB
JavaScript
/*! Universal Router | MIT License | https://www.kriasoft.com/universal-router/ */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.UniversalRouter = factory());
}(this, (function () { 'use strict';
/**
* Expose `pathToRegexp`.
*/
var pathToRegexp_1 = pathToRegexp;
var parse_1 = parse;
var compile_1 = compile;
var tokensToFunction_1 = tokensToFunction;
var tokensToRegExp_1 = tokensToRegExp;
/**
* Default configs.
*/
var DEFAULT_DELIMITER = '/';
var DEFAULT_DELIMITERS = './';
/**
* The main path matching regexp utility.
*
* @type {RegExp}
*/
var PATH_REGEXP = new RegExp([
// Match escaped characters that would otherwise appear in future matches.
// This allows the user to escape special characters that won't transform.
'(\\\\.)',
// Match Express-style parameters and un-named parameters with a prefix
// and optional suffixes. Matches appear as:
//
// "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?"]
// "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined]
'(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?'
].join('|'), 'g');
/**
* Parse a string for the raw tokens.
*
* @param {string} str
* @param {Object=} options
* @return {!Array}
*/
function parse (str, options) {
var tokens = [];
var key = 0;
var index = 0;
var path = '';
var defaultDelimiter = (options && options.delimiter) || DEFAULT_DELIMITER;
var delimiters = (options && options.delimiters) || DEFAULT_DELIMITERS;
var pathEscaped = false;
var res;
while ((res = PATH_REGEXP.exec(str)) !== null) {
var m = res[0];
var escaped = res[1];
var offset = res.index;
path += str.slice(index, offset);
index = offset + m.length;
// Ignore already escaped sequences.
if (escaped) {
path += escaped[1];
pathEscaped = true;
continue
}
var prev = '';
var next = str[index];
var name = res[2];
var capture = res[3];
var group = res[4];
var modifier = res[5];
if (!pathEscaped && path.length) {
var k = path.length - 1;
if (delimiters.indexOf(path[k]) > -1) {
prev = path[k];
path = path.slice(0, k);
}
}
// Push the current path onto the tokens.
if (path) {
tokens.push(path);
path = '';
pathEscaped = false;
}
var partial = prev !== '' && next !== undefined && next !== prev;
var repeat = modifier === '+' || modifier === '*';
var optional = modifier === '?' || modifier === '*';
var delimiter = prev || defaultDelimiter;
var pattern = capture || group;
tokens.push({
name: name || key++,
prefix: prev,
delimiter: delimiter,
optional: optional,
repeat: repeat,
partial: partial,
pattern: pattern ? escapeGroup(pattern) : '[^' + escapeString(delimiter) + ']+?'
});
}
// Push any remaining characters.
if (path || index < str.length) {
tokens.push(path + str.substr(index));
}
return tokens
}
/**
* Compile a string to a template function for the path.
*
* @param {string} str
* @param {Object=} options
* @return {!function(Object=, Object=)}
*/
function compile (str, options) {
return tokensToFunction(parse(str, options))
}
/**
* Expose a method for transforming tokens into the path function.
*/
function tokensToFunction (tokens) {
// Compile all the tokens into regexps.
var matches = new Array(tokens.length);
// Compile all the patterns before compilation.
for (var i = 0; i < tokens.length; i++) {
if (typeof tokens[i] === 'object') {
matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$');
}
}
return function (data, options) {
var path = '';
var encode = (options && options.encode) || encodeURIComponent;
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
if (typeof token === 'string') {
path += token;
continue
}
var value = data ? data[token.name] : undefined;
var segment;
if (Array.isArray(value)) {
if (!token.repeat) {
throw new TypeError('Expected "' + token.name + '" to not repeat, but got array')
}
if (value.length === 0) {
if (token.optional) continue
throw new TypeError('Expected "' + token.name + '" to not be empty')
}
for (var j = 0; j < value.length; j++) {
segment = encode(value[j]);
if (!matches[i].test(segment)) {
throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '"')
}
path += (j === 0 ? token.prefix : token.delimiter) + segment;
}
continue
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
segment = encode(String(value));
if (!matches[i].test(segment)) {
throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but got "' + segment + '"')
}
path += token.prefix + segment;
continue
}
if (token.optional) {
// Prepend partial segment prefixes.
if (token.partial) path += token.prefix;
continue
}
throw new TypeError('Expected "' + token.name + '" to be ' + (token.repeat ? 'an array' : 'a string'))
}
return path
}
}
/**
* Escape a regular expression string.
*
* @param {string} str
* @return {string}
*/
function escapeString (str) {
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
}
/**
* Escape the capturing group by escaping special characters and meaning.
*
* @param {string} group
* @return {string}
*/
function escapeGroup (group) {
return group.replace(/([=!:$/()])/g, '\\$1')
}
/**
* Get the flags for a regexp from the options.
*
* @param {Object} options
* @return {string}
*/
function flags (options) {
return options && options.sensitive ? '' : 'i'
}
/**
* Pull out keys from a regexp.
*
* @param {!RegExp} path
* @param {Array=} keys
* @return {!RegExp}
*/
function regexpToRegexp (path, keys) {
if (!keys) return path
// Use a negative lookahead to match only capturing groups.
var groups = path.source.match(/\((?!\?)/g);
if (groups) {
for (var i = 0; i < groups.length; i++) {
keys.push({
name: i,
prefix: null,
delimiter: null,
optional: false,
repeat: false,
partial: false,
pattern: null
});
}
}
return path
}
/**
* Transform an array into a regexp.
*
* @param {!Array} path
* @param {Array=} keys
* @param {Object=} options
* @return {!RegExp}
*/
function arrayToRegexp (path, keys, options) {
var parts = [];
for (var i = 0; i < path.length; i++) {
parts.push(pathToRegexp(path[i], keys, options).source);
}
return new RegExp('(?:' + parts.join('|') + ')', flags(options))
}
/**
* Create a path regexp from string input.
*
* @param {string} path
* @param {Array=} keys
* @param {Object=} options
* @return {!RegExp}
*/
function stringToRegexp (path, keys, options) {
return tokensToRegExp(parse(path, options), keys, options)
}
/**
* Expose a function for taking tokens and returning a RegExp.
*
* @param {!Array} tokens
* @param {Array=} keys
* @param {Object=} options
* @return {!RegExp}
*/
function tokensToRegExp (tokens, keys, options) {
options = options || {};
var strict = options.strict;
var end = options.end !== false;
var delimiter = escapeString(options.delimiter || DEFAULT_DELIMITER);
var delimiters = options.delimiters || DEFAULT_DELIMITERS;
var endsWith = [].concat(options.endsWith || []).map(escapeString).concat('$').join('|');
var route = '';
var isEndDelimited = false;
// Iterate over the tokens and create our regexp string.
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
if (typeof token === 'string') {
route += escapeString(token);
isEndDelimited = i === tokens.length - 1 && delimiters.indexOf(token[token.length - 1]) > -1;
} else {
var prefix = escapeString(token.prefix);
var capture = token.repeat
? '(?:' + token.pattern + ')(?:' + prefix + '(?:' + token.pattern + '))*'
: token.pattern;
if (keys) keys.push(token);
if (token.optional) {
if (token.partial) {
route += prefix + '(' + capture + ')?';
} else {
route += '(?:' + prefix + '(' + capture + '))?';
}
} else {
route += prefix + '(' + capture + ')';
}
}
}
if (end) {
if (!strict) route += '(?:' + delimiter + ')?';
route += endsWith === '$' ? '$' : '(?=' + endsWith + ')';
} else {
if (!strict) route += '(?:' + delimiter + '(?=' + endsWith + '))?';
if (!isEndDelimited) route += '(?=' + delimiter + '|' + endsWith + ')';
}
return new RegExp('^' + route, flags(options))
}
/**
* Normalize the given path string, returning a regular expression.
*
* An empty array can be passed in for the keys, which will hold the
* placeholder key descriptions. For example, using `/user/:id`, `keys` will
* contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
*
* @param {(string|RegExp|Array)} path
* @param {Array=} keys
* @param {Object=} options
* @return {!RegExp}
*/
function pathToRegexp (path, keys, options) {
if (path instanceof RegExp) {
return regexpToRegexp(path, keys)
}
if (Array.isArray(path)) {
return arrayToRegexp(/** @type {!Array} */ (path), keys, options)
}
return stringToRegexp(/** @type {string} */ (path), keys, options)
}
pathToRegexp_1.parse = parse_1;
pathToRegexp_1.compile = compile_1;
pathToRegexp_1.tokensToFunction = tokensToFunction_1;
pathToRegexp_1.tokensToRegExp = tokensToRegExp_1;
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright © 2015-present Kriasoft, LLC. All rights reserved.
*
* This source code is licensed under the Apache 2.0 license found in the
* LICENSE.txt file in the root directory of this source tree.
*/
var hasOwnProperty = Object.prototype.hasOwnProperty;
var cache = new Map();
function decodeParam(val) {
try {
return decodeURIComponent(val);
} catch (err) {
return val;
}
}
function matchPath(route, pathname, parentKeys, parentParams) {
var end = !route.children;
var cacheKey = (route.path || '') + '|' + end;
var regexp = cache.get(cacheKey);
if (!regexp) {
var keys = [];
regexp = {
keys: keys,
pattern: pathToRegexp_1(route.path || '', keys, { end: end })
};
cache.set(cacheKey, regexp);
}
var m = regexp.pattern.exec(pathname);
if (!m) {
return null;
}
var path = m[0];
var params = Object.assign({}, parentParams);
for (var i = 1; i < m.length; i += 1) {
var key = regexp.keys[i - 1];
var prop = key.name;
var value = m[i];
if (value !== undefined || !hasOwnProperty.call(params, prop)) {
if (key.repeat) {
params[prop] = value ? value.split(key.delimiter).map(decodeParam) : [];
} else {
params[prop] = value ? decodeParam(value) : value;
}
}
}
return {
path: !end && path.charAt(path.length - 1) === '/' ? path.substr(1) : path,
keys: parentKeys.concat(regexp.keys),
params: params
};
}
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright © 2015-present Kriasoft, LLC. All rights reserved.
*
* This source code is licensed under the Apache 2.0 license found in the
* LICENSE.txt file in the root directory of this source tree.
*/
function matchRoute(route, baseUrl, pathname, parentKeys, parentParams) {
var match = void 0;
var childMatches = void 0;
var childIndex = 0;
return {
next: function next(routeToSkip) {
if (route === routeToSkip) {
return { done: true };
}
if (!match) {
match = matchPath(route, pathname, parentKeys, parentParams);
if (match) {
return {
done: false,
value: {
route: route,
baseUrl: baseUrl,
path: match.path,
keys: match.keys,
params: match.params
}
};
}
}
if (match && route.children) {
while (childIndex < route.children.length) {
if (!childMatches) {
var childRoute = route.children[childIndex];
childRoute.parent = route;
childMatches = matchRoute(childRoute, baseUrl + match.path, pathname.substr(match.path.length), match.keys, match.params);
}
var childMatch = childMatches.next(routeToSkip);
if (!childMatch.done) {
return {
done: false,
value: childMatch.value
};
}
childMatches = null;
childIndex += 1;
}
}
return { done: true };
}
};
}
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright © 2015-present Kriasoft, LLC. All rights reserved.
*
* This source code is licensed under the Apache 2.0 license found in the
* LICENSE.txt file in the root directory of this source tree.
*/
function resolveRoute(context, params) {
if (typeof context.route.action === 'function') {
return context.route.action(context, params);
}
return undefined;
}
var _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; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright © 2015-present Kriasoft, LLC. All rights reserved.
*
* This source code is licensed under the Apache 2.0 license found in the
* LICENSE.txt file in the root directory of this source tree.
*/
function isChildRoute(parentRoute, childRoute) {
var route = childRoute;
while (route) {
route = route.parent;
if (route === parentRoute) {
return true;
}
}
return false;
}
var UniversalRouter = function () {
function UniversalRouter(routes) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, UniversalRouter);
if (Object(routes) !== routes) {
throw new TypeError('Invalid routes');
}
this.baseUrl = options.baseUrl || '';
this.resolveRoute = options.resolveRoute || resolveRoute;
this.context = Object.assign({ router: this }, options.context);
this.root = Array.isArray(routes) ? { path: '', children: routes, parent: null } : routes;
this.root.parent = null;
}
_createClass(UniversalRouter, [{
key: 'resolve',
value: function resolve(pathnameOrContext) {
var context = Object.assign({}, this.context, typeof pathnameOrContext === 'string' ? { pathname: pathnameOrContext } : pathnameOrContext);
var match = matchRoute(this.root, this.baseUrl, context.pathname.substr(this.baseUrl.length), [], null);
var resolve = this.resolveRoute;
var matches = null;
var nextMatches = null;
function next(resume) {
var parent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : matches.value.route;
var prevResult = arguments[2];
var routeToSkip = prevResult === null && matches.value.route;
matches = nextMatches || match.next(routeToSkip);
nextMatches = null;
if (!resume) {
if (matches.done || !isChildRoute(parent, matches.value.route)) {
nextMatches = matches;
return Promise.resolve(null);
}
}
if (matches.done) {
return Promise.reject(Object.assign(new Error('Page not found'), { context: context, status: 404, statusCode: 404 }));
}
return Promise.resolve(resolve(Object.assign({}, context, matches.value), matches.value.params)).then(function (result) {
if (result !== null && result !== undefined) {
return result;
}
return next(resume, parent, result);
});
}
context.next = next;
return next(true, this.root);
}
}]);
return UniversalRouter;
}();
UniversalRouter.pathToRegexp = pathToRegexp_1;
UniversalRouter.matchPath = matchPath;
UniversalRouter.matchRoute = matchRoute;
UniversalRouter.resolveRoute = resolveRoute;
return UniversalRouter;
})));
//# sourceMappingURL=universal-router.js.map