UNPKG

svelte5-router

Version:

A declarative Svelte routing library with SSR support

217 lines (216 loc) 7.62 kB
/** * Adapted from https://github.com/reach/router/blob/b60e6dd781d5d3a4bdaaf4de665649c0f6a7e78d/src/lib/utils.js * https://github.com/reach/router/blob/master/LICENSE */ const PARAM = /^:(.+)/; const SEGMENT_POINTS = 4; const STATIC_POINTS = 3; const DYNAMIC_POINTS = 2; const SPLAT_PENALTY = 1; const ROOT_POINTS = 1; /** * Split up the URI into segments delimited by `/` * Strip starting/ending `/` */ const segmentize = (uri) => uri.replace(/(^\/+|\/+$)/g, "").split("/"); /** * Strip `str` of potential start and end `/` */ const stripSlashes = (string) => string.replace(/(^\/+|\/+$)/g, ""); /** * Score a route depending on how its individual segments look */ const rankRoute = (route, index) => { const score = route.default ? 0 : segmentize(route.path).reduce((score, segment) => { score += SEGMENT_POINTS; if (segment === "") { score += ROOT_POINTS; } else if (PARAM.test(segment)) { score += DYNAMIC_POINTS; } else if (segment[0] === "*") { score -= SEGMENT_POINTS + SPLAT_PENALTY; } else { score += STATIC_POINTS; } return score; }, 0); return { route, score, index }; }; /** * Give a score to all routes and sort them on that * If two routes have the exact same score, we go by index instead */ const rankRoutes = (routes) => routes .map(rankRoute) .sort((a, b) => a.score < b.score ? 1 : a.score > b.score ? -1 : a.index - b.index); /** * Ranks and picks the best route to match. Each segment gets the highest * amount of points, then the type of segment gets an additional amount of * points where * * static > dynamic > splat > root * * This way we don't have to worry about the order of our routes, let the * computers do it. * * A route looks like this * * { path, default, value } * * And a returned match looks like: * * { route, params, uri } * */ const pick = (routes, uri) => { let match; let default_; const [uriPathname] = uri.split("?"); const uriSegments = segmentize(uriPathname); const isRootUri = uriSegments[0] === ""; const ranked = rankRoutes(routes); for (let i = 0, l = ranked.length; i < l; i++) { const route = ranked[i].route; let missed = false; if (route.default) { default_ = { route, params: {}, uri, }; continue; } const routeSegments = segmentize(route.path); const params = {}; const max = Math.max(uriSegments.length, routeSegments.length); let index = 0; for (; index < max; index++) { const routeSegment = routeSegments[index]; const uriSegment = uriSegments[index]; if (routeSegment && routeSegment[0] === "*") { // Hit a splat, just grab the rest, and return a match // uri: /files/documents/work // route: /files/* or /files/*splatname const splatName = routeSegment === "*" ? "*" : routeSegment.slice(1); params[splatName] = uriSegments .slice(index) .map(decodeURIComponent) .join("/"); break; } if (typeof uriSegment === "undefined") { // URI is shorter than the route, no match // uri: /users // route: /users/:userId missed = true; break; } const dynamicMatch = PARAM.exec(routeSegment); if (dynamicMatch && !isRootUri) { const value = decodeURIComponent(uriSegment); params[dynamicMatch[1]] = value; } else if (routeSegment !== uriSegment) { // Current segments don't match, not dynamic, not splat, so no match // uri: /users/123/settings // route: /users/:id/profile missed = true; break; } } if (!missed) { match = { route, params, uri: "/" + uriSegments.slice(0, index).join("/"), }; break; } } return match || default_ || null; }; /** * Add the query to the pathname if a query is given */ const addQuery = (pathname, query) => pathname + (query ? `?${query}` : ""); /** * Resolve URIs as though every path is a directory, no files. Relative URIs * in the browser can feel awkward because not only can you be "in a directory", * you can be "at a file", too. For example: * * browserSpecResolve('foo', '/bar/') => /bar/foo * browserSpecResolve('foo', '/bar') => /foo * * But on the command line of a file system, it's not as complicated. You can't * `cd` from a file, only directories. This way, links have to know less about * their current path. To go deeper you can do this: * * <Link to="deeper"/> * // instead of * <Link to=`{${props.uri}/deeper}`/> * * Just like `cd`, if you want to go deeper from the command line, you do this: * * cd deeper * # not * cd $(pwd)/deeper * * By treating every path as a directory, linking to relative paths should * require less contextual information and (fingers crossed) be more intuitive. */ const resolve = (to, base) => { // /foo/bar, /baz/qux => /foo/bar if (to.startsWith("/")) return to; const [toPathname, toQuery] = to.split("?"); const [basePathname] = base.split("?"); const toSegments = segmentize(toPathname); const baseSegments = segmentize(basePathname); // ?a=b, /users?b=c => /users?a=b if (toSegments[0] === "") return addQuery(basePathname, toQuery); // profile, /users/789 => /users/789/profile if (!toSegments[0].startsWith(".")) { const pathname = baseSegments.concat(toSegments).join("/"); return addQuery((basePathname === "/" ? "" : "/") + pathname, toQuery); } // ./ , /users/123 => /users/123 // ../ , /users/123 => /users // ../.. , /users/123 => / // ../../one, /a/b/c/d => /a/b/one // .././one , /a/b/c/d => /a/b/c/one const allSegments = baseSegments.concat(toSegments); const segments = []; allSegments.forEach((segment) => { if (segment === "..") segments.pop(); else if (segment !== ".") segments.push(segment); }); return addQuery("/" + segments.join("/"), toQuery); }; /** * Combines the `basepath` and the `path` into one path. */ const combinePaths = (basepath, path) => `${stripSlashes(path === "/" ? basepath : `${stripSlashes(basepath)}/${stripSlashes(path)}`)}/`; /** * Decides whether a given `event` should result in a navigation or not. * @param {object} event */ const shouldNavigate = (event) => !event.defaultPrevented && event.button === 0 && !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); // svelte seems to kill anchor.host value in ie11, so fall back to checking href const hostMatches = (anchor) => { const host = location.host; return (anchor.host === host || anchor.href.indexOf(`https://${host}`) === 0 || anchor.href.indexOf(`http://${host}`) === 0); }; const canUseDOM = () => typeof window !== "undefined" && "document" in window && "location" in window; export { stripSlashes, pick, resolve, combinePaths, shouldNavigate, hostMatches, canUseDOM, };