forked from ngrx/router
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(patternMatching): Switch to path-to-regexp for pattern matching (n…
…grx#57) path-to-regexp offers a broader pattern matching feature set with better performance. This replaces react-router's home grown pattern matching with path-to-regexp. For more information on the capabilities of path-to-regexp's pattern matching, checkout the documentation: https://github.com/pillarjs/path-to-regexp/blob/master/Readme.md#parameters
1 parent
5b14ef9
commit 4176112
Showing
13 changed files
with
249 additions
and
242 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,233 +1,90 @@ | ||
/** | ||
* This is a straight up copy of react-router's PatternUtils. It may be worth | ||
* investigating if the react-router team is open to splitting this out | ||
* into a separate package | ||
*/ | ||
import * as pathToRegexp from 'path-to-regexp'; | ||
|
||
export interface Params { | ||
[param: string]: string | string[]; | ||
} | ||
|
||
function escapeRegExp(input: string) { | ||
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||
} | ||
|
||
function escapeSource(input: string) { | ||
return escapeRegExp(input).replace(/\/+/g, '/+'); | ||
} | ||
|
||
export interface CompiledPattern { | ||
pattern: string; | ||
regexpSource: string; | ||
paramNames: string[]; | ||
tokens: string[]; | ||
} | ||
|
||
function _compilePattern(pattern: string): CompiledPattern { | ||
let regexpSource = ''; | ||
const paramNames: string[] = []; | ||
const tokens: string[] = []; | ||
|
||
let match: RegExpExecArray; | ||
let lastIndex = 0; | ||
const matcher = /:([a-zA-Z_$][a-zA-Z0-9_$]*)|\*\*|\*|\(|\)/g; | ||
const REGEXP_CACHE = new Map<string, { regexp: RegExp, keys: any[] }>(); | ||
const COMPILED_CACHE = new Map<string, (params: Params) => string>(); | ||
|
||
while ((match = matcher.exec(pattern))) { | ||
if (match.index !== lastIndex) { | ||
tokens.push(pattern.slice(lastIndex, match.index)); | ||
regexpSource += escapeSource(pattern.slice(lastIndex, match.index)); | ||
} | ||
|
||
if ( match[1] ) { | ||
regexpSource += '([^/]+)'; | ||
paramNames.push(match[1]); | ||
} | ||
else if ( match[0] === '**' ) { | ||
regexpSource += '(.*)'; | ||
paramNames.push('splat'); | ||
} | ||
else if ( match[0] === '*' ) { | ||
regexpSource += '(.*?)'; | ||
paramNames.push('splat'); | ||
} | ||
else if ( match[0] === '(' ) { | ||
regexpSource += '(?:'; | ||
} | ||
else if ( match[0] === ')' ) { | ||
regexpSource += ')?'; | ||
} | ||
|
||
tokens.push(match[0]); | ||
lastIndex = matcher.lastIndex; | ||
} | ||
|
||
if ( lastIndex !== pattern.length ) { | ||
tokens.push(pattern.slice(lastIndex, pattern.length)); | ||
regexpSource += escapeSource(pattern.slice(lastIndex, pattern.length)); | ||
export function getRegexp(pattern: string) { | ||
if (!REGEXP_CACHE.has(pattern)) { | ||
const keys = []; | ||
const regexp = pathToRegexp(pattern, keys, { end: false }); | ||
REGEXP_CACHE.set(pattern, { keys, regexp }); | ||
} | ||
|
||
return { | ||
pattern, | ||
regexpSource, | ||
paramNames, | ||
tokens | ||
}; | ||
return REGEXP_CACHE.get(pattern); | ||
} | ||
|
||
const CompiledPatternsCache: { [pattern: string]: CompiledPattern } = {}; | ||
|
||
export function compilePattern(pattern) { | ||
if ( !(pattern in CompiledPatternsCache) ) { | ||
CompiledPatternsCache[pattern] = _compilePattern(pattern); | ||
export function getCompiled(pattern: string) { | ||
if (!COMPILED_CACHE.has(pattern)) { | ||
COMPILED_CACHE.set(pattern, pathToRegexp.compile(pattern)); | ||
} | ||
|
||
|
||
return CompiledPatternsCache[pattern]; | ||
return COMPILED_CACHE.get(pattern); | ||
} | ||
|
||
/** | ||
* Attempts to match a pattern on the given pathname. Patterns may use | ||
* the following special characters: | ||
* | ||
* - :paramName Matches a URL segment up to the next /, ?, or #. The | ||
* captured string is considered a "param" | ||
* - () Wraps a segment of the URL that is optional | ||
* - * Consumes (non-greedy) all characters up to the next | ||
* character in the pattern, or to the end of the URL if | ||
* there is none | ||
* - ** Consumes (greedy) all characters up to the next character | ||
* in the pattern, or to the end of the URL if there is none | ||
* | ||
* The return value is an object with the following properties: | ||
* | ||
* - remainingPathname | ||
* - paramNames | ||
* - paramValues | ||
*/ | ||
export function matchPattern(pattern: string, pathname: string) { | ||
// 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); | ||
const compiled = getRegexp(pattern); | ||
const match = compiled.regexp.exec(pathname); | ||
|
||
regexpSource += '/*'; // Capture path separators | ||
|
||
// Special-case patterns like '*' for catch-all routes. | ||
if (tokens[tokens.length - 1] === '*') { | ||
regexpSource += '$'; | ||
} | ||
|
||
const match = pathname.match(new RegExp(`^${regexpSource}`, 'i')); | ||
|
||
let remainingPathname: string; | ||
let paramValues: string[]; | ||
|
||
if ( match != null ) { | ||
const matchedPath = match[0]; | ||
remainingPathname = pathname.substr(matchedPath.length); | ||
|
||
// If we didn't match the entire pathname, then make sure that the match we | ||
// did get ends at a path separator (potentially the one we added above at | ||
// the beginning of the path, if the actual match was empty). | ||
if ( | ||
remainingPathname && | ||
matchedPath.charAt(matchedPath.length - 1) !== '/' | ||
) { | ||
return { | ||
remainingPathname: null, | ||
paramNames, | ||
paramValues: null | ||
}; | ||
} | ||
|
||
paramValues = match.slice(1).map(v => v && decodeURIComponent(v)); | ||
} | ||
else { | ||
remainingPathname = paramValues = null; | ||
if (!match) { | ||
return { | ||
remainingPathname: null, | ||
paramNames: [], | ||
paramValues: [] | ||
}; | ||
} | ||
|
||
return { | ||
remainingPathname, | ||
paramNames, | ||
paramValues | ||
remainingPathname: pathname.substr(match[0].length), | ||
paramNames: compiled.keys.map(({ name }) => name), | ||
paramValues: match.slice(1).map(value => value && decodeURIComponent(value)) | ||
}; | ||
} | ||
|
||
export function getParamNames(pattern: string) { | ||
return compilePattern(pattern).paramNames; | ||
return getRegexp(pattern).keys.map(({ name }) => name); | ||
} | ||
|
||
export function makeParams(paramNames: (string | number)[], paramValues: any[]): Params { | ||
const params: Params = {}; | ||
let lastIndex = 0; | ||
|
||
paramNames.forEach(function(paramName, index) { | ||
if (typeof paramName === 'number') { | ||
paramName = lastIndex++; | ||
} | ||
|
||
params[paramName] = paramValues && paramValues[index]; | ||
}); | ||
|
||
return params; | ||
} | ||
|
||
export function getParams(pattern: string, pathname: string) { | ||
const { paramNames, paramValues } = matchPattern(pattern, pathname); | ||
const { remainingPathname, paramNames, paramValues } = matchPattern(pattern, pathname); | ||
|
||
if (paramValues != null) { | ||
return paramNames.reduce(function (memo, paramName, index) { | ||
memo[paramName] = paramValues[index]; | ||
return memo; | ||
}, {}); | ||
if (remainingPathname === null) { | ||
return null; | ||
} | ||
|
||
return null; | ||
return makeParams(paramNames, paramValues); | ||
} | ||
|
||
/** | ||
* Returns a version of the given pattern with params interpolated. Throws | ||
* if there is a dynamic segment of the pattern for which there is no param. | ||
*/ | ||
export function formatPattern(pattern: string, params: Params = {}) { | ||
const { tokens } = compilePattern(pattern); | ||
let parenCount = 0; | ||
let pathname = ''; | ||
let splatIndex = 0; | ||
|
||
let token: string; | ||
let paramName: string; | ||
let paramValue; | ||
|
||
for (let i = 0, len = tokens.length; i < len; ++i) { | ||
token = tokens[i]; | ||
|
||
if (token === '*' || token === '**') { | ||
paramValue = Array.isArray(params['splat']) ? | ||
params['splat'][splatIndex++] : | ||
params['splat']; | ||
|
||
if ( paramValue != null || parenCount > 0 ) { | ||
console.error('Missing splat #%s for path "%s"', splatIndex, pattern); | ||
} | ||
|
||
if ( paramValue != null ) { | ||
pathname += encodeURI(paramValue); | ||
} | ||
} | ||
else if ( token === '(' ) { | ||
parenCount += 1; | ||
} | ||
else if ( token === ')' ) { | ||
parenCount -= 1; | ||
} | ||
else if ( token.charAt(0) === ':' ) { | ||
paramName = token.substring(1); | ||
paramValue = params[paramName]; | ||
|
||
if ( !(paramValue != null || parenCount > 0) ) { | ||
console.error('Missing "%s" parameter for path "%s"', | ||
paramName, pattern); | ||
} | ||
|
||
if ( paramValue != null ) { | ||
pathname += encodeURIComponent(paramValue); | ||
} | ||
} | ||
else { | ||
pathname += token; | ||
} | ||
} | ||
|
||
return pathname.replace(/\/+/g, '/'); | ||
export function formatPattern(pattern: string, params: Params = {}) { | ||
return getCompiled(pattern)(params); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
declare module 'path-to-regexp' { | ||
function pathToRegexp(path: string, values: any[], options?: pathToRegexp.IOptions): RegExp; | ||
|
||
namespace pathToRegexp { | ||
interface IOptions { | ||
sensitive?: boolean; | ||
strict?: boolean; | ||
end?: boolean; | ||
} | ||
|
||
interface IToken { | ||
name: string | number; | ||
prefix: string; | ||
delimiter: string; | ||
optional: boolean; | ||
repeat: boolean; | ||
pattern: string; | ||
} | ||
|
||
interface CompiledRegExp extends RegExp { | ||
keys: IToken[]; | ||
} | ||
|
||
function parse(path: string): Array<string | IToken>; | ||
function compile(path: string): (params: any) => string; | ||
function tokensToRegExp(tokens: Array<string | IToken>): RegExp; | ||
function tokensToFunction(tokens: Array<string | IToken>): (params: any) => string; | ||
} | ||
|
||
export = pathToRegexp; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,6 +67,7 @@ | |
"zone.js": "^0.6.8" | ||
}, | ||
"dependencies": { | ||
"path-to-regexp": "^1.2.1", | ||
"query-string": "^4.1.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters