Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for escaped parentheses in <Route path> #4202

Merged
merged 1 commit into from
Jan 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/guides/RouteMatching.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)
})
})
})
})