UNPKG

abstract-nested-router

Version:

Minimal nested-routing impl!

336 lines (279 loc) 8.41 kB
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;