diff --git a/docs/guides/RouteMatching.md b/docs/guides/RouteMatching.md index 0ed1259d47..1f7ab697de 100644 --- a/docs/guides/RouteMatching.md +++ b/docs/guides/RouteMatching.md @@ -13,7 +13,7 @@ React Router uses the concept of nested routes to let you declare nested sets of A route path is [a string pattern](/docs/Glossary.md#routepattern) that is used to match a URL (or a portion of one). Route paths are interpreted literally, except for the following special symbols: - `:paramName` – matches a URL segment up to the next `/`, `?`, or `#`. The matched string is called a [param](/docs/Glossary.md#params) - - `()` – Wraps a portion of the URL that is optional + - `()` – Wraps a portion of the URL that is optional. You may escape parentheses if you want to use them in a url using a blackslash \ - `*` – Matches all characters (non-greedy) up to the next character in the pattern, or to the end of the URL if there is none, and creates a `splat` [param](/docs/Glossary.md#params) - `**` - Matches all characters (greedy) until the next `/`, `?`, or `#` and creates a `splat` [param](/docs/Glossary.md#params) @@ -22,6 +22,7 @@ A route path is [a string pattern](/docs/Glossary.md#routepattern) that is used // matches /hello, /hello/michael, and /hello/ryan // matches /files/hello.jpg and /files/hello.html // matches /files/hello.jpg and /files/path/to/file.jpg + // matches /hello(michael) ``` If a route uses a relative `path`, it builds upon the accumulated `path` of its ancestors. Nested routes may opt-out of this behavior by [using an absolute `path`](RouteConfiguration.md#decoupling-the-ui-from-the-url). diff --git a/modules/PatternUtils.js b/modules/PatternUtils.js index 57a4f964c0..41d5e1e644 100644 --- a/modules/PatternUtils.js +++ b/modules/PatternUtils.js @@ -9,7 +9,7 @@ function _compilePattern(pattern) { const paramNames = [] const tokens = [] - let match, lastIndex = 0, matcher = /:([a-zA-Z_$][a-zA-Z0-9_$]*)|\*\*|\*|\(|\)/g + let match, lastIndex = 0, matcher = /:([a-zA-Z_$][a-zA-Z0-9_$]*)|\*\*|\*|\(|\)|\\\(|\\\)/g while ((match = matcher.exec(pattern))) { if (match.index !== lastIndex) { tokens.push(pattern.slice(lastIndex, match.index)) @@ -29,6 +29,10 @@ function _compilePattern(pattern) { regexpSource += '(?:' } else if (match[0] === ')') { regexpSource += ')?' + } else if (match[0] === '\\(') { + regexpSource += '\\(' + } else if (match[0] === '\\)') { + regexpSource += '\\)' } tokens.push(match[0]) @@ -177,6 +181,10 @@ export function formatPattern(pattern, params) { parenHistory[parenCount - 1] += parenText else pathname += parenText + } else if (token === '\\(') { + pathname += '(' + } else if (token === '\\)') { + pathname += ')' } else if (token.charAt(0) === ':') { paramName = token.substring(1) paramValue = params[paramName] diff --git a/modules/__tests__/formatPattern-test.js b/modules/__tests__/formatPattern-test.js index 4daf43d393..07ddc4e0be 100644 --- a/modules/__tests__/formatPattern-test.js +++ b/modules/__tests__/formatPattern-test.js @@ -159,6 +159,22 @@ describe('formatPattern', function () { }) }) + describe('and a param is parentheses escaped', function () { + const pattern = '/comments\\(:id\\)' + + it('returns the correct path when param is supplied', function () { + expect(formatPattern(pattern, { id:'123' })).toEqual('/comments(123)') + }) + }) + + describe('and a param is parentheses escaped with additional param', function () { + const pattern = '/comments\\(:id\\)/:mode' + + it('returns the correct path when param is supplied', function () { + expect(formatPattern(pattern, { id:'123', mode: 'edit' })).toEqual('/comments(123)/edit') + }) + }) + describe('and all params are present', function () { it('returns the correct path', function () { expect(formatPattern(pattern, { id: 'abc' })).toEqual('/comments/abc/edit') diff --git a/modules/__tests__/getParams-test.js b/modules/__tests__/getParams-test.js index 336be565f3..2b9a50aadb 100644 --- a/modules/__tests__/getParams-test.js +++ b/modules/__tests__/getParams-test.js @@ -179,4 +179,36 @@ describe('getParams', function () { }) }) }) + + describe('and the pattern is parentheses escaped', function () { + const pattern = '/comments\\(test\\)' + + describe('and the path matches with supplied param', function () { + it('returns an object with the params', function () { + expect(getParams(pattern, '/comments(test)')).toEqual({ }) + }) + }) + + describe('and the path does not match without parentheses', function () { + it('returns an object with an undefined param', function () { + expect(getParams(pattern, '/commentstest')).toBe(null) + }) + }) + }) + + describe('and the pattern is parentheses escaped', function () { + const pattern = '/comments\\(:id\\)' + + describe('and the path matches with supplied param', function () { + it('returns an object with the params', function () { + expect(getParams(pattern, '/comments(123)')).toEqual({ id: '123' }) + }) + }) + + describe('and the path does not match without parentheses', function () { + it('returns an object with an undefined param', function () { + expect(getParams(pattern, '/commentsedit')).toBe(null) + }) + }) + }) })