Skip to content

Commit fae04bc

Browse files
author
Breno Calazans
committed
[added] Greedy splat (**)
Fix #2284
1 parent c5aae35 commit fae04bc

File tree

6 files changed

+58
-4
lines changed

6 files changed

+58
-4
lines changed

docs/guides/basics/RouteMatching.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ A route path is [a string pattern](/docs/Glossary.md#routepattern) that is used
1515
- `:paramName` – matches a URL segment up to the next `/`, `?`, or `#`. The matched string is called a [param](/docs/Glossary.md#params)
1616
- `()` – Wraps a portion of the URL that is optional
1717
- `*` – 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)
18+
- `**` - Matches all characters (greedy) until the next `/`, `?`, or `#` and creates a `splat` [param](/docs/Glossary.md#params)
1819

1920
```js
2021
<Route path="/hello/:name"> // matches /hello/michael and /hello/ryan
2122
<Route path="/hello(/:name)"> // matches /hello, /hello/michael, and /hello/ryan
2223
<Route path="/files/*.*"> // matches /files/hello.jpg and /files/path/to/hello.jpg
24+
<Route path="/**/*.jpg"> // matches /files/hello.jpg and /files/path/to/file.jpg
2325
```
2426

2527
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).

modules/PatternUtils.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ function _compilePattern(pattern) {
1313
const paramNames = []
1414
const tokens = []
1515

16-
let match, lastIndex = 0, matcher = /:([a-zA-Z_$][a-zA-Z0-9_$]*)|\*|\(|\)/g
16+
let match, lastIndex = 0, matcher = /:([a-zA-Z_$][a-zA-Z0-9_$]*)|\*\*|\*|\(|\)/g
1717
while ((match = matcher.exec(pattern))) {
1818
if (match.index !== lastIndex) {
1919
tokens.push(pattern.slice(lastIndex, match.index))
@@ -23,6 +23,9 @@ function _compilePattern(pattern) {
2323
if (match[1]) {
2424
regexpSource += '([^/?#]+)'
2525
paramNames.push(match[1])
26+
} else if (match[0] === '**') {
27+
regexpSource += '([\\s\\S]*)'
28+
paramNames.push('splat')
2629
} else if (match[0] === '*') {
2730
regexpSource += '([\\s\\S]*?)'
2831
paramNames.push('splat')
@@ -141,7 +144,7 @@ export function formatPattern(pattern, params) {
141144
for (let i = 0, len = tokens.length; i < len; ++i) {
142145
token = tokens[i]
143146

144-
if (token === '*') {
147+
if (token === '*' || token === '**') {
145148
paramValue = Array.isArray(params.splat) ? params.splat[splatIndex++] : params.splat
146149

147150
invariant(

modules/__tests__/PatternUtils-test.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('matchPattern', function () {
1010
paramValues
1111
})
1212
}
13-
13+
1414
it('works without params', function () {
1515
assertMatch('/', '/path', 'path', [], [])
1616
})
@@ -28,4 +28,12 @@ describe('matchPattern', function () {
2828
assertMatch('/:id', '/path/', '', [ 'id' ], [ 'path' ])
2929
})
3030

31+
it('works with greedy splat (**)', function () {
32+
assertMatch('/**/g', '/greedy/is/good/g', '', [ 'splat' ], [ 'greedy/is/good' ])
33+
})
34+
35+
it('works with greedy and non-greedy splat', function () {
36+
assertMatch('/**/*.jpg', '/files/path/to/file.jpg', '', [ 'splat', 'splat' ], [ 'files/path/to', 'file' ])
37+
})
38+
3139
})

modules/__tests__/formatPattern-test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ describe('formatPattern', function () {
9898
})
9999
})
100100

101+
describe('when a pattern has a greedy splat', function () {
102+
it('returns the correct path', function () {
103+
expect(formatPattern('/a/**/d', { splat: 'b/c/d' })).toEqual('/a/b/c/d/d')
104+
expect(formatPattern('/a/**/d/**', { splat: [ 'b/c/d', 'e' ] })).toEqual('/a/b/c/d/d/e')
105+
})
106+
107+
it('complains if not given enough splat values', function () {
108+
expect(function () {
109+
formatPattern('/a/**/d/**', { splat: [ 'b/c/d' ] })
110+
}).toThrow(Error)
111+
})
112+
})
113+
101114
describe('when a pattern has dots', function () {
102115
it('returns the correct path', function () {
103116
expect(formatPattern('/foo.bar.baz')).toEqual('/foo.bar.baz')

modules/__tests__/getParams-test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,20 @@ describe('getParams', function () {
131131
})
132132
})
133133

134+
describe('when a pattern has a **', function () {
135+
describe('and the path matches', function () {
136+
it('return an object with the params', function () {
137+
expect(getParams('/**/f', '/foo/bar/f')).toEqual({ splat: 'foo/bar' })
138+
})
139+
})
140+
141+
describe('and the path does not match', function () {
142+
it('returns null', function () {
143+
expect(getParams('/**/f', '/foo/bar/')).toBe(null)
144+
})
145+
})
146+
})
147+
134148
describe('when a pattern has an optional group', function () {
135149
const pattern = '/archive(/:name)'
136150

modules/__tests__/matchRoutes-test.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Route from '../Route'
77

88
describe('matchRoutes', function () {
99

10-
let routes, RootRoute, UsersRoute, UsersIndexRoute, UserRoute, PostRoute, FilesRoute, AboutRoute, TeamRoute, ProfileRoute, CatchAllRoute
10+
let routes, RootRoute, UsersRoute, UsersIndexRoute, UserRoute, PostRoute, FilesRoute, AboutRoute, TeamRoute, ProfileRoute, GreedyRoute, CatchAllRoute
1111
let createLocation = createMemoryHistory().createLocation
1212
beforeEach(function () {
1313
/*
@@ -54,6 +54,9 @@ describe('matchRoutes', function () {
5454
AboutRoute = {
5555
path: '/about'
5656
},
57+
GreedyRoute = {
58+
path: '/**/f'
59+
},
5760
CatchAllRoute = {
5861
path: '*'
5962
}
@@ -104,6 +107,17 @@ describe('matchRoutes', function () {
104107
})
105108
})
106109

110+
describe('when the location matches a nested route with a greedy splat param', function () {
111+
it('matches the correct routes and params', function (done) {
112+
matchRoutes(routes, createLocation('/foo/bar/f'), function (error, match) {
113+
expect(match).toExist()
114+
expect(match.routes).toEqual([ GreedyRoute ])
115+
expect(match.params).toEqual({ splat: 'foo/bar' })
116+
done()
117+
})
118+
})
119+
})
120+
107121
describe('when the location matches a route with hash', function () {
108122
it('matches the correct routes', function (done) {
109123
matchRoutes(routes, createLocation('/users#about'), function (error, match) {

0 commit comments

Comments
 (0)