From 9e449f580e8c9a73a855b31eef49514791300bfa Mon Sep 17 00:00:00 2001 From: Jimmy Jia Date: Fri, 30 Oct 2015 13:15:43 -0400 Subject: [PATCH] [fixed] Match routes piece-by-piece --- modules/PatternUtils.js | 43 +++++++++++++++++---- modules/isActive.js | 27 +++++++++---- modules/matchRoutes.js | 84 +++++++++++++++++++++++++---------------- 3 files changed, 107 insertions(+), 47 deletions(-) diff --git a/modules/PatternUtils.js b/modules/PatternUtils.js index e0ff1b57e4..a1b6510578 100644 --- a/modules/PatternUtils.js +++ b/modules/PatternUtils.js @@ -82,28 +82,57 @@ export function compilePattern(pattern) { * - paramValues */ export function matchPattern(pattern, pathname) { + // Make leading slashes consistent between pattern and pathname. + if (pattern.charAt(0) !== '/') { + pattern = `/${pattern}` + } + if (pathname.charAt(0) !== '/') { + pathname = `/${pathname}` + } + let { regexpSource, paramNames, tokens } = compilePattern(pattern) regexpSource += '/*' // Ignore trailing slashes + // Special-case patterns like '*' for catch-all routes. const captureRemaining = tokens[tokens.length - 1] !== '*' - if (captureRemaining) + if (captureRemaining) { + // This will match newlines in the remaining path. regexpSource += '([\\s\\S]*?)' + } const match = pathname.match(new RegExp('^' + regexpSource + '$', 'i')) let remainingPathname, paramValues if (match != null) { - paramValues = Array.prototype.slice.call(match, 1).map(function (v) { - return v != null ? decodeURIComponent(v) : v - }) - + let matchedPath if (captureRemaining) { - remainingPathname = paramValues.pop() + remainingPathname = match.pop() + matchedPath = + match[0].substr(0, match[0].length - remainingPathname.length) } else { - remainingPathname = pathname.replace(match[0], '') + // If this matched at all, then the match was the entire pathname. + matchedPath = match[0] + remainingPathname = '' + } + + // Ensure we actually match at a path boundary. + if (remainingPathname && remainingPathname.charAt(0) !== '/') { + // This depends on the leading slash getting added to pathname above to + // work in all cases. + if (!matchedPath || matchedPath.charAt(matchedPath.length - 1) !== '/') { + return { + remainingPathname: null, + paramNames, + paramValues: null + } + } } + + paramValues = match.slice(1).map( + v => v != null ? decodeURIComponent(v) : v + ) } else { remainingPathname = paramValues = null } diff --git a/modules/isActive.js b/modules/isActive.js index 35f85536f2..8f95888a99 100644 --- a/modules/isActive.js +++ b/modules/isActive.js @@ -37,26 +37,39 @@ function deepEqual(a, b) { } function paramsAreActive(paramNames, paramValues, activeParams) { + // FIXME: This doesn't work on repeated params in activeParams. return paramNames.every(function (paramName, index) { return String(paramValues[index]) === String(activeParams[paramName]) }) } function getMatchingRoute(pathname, activeRoutes, activeParams) { - let route, pattern, basename = '' + let route, pattern + let remainingPathname = pathname, paramNames = [], paramValues = [] + for (let i = 0, len = activeRoutes.length; i < len; ++i) { route = activeRoutes[i] pattern = route.path || '' - if (pattern.charAt(0) !== '/') - pattern = basename.replace(/\/*$/, '/') + pattern // Relative paths build on the parent's path. + if (pattern.charAt(0) === '/') { + remainingPathname = pathname + paramNames = [] + paramValues = [] + } - let { remainingPathname, paramNames, paramValues } = matchPattern(pattern, pathname) + if (remainingPathname !== null) { + const matched = matchPattern(pattern, remainingPathname) + remainingPathname = matched.remainingPathname + paramNames = [ ...paramNames, ...matched.paramNames ] + paramValues = [ ...paramValues, ...matched.paramValues ] + } - if (remainingPathname === '' && route.path && paramsAreActive(paramNames, paramValues, activeParams)) + if ( + remainingPathname === '' && + route.path && + paramsAreActive(paramNames, paramValues, activeParams) + ) return route - - basename = pattern } return null diff --git a/modules/matchRoutes.js b/modules/matchRoutes.js index 58666d66fb..68c20cf052 100644 --- a/modules/matchRoutes.js +++ b/modules/matchRoutes.js @@ -44,13 +44,13 @@ function getIndexRoute(route, location, callback) { } function assignParams(params, paramNames, paramValues) { - return paramNames.reduceRight(function (params, paramName, index) { + return paramNames.reduce(function (params, paramName, index) { const paramValue = paramValues && paramValues[index] if (Array.isArray(params[paramName])) { - params[paramName].unshift(paramValue) + params[paramName].push(paramValue) } else if (paramName in params) { - params[paramName] = [ paramValue, params[paramName] ] + params[paramName] = [ params[paramName], paramValue ] } else { params[paramName] = paramValue } @@ -63,34 +63,46 @@ function createParams(paramNames, paramValues) { return assignParams({}, paramNames, paramValues) } -function matchRouteDeep(basename, route, location, callback) { +function matchRouteDeep( + route, location, remainingPathname, paramNames, paramValues, callback +) { let pattern = route.path || '' - if (pattern.charAt(0) !== '/') - pattern = basename.replace(/\/*$/, '/') + pattern // Relative paths build on the parent's path. + if (pattern.charAt(0) === '/') { + remainingPathname = location.pathname + paramNames = [] + paramValues = [] + } - const { remainingPathname, paramNames, paramValues } = matchPattern(pattern, location.pathname) - const isExactMatch = remainingPathname === '' + if (remainingPathname !== null) { + const matched = matchPattern(pattern, remainingPathname) + remainingPathname = matched.remainingPathname + paramNames = [ ...paramNames, ...matched.paramNames ] + paramValues = [ ...paramValues, ...matched.paramValues ] - if (isExactMatch && route.path) { - const match = { - routes: [ route ], - params: createParams(paramNames, paramValues) - } + if (remainingPathname === '' && route.path) { + const match = { + routes: [ route ], + params: createParams(paramNames, paramValues) + } - getIndexRoute(route, location, function (error, indexRoute) { - if (error) { - callback(error) - } else { - if (Array.isArray(indexRoute)) - match.routes.push(...indexRoute) - else if (indexRoute) - match.routes.push(indexRoute) + getIndexRoute(route, location, function (error, indexRoute) { + if (error) { + callback(error) + } else { + if (Array.isArray(indexRoute)) + match.routes.push(...indexRoute) + else if (indexRoute) + match.routes.push(indexRoute) - callback(null, match) - } - }) - } else if (remainingPathname != null || route.childRoutes) { + callback(null, match) + } + }) + return + } + } + + if (remainingPathname != null || route.childRoutes) { // Either a) this route matched at least some of the path or b) // we don't have to load this route's children asynchronously. In // either case continue checking for matches in the subtree. @@ -109,7 +121,7 @@ function matchRouteDeep(basename, route, location, callback) { } else { callback() } - }, pattern) + }, remainingPathname, paramNames, paramValues) } else { callback() } @@ -130,15 +142,21 @@ function matchRouteDeep(basename, route, location, callback) { * Note: This operation may finish synchronously if no routes have an * asynchronous getChildRoutes method. */ -function matchRoutes(routes, location, callback, basename='') { +function matchRoutes( + routes, location, callback, + remainingPathname=location.pathname, paramNames=[], paramValues=[] +) { loopAsync(routes.length, function (index, next, done) { - matchRouteDeep(basename, routes[index], location, function (error, match) { - if (error || match) { - done(error, match) - } else { - next() + matchRouteDeep( + routes[index], location, remainingPathname, paramNames, paramValues, + function (error, match) { + if (error || match) { + done(error, match) + } else { + next() + } } - }) + ) }, callback) }