diff --git a/index.js b/index.js index 500d1da..95d2f4b 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,13 @@ /** - * Expose `pathtoRegexp`. + * Expose `pathToRegexp`. */ -module.exports = pathtoRegexp; +module.exports = pathToRegexp; /** * Match matching groups in a regular expression. */ -var MATCHING_GROUP_REGEXP = /\((?!\?)/g; +var MATCHING_GROUP_REGEXP = /\\.|\((?:\?<(.*?)>)?(?!\?)/g; /** * Normalize the given path string, @@ -25,22 +25,27 @@ var MATCHING_GROUP_REGEXP = /\((?!\?)/g; * @api private */ -function pathtoRegexp(path, keys, options) { +function pathToRegexp(path, keys, options) { options = options || {}; keys = keys || []; var strict = options.strict; var end = options.end !== false; var flags = options.sensitive ? '' : 'i'; + var lookahead = options.lookahead !== false; var extraOffset = 0; var keysOffset = keys.length; var i = 0; var name = 0; + var pos = 0; + var backtrack = ''; var m; if (path instanceof RegExp) { while (m = MATCHING_GROUP_REGEXP.exec(path.source)) { + if (m[0][0] === '\\') continue; + keys.push({ - name: name++, + name: m[1] || name++, optional: false, offset: m.index }); @@ -54,20 +59,57 @@ function pathtoRegexp(path, keys, options) { // 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 pathToRegexp(value, keys, options).source; }); - return new RegExp('(?:' + path.join('|') + ')', flags); + return new RegExp(path.join('|'), flags); + } + + if (typeof path !== 'string') { + throw new TypeError('path must be a string, array of strings, or regular expression'); } - path = ('^' + path + (strict ? '' : path[path.length - 1] === '/' ? '?' : '/?')) - .replace(/\/\(/g, '/(?:') - .replace(/([\/\.])/g, '\\$1') - .replace(/(\\\/)?(\\\.)?:(\w+)(\(.*?\))?(\*)?(\?)?/g, function (match, slash, format, key, capture, star, optional, offset) { + path = path.replace( + /\\.|(\/)?(\.)?:(\w+)(\(.*?\))?(\*)?(\?)?|[.*]|\/\(/g, + function (match, slash, format, key, capture, star, optional, offset) { + if (match[0] === '\\') { + backtrack += match; + pos += 2; + return match; + } + + if (match === '.') { + backtrack += '\\.'; + extraOffset += 1; + pos += 1; + return '\\.'; + } + + if (slash || format) { + backtrack = ''; + } else { + backtrack += path.slice(pos, offset); + } + + pos = offset + match.length; + + if (match === '*') { + extraOffset += 3; + return '(.*)'; + } + + if (match === '/(') { + backtrack += '/'; + extraOffset += 2; + return '/(?:'; + } + slash = slash || ''; - format = format || ''; - capture = capture || '([^\\/' + format + ']+?)'; + format = format ? '\\.' : ''; optional = optional || ''; + capture = capture ? + capture.replace(/\\.|\*/, function (m) { return m === '*' ? '(.*)' : m; }) : + (backtrack ? '((?:(?!/|' + backtrack + ').)+?)' : '([^/' + format + ']+?)'); keys.push({ name: key, @@ -75,41 +117,20 @@ function pathtoRegexp(path, keys, options) { offset: offset + extraOffset }); - var result = '' - + (optional ? '' : slash) - + '(?:' - + format + (optional ? slash : '') + capture - + (star ? '((?:[\\/' + format + '].+?)?)' : '') + var result = '(?:' + + format + slash + capture + + (star ? '((?:[/' + format + '].+?)?)' : '') + ')' + optional; extraOffset += result.length - match.length; return result; - }) - .replace(/\*/g, function (star, index) { - var len = keys.length - - while (len-- > keysOffset && keys[len].offset > index) { - keys[len].offset += 3; // Replacement length minus asterisk length. - } - - return '(.*)'; }); // This is a workaround for handling unnamed matching groups. while (m = MATCHING_GROUP_REGEXP.exec(path)) { - var escapeCount = 0; - var index = m.index; - - while (path.charAt(--index) === '\\') { - escapeCount++; - } - - // It's possible to escape the bracket. - if (escapeCount % 2 === 1) { - continue; - } + if (m[0][0] === '\\') continue; if (keysOffset + i === keys.length || keys[keysOffset + i].offset > m.index) { keys.splice(keysOffset + i, 0, { @@ -122,8 +143,14 @@ function pathtoRegexp(path, keys, options) { i++; } + path += strict ? '' : path[path.length - 1] === '/' ? '?' : '/?'; + // If the path is non-ending, match until the end or a slash. - path += (end ? '$' : (path[path.length - 1] === '/' ? '' : '(?=\\/|$)')); + if (end) { + path += '$'; + } else if (path[path.length - 1] !== '/') { + path += lookahead ? '(?=/|$)' : '(?:/|$)'; + } - return new RegExp(path, flags); + return new RegExp('^' + path, flags); }; diff --git a/package.json b/package.json index d4e51b5..23b4b6a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "path-to-regexp", "description": "Express style path to RegExp utility", - "version": "0.1.7", + "version": "0.1.12", "files": [ "index.js", "LICENSE" @@ -21,7 +21,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/component/path-to-regexp.git" + "url": "https://github.com/pillarjs/path-to-regexp.git" }, "devDependencies": { "mocha": "^1.17.1", diff --git a/test.js b/test.js index 28fce0a..537160e 100644 --- a/test.js +++ b/test.js @@ -2,6 +2,16 @@ var pathToRegExp = require('./'); var assert = require('assert'); describe('path-to-regexp', function () { + it('should throw on invalid input', function () { + assert.throws(function () { + pathToRegExp(function () {}); + }, /path must be a string, array of strings, or regular expression/); + }); + + it('should generate a regex without backtracking', function () { + assert.deepEqual(pathToRegExp('/:a-:b'), /^(?:\/([^/]+?))-(?:((?:(?!\/|-).)+?))\/?$/i); + }); + describe('strings', function () { it('should match simple paths', function () { var params = []; @@ -527,6 +537,19 @@ describe('path-to-regexp', function () { assert.equal(m[1], 'test'); }); + it('should do non-ending matches (no lookahead)', function () { + var params = []; + var m = pathToRegExp('/:test', params, { end: false, lookahead: 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 match trailing slashes in non-ending non-strict mode', function () { var params = []; var re = pathToRegExp('/:test', params, { end: false }); @@ -571,9 +594,38 @@ describe('path-to-regexp', function () { assert.equal(m[0], '/route/'); }); + it('should match trailing slashes in non-ending non-strict mode (no lookahead)', function () { + var params = []; + var re = pathToRegExp('/route/', params, { end: false, lookahead: false }); + var m; + + 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.equal(m.length, 1); + assert.equal(m[0], '/route'); + + m = re.exec('/route//'); + + assert.equal(m.length, 1); + assert.equal(m[0], '/route//'); + }); + it('should match trailing slashing in non-ending strict mode', function () { var params = []; var re = pathToRegExp('/route/', params, { end: false, strict: true }); + var m; assert.equal(params.length, 0); @@ -600,6 +652,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 m; assert.equal(params.length, 0); @@ -614,9 +667,28 @@ describe('path-to-regexp', function () { assert.equal(m[0], '/route'); }); + it('should not match trailing slashes in non-ending strict mode (no lookahead)', function () { + var params = []; + var re = pathToRegExp('/route', params, { end: false, strict: true, lookahead: false }); + var m; + + 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 match text after an express param', function () { var params = []; var re = pathToRegExp('/(:test)route', params); + var m; assert.equal(params.length, 1); assert.equal(params[0].name, 'test'); @@ -701,6 +773,44 @@ describe('path-to-regexp', function () { assert.equal(m[0], '/test.json'); assert.equal(m[1], 'test.json'); }); + + it('should match after a non-slash or format character', function () { + var params = []; + var re = pathToRegExp('/:x-:y', params); + var m; + + assert.equal(params.length, 2); + assert.equal(params[0].name, 'x'); + assert.equal(params[0].optional, false); + assert.equal(params[1].name, 'y'); + assert.equal(params[1].optional, false); + + m = re.exec('/1-2'); + + assert.equal(m.length, 3); + assert.equal(m[0], '/1-2'); + assert.equal(m[1], '1'); + assert.equal(m[2], '2'); + }); + + it('should replace asterisk in capture group', function () { + var params = []; + var re = pathToRegExp('/files/:file(*)', params); + var m; + + assert.equal(params.length, 2); + assert.equal(params[0].name, 'file'); + assert.equal(params[0].optional, false); + assert.equal(params[1].name, 0); + assert.equal(params[1].optional, false); + + m = re.exec('/files/test'); + + assert.equal(m.length, 3); + assert.equal(m[0], '/files/test'); + assert.equal(m[1], 'test'); + assert.equal(m[2], 'test'); + }) }); describe('regexps', function () { @@ -723,6 +833,34 @@ describe('path-to-regexp', function () { assert.equal(m[0], '/route'); assert.equal(m[1], '/route'); }); + + it('should pull out matching named groups', function () { + const majorVersion = Number(process.version.split('.')[0].replace('v', '')); + if (majorVersion < 10) { + console.log('skipping test: node <10 does not support named capture groups'); + return; + } + + var params = []; + var re = pathToRegExp(/\/(.*)\/(?.*)\/(.*)/, params); + var m; + + assert.equal(params.length, 3); + assert.equal(params[0].name, 0); + assert.equal(params[0].optional, false); + assert.equal(params[1].name, 'foo'); + assert.equal(params[1].optional, false); + assert.equal(params[2].name, 1); + assert.equal(params[2].optional, false); + + m = re.exec('/foo/bar/baz'); + + assert.equal(m.length, 4); + assert.equal(m[0], '/foo/bar/baz'); + assert.equal(m[1], 'foo'); + assert.equal(m[2], 'bar'); + assert.equal(m[3], 'baz'); + }); }); describe('arrays', function () {