UNPKG

mcrm-svelte-navigator

Version:
373 lines (338 loc) 10.2 kB
import { segmentize, join, addQuery, startsWith, paramRegex, isSplat, isRootSegment, isDynamic, stripSplat, normalizePath, substr, } from "./paths"; import { ROUTER_ID, fail } from "./warning"; import { isUndefined } from "./utils"; const SEGMENT_POINTS = 4; const STATIC_POINTS = 3; const DYNAMIC_POINTS = 2; const SPLAT_PENALTY = 1; const ROOT_POINTS = 1; /** * Score a route depending on how its individual segments look * @param {object} route * @param {number} index * @return {object} */ export function rankRoute(route, index) { const score = route.default ? 0 : segmentize(route.fullPath).reduce((acc, segment) => { let nextScore = acc; nextScore += SEGMENT_POINTS; if (isRootSegment(segment)) { nextScore += ROOT_POINTS; } else if (isDynamic(segment)) { nextScore += DYNAMIC_POINTS; } else if (isSplat(segment)) { nextScore -= SEGMENT_POINTS + SPLAT_PENALTY; } else { nextScore += STATIC_POINTS; } return nextScore; }, 0); return { route, score, index }; } /** * Give a score to all routes and sort them on that * @param {object[]} routes * @return {object[]} */ export function rankRoutes(routes) { return ( routes .map(rankRoute) // If two routes have the exact same score, we go by index instead .sort((a, b) => { if (a.score < b.score) { return 1; } if (a.score > b.score) { return -1; } return 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 * * { fullPath, default, value } * * And a returned match looks like: * * { route, params, uri } * * @param {object[]} routes * @param {string} uri * @return {?object} */ export function pick(routes, uri) { let bestMatch; let defaultMatch; 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]; let missed = false; const params = {}; // eslint-disable-next-line no-shadow const createMatch = uri => ({ ...route, params, uri }); if (route.default) { defaultMatch = createMatch(uri); continue; } const routeSegments = segmentize(route.fullPath); const max = Math.max(uriSegments.length, routeSegments.length); let index = 0; for (; index < max; index++) { const routeSegment = routeSegments[index]; const uriSegment = uriSegments[index]; if (!isUndefined(routeSegment) && isSplat(routeSegment)) { // 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 (isUndefined(uriSegment)) { // URI is shorter than the route, no match // uri: /users // route: /users/:userId missed = true; break; } const dynamicMatch = paramRegex.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) { bestMatch = createMatch(join(...uriSegments.slice(0, index))); break; } } return bestMatch || defaultMatch || null; } /** * Check if the `route.fullPath` matches the `uri`. * @param {Object} route * @param {string} uri * @return {?object} */ export function match(route, uri) { return pick([route], uri); } /** * 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. * @param {string} to * @param {string} base * @return {string} */ export function resolve(to, base) { // /foo/bar, /baz/qux => /foo/bar if (startsWith(to, "/")) { 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 (!startsWith(toSegments[0], ".")) { 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); } /** * Normalizes a location for consumption by `Route` children and the `Router`. * It removes the apps basepath from the pathname * and sets default values for `search` and `hash` properties. * * @param {Object} location The current global location supplied by the history component * @param {string} basepath The applications basepath (i.e. when serving from a subdirectory) * * @returns The normalized location */ export function normalizeLocation(location, basepath) { const { pathname, hash = "", search = "", state } = location; const baseSegments = segmentize(basepath, true); const pathSegments = segmentize(pathname, true); while (baseSegments.length) { if (baseSegments[0] !== pathSegments[0]) { fail( ROUTER_ID, `Invalid state: All locations must begin with the basepath "${basepath}", found "${pathname}"`, ); } baseSegments.shift(); pathSegments.shift(); } return { pathname: join(...pathSegments), hash, search, state, }; } const normalizeUrlFragment = frag => (frag.length === 1 ? "" : frag); /** * Creates a location object from an url. * It is used to create a location from the url prop used in SSR * * @param {string} url The url string (e.g. "/path/to/somewhere") * @returns {{ pathname: string; search: string; hash: string }} The location * * @example * ```js * const path = "/search?q=falafel#result-3"; * const location = parsePath(path); * // -> { * // pathname: "/search", * // search: "?q=falafel", * // hash: "#result-3", * // }; * ``` */ export const parsePath = path => { const searchIndex = path.indexOf("?"); const hashIndex = path.indexOf("#"); const hasSearchIndex = searchIndex !== -1; const hasHashIndex = hashIndex !== -1; const hash = hasHashIndex ? normalizeUrlFragment(substr(path, hashIndex)) : ""; const pathnameAndSearch = hasHashIndex ? substr(path, 0, hashIndex) : path; const search = hasSearchIndex ? normalizeUrlFragment(substr(pathnameAndSearch, searchIndex)) : ""; const pathname = (hasSearchIndex ? substr(pathnameAndSearch, 0, searchIndex) : pathnameAndSearch) || "/"; return { pathname, search, hash }; }; /** * Joins a location object to one path string. * * @param {{ pathname: string; search: string; hash: string }} location The location object * @returns {string} A path, created from the location * * @example * ```js * const location = { * pathname: "/search", * search: "?q=falafel", * hash: "#result-3", * }; * const path = stringifyPath(location); * // -> "/search?q=falafel#result-3" * ``` */ export const stringifyPath = location => { const { pathname, search, hash } = location; return pathname + search + hash; }; /** * Resolves a link relative to the parent Route and the Routers basepath. * * @param {string} path The given path, that will be resolved * @param {string} routeBase The current Routes base path * @param {string} appBase The basepath of the app. Used, when serving from a subdirectory * @returns {string} The resolved path * * @example * resolveLink("relative", "/routeBase", "/") // -> "/routeBase/relative" * resolveLink("/absolute", "/routeBase", "/") // -> "/absolute" * resolveLink("relative", "/routeBase", "/base") // -> "/base/routeBase/relative" * resolveLink("/absolute", "/routeBase", "/base") // -> "/base/absolute" */ export function resolveLink(path, routeBase, appBase) { return join(appBase, resolve(path, routeBase)); } /** * Get the uri for a Route, by matching it against the current location. * * @param {string} routePath The Routes resolved path * @param {string} pathname The current locations pathname */ export function extractBaseUri(routePath, pathname) { const fullPath = normalizePath(stripSplat(routePath)); const baseSegments = segmentize(fullPath, true); const pathSegments = segmentize(pathname, true).slice(0, baseSegments.length); const routeMatch = match({ fullPath }, join(...pathSegments)); return routeMatch && routeMatch.uri; }