Skip to content

Commit

Permalink
Adding support for escaped parentheses in Route Paths (#4202)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastiandeutsch authored and timdorr committed Jan 11, 2017
1 parent bc66a96 commit f31a58a
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 2 deletions.
3 changes: 2 additions & 1 deletion docs/guides/RouteMatching.md
Expand Up @@ -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)

Expand All @@ -22,6 +22,7 @@ A route path is [a string pattern](/docs/Glossary.md#routepattern) that is used
<Route path="/hello(/:name)"> // matches /hello, /hello/michael, and /hello/ryan
<Route path="/files/*.*"> // matches /files/hello.jpg and /files/hello.html
<Route path="/**/*.jpg"> // matches /files/hello.jpg and /files/path/to/file.jpg
<Route path="/hello\\(:name\\)"> // 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).
Expand Down
10 changes: 9 additions & 1 deletion modules/PatternUtils.js
Expand Up @@ -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))
Expand All @@ -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])
Expand Down Expand Up @@ -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]
Expand Down
16 changes: 16 additions & 0 deletions modules/__tests__/formatPattern-test.js
Expand Up @@ -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')
Expand Down
32 changes: 32 additions & 0 deletions modules/__tests__/getParams-test.js
Expand Up @@ -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)
})
})
})
})

0 comments on commit f31a58a

Please sign in to comment.