From a61424344b8ab332db711f552aab69e7e8c7408d Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 17 Mar 2014 15:34:36 -0300 Subject: [PATCH 01/12] Remove trailing text support after an express param. --- index.js | 1 - test.js | 47 ++++++----------------------------------------- 2 files changed, 6 insertions(+), 42 deletions(-) diff --git a/index.js b/index.js index 11c32bf..b2a5755 100644 --- a/index.js +++ b/index.js @@ -32,7 +32,6 @@ function pathtoRegexp(path, keys, options) { path = path .concat(strict ? '' : '/?') - .replace(/\/\(/g, '/(?:') .replace(/([\/\.])/g, '\\$1') .replace(/(\\\/)?(\\\.)?:(\w+)(\(.*?\))?(\*)?(\?)?/g, function (match, slash, format, key, capture, star, optional) { slash = slash || ''; diff --git a/test.js b/test.js index bdce042..3cbeec2 100644 --- a/test.js +++ b/test.js @@ -403,53 +403,18 @@ describe('path-to-regexp', function () { assert.equal(m[1], 'test'); }); - it('should match text after an express param', function () { + it('should allow matching regexps after a slash', function () { var params = []; - var re = pathToRegExp('/(:test)route', params); - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - - m = re.exec('/route'); - - assert.ok(!m); - - m = re.exec('/testroute'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/testroute'); - assert.equal(m[1], 'test'); - - m = re.exec('testroute'); - - assert.ok(!m); - }); - - it('should match text after an optional express param', function () { - var params = []; - var re = pathToRegExp('/(:test?)route', params); + var re = pathToRegExp('/(\\d+)', params); var m; - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, true); - - m = re.exec('/route'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/route'); - assert.equal(m[1], undefined); + assert.equal(params.length, 0); - m = re.exec('/testroute'); + m = re.exec('/123'); assert.equal(m.length, 2); - assert.equal(m[0], '/testroute'); - assert.equal(m[1], 'test'); - - m = re.exec('route'); - - assert.ok(!m); + assert.equal(m[0], '/123'); + assert.equal(m[1], '123'); }); it('should match optional formats', function () { From 83b93987a82cd03abea2c93fc4e6d21c4a426896 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Fri, 11 Apr 2014 10:05:21 +1000 Subject: [PATCH 02/12] Compatibility updates * Better support for non-ending strict mode matches with a trailing slash * Proper support for passing in arrays --- index.js | 32 +++++++++++++----- test.js | 100 +++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 112 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index b2a5755..efa9cd2 100644 --- a/index.js +++ b/index.js @@ -20,23 +20,34 @@ module.exports = pathtoRegexp; * @api private */ -function pathtoRegexp(path, keys, options) { +function pathtoRegexp (path, keys, options) { options = options || {}; - var sensitive = options.sensitive; var strict = options.strict; var end = options.end !== false; + var flags = options.sensitive ? '' : 'i'; keys = keys || []; - if (path instanceof RegExp) return path; - if (path instanceof Array) path = '(' + path.join('|') + ')'; + if (path instanceof RegExp) { + return path; + } - path = path - .concat(strict ? '' : '/?') - .replace(/([\/\.])/g, '\\$1') + if (Array.isArray(path)) { + // Map array parts into regexps and return their source. We also pass + // the same keys and options instance into every generation to get + // consistent matching groups before we join the sources together. + path = path.map(function (value) { + return pathtoRegexp(value, keys, options).source; + }); + + return new RegExp('(?:' + path.join('|') + ')', flags); + } + + path = ('^' + path + (strict ? '' : '/?')) + .replace(/([\/\.\|])/g, '\\$1') .replace(/(\\\/)?(\\\.)?:(\w+)(\(.*?\))?(\*)?(\?)?/g, function (match, slash, format, key, capture, star, optional) { slash = slash || ''; format = format || ''; - capture = capture || '([^/' + format + ']+?)'; + capture = capture || '([^\\/' + format + ']+?)'; optional = optional || ''; keys.push({ name: key, optional: !!optional }); @@ -51,5 +62,8 @@ function pathtoRegexp(path, keys, options) { }) .replace(/\*/g, '(.*)'); - return new RegExp('^' + path + (end ? '$' : '(?=\/|$)'), sensitive ? '' : 'i'); + // If the path is non-ending, match until the end or a slash. + path += (end ? '$' : (path[path.length - 1] === '/' ? '' : '(?=\\/|$)')); + + return new RegExp(path, flags); }; diff --git a/test.js b/test.js index 3cbeec2..afc620f 100644 --- a/test.js +++ b/test.js @@ -46,6 +46,30 @@ describe('path-to-regexp', function () { assert.ok(!m); }); + it('should do strict matches with trailing slashes', function () { + var params = []; + var re = pathToRegExp('/:test/', params, { strict: true }); + var m; + + assert.equal(params.length, 1); + assert.equal(params[0].name, 'test'); + assert.equal(params[0].optional, false); + + m = re.exec('/route'); + + assert.ok(!m); + + m = re.exec('/route/'); + + assert.equal(m.length, 2); + assert.equal(m[0], '/route/'); + assert.equal(m[1], 'route'); + + m = re.exec('/route//'); + + assert.ok(!m); + }); + it('should allow optional express format params', function () { var params = []; var re = pathToRegExp('/:test?', params); @@ -388,19 +412,47 @@ describe('path-to-regexp', function () { assert.equal(m[1], 'test'); }); + it('should match trailing slashing in non-ending strict mode', function () { + var params = []; + var re = pathToRegExp('/route/', params, { end: false, strict: true }); + + assert.equal(params.length, 0); + + m = re.exec('/route/'); + + assert.equal(m.length, 1); + assert.equal(m[0], '/route/'); + + m = re.exec('/route/test'); + + assert.equal(m.length, 1); + assert.equal(m[0], '/route/'); + + m = re.exec('/route'); + + assert.ok(!m); + + m = re.exec('/route//'); + + assert.equal(m.length, 1); + assert.equal(m[0], '/route/'); + }); + it('should not match trailing slashes in non-ending strict mode', function () { var params = []; - var re = pathToRegExp('/:test', params, { end: false, strict: true }); + var re = pathToRegExp('/route', params, { end: false, strict: true }); - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); + assert.equal(params.length, 0); - m = re.exec('/test/'); + m = re.exec('/route'); - assert.equal(m.length, 2); - assert.equal(m[0], '/test'); - assert.equal(m[1], 'test'); + assert.equal(m.length, 1); + assert.equal(m[0], '/route'); + + m = re.exec('/route/'); + + assert.ok(m.length, 1); + assert.equal(m[0], '/route'); }); it('should allow matching regexps after a slash', function () { @@ -467,9 +519,35 @@ describe('path-to-regexp', function () { it('should join arrays parts', function () { var re = pathToRegExp(['/test', '/route']); - assert.ok(re.exec('/test')); - assert.ok(re.exec('/route')); - assert.ok(!re.exec('/else')); + assert.ok(re.test('/test')); + assert.ok(re.test('/route')); + assert.ok(!re.test('/else')); + }); + + it('should match parts properly', function () { + var params = []; + var re = pathToRegExp(['/:test', '/test/:route'], params); + var m; + + assert.equal(params.length, 2); + assert.equal(params[0].name, 'test'); + assert.equal(params[0].optional, false); + assert.equal(params[1].name, 'route'); + assert.equal(params[1].optional, false); + + m = re.exec('/route'); + + assert.equal(m.length, 3); + assert.equal(m[0], '/route'); + assert.equal(m[1], 'route'); + assert.equal(m[2], undefined); + + m = re.exec('/test/path'); + + assert.equal(m.length, 3); + assert.equal(m[0], '/test/path'); + assert.equal(m[1], undefined); + assert.equal(m[2], 'path'); }); }); }); From 5cc2f20844e2cbab1926a8c821656a9b1782d961 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Fri, 11 Apr 2014 18:08:38 +1000 Subject: [PATCH 03/12] Add travis ci support --- .travis.yml | 5 +++++ package.json | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4a83e22 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - "0.11" + - "0.10" + - "0.8" diff --git a/package.json b/package.json index f8892d6..e64f4df 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "url": "https://github.com/component/path-to-regexp.git" }, "devDependencies": { - "mocha": "^1.17.1", - "istanbul": "^0.2.6" + "istanbul": "~0.2.6", + "mocha": "~1.18.2" } } From dae470dd5fc82032df88585e0a593ce1b91fa6d9 Mon Sep 17 00:00:00 2001 From: Forbes Lindesay Date: Tue, 22 Apr 2014 15:58:59 +0100 Subject: [PATCH 04/12] Add badges Then it's easy to see what version we are on and whether tests are passing --- Readme.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 9199e38..d9de358 100644 --- a/Readme.md +++ b/Readme.md @@ -1,8 +1,10 @@ - # Path-to-RegExp Turn an Express-style path string such as `/user/:name` into a regular expression. +[![Build Status](https://img.shields.io/travis/component/path-to-regexp/master.svg)](https://travis-ci.org/component/path-to-regexp) +[![NPM version](https://img.shields.io/npm/v/path-to-regexp.svg)](https://www.npmjs.org/package/path-to-regexp) + ## Usage ```javascript @@ -30,4 +32,4 @@ You can see a live demo of this library in use at [express-route-tester](http:// ## License - MIT \ No newline at end of file + MIT From a10a7fe554f2df1ff6778ffe184f834ac2913961 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 26 Mar 2014 19:41:52 -0300 Subject: [PATCH 05/12] Update readme and improve general consistency. --- Readme.md | 88 +++++++++++++++++++++++++++++----- index.js | 18 ++++--- test.js | 141 +++++++++++++++++++++++++++++++++++------------------- 3 files changed, 176 insertions(+), 71 deletions(-) diff --git a/Readme.md b/Readme.md index d9de358..1822797 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,6 @@ # Path-to-RegExp - Turn an Express-style path string such as `/user/:name` into a regular expression. +Turn an Express-style path string such as `/user/:name` into a regular expression. [![Build Status](https://img.shields.io/travis/component/path-to-regexp/master.svg)](https://travis-ci.org/component/path-to-regexp) [![NPM version](https://img.shields.io/npm/v/path-to-regexp.svg)](https://www.npmjs.org/package/path-to-regexp) @@ -9,21 +9,85 @@ ```javascript var pathToRegexp = require('path-to-regexp'); + +// pathToRegexp(path, keys, options); ``` -### pathToRegexp(path, keys, options) - - **path** A string in the express format, an array of such strings, or a regular expression - - **keys** An array to be populated with the keys present in the url. Once the function completes, this will be an array of strings. - - **options** - - **options.sensitive** Defaults to false, set this to true to make routes case sensitive - - **options.strict** Defaults to false, set this to true to make the trailing slash matter. - - **options.end** Defaults to true, set this to false to only match the prefix of the URL. +- **path** A string in the express format, an array of strings, or a regular expression. +- **keys** An array to be populated with the keys present in the url. +- **options** + - **options.sensitive** When set to `true` the route will be case sensitive. + - **options.strict** When set to `true` a trailing slash will affect the url matching. + - **options.end** When set to `false` the url will match only the prefix. ```javascript var keys = []; -var exp = pathToRegexp('/foo/:bar', keys); -//keys = ['bar'] -//exp = /^\/foo\/(?:([^\/]+?))\/?$/i +var re = pathToRegexp('/foo/:bar', keys); +// re = /^\/foo\/(?:([^\/]+?))\/?$/i +// keys = [{ name: 'bar', optional: false }] +``` + +### Named parameters + +Paths have the ability to define named parameters that populate the keys array. Named parameters are defined by prefixing a colon to a parameter name (`:foo`) and optionally suffixing a number of different modifiers. A named parameter will match any text until the next slash. + +```javascript +var re = pathToRegexp('/:foo/:bar'); + +re.exec('/test/route'); +//=> ['/test/route', 'test', 'route'] +``` + +#### Optional Matches + +Named parameters can be suffixed with a question mark to indicate an optional match. + +```javascript +var re = pathToRegExp('/:foo?'); + +re.exec('/'); +//=> ['/', undefined] +``` + +Please note: Optional matches can be combined with the greedy match to only have it take effect with the parameter exists. E.g. `/:foo*?`. + +#### Custom Matching Groups + +Named parameters can be provided a custom matching group and override the default. Please note: Backslashes will need to be escaped. + +```javascript +var re = pathToRegexp('/:foo(\\d+)'); + +re.exec('/123'); +//=> ['/123', '123'] + +re.exec('/abc'); +//=> null +``` + +#### Prefixes + +By default a named parameter will match any character up until the next slash, but if the parameter is prefixed with a period it will only match to the next period. + +```javascript +var re = pathToRegexp('/test.:foo'); + +re.exec('/test.json'); +//=> ['/test.json', 'json'] + +re.exec('/test.html.json'); +//=> null +``` + +### Greedy Matching + +The path uses an asterisk to greedily match any trailing characters. This can be placed anywhere in the route, including after a named parameter. + +```javascript +var re = pathToRegexp('/foo*'); + +re.exec('/foo/bar.json'); +//=> ['/foo/bar', '/bar.json'] ``` ## Live Demo @@ -32,4 +96,4 @@ You can see a live demo of this library in use at [express-route-tester](http:// ## License - MIT +MIT diff --git a/index.js b/index.js index efa9cd2..e95ad06 100644 --- a/index.js +++ b/index.js @@ -5,19 +5,17 @@ module.exports = pathtoRegexp; /** - * Normalize the given path string, - * returning a regular expression. + * Normalize the given path string, returning a regular expression. * - * An empty array should be passed, - * which will contain the placeholder - * key names. For example "/user/:id" will - * then contain ["id"]. + * An empty array should be passed, which will contain the placeholder key + * names. For example "/user/:id" will then contain: * - * @param {String|RegExp|Array} path - * @param {Array} keys - * @param {Object} options + * [{ name: "id", optional: false }] + * + * @param {(String|RegExp|Array)} path + * @param {Array} keys + * @param {Object} options * @return {RegExp} - * @api private */ function pathtoRegexp (path, keys, options) { diff --git a/test.js b/test.js index afc620f..a989cff 100644 --- a/test.js +++ b/test.js @@ -1,11 +1,11 @@ -var pathToRegExp = require('./'); +var pathToRegexp = require('./'); var assert = require('assert'); describe('path-to-regexp', function () { describe('strings', function () { it('should match simple paths', function () { var params = []; - var m = pathToRegExp('/test', params).exec('/test'); + var m = pathToRegexp('/test', params).exec('/test'); assert.equal(params.length, 0); @@ -13,9 +13,9 @@ describe('path-to-regexp', function () { assert.equal(m[0], '/test'); }); - it('should match express format params', function () { + it('should match named params', function () { var params = []; - var m = pathToRegExp('/:test', params).exec('/pathname'); + var m = pathToRegexp('/:test', params).exec('/pathname'); assert.equal(params.length, 1); assert.equal(params[0].name, 'test'); @@ -28,7 +28,7 @@ describe('path-to-regexp', function () { it('should do strict matches', function () { var params = []; - var re = pathToRegExp('/:test', params, { strict: true }); + var re = pathToRegexp('/:test', params, { strict: true }); var m; assert.equal(params.length, 1); @@ -48,7 +48,7 @@ describe('path-to-regexp', function () { it('should do strict matches with trailing slashes', function () { var params = []; - var re = pathToRegExp('/:test/', params, { strict: true }); + var re = pathToRegexp('/:test/', params, { strict: true }); var m; assert.equal(params.length, 1); @@ -70,9 +70,9 @@ describe('path-to-regexp', function () { assert.ok(!m); }); - it('should allow optional express format params', function () { + it('should allow optional named params', function () { var params = []; - var re = pathToRegExp('/:test?', params); + var re = pathToRegexp('/:test?', params); var m; assert.equal(params.length, 1); @@ -92,9 +92,9 @@ describe('path-to-regexp', function () { assert.equal(m[1], undefined); }); - it('should allow express format param regexps', function () { + it('should allow params to have custom matching groups', function () { var params = []; - var m = pathToRegExp('/:page(\\d+)', params).exec('/56'); + var m = pathToRegexp('/:page(\\d+)', params).exec('/56'); assert.equal(params.length, 1); assert.equal(params[0].name, 'page'); @@ -107,7 +107,7 @@ describe('path-to-regexp', function () { it('should match without a prefixed slash', function () { var params = []; - var m = pathToRegExp(':test', params).exec('string'); + var m = pathToRegexp(':test', params).exec('string'); assert.equal(params.length, 1); assert.equal(params[0].name, 'test'); @@ -118,9 +118,9 @@ describe('path-to-regexp', function () { assert.equal(m[1], 'string'); }); - it('should not match format parts', function () { + it('should not match the format', function () { var params = []; - var m = pathToRegExp('/:test.json', params).exec('/route.json'); + var m = pathToRegexp('/:test.json', params).exec('/route.json'); assert.equal(params.length, 1); assert.equal(params[0].name, 'test'); @@ -131,9 +131,9 @@ describe('path-to-regexp', function () { assert.equal(m[1], 'route'); }); - it('should match format parts', function () { + it('should match format params', function () { var params = []; - var re = pathToRegExp('/:test.:format', params); + var re = pathToRegexp('/:test.:format', params); var m; assert.equal(params.length, 2); @@ -154,9 +154,9 @@ describe('path-to-regexp', function () { assert.ok(!m); }); - it('should match route parts with a trailing format', function () { + it('should match a param with a trailing format', function () { var params = []; - var m = pathToRegExp('/:test.json', params).exec('/route.json'); + var m = pathToRegexp('/:test.json', params).exec('/route.json'); assert.equal(params.length, 1); assert.equal(params[0].name, 'test'); @@ -167,20 +167,29 @@ describe('path-to-regexp', function () { assert.equal(m[1], 'route'); }); - it('should match optional trailing routes', function () { + it('should do greedy matches', function () { var params = []; - var m = pathToRegExp('/test*', params).exec('/test/route'); + var re = pathToRegexp('/test*', params); + var m; assert.equal(params.length, 0); + m = re.exec('/test/route'); + assert.equal(m.length, 2); assert.equal(m[0], '/test/route'); assert.equal(m[1], '/route'); + + m = re.exec('/test'); + + assert.equal(m.length, 2); + assert.equal(m[0], '/test'); + assert.equal(m[1], ''); }); - it('should match optional trailing routes after a param', function () { + it('should do greedy param matches', function () { var params = []; - var re = pathToRegExp('/:test*', params); + var re = pathToRegexp('/:test*', params); var m; assert.equal(params.length, 1); @@ -202,9 +211,9 @@ describe('path-to-regexp', function () { assert.equal(m[2], ''); }); - it('should match optional trailing routes before a format', function () { + it('should do greedy matches with a trailing format', function () { var params = []; - var re = pathToRegExp('/test*.json', params); + var re = pathToRegexp('/test*.json', params); var m; assert.equal(params.length, 0); @@ -228,9 +237,9 @@ describe('path-to-regexp', function () { assert.equal(m[1], '/route'); }); - it('should match optional trailing routes after a param and before a format', function () { + it('should do greedy param matches with a trailing format', function () { var params = []; - var re = pathToRegExp('/:test*.json', params); + var re = pathToRegexp('/:test*.json', params); var m; assert.equal(params.length, 1); @@ -256,9 +265,9 @@ describe('path-to-regexp', function () { assert.ok(!m); }); - it('should match optional trailing routes between a normal param and a format param', function () { + it('should do greedy param matches with a trailing format param', function () { var params = []; - var re = pathToRegExp('/:test*.:format', params); + var re = pathToRegexp('/:test*.:format', params); var m; assert.equal(params.length, 2); @@ -292,9 +301,9 @@ describe('path-to-regexp', function () { assert.ok(!m); }); - it('should match optional trailing routes after a param and before an optional format param', function () { + it('should do greedy param matches with an optional trailing format param', function () { var params = []; - var re = pathToRegExp('/:test*.:format?', params); + var re = pathToRegexp('/:test*.:format?', params); var m; assert.equal(params.length, 2); @@ -332,9 +341,9 @@ describe('path-to-regexp', function () { assert.ok(!m); }); - it('should match optional trailing routes inside optional express param', function () { + it('should do greedy, optional param matching', function () { var params = []; - var re = pathToRegExp('/:test*?', params); + var re = pathToRegexp('/:test*?', params); var m; assert.equal(params.length, 1); @@ -363,14 +372,42 @@ describe('path-to-regexp', function () { assert.equal(m[2], undefined); }); + it('should do greedy, optional param matching with a custom matching group', function () { + var params = []; + var re = pathToRegexp('/:test(\\d+)*?', params); + var m; + + assert.equal(params.length, 1); + assert.equal(params[0].name, 'test'); + assert.equal(params[0].optional, true); + + m = re.exec('/123'); + + assert.equal(m.length, 3); + assert.equal(m[0], '/123'); + assert.equal(m[1], '123'); + assert.equal(m[2], ''); + + m = re.exec('/123/foo/bar'); + + assert.equal(m.length, 3); + assert.equal(m[0], '/123/foo/bar'); + assert.equal(m[1], '123'); + assert.equal(m[2], '/foo/bar'); + + m = re.exec('/foo/bar'); + + assert.ok(!m); + }); + it('should do case insensitive matches', function () { - var m = pathToRegExp('/test').exec('/TEST'); + var m = pathToRegexp('/test').exec('/TEST'); assert.equal(m[0], '/TEST'); }); it('should do case sensitive matches', function () { - var re = pathToRegExp('/test', null, { sensitive: true }); + var re = pathToRegexp('/test', null, { sensitive: true }); var m; m = re.exec('/test'); @@ -385,7 +422,7 @@ describe('path-to-regexp', function () { it('should do non-ending matches', function () { var params = []; - var m = pathToRegExp('/:test', params, { end: false }).exec('/test/route'); + var m = pathToRegexp('/:test', params, { end: false }).exec('/test/route'); assert.equal(params.length, 1); assert.equal(params[0].name, 'test'); @@ -396,25 +433,31 @@ describe('path-to-regexp', function () { assert.equal(m[1], 'test'); }); - it('should match trailing slashes in non-ending non-strict mode', function () { + it('should work with trailing slashes in non-ending mode', function () { var params = []; - var re = pathToRegExp('/:test', params, { end: false }); + var re = pathToRegexp('/:test', params, { end: false }); var m; assert.equal(params.length, 1); assert.equal(params[0].name, 'test'); assert.equal(params[0].optional, false); - m = re.exec('/test/'); + m = re.exec('/foo/bar'); assert.equal(m.length, 2); - assert.equal(m[0], '/test/'); - assert.equal(m[1], 'test'); + assert.equal(m[0], '/foo'); + assert.equal(m[1], 'foo'); + + m = re.exec('/foo/'); + + assert.equal(m.length, 2); + assert.equal(m[0], '/foo/'); + assert.equal(m[1], 'foo'); }); it('should match trailing slashing in non-ending strict mode', function () { var params = []; - var re = pathToRegExp('/route/', params, { end: false, strict: true }); + var re = pathToRegexp('/route/', params, { end: false, strict: true }); assert.equal(params.length, 0); @@ -440,7 +483,7 @@ describe('path-to-regexp', function () { it('should not match trailing slashes in non-ending strict mode', function () { var params = []; - var re = pathToRegExp('/route', params, { end: false, strict: true }); + var re = pathToRegexp('/route', params, { end: false, strict: true }); assert.equal(params.length, 0); @@ -457,7 +500,7 @@ describe('path-to-regexp', function () { it('should allow matching regexps after a slash', function () { var params = []; - var re = pathToRegExp('/(\\d+)', params); + var re = pathToRegexp('/(\\d+)', params); var m; assert.equal(params.length, 0); @@ -469,9 +512,9 @@ describe('path-to-regexp', function () { assert.equal(m[1], '123'); }); - it('should match optional formats', function () { + it('should match optional format params', function () { var params = []; - var re = pathToRegExp('/:test.:format?', params); + var re = pathToRegexp('/:test.:format?', params); var m; assert.equal(params.length, 2); @@ -495,9 +538,9 @@ describe('path-to-regexp', function () { assert.equal(m[2], 'json'); }); - it('should match full paths with format by default', function () { + it('should match full paths when not prefixed with a period', function () { var params = []; - var m = pathToRegExp('/:test', params).exec('/test.json'); + var m = pathToRegexp('/:test', params).exec('/test.json'); assert.equal(params.length, 1); assert.equal(params[0].name, 'test'); @@ -511,13 +554,13 @@ describe('path-to-regexp', function () { describe('regexps', function () { it('should return the regexp', function () { - assert.deepEqual(pathToRegExp(/.*/), /.*/); + assert.deepEqual(pathToRegexp(/.*/), /.*/); }); }); describe('arrays', function () { it('should join arrays parts', function () { - var re = pathToRegExp(['/test', '/route']); + var re = pathToRegexp(['/test', '/route']); assert.ok(re.test('/test')); assert.ok(re.test('/route')); @@ -526,7 +569,7 @@ describe('path-to-regexp', function () { it('should match parts properly', function () { var params = []; - var re = pathToRegExp(['/:test', '/test/:route'], params); + var re = pathToRegexp(['/:test', '/test/:route'], params); var m; assert.equal(params.length, 2); From 28d425fa9f1116f5757ca8d2b6c98754b55b2b56 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sun, 11 May 2014 19:15:36 +1000 Subject: [PATCH 06/12] Update semantics of pathToRegexp * Allow escaped regexp characters to pass untransformed * Allow "unnamed" capturing groups and automatically assign a named index key * Escapes some regexp characters which add no overall value * Adds greedy matches to params as a named index key * Adds the ability for already escaped characters to be ignored * Update a couple of failing tests * Add a test from Express to ensure asterisk inside matching group still works Notes: * Still 100% compatible with Express * Removes the ability for positive/negatives lookaheads and non-matching groups * Removes the ability for nested regexp groups * Retains the ability for `*` to be converted to a greedy match in matching groups --- index.js | 73 ++++++++++++++++++++++++++++++++++++++++++++++---------- test.js | 28 +++++++++++++++++++--- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index e95ad06..880ca11 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,35 @@ module.exports = pathtoRegexp; +var PATH_REGEXP = new RegExp([ + // Match already escaped characters that would otherwise incorrectly appear + // in future matches. This allows the user to escape special characters that + // shouldn't be transformed. + '(\\\\.)', + // Match Express-style params and un-named params with a prefix and optional + // suffixes. Matches appear as: + // + // "/:test(\\d+)*?" => ["/", "test", "\d+", undefined, "*", "?"] + // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined] + '([\\/\\.])?(?:\:(\\w+)(?:\\((.*)\\))?|\\((.*)\\))(\\*)?(\\?)?', + // Match regexp special characters that should always be escaped. + '([=!:$|\\.\\/])', + // Finally, enable automatic greedy matching. + '(\\*)' +].join('|'), 'g'); + +/** + * Escape the capturing group by escaping special characters and meaning. + * + * @param {String} group + * @return {String} + */ +function escapeGroup (group) { + return group + .replace(/([=!:$|\.\/\(\)])/g, '\\$1') + .replace(/\*/g, '.*'); +} + /** * Normalize the given path string, returning a regular expression. * @@ -17,13 +46,14 @@ module.exports = pathtoRegexp; * @param {Object} options * @return {RegExp} */ - function pathtoRegexp (path, keys, options) { + keys = keys || []; options = options || {}; + var strict = options.strict; var end = options.end !== false; var flags = options.sensitive ? '' : 'i'; - keys = keys || []; + var index = 0; if (path instanceof RegExp) { return path; @@ -40,25 +70,44 @@ function pathtoRegexp (path, keys, options) { return new RegExp('(?:' + path.join('|') + ')', flags); } - path = ('^' + path + (strict ? '' : '/?')) - .replace(/([\/\.\|])/g, '\\$1') - .replace(/(\\\/)?(\\\.)?:(\w+)(\(.*?\))?(\*)?(\?)?/g, function (match, slash, format, key, capture, star, optional) { - slash = slash || ''; - format = format || ''; - capture = capture || '([^\\/' + format + ']+?)'; + // Alter the path string into a usable regexp. + path = path.replace(PATH_REGEXP, function (match, escaped, prefix, key, capture, group, star, optional, escape, greedy) { + // Keep escaped characters the same. + if (escaped) { + return escaped; + } + + // Escape special characters. + if (escape) { + return '\\' + escape; + } + optional = optional || ''; - keys.push({ name: key, optional: !!optional }); + var name = key || index++; + var slash = (prefix === '/' ? '\\/' : ''); + var format = (prefix === '.' ? '\\.' : ''); + var regexp = capture || group || '[^\\/' + format + ']+?'; + + keys.push({ name: name, optional: !!optional }); + + // Return the greedy regexp match early. + if (greedy) { + return '(.*)'; + } return '' + (optional ? '' : slash) + '(?:' - + format + (optional ? slash : '') + capture + + format + (optional ? slash : '') + + '(' + escapeGroup(regexp) + ')' + (star ? '((?:[\\/' + format + '].+?)?)' : '') + ')' + optional; - }) - .replace(/\*/g, '(.*)'); + }); + + // Wrap the path in a regexp match. + path = ('^' + path + (strict ? '' : '\\/?')); // If the path is non-ending, match until the end or a slash. path += (end ? '$' : (path[path.length - 1] === '/' ? '' : '(?=\\/|$)')); diff --git a/test.js b/test.js index a989cff..3c63df0 100644 --- a/test.js +++ b/test.js @@ -172,7 +172,9 @@ describe('path-to-regexp', function () { var re = pathToRegexp('/test*', params); var m; - assert.equal(params.length, 0); + assert.equal(params.length, 1); + assert.equal(params[0].name, 0); + assert.equal(params[0].optional, false); m = re.exec('/test/route'); @@ -216,7 +218,9 @@ describe('path-to-regexp', function () { var re = pathToRegexp('/test*.json', params); var m; - assert.equal(params.length, 0); + assert.equal(params.length, 1); + assert.equal(params[0].name, 0); + assert.equal(params[0].optional, false); m = re.exec('/test.json'); @@ -503,7 +507,9 @@ describe('path-to-regexp', function () { var re = pathToRegexp('/(\\d+)', params); var m; - assert.equal(params.length, 0); + assert.equal(params.length, 1); + assert.equal(params[0].name, 0); + assert.equal(params[0].optional, false); m = re.exec('/123'); @@ -550,6 +556,22 @@ describe('path-to-regexp', function () { assert.equal(m[0], '/test.json'); assert.equal(m[1], 'test.json'); }); + + it('should allow naming of *', function () { + var params = []; + var re = pathToRegexp('/api/:resource(*)', params); + var m; + + assert.equal(params.length, 1); + assert.equal(params[0].name, 'resource'); + assert.equal(params[0].optional, false); + + m = re.exec('/api/users/0.json'); + + assert.equal(m.length, 2); + assert.equal(m[0], '/api/users/0.json'); + assert.equal(m[1], 'users/0.json'); + }); }); describe('regexps', function () { From 30abcf9e427ca258b26dbcac652d05756fafa2b4 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 15 May 2014 16:58:54 +1000 Subject: [PATCH 07/12] Complete refactor * 100% test coverage * Defensively programmed from the start * Improved test suite * Improved interoperability between strings, arrays and regexps * No more undefined keys * Removed ability to regexp outside of parameters * Removed nested regexp inside parameters * Updated readme --- Readme.md | 70 +++-- index.js | 119 ++++---- test.js | 797 +++++++++++++----------------------------------------- 3 files changed, 288 insertions(+), 698 deletions(-) diff --git a/Readme.md b/Readme.md index 1822797..b935fe2 100644 --- a/Readme.md +++ b/Readme.md @@ -17,45 +17,52 @@ var pathToRegexp = require('path-to-regexp'); - **keys** An array to be populated with the keys present in the url. - **options** - **options.sensitive** When set to `true` the route will be case sensitive. - - **options.strict** When set to `true` a trailing slash will affect the url matching. - - **options.end** When set to `false` the url will match only the prefix. + - **options.strict** When set to `true` a slash is allowed to be trailing the path. + - **options.end** When set to `false` the path will match at the beginning. ```javascript var keys = []; var re = pathToRegexp('/foo/:bar', keys); -// re = /^\/foo\/(?:([^\/]+?))\/?$/i -// keys = [{ name: 'bar', optional: false }] +// re = /^\/foo\/([^\/]+?)\/?$/i +// keys = ['bar'] ``` -### Named parameters +### Parameters -Paths have the ability to define named parameters that populate the keys array. Named parameters are defined by prefixing a colon to a parameter name (`:foo`) and optionally suffixing a number of different modifiers. A named parameter will match any text until the next slash. +The path has the ability to define parameters and automatically populate the keys array. -```javascript +#### Named Parameters + +Named parameters are defined by prefixing a colon to the parameter name (`:foo`). By default, this parameter will match up to the next path segment. + +```js var re = pathToRegexp('/:foo/:bar'); +// keys = ['foo', 'bar'] re.exec('/test/route'); //=> ['/test/route', 'test', 'route'] ``` -#### Optional Matches +#### Optional Parameters -Named parameters can be suffixed with a question mark to indicate an optional match. +Optional parameters can be denoted by suffixing a question mark. This will also make any prefixed path segment optional (`/` or `.`). -```javascript -var re = pathToRegExp('/:foo?'); +```js +var re = pathToRegexp('/:foo/:bar?'); +// keys = ['foo', 'bar'] -re.exec('/'); -//=> ['/', undefined] -``` +re.exec('/test'); +//=> ['/test', 'test', undefined] -Please note: Optional matches can be combined with the greedy match to only have it take effect with the parameter exists. E.g. `/:foo*?`. +re.exec('/test/route'); +//=> ['/test', 'test', 'route'] +``` -#### Custom Matching Groups +#### Custom Matches -Named parameters can be provided a custom matching group and override the default. Please note: Backslashes will need to be escaped. +All parameters can be provided a custom matching regexp and override the default. Please note: Backslashes need to be escaped in strings. -```javascript +```js var re = pathToRegexp('/:foo(\\d+)'); re.exec('/123'); @@ -65,29 +72,16 @@ re.exec('/abc'); //=> null ``` -#### Prefixes +#### Unnamed Parameters -By default a named parameter will match any character up until the next slash, but if the parameter is prefixed with a period it will only match to the next period. - -```javascript -var re = pathToRegexp('/test.:foo'); +It is possible to write an unnamed parameter that is only a matching group. It works the same as a named parameter, except it will be numerically indexed. -re.exec('/test.json'); -//=> ['/test.json', 'json'] +```js +var re = pathToRegexp('/:foo/(.*)', keys); +// keys = ['foo', '0'] -re.exec('/test.html.json'); -//=> null -``` - -### Greedy Matching - -The path uses an asterisk to greedily match any trailing characters. This can be placed anywhere in the route, including after a named parameter. - -```javascript -var re = pathToRegexp('/foo*'); - -re.exec('/foo/bar.json'); -//=> ['/foo/bar', '/bar.json'] +re.exec('/test/route'); +//=> ['/test/route', 'test', 'route'] ``` ## Live Demo diff --git a/index.js b/index.js index 880ca11..f652bd8 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,6 @@ /** * Expose `pathtoRegexp`. */ - module.exports = pathtoRegexp; var PATH_REGEXP = new RegExp([ @@ -9,16 +8,14 @@ var PATH_REGEXP = new RegExp([ // in future matches. This allows the user to escape special characters that // shouldn't be transformed. '(\\\\.)', - // Match Express-style params and un-named params with a prefix and optional - // suffixes. Matches appear as: + // Match Express-style parameters and un-named parameters with a prefix + // and optional suffixes. Matches appear as: // - // "/:test(\\d+)*?" => ["/", "test", "\d+", undefined, "*", "?"] - // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined] - '([\\/\\.])?(?:\:(\\w+)(?:\\((.*)\\))?|\\((.*)\\))(\\*)?(\\?)?', + // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?"] + // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined] + '([\\/.])?(?:\\:(\\w+)(?:\\((.+)\\))?|\\((.+)\\))([?])?', // Match regexp special characters that should always be escaped. - '([=!:$|\\.\\/])', - // Finally, enable automatic greedy matching. - '(\\*)' + '([.+*?=^!:${}()[\\]|\\/])' ].join('|'), 'g'); /** @@ -27,19 +24,15 @@ var PATH_REGEXP = new RegExp([ * @param {String} group * @return {String} */ -function escapeGroup (group) { - return group - .replace(/([=!:$|\.\/\(\)])/g, '\\$1') - .replace(/\*/g, '.*'); +function wrapGroup (group) { + return '(' + group.replace(/([=!:$\/()])/g, '\\$1') + ')'; } /** * Normalize the given path string, returning a regular expression. * - * An empty array should be passed, which will contain the placeholder key - * names. For example "/user/:id" will then contain: - * - * [{ name: "id", optional: false }] + * An empty array should be passed in, which will contain the placeholder key + * names. For example `/user/:id` will then contain `["id"]`. * * @param {(String|RegExp|Array)} path * @param {Array} keys @@ -56,6 +49,13 @@ function pathtoRegexp (path, keys, options) { var index = 0; if (path instanceof RegExp) { + // Match all capturing groups of a regexp. + var groups = path.source.match(/\((?!\?)/g) || []; + + // Map all the matches to their numeric keys and push into the keys. + keys.push.apply(keys, groups.map(function (m, i) { return i; })); + + // Return the source back to the user. return path; } @@ -67,50 +67,55 @@ function pathtoRegexp (path, keys, options) { return pathtoRegexp(value, keys, options).source; }); + // Generate a new regexp instance by joining all the parts together. return new RegExp('(?:' + path.join('|') + ')', flags); } // Alter the path string into a usable regexp. - path = path.replace(PATH_REGEXP, function (match, escaped, prefix, key, capture, group, star, optional, escape, greedy) { - // Keep escaped characters the same. - if (escaped) { - return escaped; - } - - // Escape special characters. - if (escape) { - return '\\' + escape; - } - - optional = optional || ''; - - var name = key || index++; - var slash = (prefix === '/' ? '\\/' : ''); - var format = (prefix === '.' ? '\\.' : ''); - var regexp = capture || group || '[^\\/' + format + ']+?'; - - keys.push({ name: name, optional: !!optional }); - - // Return the greedy regexp match early. - if (greedy) { - return '(.*)'; - } - - return '' - + (optional ? '' : slash) - + '(?:' - + format + (optional ? slash : '') - + '(' + escapeGroup(regexp) + ')' - + (star ? '((?:[\\/' + format + '].+?)?)' : '') - + ')' - + optional; - }); - - // Wrap the path in a regexp match. - path = ('^' + path + (strict ? '' : '\\/?')); + path = path.replace(PATH_REGEXP, function (match, escaped, prefix, key, capture, group, optional, escape) { + // Avoiding re-escaping escaped characters. + if (escaped) { + return escaped; + } + + // Escape regexp special characters. + if (escape) { + return '\\' + escape; + } + + keys.push(key || index++); + + // Special behaviour for format params. + var format = prefix === '.' ? '\\.' : ''; + + // Escape the prefix and ensure both optional matches are empty strings. + prefix = prefix ? '\\' + prefix : ''; + optional = optional || ''; + + // Match using the custom capturing group, or fallback to capturing + // everything up to the next slash (or next period if the param was + // prefixed with a period). + var regexp = wrapGroup(capture || group || '[^\\/' + format + ']+?'); + + if (optional) { + return '(?:' + prefix + regexp + ')' + optional; + } + + return prefix + regexp; + }); + + // If we are doing a non-ending match, we need to prompt the matching groups + // to match as much as possible. To do this, we add a positive lookahead for + // the next path fragment or the end. However, if the regexp already ends + // in a path fragment, we'll run into problems. + if (!end && path[path.length - 1] !== '/') { + path += '(?=\\/|$)'; + } - // If the path is non-ending, match until the end or a slash. - path += (end ? '$' : (path[path.length - 1] === '/' ? '' : '(?=\\/|$)')); + // Allow trailing slashes to be matched in non-strict, ending mode. + if (end && !strict) { + path += '\\/?'; + } - return new RegExp(path, flags); + return new RegExp('^' + path + (end ? '$' : ''), flags); }; diff --git a/test.js b/test.js index 3c63df0..43cea09 100644 --- a/test.js +++ b/test.js @@ -1,618 +1,209 @@ -var pathToRegexp = require('./'); +var util = require('util'); var assert = require('assert'); +var pathToRegexp = require('./'); -describe('path-to-regexp', function () { - describe('strings', function () { - it('should match simple paths', function () { - var params = []; - var m = pathToRegexp('/test', params).exec('/test'); - - assert.equal(params.length, 0); - - assert.equal(m.length, 1); - assert.equal(m[0], '/test'); - }); - - it('should match named params', function () { - var params = []; - var m = pathToRegexp('/:test', params).exec('/pathname'); - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - - assert.equal(m.length, 2); - assert.equal(m[0], '/pathname'); - assert.equal(m[1], 'pathname'); - }); - - it('should do strict matches', function () { - var params = []; - var re = pathToRegexp('/:test', params, { strict: true }); - var m; - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - - m = re.exec('/route'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/route'); - assert.equal(m[1], 'route'); - - m = re.exec('/route/'); - - assert.ok(!m); - }); - - it('should do strict matches with trailing slashes', function () { - var params = []; - var re = pathToRegexp('/:test/', params, { strict: true }); - var m; - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - - m = re.exec('/route'); - - assert.ok(!m); - - m = re.exec('/route/'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/route/'); - assert.equal(m[1], 'route'); - - m = re.exec('/route//'); - - assert.ok(!m); - }); - - it('should allow optional named params', function () { - var params = []; - var re = pathToRegexp('/:test?', params); - var m; - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, true); - - m = re.exec('/route'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/route'); - assert.equal(m[1], 'route'); - - m = re.exec('/'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/'); - assert.equal(m[1], undefined); - }); - - it('should allow params to have custom matching groups', function () { - var params = []; - var m = pathToRegexp('/:page(\\d+)', params).exec('/56'); - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'page'); - assert.equal(params[0].optional, false); - - assert.equal(m.length, 2); - assert.equal(m[0], '/56'); - assert.equal(m[1], '56'); - }); - - it('should match without a prefixed slash', function () { - var params = []; - var m = pathToRegexp(':test', params).exec('string'); - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - - assert.equal(m.length, 2); - assert.equal(m[0], 'string'); - assert.equal(m[1], 'string'); - }); - - it('should not match the format', function () { - var params = []; - var m = pathToRegexp('/:test.json', params).exec('/route.json'); - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - - assert.equal(m.length, 2); - assert.equal(m[0], '/route.json'); - assert.equal(m[1], 'route'); - }); - - it('should match format params', function () { - var params = []; - var re = pathToRegexp('/:test.:format', params); - var m; - - assert.equal(params.length, 2); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - assert.equal(params[1].name, 'format'); - assert.equal(params[1].optional, false); - - m = re.exec('/route.json'); - - assert.equal(m.length, 3); - assert.equal(m[0], '/route.json'); - assert.equal(m[1], 'route'); - assert.equal(m[2], 'json'); - - m = re.exec('/route'); - - assert.ok(!m); - }); - - it('should match a param with a trailing format', function () { - var params = []; - var m = pathToRegexp('/:test.json', params).exec('/route.json'); - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - - assert.equal(m.length, 2); - assert.equal(m[0], '/route.json'); - assert.equal(m[1], 'route'); - }); - - it('should do greedy matches', function () { - var params = []; - var re = pathToRegexp('/test*', params); - var m; - - assert.equal(params.length, 1); - assert.equal(params[0].name, 0); - assert.equal(params[0].optional, false); - - m = re.exec('/test/route'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/test/route'); - assert.equal(m[1], '/route'); - - m = re.exec('/test'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/test'); - assert.equal(m[1], ''); - }); - - it('should do greedy param matches', function () { - var params = []; - var re = pathToRegexp('/:test*', params); - var m; - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - - m = re.exec('/test/route'); - - assert.equal(m.length, 3); - assert.equal(m[0], '/test/route'); - assert.equal(m[1], 'test'); - assert.equal(m[2], '/route'); - - m = re.exec('/testing'); - - assert.equal(m.length, 3); - assert.equal(m[0], '/testing'); - assert.equal(m[1], 'testing'); - assert.equal(m[2], ''); - }); - - it('should do greedy matches with a trailing format', function () { - var params = []; - var re = pathToRegexp('/test*.json', params); - var m; - - assert.equal(params.length, 1); - assert.equal(params[0].name, 0); - assert.equal(params[0].optional, false); - - m = re.exec('/test.json'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/test.json'); - assert.equal(m[1], ''); - - m = re.exec('/testing.json'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/testing.json'); - assert.equal(m[1], 'ing'); - - m = re.exec('/test/route.json'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/test/route.json'); - assert.equal(m[1], '/route'); - }); - - it('should do greedy param matches with a trailing format', function () { - var params = []; - var re = pathToRegexp('/:test*.json', params); - var m; - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - - m = re.exec('/testing.json'); - - assert.equal(m.length, 3); - assert.equal(m[0], '/testing.json'); - assert.equal(m[1], 'testing'); - assert.equal(m[2], ''); - - m = re.exec('/test/route.json'); - - assert.equal(m.length, 3); - assert.equal(m[0], '/test/route.json'); - assert.equal(m[1], 'test'); - assert.equal(m[2], '/route'); - - m = re.exec('.json'); - - assert.ok(!m); - }); - - it('should do greedy param matches with a trailing format param', function () { - var params = []; - var re = pathToRegexp('/:test*.:format', params); - var m; - - assert.equal(params.length, 2); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - assert.equal(params[1].name, 'format'); - assert.equal(params[1].optional, false); - - m = re.exec('/testing.json'); - - assert.equal(m.length, 4); - assert.equal(m[0], '/testing.json'); - assert.equal(m[1], 'testing'); - assert.equal(m[2], ''); - assert.equal(m[3], 'json'); - - m = re.exec('/test/route.json'); - - assert.equal(m.length, 4); - assert.equal(m[0], '/test/route.json'); - assert.equal(m[1], 'test'); - assert.equal(m[2], '/route'); - assert.equal(m[3], 'json'); - - m = re.exec('/test'); - - assert.ok(!m); - - m = re.exec('.json'); - - assert.ok(!m); - }); - - it('should do greedy param matches with an optional trailing format param', function () { - var params = []; - var re = pathToRegexp('/:test*.:format?', params); - var m; - - assert.equal(params.length, 2); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - assert.equal(params[1].name, 'format'); - assert.equal(params[1].optional, true); - - m = re.exec('/testing.json'); - - assert.equal(m.length, 4); - assert.equal(m[0], '/testing.json'); - assert.equal(m[1], 'testing'); - assert.equal(m[2], ''); - assert.equal(m[3], 'json'); - - m = re.exec('/test/route.json'); - - assert.equal(m.length, 4); - assert.equal(m[0], '/test/route.json'); - assert.equal(m[1], 'test'); - assert.equal(m[2], '/route'); - assert.equal(m[3], 'json'); - - m = re.exec('/test'); - - assert.equal(m.length, 4); - assert.equal(m[0], '/test'); - assert.equal(m[1], 'test'); - assert.equal(m[2], ''); - assert.equal(m[3], undefined); - - m = re.exec('.json'); - - assert.ok(!m); - }); - - it('should do greedy, optional param matching', function () { - var params = []; - var re = pathToRegexp('/:test*?', params); - var m; - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, true); - - m = re.exec('/test/route'); - - assert.equal(m.length, 3); - assert.equal(m[0], '/test/route'); - assert.equal(m[1], 'test'); - assert.equal(m[2], '/route'); - - m = re.exec('/test'); - - assert.equal(m.length, 3); - assert.equal(m[0], '/test'); - assert.equal(m[1], 'test'); - assert.equal(m[2], ''); - - m = re.exec('/'); - - assert.equal(m.length, 3); - assert.equal(m[0], '/'); - assert.equal(m[1], undefined); - assert.equal(m[2], undefined); - }); - - it('should do greedy, optional param matching with a custom matching group', function () { - var params = []; - var re = pathToRegexp('/:test(\\d+)*?', params); - var m; - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, true); - - m = re.exec('/123'); - - assert.equal(m.length, 3); - assert.equal(m[0], '/123'); - assert.equal(m[1], '123'); - assert.equal(m[2], ''); - - m = re.exec('/123/foo/bar'); - - assert.equal(m.length, 3); - assert.equal(m[0], '/123/foo/bar'); - assert.equal(m[1], '123'); - assert.equal(m[2], '/foo/bar'); - - m = re.exec('/foo/bar'); - - assert.ok(!m); - }); - - it('should do case insensitive matches', function () { - var m = pathToRegexp('/test').exec('/TEST'); - - assert.equal(m[0], '/TEST'); - }); - - it('should do case sensitive matches', function () { - var re = pathToRegexp('/test', null, { sensitive: true }); - var m; - - m = re.exec('/test'); - - assert.equal(m.length, 1); - assert.equal(m[0], '/test'); - - m = re.exec('/TEST'); - - assert.ok(!m); - }); - - it('should do non-ending matches', function () { - var params = []; - var m = pathToRegexp('/:test', params, { end: false }).exec('/test/route'); - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - - assert.equal(m.length, 2); - assert.equal(m[0], '/test'); - assert.equal(m[1], 'test'); - }); - - it('should work with trailing slashes in non-ending mode', function () { - var params = []; - var re = pathToRegexp('/:test', params, { end: false }); - var m; - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - - m = re.exec('/foo/bar'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/foo'); - assert.equal(m[1], 'foo'); - - m = re.exec('/foo/'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/foo/'); - assert.equal(m[1], 'foo'); - }); - - it('should match trailing slashing in non-ending strict mode', function () { - var params = []; - var re = pathToRegexp('/route/', params, { end: false, strict: true }); - - assert.equal(params.length, 0); - - m = re.exec('/route/'); - - assert.equal(m.length, 1); - assert.equal(m[0], '/route/'); - - m = re.exec('/route/test'); - - assert.equal(m.length, 1); - assert.equal(m[0], '/route/'); - - m = re.exec('/route'); - - assert.ok(!m); - - m = re.exec('/route//'); - - assert.equal(m.length, 1); - assert.equal(m[0], '/route/'); - }); - - it('should not match trailing slashes in non-ending strict mode', function () { - var params = []; - var re = pathToRegexp('/route', params, { end: false, strict: true }); - - assert.equal(params.length, 0); - - m = re.exec('/route'); - - assert.equal(m.length, 1); - assert.equal(m[0], '/route'); - - m = re.exec('/route/'); - - assert.ok(m.length, 1); - assert.equal(m[0], '/route'); - }); - - it('should allow matching regexps after a slash', function () { - var params = []; - var re = pathToRegexp('/(\\d+)', params); - var m; - - assert.equal(params.length, 1); - assert.equal(params[0].name, 0); - assert.equal(params[0].optional, false); - - m = re.exec('/123'); - - assert.equal(m.length, 2); - assert.equal(m[0], '/123'); - assert.equal(m[1], '123'); - }); - - it('should match optional format params', function () { - var params = []; - var re = pathToRegexp('/:test.:format?', params); - var m; - - assert.equal(params.length, 2); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - assert.equal(params[1].name, 'format'); - assert.equal(params[1].optional, true); - - m = re.exec('/route'); - - assert.equal(m.length, 3); - assert.equal(m[0], '/route'); - assert.equal(m[1], 'route'); - assert.equal(m[2], undefined); - - m = re.exec('/route.json'); - - assert.equal(m.length, 3); - assert.equal(m[0], '/route.json'); - assert.equal(m[1], 'route'); - assert.equal(m[2], 'json'); - }); - - it('should match full paths when not prefixed with a period', function () { - var params = []; - var m = pathToRegexp('/:test', params).exec('/test.json'); - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - - assert.equal(m.length, 2); - assert.equal(m[0], '/test.json'); - assert.equal(m[1], 'test.json'); - }); - - it('should allow naming of *', function () { - var params = []; - var re = pathToRegexp('/api/:resource(*)', params); - var m; - - assert.equal(params.length, 1); - assert.equal(params[0].name, 'resource'); - assert.equal(params[0].optional, false); - - m = re.exec('/api/users/0.json'); +/** + * Execute a regular expression and return a flat array for comparison. + * + * @param {RegExp} re + * @param {String} str + * @return {Array} + */ +var exec = function (re, str) { + var match = re.exec(str); + + return match && Array.prototype.slice.call(match); +}; + +/** + * An array of test cases with expected inputs and outputs. The format of each + * array item is: + * + * ["path", "expected params", "route", "expected output", "options"] + * + * @type {Array} + */ +var TESTS = [ + // Simple paths. + ['/test', [], '/test', ['/test']], + ['/test', [], '/route', null], + ['/test', [], '/test/route', null], + ['/test', [], '/test/', ['/test/']], + + // Case-sensitive paths. + ['/test', [], '/test', ['/test'], { sensitive: true }], + ['/test', [], '/TEST', null, { sensitive: true }], + ['/TEST', [], '/test', null, { sensitive: true }], + + // Strict mode. + ['/test', [], '/test', ['/test'], { strict: true }], + ['/test', [], '/test/', null, { strict: true }], + ['/test/', [], '/test', null, { strict: true }], + ['/test/', [], '/test/', ['/test/'], { strict: true }], + ['/test/', [], '/test//', null, { strict: true }], + + // Non-ending mode. + ['/test', [], '/test', ['/test'], { end: false }], + ['/test', [], '/test/route', ['/test'], { end: false }], + + // Combine modes. + ['/test', [], '/test', ['/test'], { end: false, strict: true }], + ['/test', [], '/test/', ['/test'], { end: false, strict: true }], + ['/test', [], '/test/route', ['/test'], { end: false, strict: true }], + ['/test/', [], '/test', null, { end: false, strict: true }], + ['/test/', [], '/test/', ['/test/'], { end: false, strict: true }], + ['/test/', [], '/test//', ['/test/'], { end: false, strict: true }], + ['/test/', [], '/test/route', ['/test/'], { end: false, strict: true }], + ['/test.json', [], '/test.json', ['/test.json'], { end: false, strict: true }], + ['/test.json', [], '/test.json.hbs', null, { end: false, strict: true }], + + // Arrays of simple paths. + [['/one', '/two'], [], '/one', ['/one']], + [['/one', '/two'], [], '/two', ['/two']], + [['/one', '/two'], [], '/three', null], + [['/one', '/two'], [], '/one/two', null], + + // Non-ending simple path. + ['/test', [], '/test/route', ['/test'], { end: false }], + + // Single named parameter + ['/:test', ['test'], '/route', ['/route', 'route']], + ['/:test', ['test'], '/another', ['/another', 'another']], + ['/:test', ['test'], '/something/else', null], + ['/:test', ['test'], '/route.json', ['/route.json', 'route.json']], + ['/:test', ['test'], '/route', ['/route', 'route'], { strict: true }], + ['/:test', ['test'], '/route/', null, { strict: true }], + ['/:test/', ['test'], '/route/', ['/route/', 'route'], { strict: true }], + ['/:test/', ['test'], '/route//', null, { strict: true }],, + ['/:test', ['test'], '/route.json', ['/route.json', 'route.json'], { end: false }], + + // Optional named parameter. + ['/:test?', ['test'], '/route', ['/route', 'route']], + ['/:test?', ['test'], '/route/nested', null], + ['/:test?', ['test'], '/', ['/', undefined]], + ['/:test?', ['test'], '/route', ['/route', 'route'], { strict: true }], + ['/:test?', ['test'], '/', null, { strict: true }], // Questionable behaviour. + ['/:test?/', ['test'], '/', ['/', undefined], { strict: true }], + ['/:test?/', ['test'], '//', null, { strict: true }], + + // Custom named parameters. + ['/:test(\\d+)', ['test'], '/123', ['/123', '123']], + ['/:test(\\d+)', ['test'], '/abc', null], + ['/:test(\\d+)', ['test'], '/123/abc', null], + ['/:test(\\d+)', ['test'], '/123/abc', ['/123', '123'], { end: false }], + ['/:test(.*)', ['test'], '/anything/goes/here', ['/anything/goes/here', 'anything/goes/here']], + ['/:route([a-z]+)', ['route'], '/abcde', ['/abcde', 'abcde']], + ['/:route([a-z]+)', ['route'], '/12345', null], + ['/:route(this|that)', ['route'], '/this', ['/this', 'this']], + ['/:route(this|that)', ['route'], '/that', ['/that', 'that']], + + // Prefixed slashes could be omitted. + ['test', [], 'test', ['test']], + [':test', ['test'], 'route', ['route', 'route']], + [':test', ['test'], '/route', null], + [':test', ['test'], 'route/', ['route/', 'route']], + [':test', ['test'], 'route/', null, { strict: true }], + [':test', ['test'], 'route/', ['route', 'route'], { end: false }], + + // Formats. + ['/test.json', [], '/test.json', ['/test.json']], + ['/test.json', [], '/route.json', null], + ['/:test.json', ['test'], '/route.json', ['/route.json', 'route']], + ['/:test.json', ['test'], '/route.json.json', ['/route.json.json', 'route.json']], + ['/:test.json', ['test'], '/route.json', ['/route.json', 'route'], { end: false }], + ['/:test.json', ['test'], '/route.json.json', ['/route.json.json', 'route.json'], { end: false }], + + // Format params. + ['/test.:format', ['format'], '/test.html', ['/test.html', 'html']], + ['/test.:format', ['format'], '/test.hbs.html', null], + ['/test.:format.:format', ['format', 'format'], '/test.hbs.html', ['/test.hbs.html', 'hbs', 'html']], + ['/test.:format', ['format'], '/test.hbs.html', null, { end: false }], + ['/test.:format.', ['format'], '/test.hbs.html', null, { end: false }], + // Format and path params. + ['/:test.:format', ['test', 'format'], '/route.html', ['/route.html', 'route', 'html']], + ['/:test.:format', ['test', 'format'], '/route', null], + ['/:test.:format', ['test', 'format'], '/route', null], + ['/:test.:format?', ['test', 'format'], '/route', ['/route', 'route', undefined]], + ['/:test.:format?', ['test', 'format'], '/route.json', ['/route.json', 'route', 'json']], + ['/:test.:format?', ['test', 'format'], '/route', ['/route', 'route', undefined], { end: false }], + ['/:test.:format?', ['test', 'format'], '/route.json', ['/route.json', 'route', 'json'], { end: false }], + ['/:test.:format?', ['test', 'format'], '/route.json.html', ['/route.json.html', 'route.json', 'html'], { end: false }], + ['/test.:format(.*)z', ['format'], '/test.abc', null, { end: false }], + ['/test.:format(.*)z', ['format'], '/test.abcz', ['/test.abcz', 'abc'], { end: false }], + + // Unnamed params. + ['/(\\d+)', ['0'], '/123', ['/123', '123']], + ['/(\\d+)', ['0'], '/abc', null], + ['/(\\d+)', ['0'], '/123/abc', null], + ['/(\\d+)', ['0'], '/123/abc', ['/123', '123'], { end: false }], + ['/(\\d+)', ['0'], '/abc', null, { end: false }], + ['/(\\d+)?', ['0'], '/', ['/', undefined]], + ['/(\\d+)?', ['0'], '/123', ['/123', '123']], + ['/(.*)', ['0'], '/route', ['/route', 'route']], + ['/(.*)', ['0'], '/route/nested', ['/route/nested', 'route/nested']], + + // Regexps. + [/.*/, [], '/match/anything', ['/match/anything']], + [/(.*)/, ['0'], '/match/anything', ['/match/anything', '/match/anything']], + [/\/(\d+)/, ['0'], '/123', ['/123', '123']], + + // Mixed arrays. + [['/test', /\/(\d+)/], ['0'], '/test', ['/test', undefined]], + [['/:test(\\d+)', /(.*)/], ['test', '0'], '/123', ['/123', '123', undefined]], + [['/:test(\\d+)', /(.*)/], ['test', '0'], '/abc', ['/abc', undefined, '/abc']], + + // Correct names and indexes. + [['/:test', '/route/:test'], ['test', 'test'], '/test', ['/test', 'test', undefined]], + [['/:test', '/route/:test'], ['test', 'test'], '/route/test', ['/route/test', undefined, 'test']], + [[/^\/([^\/]+)$/, /^\/route\/([^\/]+)$/], ['0', '0'], '/test', ['/test', 'test', undefined]], + [[/^\/([^\/]+)$/, /^\/route\/([^\/]+)$/], ['0', '0'], '/route/test', ['/route/test', undefined, 'test']], + + // Ignore non-matching groups in regexps. + [/(?:.*)/, [], '/anything/you/want', ['/anything/you/want']], + + // Respect escaped characters. + ['/\\(testing\\)', [], '/testing', null], + ['/\\(testing\\)', [], '/(testing)', ['/(testing)']], + + // Regexp special characters should be ignored outside matching groups. + ['/.+*?=^!:${}[]|', [], '/.+*?=^!:${}[]|', ['/.+*?=^!:${}[]|']] +]; - assert.equal(m.length, 2); - assert.equal(m[0], '/api/users/0.json'); - assert.equal(m[1], 'users/0.json'); - }); - }); +describe('path-to-regexp', function () { + it('should not break when keys aren\'t provided', function () { + var re = pathToRegexp('/:foo/:bar'); - describe('regexps', function () { - it('should return the regexp', function () { - assert.deepEqual(pathToRegexp(/.*/), /.*/); - }); + assert.deepEqual(exec(re, '/test/route'), ['/test/route', 'test', 'route']); }); - describe('arrays', function () { - it('should join arrays parts', function () { - var re = pathToRegexp(['/test', '/route']); - - assert.ok(re.test('/test')); - assert.ok(re.test('/route')); - assert.ok(!re.test('/else')); - }); + TESTS.forEach(function (test) { + var description = ''; + var options = test[4] || {}; - it('should match parts properly', function () { - var params = []; - var re = pathToRegexp(['/:test', '/test/:route'], params); - var m; + // Generate a base description using the test values. + description += 'should ' + (test[3] ? '' : 'not ') + 'match '; + description += util.inspect(test[2]) + ' against ' + util.inspect(test[0]); - assert.equal(params.length, 2); - assert.equal(params[0].name, 'test'); - assert.equal(params[0].optional, false); - assert.equal(params[1].name, 'route'); - assert.equal(params[1].optional, false); + // If additional options have been defined, we should render the options + // in the test descriptions. + if (Object.keys(options).length) { + var optionsDescription = Object.keys(options).map(function (key) { + return (options[key] === false ? 'non-' : '') + key; + }).join(', '); - m = re.exec('/route'); + description += ' in ' + optionsDescription + ' mode'; + } - assert.equal(m.length, 3); - assert.equal(m[0], '/route'); - assert.equal(m[1], 'route'); - assert.equal(m[2], undefined); + // Execute the test and check each parameter is as expected. + it(description, function () { + var params = []; + var re = pathToRegexp(test[0], params, test[4]); - m = re.exec('/test/path'); + // Check the params are as expected. + assert.deepEqual(params, test[1]); - assert.equal(m.length, 3); - assert.equal(m[0], '/test/path'); - assert.equal(m[1], undefined); - assert.equal(m[2], 'path'); + // Run the regexp and check the result is expected. + assert.deepEqual(exec(re, test[2]), test[3]); }); }); }); From 0702846a713d8bc4ea32ed9d67df1a8870c098e7 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 28 May 2014 21:10:57 +1000 Subject: [PATCH 08/12] Add support for + and * parameter suffixes * Updated readme to reflect changes * Added edge case handling for trailing slashes in non-strict mode when the path passed in already ends in a slash * Tests --- Readme.md | 43 ++++++++++++++++++++++++++++++++++++++----- index.js | 51 +++++++++++++++++++++++++++++++-------------------- test.js | 24 ++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 25 deletions(-) diff --git a/Readme.md b/Readme.md index b935fe2..94e4297 100644 --- a/Readme.md +++ b/Readme.md @@ -36,19 +36,21 @@ The path has the ability to define parameters and automatically populate the key Named parameters are defined by prefixing a colon to the parameter name (`:foo`). By default, this parameter will match up to the next path segment. ```js -var re = pathToRegexp('/:foo/:bar'); +var re = pathToRegexp('/:foo/:bar', keys); // keys = ['foo', 'bar'] re.exec('/test/route'); //=> ['/test/route', 'test', 'route'] ``` -#### Optional Parameters +#### Parameter Suffixes -Optional parameters can be denoted by suffixing a question mark. This will also make any prefixed path segment optional (`/` or `.`). +##### Optional + +Parameters can be suffixed with a question mark (`?`) to make the entire parameter optional. This will also make any prefixed path delimiter optional (`/` or `.`). ```js -var re = pathToRegexp('/:foo/:bar?'); +var re = pathToRegexp('/:foo/:bar?', keys); // keys = ['foo', 'bar'] re.exec('/test'); @@ -58,12 +60,43 @@ re.exec('/test/route'); //=> ['/test', 'test', 'route'] ``` +##### Zero or more + +Parameters can be suffixed with an asterisk (`*`) to denote a zero or more parameter match. The prefixed path delimiter is also taken into account for the match. + +```js +var re = pathToRegexp('/:foo*', keys); +// keys = ['foo'] + +re.exec('/'); +//=> ['/', undefined] + +re.exec('/bar/baz'); +//=> ['/bar/baz', 'bar/baz'] +``` + +##### One or more + +Parameters can be suffixed with a plus sign (`+`) to denote a one or more parameters match. The prefixed path delimiter is included in the match. + +```js +var re = pathToRegexp('/:foo+', keys); +// keys = ['foo'] + +re.exec('/'); +//=> null + +re.exec('/bar/baz'); +//=> ['/bar/baz', 'bar/baz'] +``` + #### Custom Matches All parameters can be provided a custom matching regexp and override the default. Please note: Backslashes need to be escaped in strings. ```js -var re = pathToRegexp('/:foo(\\d+)'); +var re = pathToRegexp('/:foo(\\d+)', keys); +// keys = ['foo'] re.exec('/123'); //=> ['/123', '123'] diff --git a/index.js b/index.js index f652bd8..362f92a 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ var PATH_REGEXP = new RegExp([ // // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?"] // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined] - '([\\/.])?(?:\\:(\\w+)(?:\\((.+)\\))?|\\((.+)\\))([?])?', + '([\\/.])?(?:\\:(\\w+)(?:\\((.+)\\))?|\\((.+)\\))([+*?])?', // Match regexp special characters that should always be escaped. '([.+*?=^!:${}()[\\]|\\/])' ].join('|'), 'g'); @@ -24,8 +24,8 @@ var PATH_REGEXP = new RegExp([ * @param {String} group * @return {String} */ -function wrapGroup (group) { - return '(' + group.replace(/([=!:$\/()])/g, '\\$1') + ')'; +function escapeGroup (group) { + return group.replace(/([=!:$\/()])/g, '\\$1'); } /** @@ -72,7 +72,7 @@ function pathtoRegexp (path, keys, options) { } // Alter the path string into a usable regexp. - path = path.replace(PATH_REGEXP, function (match, escaped, prefix, key, capture, group, optional, escape) { + path = path.replace(PATH_REGEXP, function (match, escaped, prefix, key, capture, group, suffix, escape) { // Avoiding re-escaping escaped characters. if (escaped) { return escaped; @@ -88,33 +88,44 @@ function pathtoRegexp (path, keys, options) { // Special behaviour for format params. var format = prefix === '.' ? '\\.' : ''; - // Escape the prefix and ensure both optional matches are empty strings. + // Escape the prefix character. prefix = prefix ? '\\' + prefix : ''; - optional = optional || ''; // Match using the custom capturing group, or fallback to capturing // everything up to the next slash (or next period if the param was // prefixed with a period). - var regexp = wrapGroup(capture || group || '[^\\/' + format + ']+?'); + capture = escapeGroup(capture || group || '[^\\/' + format + ']+?'); - if (optional) { - return '(?:' + prefix + regexp + ')' + optional; + // More complex regexp is required for suffix support. + if (suffix) { + if (suffix === '+') { + return prefix + '(' + capture + '(?:' + prefix + capture + ')*)' + } + + if (suffix === '*') { + return '(?:' + prefix + '(' + capture + '(?:' + prefix + capture + ')*|' + capture + '))?'; + } + + return '(?:' + prefix + '(' + capture + '))?'; } - return prefix + regexp; + // Basic parameter support. + return prefix + '(' + capture + ')'; }); - // If we are doing a non-ending match, we need to prompt the matching groups - // to match as much as possible. To do this, we add a positive lookahead for - // the next path fragment or the end. However, if the regexp already ends - // in a path fragment, we'll run into problems. - if (!end && path[path.length - 1] !== '/') { - path += '(?=\\/|$)'; - } + if (path[path.length - 1] !== '/') { + // If we are doing a non-ending match, we need to prompt the matching groups + // to match as much as possible. To do this, we add a positive lookahead for + // the next path fragment or the end. However, if the regexp already ends + // in a path fragment, we'll run into problems. + if (!end) { + path += '(?=\\/|$)'; + } - // Allow trailing slashes to be matched in non-strict, ending mode. - if (end && !strict) { - path += '\\/?'; + // Allow trailing slashes to be matched in non-strict, ending mode. + if (end && !strict) { + path += '\\/?'; + } } return new RegExp('^' + path + (end ? '$' : ''), flags); diff --git a/test.js b/test.js index 43cea09..022078d 100644 --- a/test.js +++ b/test.js @@ -25,10 +25,13 @@ var exec = function (re, str) { */ var TESTS = [ // Simple paths. + ['/', [], '/', ['/']], ['/test', [], '/test', ['/test']], ['/test', [], '/route', null], ['/test', [], '/test/route', null], ['/test', [], '/test/', ['/test/']], + ['/test/', [], '/test/', ['/test/']], + ['/test/', [], '/test//', null], // Case-sensitive paths. ['/test', [], '/test', ['/test'], { sensitive: true }], @@ -84,8 +87,29 @@ var TESTS = [ ['/:test?', ['test'], '/route', ['/route', 'route'], { strict: true }], ['/:test?', ['test'], '/', null, { strict: true }], // Questionable behaviour. ['/:test?/', ['test'], '/', ['/', undefined], { strict: true }], + ['/:test?/', ['test'], '//', null], ['/:test?/', ['test'], '//', null, { strict: true }], + // Repeated once or more times parameters. + ['/:test+', ['test'], '/', null], + ['/:test+', ['test'], '/route', ['/route', 'route']], + ['/:test+', ['test'], '/some/basic/route', ['/some/basic/route', 'some/basic/route']], + ['/:test(\\d+)+', ['test'], '/abc/456/789', null], + ['/:test(\\d+)+', ['test'], '/123/456/789', ['/123/456/789', '123/456/789']], + ['/route.:ext(json|xml)+', ['ext'], '/route.json', ['/route.json', 'json']], + ['/route.:ext(json|xml)+', ['ext'], '/route.xml.json', ['/route.xml.json', 'xml.json']], + ['/route.:ext(json|xml)+', ['ext'], '/route.html', null], + + // Repeated zero or more times parameters. + ['/:test*', ['test'], '/', ['/', undefined]], + ['/:test*', ['test'], '//', null], + ['/:test*', ['test'], '/route', ['/route', 'route']], + ['/:test*', ['test'], '/some/basic/route', ['/some/basic/route', 'some/basic/route']], + ['/route.:ext([a-z]+)*', ['ext'], '/route', ['/route', undefined]], + ['/route.:ext([a-z]+)*', ['ext'], '/route.json', ['/route.json', 'json']], + ['/route.:ext([a-z]+)*', ['ext'], '/route.xml.json', ['/route.xml.json', 'xml.json']], + ['/route.:ext([a-z]+)*', ['ext'], '/route.123', null], + // Custom named parameters. ['/:test(\\d+)', ['test'], '/123', ['/123', '123']], ['/:test(\\d+)', ['test'], '/abc', null], From ce0eb31710955d871e76773fbf03199d99aede2a Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 28 May 2014 21:19:16 +1000 Subject: [PATCH 09/12] Update path to mocha file --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e64f4df..8badd71 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "Express style path to RegExp utility", "version": "0.1.2", "scripts": { - "test": "istanbul cover _mocha -- -R spec" + "test": "istanbul cover node_modules/mocha/bin/_mocha -- -R spec" }, "keywords": [ "express", From a7ce0ca00d555af4780ec1455e3ef557de820ecd Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 9 Jun 2014 20:11:57 -0700 Subject: [PATCH 10/12] Update keys definition behaviour * Keys now provide the `name`, `delimiter`, `optional` and `repeat` options * Updated tests to match updated keys behaviour --- index.js | 21 +- test.js | 827 ++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 717 insertions(+), 131 deletions(-) diff --git a/index.js b/index.js index 362f92a..d56f4fc 100644 --- a/index.js +++ b/index.js @@ -53,7 +53,14 @@ function pathtoRegexp (path, keys, options) { var groups = path.source.match(/\((?!\?)/g) || []; // Map all the matches to their numeric keys and push into the keys. - keys.push.apply(keys, groups.map(function (m, i) { return i; })); + keys.push.apply(keys, groups.map(function (match, index) { + return { + name: index, + delimiter: null, + optional: false, + repeat: false + }; + })); // Return the source back to the user. return path; @@ -83,10 +90,12 @@ function pathtoRegexp (path, keys, options) { return '\\' + escape; } - keys.push(key || index++); - - // Special behaviour for format params. - var format = prefix === '.' ? '\\.' : ''; + keys.push({ + name: key || index++, + delimiter: prefix || '/', + optional: suffix === '?' || suffix === '*', + repeat: suffix === '+' || suffix === '*' + }); // Escape the prefix character. prefix = prefix ? '\\' + prefix : ''; @@ -94,7 +103,7 @@ function pathtoRegexp (path, keys, options) { // Match using the custom capturing group, or fallback to capturing // everything up to the next slash (or next period if the param was // prefixed with a period). - capture = escapeGroup(capture || group || '[^\\/' + format + ']+?'); + capture = escapeGroup(capture || group || '[^' + (prefix || '\\/') + ']+?'); // More complex regexp is required for suffix support. if (suffix) { diff --git a/test.js b/test.js index 022078d..a811ed0 100644 --- a/test.js +++ b/test.js @@ -24,7 +24,9 @@ var exec = function (re, str) { * @type {Array} */ var TESTS = [ - // Simple paths. + /** + * Simple paths. + */ ['/', [], '/', ['/']], ['/test', [], '/test', ['/test']], ['/test', [], '/route', null], @@ -33,23 +35,31 @@ var TESTS = [ ['/test/', [], '/test/', ['/test/']], ['/test/', [], '/test//', null], - // Case-sensitive paths. + /** + * Case-sensitive paths. + */ ['/test', [], '/test', ['/test'], { sensitive: true }], ['/test', [], '/TEST', null, { sensitive: true }], ['/TEST', [], '/test', null, { sensitive: true }], - // Strict mode. + /** + * Strict mode. + */ ['/test', [], '/test', ['/test'], { strict: true }], ['/test', [], '/test/', null, { strict: true }], ['/test/', [], '/test', null, { strict: true }], ['/test/', [], '/test/', ['/test/'], { strict: true }], ['/test/', [], '/test//', null, { strict: true }], - // Non-ending mode. + /** + * Non-ending mode. + */ ['/test', [], '/test', ['/test'], { end: false }], ['/test', [], '/test/route', ['/test'], { end: false }], - // Combine modes. + /** + * Combine modes. + */ ['/test', [], '/test', ['/test'], { end: false, strict: true }], ['/test', [], '/test/', ['/test'], { end: false, strict: true }], ['/test', [], '/test/route', ['/test'], { end: false, strict: true }], @@ -60,139 +70,706 @@ var TESTS = [ ['/test.json', [], '/test.json', ['/test.json'], { end: false, strict: true }], ['/test.json', [], '/test.json.hbs', null, { end: false, strict: true }], - // Arrays of simple paths. + /** + * Arrays of simple paths. + */ [['/one', '/two'], [], '/one', ['/one']], [['/one', '/two'], [], '/two', ['/two']], [['/one', '/two'], [], '/three', null], [['/one', '/two'], [], '/one/two', null], - // Non-ending simple path. + /** + * Non-ending simple path. + */ ['/test', [], '/test/route', ['/test'], { end: false }], - // Single named parameter - ['/:test', ['test'], '/route', ['/route', 'route']], - ['/:test', ['test'], '/another', ['/another', 'another']], - ['/:test', ['test'], '/something/else', null], - ['/:test', ['test'], '/route.json', ['/route.json', 'route.json']], - ['/:test', ['test'], '/route', ['/route', 'route'], { strict: true }], - ['/:test', ['test'], '/route/', null, { strict: true }], - ['/:test/', ['test'], '/route/', ['/route/', 'route'], { strict: true }], - ['/:test/', ['test'], '/route//', null, { strict: true }],, - ['/:test', ['test'], '/route.json', ['/route.json', 'route.json'], { end: false }], - - // Optional named parameter. - ['/:test?', ['test'], '/route', ['/route', 'route']], - ['/:test?', ['test'], '/route/nested', null], - ['/:test?', ['test'], '/', ['/', undefined]], - ['/:test?', ['test'], '/route', ['/route', 'route'], { strict: true }], - ['/:test?', ['test'], '/', null, { strict: true }], // Questionable behaviour. - ['/:test?/', ['test'], '/', ['/', undefined], { strict: true }], - ['/:test?/', ['test'], '//', null], - ['/:test?/', ['test'], '//', null, { strict: true }], + /** + * Single named parameter. + */ + [ + '/:test', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/route', + ['/route', 'route'] + ], + [ + '/:test', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/another', + ['/another', 'another'] + ], + [ + '/:test', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/something/else', + null + ], + [ + '/:test', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/route.json', + ['/route.json', 'route.json'] + ], + [ + '/:test', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/route', + ['/route', 'route'], + { strict: true }], + [ + '/:test', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/route/', + null, + { strict: true } + ], + [ + '/:test/', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/route/', + ['/route/', 'route'], + { strict: true } + ], + [ + '/:test/', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/route//', + null, + { strict: true } + ], + [ + '/:test', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/route.json', + ['/route.json', 'route.json'], + { end: false } + ], + + /** + * Optional named parameter. + */ + [ + '/:test?', + [{ name: 'test', delimiter: '/', optional: true, repeat: false }], + '/route', + ['/route', 'route'] + ], + [ + '/:test?', + [{ name: 'test', delimiter: '/', optional: true, repeat: false }], + '/route/nested', + null + ], + [ + '/:test?', + [{ name: 'test', delimiter: '/', optional: true, repeat: false }], + '/', + ['/', undefined] + ], + [ + '/:test?', + [{ name: 'test', delimiter: '/', optional: true, repeat: false }], + '/route', + ['/route', 'route'], + { strict: true } + ], + [ + '/:test?', + [{ name: 'test', delimiter: '/', optional: true, repeat: false }], + '/', + null, // Questionable behaviour. + { strict: true } + ], + [ + '/:test?/', + [{ name: 'test', delimiter: '/', optional: true, repeat: false }], + '/', + ['/', undefined], + { strict: true } + ], + [ + '/:test?/', + [{ name: 'test', delimiter: '/', optional: true, repeat: false }], + '//', + null + ], + [ + '/:test?/', + [{ name: 'test', delimiter: '/', optional: true, repeat: false }], + '//', + null, + { strict: true } + ], // Repeated once or more times parameters. - ['/:test+', ['test'], '/', null], - ['/:test+', ['test'], '/route', ['/route', 'route']], - ['/:test+', ['test'], '/some/basic/route', ['/some/basic/route', 'some/basic/route']], - ['/:test(\\d+)+', ['test'], '/abc/456/789', null], - ['/:test(\\d+)+', ['test'], '/123/456/789', ['/123/456/789', '123/456/789']], - ['/route.:ext(json|xml)+', ['ext'], '/route.json', ['/route.json', 'json']], - ['/route.:ext(json|xml)+', ['ext'], '/route.xml.json', ['/route.xml.json', 'xml.json']], - ['/route.:ext(json|xml)+', ['ext'], '/route.html', null], - - // Repeated zero or more times parameters. - ['/:test*', ['test'], '/', ['/', undefined]], - ['/:test*', ['test'], '//', null], - ['/:test*', ['test'], '/route', ['/route', 'route']], - ['/:test*', ['test'], '/some/basic/route', ['/some/basic/route', 'some/basic/route']], - ['/route.:ext([a-z]+)*', ['ext'], '/route', ['/route', undefined]], - ['/route.:ext([a-z]+)*', ['ext'], '/route.json', ['/route.json', 'json']], - ['/route.:ext([a-z]+)*', ['ext'], '/route.xml.json', ['/route.xml.json', 'xml.json']], - ['/route.:ext([a-z]+)*', ['ext'], '/route.123', null], + [ + '/:test+', + [{ name: 'test', delimiter: '/', optional: false, repeat: true }], + '/', + null + ], + [ + '/:test+', + [{ name: 'test', delimiter: '/', optional: false, repeat: true }], + '/route', + ['/route', 'route'] + ], + [ + '/:test+', + [{ name: 'test', delimiter: '/', optional: false, repeat: true }], + '/some/basic/route', + ['/some/basic/route', 'some/basic/route'] + ], + [ + '/:test(\\d+)+', + [{ name: 'test', delimiter: '/', optional: false, repeat: true }], + '/abc/456/789', + null + ], + [ + '/:test(\\d+)+', + [{ name: 'test', delimiter: '/', optional: false, repeat: true }], + '/123/456/789', + ['/123/456/789', '123/456/789'] + ], + [ + '/route.:ext(json|xml)+', + [{ name: 'ext', delimiter: '.', optional: false, repeat: true }], + '/route.json', + ['/route.json', 'json'] + ], + [ + '/route.:ext(json|xml)+', + [{ name: 'ext', delimiter: '.', optional: false, repeat: true }], + '/route.xml.json', + ['/route.xml.json', 'xml.json'] + ], + [ + '/route.:ext(json|xml)+', + [{ name: 'ext', delimiter: '.', optional: false, repeat: true }], + '/route.html', + null + ], + + /** + * Repeated zero or more times parameters. + */ + [ + '/:test*', + [{ name: 'test', delimiter: '/', optional: true, repeat: true }], + '/', + ['/', undefined] + ], + [ + '/:test*', + [{ name: 'test', delimiter: '/', optional: true, repeat: true }], + '//', + null + ], + [ + '/:test*', + [{ name: 'test', delimiter: '/', optional: true, repeat: true }], + '/route', + ['/route', 'route'] + ], + [ + '/:test*', + [{ name: 'test', delimiter: '/', optional: true, repeat: true }], + '/some/basic/route', + ['/some/basic/route', 'some/basic/route'] + ], + [ + '/route.:ext([a-z]+)*', + [{ name: 'ext', delimiter: '.', optional: true, repeat: true }], + '/route', + ['/route', undefined] + ], + [ + '/route.:ext([a-z]+)*', + [{ name: 'ext', delimiter: '.', optional: true, repeat: true }], + '/route.json', + ['/route.json', 'json'] + ], + [ + '/route.:ext([a-z]+)*', + [{ name: 'ext', delimiter: '.', optional: true, repeat: true }], + '/route.xml.json', + ['/route.xml.json', 'xml.json'] + ], + [ + '/route.:ext([a-z]+)*', + [{ name: 'ext', delimiter: '.', optional: true, repeat: true }], + '/route.123', + null + ], // Custom named parameters. - ['/:test(\\d+)', ['test'], '/123', ['/123', '123']], - ['/:test(\\d+)', ['test'], '/abc', null], - ['/:test(\\d+)', ['test'], '/123/abc', null], - ['/:test(\\d+)', ['test'], '/123/abc', ['/123', '123'], { end: false }], - ['/:test(.*)', ['test'], '/anything/goes/here', ['/anything/goes/here', 'anything/goes/here']], - ['/:route([a-z]+)', ['route'], '/abcde', ['/abcde', 'abcde']], - ['/:route([a-z]+)', ['route'], '/12345', null], - ['/:route(this|that)', ['route'], '/this', ['/this', 'this']], - ['/:route(this|that)', ['route'], '/that', ['/that', 'that']], - - // Prefixed slashes could be omitted. - ['test', [], 'test', ['test']], - [':test', ['test'], 'route', ['route', 'route']], - [':test', ['test'], '/route', null], - [':test', ['test'], 'route/', ['route/', 'route']], - [':test', ['test'], 'route/', null, { strict: true }], - [':test', ['test'], 'route/', ['route', 'route'], { end: false }], - - // Formats. - ['/test.json', [], '/test.json', ['/test.json']], - ['/test.json', [], '/route.json', null], - ['/:test.json', ['test'], '/route.json', ['/route.json', 'route']], - ['/:test.json', ['test'], '/route.json.json', ['/route.json.json', 'route.json']], - ['/:test.json', ['test'], '/route.json', ['/route.json', 'route'], { end: false }], - ['/:test.json', ['test'], '/route.json.json', ['/route.json.json', 'route.json'], { end: false }], - - // Format params. - ['/test.:format', ['format'], '/test.html', ['/test.html', 'html']], - ['/test.:format', ['format'], '/test.hbs.html', null], - ['/test.:format.:format', ['format', 'format'], '/test.hbs.html', ['/test.hbs.html', 'hbs', 'html']], - ['/test.:format', ['format'], '/test.hbs.html', null, { end: false }], - ['/test.:format.', ['format'], '/test.hbs.html', null, { end: false }], - // Format and path params. - ['/:test.:format', ['test', 'format'], '/route.html', ['/route.html', 'route', 'html']], - ['/:test.:format', ['test', 'format'], '/route', null], - ['/:test.:format', ['test', 'format'], '/route', null], - ['/:test.:format?', ['test', 'format'], '/route', ['/route', 'route', undefined]], - ['/:test.:format?', ['test', 'format'], '/route.json', ['/route.json', 'route', 'json']], - ['/:test.:format?', ['test', 'format'], '/route', ['/route', 'route', undefined], { end: false }], - ['/:test.:format?', ['test', 'format'], '/route.json', ['/route.json', 'route', 'json'], { end: false }], - ['/:test.:format?', ['test', 'format'], '/route.json.html', ['/route.json.html', 'route.json', 'html'], { end: false }], - ['/test.:format(.*)z', ['format'], '/test.abc', null, { end: false }], - ['/test.:format(.*)z', ['format'], '/test.abcz', ['/test.abcz', 'abc'], { end: false }], - - // Unnamed params. - ['/(\\d+)', ['0'], '/123', ['/123', '123']], - ['/(\\d+)', ['0'], '/abc', null], - ['/(\\d+)', ['0'], '/123/abc', null], - ['/(\\d+)', ['0'], '/123/abc', ['/123', '123'], { end: false }], - ['/(\\d+)', ['0'], '/abc', null, { end: false }], - ['/(\\d+)?', ['0'], '/', ['/', undefined]], - ['/(\\d+)?', ['0'], '/123', ['/123', '123']], - ['/(.*)', ['0'], '/route', ['/route', 'route']], - ['/(.*)', ['0'], '/route/nested', ['/route/nested', 'route/nested']], - - // Regexps. - [/.*/, [], '/match/anything', ['/match/anything']], - [/(.*)/, ['0'], '/match/anything', ['/match/anything', '/match/anything']], - [/\/(\d+)/, ['0'], '/123', ['/123', '123']], - - // Mixed arrays. - [['/test', /\/(\d+)/], ['0'], '/test', ['/test', undefined]], - [['/:test(\\d+)', /(.*)/], ['test', '0'], '/123', ['/123', '123', undefined]], - [['/:test(\\d+)', /(.*)/], ['test', '0'], '/abc', ['/abc', undefined, '/abc']], - - // Correct names and indexes. - [['/:test', '/route/:test'], ['test', 'test'], '/test', ['/test', 'test', undefined]], - [['/:test', '/route/:test'], ['test', 'test'], '/route/test', ['/route/test', undefined, 'test']], - [[/^\/([^\/]+)$/, /^\/route\/([^\/]+)$/], ['0', '0'], '/test', ['/test', 'test', undefined]], - [[/^\/([^\/]+)$/, /^\/route\/([^\/]+)$/], ['0', '0'], '/route/test', ['/route/test', undefined, 'test']], - - // Ignore non-matching groups in regexps. - [/(?:.*)/, [], '/anything/you/want', ['/anything/you/want']], - - // Respect escaped characters. - ['/\\(testing\\)', [], '/testing', null], - ['/\\(testing\\)', [], '/(testing)', ['/(testing)']], - - // Regexp special characters should be ignored outside matching groups. - ['/.+*?=^!:${}[]|', [], '/.+*?=^!:${}[]|', ['/.+*?=^!:${}[]|']] + [ + '/:test(\\d+)', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/123', + ['/123', '123'] + ], + [ + '/:test(\\d+)', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/abc', + null + ], + [ + '/:test(\\d+)', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/123/abc', + null + ], + [ + '/:test(\\d+)', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/123/abc', + ['/123', '123'], + { end: false } + ], + [ + '/:test(.*)', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/anything/goes/here', + ['/anything/goes/here', 'anything/goes/here'] + ], + [ + '/:route([a-z]+)', + [{ name: 'route', delimiter: '/', optional: false, repeat: false }], + '/abcde', + ['/abcde', 'abcde'] + ], + [ + '/:route([a-z]+)', + [{ name: 'route', delimiter: '/', optional: false, repeat: false }], + '/12345', + null + ], + [ + '/:route(this|that)', + [{ name: 'route', delimiter: '/', optional: false, repeat: false }], + '/this', + ['/this', 'this'] + ], + [ + '/:route(this|that)', + [{ name: 'route', delimiter: '/', optional: false, repeat: false }], + '/that', + ['/that', 'that'] + ], + + /** + * Prefixed slashes could be omitted. + */ + [ + 'test', + [], + 'test', + ['test'] + ], + [ + ':test', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + 'route', + ['route', 'route'] + ], + [ + ':test', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/route', + null + ], + [ + ':test', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + 'route/', + ['route/', 'route'] + ], + [ + ':test', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + 'route/', + null, + { strict: true } + ], + [ + ':test', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + 'route/', + ['route', 'route'], + { end: false } + ], + + /** + * Formats. + */ + [ + '/test.json', + [], + '/test.json', + ['/test.json'] + ], + [ + '/test.json', + [], + '/route.json', + null + ], + [ + '/:test.json', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/route.json', + ['/route.json', 'route'] + ], + [ + '/:test.json', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/route.json.json', + ['/route.json.json', 'route.json'] + ], + [ + '/:test.json', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/route.json', + ['/route.json', 'route'], + { end: false } + ], + [ + '/:test.json', + [{ name: 'test', delimiter: '/', optional: false, repeat: false }], + '/route.json.json', + ['/route.json.json', 'route.json'], + { end: false } + ], + + /** + * Format params. + */ + [ + '/test.:format', + [{ name: 'format', delimiter: '.', optional: false, repeat: false }], + '/test.html', + ['/test.html', 'html'] + ], + [ + '/test.:format', + [{ name: 'format', delimiter: '.', optional: false, repeat: false }], + '/test.hbs.html', + null + ], + [ + '/test.:format.:format', + [ + { name: 'format', delimiter: '.', optional: false, repeat: false }, + { name: 'format', delimiter: '.', optional: false, repeat: false } + ], + '/test.hbs.html', + ['/test.hbs.html', 'hbs', 'html'] + ], + [ + '/test.:format+', + [ + { name: 'format', delimiter: '.', optional: false, repeat: true } + ], + '/test.hbs.html', + ['/test.hbs.html', 'hbs.html'] + ], + [ + '/test.:format', + [{ name: 'format', delimiter: '.', optional: false, repeat: false }], + '/test.hbs.html', + null, + { end: false } + ], + [ + '/test.:format.', + [{ name: 'format', delimiter: '.', optional: false, repeat: false }], + '/test.hbs.html', + null, + { end: false } + ], + + /** + * Format and path params. + */ + [ + '/:test.:format', + [ + { name: 'test', delimiter: '/', optional: false, repeat: false }, + { name: 'format', delimiter: '.', optional: false, repeat: false } + ], + '/route.html', + ['/route.html', 'route', 'html'] + ], + [ + '/:test.:format', + [ + { name: 'test', delimiter: '/', optional: false, repeat: false }, + { name: 'format', delimiter: '.', optional: false, repeat: false } + ], + '/route', + null + ], + [ + '/:test.:format', + [ + { name: 'test', delimiter: '/', optional: false, repeat: false }, + { name: 'format', delimiter: '.', optional: false, repeat: false } + ], + '/route', + null + ], + [ + '/:test.:format?', + [ + { name: 'test', delimiter: '/', optional: false, repeat: false }, + { name: 'format', delimiter: '.', optional: true, repeat: false } + ], + '/route', + ['/route', 'route', undefined] + ], + [ + '/:test.:format?', + [ + { name: 'test', delimiter: '/', optional: false, repeat: false }, + { name: 'format', delimiter: '.', optional: true, repeat: false } + ], + '/route.json', + ['/route.json', 'route', 'json'] + ], + [ + '/:test.:format?', + [ + { name: 'test', delimiter: '/', optional: false, repeat: false }, + { name: 'format', delimiter: '.', optional: true, repeat: false } + ], + '/route', + ['/route', 'route', undefined], + { end: false } + ], + [ + '/:test.:format?', + [ + { name: 'test', delimiter: '/', optional: false, repeat: false }, + { name: 'format', delimiter: '.', optional: true, repeat: false } + ], + '/route.json', + ['/route.json', 'route', 'json'], + { end: false } + ], + [ + '/:test.:format?', + [ + { name: 'test', delimiter: '/', optional: false, repeat: false }, + { name: 'format', delimiter: '.', optional: true, repeat: false } + ], + '/route.json.html', + ['/route.json.html', 'route.json', 'html'], + { end: false } + ], + [ + '/test.:format(.*)z', + [{ name: 'format', delimiter: '.', optional: false, repeat: false }], + '/test.abc', + null, + { end: false } + ], + [ + '/test.:format(.*)z', + [{ name: 'format', delimiter: '.', optional: false, repeat: false }], + '/test.abcz', + ['/test.abcz', 'abc'], + { end: false } + ], + + /** + * Unnamed params. + */ + [ + '/(\\d+)', + [{ name: '0', delimiter: '/', optional: false, repeat: false }], + '/123', + ['/123', '123'] + ], + [ + '/(\\d+)', + [{ name: '0', delimiter: '/', optional: false, repeat: false }], + '/abc', + null + ], + [ + '/(\\d+)', + [{ name: '0', delimiter: '/', optional: false, repeat: false }], + '/123/abc', + null + ], + [ + '/(\\d+)', + [{ name: '0', delimiter: '/', optional: false, repeat: false }], + '/123/abc', + ['/123', '123'], + { end: false } + ], + [ + '/(\\d+)', + [{ name: '0', delimiter: '/', optional: false, repeat: false }], + '/abc', + null, + { end: false } + ], + [ + '/(\\d+)?', + [{ name: '0', delimiter: '/', optional: true, repeat: false }], + '/', + ['/', undefined] + ], + [ + '/(\\d+)?', + [{ name: '0', delimiter: '/', optional: true, repeat: false }], + '/123', + ['/123', '123'] + ], + [ + '/(.*)', + [{ name: '0', delimiter: '/', optional: false, repeat: false }], + '/route', + ['/route', 'route'] + ], + [ + '/(.*)', + [{ name: '0', delimiter: '/', optional: false, repeat: false }], + '/route/nested', + ['/route/nested', 'route/nested'] + ], + + /** + * Regexps. + */ + [ + /.*/, + [], + '/match/anything', + ['/match/anything'] + ], + [ + /(.*)/, + [{ name: '0', delimiter: null, optional: false, repeat: false }], + '/match/anything', + ['/match/anything', '/match/anything'] + ], + [ + /\/(\d+)/, + [{ name: '0', delimiter: null, optional: false, repeat: false }], + '/123', + ['/123', '123'] + ], + + /** + * Mixed arrays. + */ + [ + ['/test', /\/(\d+)/], + [{ name: '0', delimiter: null, optional: false, repeat: false }], + '/test', + ['/test', undefined] + ], + [ + ['/:test(\\d+)', /(.*)/], + [ + { name: 'test', delimiter: '/', optional: false, repeat: false }, + { name: '0', delimiter: null, optional: false, repeat: false } + ], + '/123', + ['/123', '123', undefined] + ], + [ + ['/:test(\\d+)', /(.*)/], + [ + { name: 'test', delimiter: '/', optional: false, repeat: false }, + { name: '0', delimiter: null, optional: false, repeat: false } + ], + '/abc', + ['/abc', undefined, '/abc'] + ], + + /** + * Correct names and indexes. + */ + [ + ['/:test', '/route/:test'], + [ + { name: 'test', delimiter: '/', optional: false, repeat: false }, + { name: 'test', delimiter: '/', optional: false, repeat: false } + ], + '/test', + ['/test', 'test', undefined] + ], + [ + ['/:test', '/route/:test'], + [ + { name: 'test', delimiter: '/', optional: false, repeat: false }, + { name: 'test', delimiter: '/', optional: false, repeat: false } + ], + '/route/test', + ['/route/test', undefined, 'test'] + ], + [ + [/^\/([^\/]+)$/, /^\/route\/([^\/]+)$/], + [ + { name: '0', delimiter: null, optional: false, repeat: false }, + { name: '0', delimiter: null, optional: false, repeat: false } + ], + '/test', + ['/test', 'test', undefined] + ], + [ + [/^\/([^\/]+)$/, /^\/route\/([^\/]+)$/], + [ + { name: '0', delimiter: null, optional: false, repeat: false }, + { name: '0', delimiter: null, optional: false, repeat: false } + ], + '/route/test', + ['/route/test', undefined, 'test'] + ], + + /** + * Ignore non-matching groups in regexps. + */ + [ + /(?:.*)/, + [], + '/anything/you/want', + ['/anything/you/want'] + ], + + /** + * Respect escaped characters. + */ + [ + '/\\(testing\\)', + [], + '/testing', + null + ], + [ + '/\\(testing\\)', + [], + '/(testing)', + ['/(testing)'] + ], + [ + '/.+*?=^!:${}[]|', + [], + '/.+*?=^!:${}[]|', + ['/.+*?=^!:${}[]|'] + ] ]; +/** + * Dynamically generate the entire test suite. + */ describe('path-to-regexp', function () { it('should not break when keys aren\'t provided', function () { var re = pathToRegexp('/:foo/:bar'); From 5a3604d6d6e6005eba2d25282a7dcb549c8beab4 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 9 Jun 2014 20:31:55 -0700 Subject: [PATCH 11/12] Update readme * Update to new keys syntax * Provide backward compatibility section --- Readme.md | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/Readme.md b/Readme.md index 94e4297..161ae77 100644 --- a/Readme.md +++ b/Readme.md @@ -24,7 +24,7 @@ var pathToRegexp = require('path-to-regexp'); var keys = []; var re = pathToRegexp('/foo/:bar', keys); // re = /^\/foo\/([^\/]+?)\/?$/i -// keys = ['bar'] +// keys = [{ name: 'bar', delimiter: '/', repeat: false, optional: false }] ``` ### Parameters @@ -37,13 +37,13 @@ Named parameters are defined by prefixing a colon to the parameter name (`:foo`) ```js var re = pathToRegexp('/:foo/:bar', keys); -// keys = ['foo', 'bar'] +// keys = [{ name: 'foo', ... }, { name: 'bar', ... }] re.exec('/test/route'); //=> ['/test/route', 'test', 'route'] ``` -#### Parameter Suffixes +#### Suffixed Parameters ##### Optional @@ -51,7 +51,7 @@ Parameters can be suffixed with a question mark (`?`) to make the entire paramet ```js var re = pathToRegexp('/:foo/:bar?', keys); -// keys = ['foo', 'bar'] +// keys = [{ name: 'foo', ... }, { name: 'bar', delimiter: '/', optional: true, repeat: false }] re.exec('/test'); //=> ['/test', 'test', undefined] @@ -66,7 +66,7 @@ Parameters can be suffixed with an asterisk (`*`) to denote a zero or more param ```js var re = pathToRegexp('/:foo*', keys); -// keys = ['foo'] +// keys = [{ name: 'foo', delimiter: '/', optional: true, repeat: true }] re.exec('/'); //=> ['/', undefined] @@ -81,7 +81,7 @@ Parameters can be suffixed with a plus sign (`+`) to denote a one or more parame ```js var re = pathToRegexp('/:foo+', keys); -// keys = ['foo'] +// keys = [{ name: 'foo', delimiter: '/', optional: false, repeat: true }] re.exec('/'); //=> null @@ -90,13 +90,13 @@ re.exec('/bar/baz'); //=> ['/bar/baz', 'bar/baz'] ``` -#### Custom Matches +#### Custom Match Parameters All parameters can be provided a custom matching regexp and override the default. Please note: Backslashes need to be escaped in strings. ```js var re = pathToRegexp('/:foo(\\d+)', keys); -// keys = ['foo'] +// keys = [{ name: 'foo', ... }] re.exec('/123'); //=> ['/123', '123'] @@ -111,12 +111,21 @@ It is possible to write an unnamed parameter that is only a matching group. It w ```js var re = pathToRegexp('/:foo/(.*)', keys); -// keys = ['foo', '0'] +// keys = [{ name: 'foo', ... }, { name: '0', ... }] re.exec('/test/route'); //=> ['/test/route', 'test', 'route'] ``` +## Compatibility with Express 3.x + +Path-To-RegExp breaks compatibility with Express 3.x in a few ways: + +* RegExp special characters can now be used in the regular path. E.g. `/user[(\\d+)]` +* All RegExp special characters can now be used inside the custom match. E.g. `/:user(.*)` +* No more support for asterisk matching - use an explicit parameter instead. E.g. `/(.*)` +* Parameters can have suffixes that augment meaning - `*`, `+` and `?`. E.g. `/:user*` + ## Live Demo You can see a live demo of this library in use at [express-route-tester](http://forbeslindesay.github.com/express-route-tester/). From 1c24c4c16bee21a30298e5c41b6cbbef1f394cf2 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 9 Jun 2014 20:51:11 -0700 Subject: [PATCH 12/12] Release v0.2.0 --- History.md | 27 +++++++++++++++++++++++++-- component.json | 2 +- package.json | 6 ++++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/History.md b/History.md index 68aedb6..e7fa9ff 100644 --- a/History.md +++ b/History.md @@ -1,11 +1,34 @@ +0.2.0 / 2014-06-09 +================== + + * Improved support for arrays + * Improved support for regexps + * Better support for non-ending strict mode matches with a trailing slash + * Travis CI support + * Block using regexp special characters in the path + * Removed support for the asterisk to match all + * New support for parameter suffixes - `*`, `+` and `?` + * Updated readme + * Provide delimiter information with keys array + +0.1.2 / 2014-03-10 +================== + + * Move testing dependencies to `devDependencies` + +0.1.1 / 2014-03-10 +================== + + * Match entire substring with `options.end` + * Properly handle ending and non-ending matches 0.1.0 / 2014-03-06 ================== - * add options.end + * Add `options.end` 0.0.2 / 2013-02-10 ================== * Update to match current express - * add .license property to component.json + * Add .license property to component.json diff --git a/component.json b/component.json index 90f836c..a413a5a 100644 --- a/component.json +++ b/component.json @@ -1,7 +1,7 @@ { "name": "path-to-regexp", "description": "Express style path to RegExp utility", - "version": "0.1.0", + "version": "0.2.0", "keywords": [ "express", "regexp", diff --git a/package.json b/package.json index 8badd71..27df532 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,15 @@ { "name": "path-to-regexp", "description": "Express style path to RegExp utility", - "version": "0.1.2", + "version": "0.2.0", "scripts": { "test": "istanbul cover node_modules/mocha/bin/_mocha -- -R spec" }, "keywords": [ "express", - "regexp" + "regexp", + "route", + "routing" ], "component": { "scripts": {