abstract-nested-router
Version:
Minimal nested-routing impl!
336 lines (279 loc) • 8.41 kB
JavaScript
var defaultExport = /*@__PURE__*/(function (Error) {
function defaultExport(route, path) {
var message = "Unreachable '" + (route !== '/' ? route.replace(/\/$/, '') : route) + "', segment '" + path + "' is not defined";
Error.call(this, message);
this.message = message;
this.route = route;
this.path = path;
}
if ( Error ) defaultExport.__proto__ = Error;
defaultExport.prototype = Object.create( Error && Error.prototype );
defaultExport.prototype.constructor = defaultExport;
return defaultExport;
}(Error));
function buildMatcher(path, parent) {
var regex;
var _isSplat;
var _priority = -100;
var keys = [];
regex = path.replace(/[-$.]/g, '\\$&').replace(/\(/g, '(?:').replace(/\)/g, ')?').replace(/([:*]\w+)(?:<([^<>]+?)>)?/g, function (_, key, expr) {
keys.push(key.substr(1));
if (key.charAt() === ':') {
_priority += 100;
return ("((?!#)" + (expr || '[^#/]+?') + ")");
}
_isSplat = true;
_priority += 500;
return ("((?!#)" + (expr || '[^#]+?') + ")");
});
try {
regex = new RegExp(("^" + regex + "$"));
} catch (e) {
throw new TypeError(("Invalid route expression, given '" + parent + "'"));
}
var _hashed = path.includes('#') ? 0.5 : 1;
var _depth = path.length * _priority * _hashed;
return {
keys: keys,
regex: regex,
_depth: _depth,
_isSplat: _isSplat
};
}
var PathMatcher = function PathMatcher(path, parent) {
var ref = buildMatcher(path, parent);
var keys = ref.keys;
var regex = ref.regex;
var _depth = ref._depth;
var _isSplat = ref._isSplat;
function fn(value) {
var matches = value.match(regex);
if (matches) {
return keys.reduce(function (prev, cur, i) {
prev[cur] = typeof matches[i + 1] === 'string' ? decodeURIComponent(matches[i + 1]) : null;
return prev;
}, {});
}
}
fn.regex = regex;
fn.keys = keys;
return {
_isSplat: _isSplat,
_depth: _depth,
match: fn
};
};
PathMatcher.push = function push (key, prev, leaf, parent) {
var root = prev[key] || (prev[key] = {});
if (!root.pattern) {
root.pattern = new PathMatcher(key, parent);
root.route = (leaf || '').replace(/\/$/, '') || '/';
}
prev.keys = prev.keys || [];
if (!prev.keys.includes(key)) {
prev.keys.push(key);
PathMatcher.sort(prev);
}
return root;
};
PathMatcher.sort = function sort (root) {
root.keys.sort(function (a, b) {
return root[a].pattern._depth - root[b].pattern._depth;
});
};
function merge(path, parent) {
return ("" + (parent && parent !== '/' ? parent : '') + (path || ''));
}
function walk(path, cb) {
var matches = path.match(/<[^<>]*\/[^<>]*>/);
if (matches) {
throw new TypeError(("RegExp cannot contain slashes, given '" + matches + "'"));
}
var parts = path.split(/(?=\/|#)/);
var root = [];
if (parts[0] !== '/') {
parts.unshift('/');
}
parts.some(function (x, i) {
var parent = root.slice(1).concat(x).join('') || null;
var segment = parts.slice(i + 1).join('') || null;
var retval = cb(x, parent, segment ? ("" + (x !== '/' ? x : '') + segment) : null);
root.push(x);
return retval;
});
}
function reduce(key, root) {
var parent = root.refs;
var data = {};
var out = [];
var splat;
walk(key, function (x, leaf, extra) {
if (!root.keys) {
throw new defaultExport(key, x);
}
var found;
root.keys.some(function (k) {
var ref = root[k].pattern;
var match = ref.match;
var _length = ref._length;
var _isSplat = ref._isSplat;
var matches = match(_isSplat ? extra || x : x);
if (matches) {
var routes = (parent[root[k].route] || []).concat(parent[root[k].route + '/'] || []).concat(parent[root[k].route + '#'] || []);
Object.assign(data, matches);
routes.forEach(function (route) {
if (!out.some(function (x) { return x.key === route; })) {
var routeInfo = Object.assign({}, parent[route]); // properly handle exact-routes!
var hasMatch = false;
if (routeInfo.exact) {
hasMatch = extra === null;
} else {
hasMatch = !(x && leaf === null) || x === leaf || _isSplat || !extra;
}
routeInfo.matches = hasMatch;
routeInfo.params = Object.assign({}, data);
routeInfo.route = routeInfo.fullpath;
routeInfo.depth += match.keys.length;
routeInfo.path = _isSplat && extra || leaf || x;
delete routeInfo.fullpath;
out.push(routeInfo);
}
});
if (extra === null && !root[k].keys) {
return true;
}
if (!_isSplat && !extra && root.keys.some(function (x) { return x.includes('*'); })) {
return false;
}
splat = _isSplat;
root = root[k];
found = true;
return true;
}
return false;
});
if (!(found || root.keys.some(function (k) { return root[k].pattern.match(x); }))) {
throw new defaultExport(key, x);
}
return splat || !found;
});
return out.sort(function (a, b) {
if (b.fallback && !a.fallback) { return -1; }
if (a.fallback && !b.fallback) { return 1; }
if (b.route.includes('#') && !a.route.includes('#')) { return -1; }
if (a.route.includes('#') && !b.route.includes('#')) { return 1; }
return a.depth - b.depth;
});
}
function find(path, routes, retries) {
var get = reduce.bind(null, path, routes);
var set = [];
while (retries > 0) {
retries -= 1;
try {
return get(set);
} catch (e) {
if (retries > 0) {
return get(set);
}
throw e;
}
}
}
function add(path, routes, parent, routeInfo) {
var fullpath = merge(path, parent);
var depth = fullpath.split(/(?=[#:/*.]\w)/g).length;
var params = Object.assign({}, routeInfo, {
fullpath: fullpath,
depth: depth
});
if (!path || !'#/'.includes(path.charAt())) {
throw new TypeError(("Routes should have a valid path, given " + (JSON.stringify(path))));
}
if (!params.key) {
throw new TypeError(("Routes should have a key, given " + (JSON.stringify(params))));
}
routes.refs[params.key] = params;
routes.refs[fullpath] = routes.refs[fullpath] ? routes.refs[fullpath].concat(params.key) : [params.key];
var root = routes;
walk(fullpath, function (x, leaf) {
root = PathMatcher.push(x, root, leaf, fullpath);
});
return fullpath;
}
function rm(path, routes, parent) {
var fullpath = merge(path, parent);
var root = routes;
var leaf = null;
var key = null;
walk(fullpath, function (x) {
if (!root) {
leaf = null;
return true;
}
if (!root.keys) {
throw new defaultExport(path, x);
}
key = x;
leaf = root;
root = root[key];
});
if (!(leaf && key)) {
throw new defaultExport(path, key);
}
if (leaf === routes) {
leaf = routes['/'];
}
if (leaf.route !== key) {
var offset = leaf.keys.indexOf(key);
if (offset === -1) {
throw new defaultExport(path, key);
}
leaf.keys.splice(offset, 1);
PathMatcher.sort(leaf);
delete leaf[key];
}
if (leaf.route === root.route) {
delete routes.refs[fullpath];
}
}
var Router = function Router() {
var routes = {
refs: {}
};
var stack = [];
return {
routes: routes,
resolve: function (path, cb) {
var url = path.split('?')[0];
var seen = [];
walk(url, function (x, leaf, extra) {
try {
cb(null, find(leaf, routes, 2).filter(function (r) {
if (!seen.includes(r.route)) {
seen.push(r.route);
return true;
}
return false;
}), leaf);
} catch (e) {
cb(e, []);
}
});
},
mount: function (path, cb) {
if (path !== '/') {
stack.push(path);
}
cb();
stack.pop();
},
find: function (path, retries) { return find(path, routes, retries === true ? 2 : retries || 1); },
add: function (path, routeInfo) { return add(path, routes, stack.join(''), routeInfo); },
rm: function (path) { return rm(path, routes, stack.join('')); }
};
};
Router.matches = function matches (uri, path) {
return buildMatcher(uri, path).regex.test(path);
};
export default Router;