var isArray = Array.isArray || function (arr) { return Object.prototype.toString.call(arr) == '[object Array]'; }; /** * Expose `pathToRegexp`. */ // module.exports = pathToRegexp /** * The main path matching regexp utility. * * @type {RegExp} */ var PATH_REGEXP = new RegExp([ // Match escaped characters that would otherwise appear in future matches. // This allows the user to escape special characters that won't transform. '(\\\\.)', // 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] '([\\/.])?(?:\\:(\\w+)(?:\\(((?:\\\\.|[^)])*)\\))?|\\(((?:\\\\.|[^)])*)\\))([+*?])?', // Match regexp special characters that are always escaped. '([.+*?=^!:${}()[\\]|\\/])' ].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'); } /** * Attach the keys as a property of the regexp. * * @param {RegExp} re * @param {Array} keys * @return {RegExp} */ function attachKeys (re, keys) { re.keys = keys; return re; } /** * Get the flags for a regexp from the options. * * @param {Object} options * @return {String} */ function flags (options) { return options.sensitive ? '' : 'i'; } /** * Pull out keys from a regexp. * * @param {RegExp} path * @param {Array} keys * @return {RegExp} */ function regexpToRegexp (path, keys) { // Use a negative lookahead to match only capturing groups. var groups = path.source.match(/\((?!\?)/g); if (groups) { for (var i = 0; i < groups.length; i++) { keys.push({ name: i, delimiter: null, optional: false, repeat: false }); } } return attachKeys(path, keys); } /** * Transform an array into a regexp. * * @param {Array} path * @param {Array} keys * @param {Object} options * @return {RegExp} */ function arrayToRegexp (path, keys, options) { var parts = []; for (var i = 0; i < path.length; i++) { parts.push(pathToRegexp(path[i], keys, options).source); } var regexp = new RegExp('(?:' + parts.join('|') + ')', flags(options)); return attachKeys(regexp, keys); } /** * Replace the specific tags with regexp strings. * * @param {String} path * @param {Array} keys * @return {String} */ function replacePath (path, keys) { var index = 0; function replace (_, escaped, prefix, key, capture, group, suffix, escape) { if (escaped) { return escaped; } if (escape) { return '\\' + escape; } var repeat = suffix === '+' || suffix === '*'; var optional = suffix === '?' || suffix === '*'; keys.push({ name: key || index++, delimiter: prefix || '/', optional: optional, repeat: repeat }); prefix = prefix ? ('\\' + prefix) : ''; capture = escapeGroup(capture || group || '[^' + (prefix || '\\/') + ']+?'); if (repeat) { capture = capture + '(?:' + prefix + capture + ')*'; } if (optional) { return '(?:' + prefix + '(' + capture + '))?'; } // Basic parameter support. return prefix + '(' + capture + ')'; } return path.replace(PATH_REGEXP, replace); } /** * Normalize the given path string, returning a regular expression. * * An empty array can be passed in for the keys, which will hold the * placeholder key descriptions. For example, using `/user/:id`, `keys` will * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. * * @param {(String|RegExp|Array)} path * @param {Array} [keys] * @param {Object} [options] * @return {RegExp} */ function pathToRegexp (path, keys, options) { keys = keys || []; if (!isArray(keys)) { options = keys; keys = []; } else if (!options) { options = {}; } if (path instanceof RegExp) { return regexpToRegexp(path, keys, options); } if (isArray(path)) { return arrayToRegexp(path, keys, options); } var strict = options.strict; var end = options.end !== false; var route = replacePath(path, keys); var endsWithSlash = path.charAt(path.length - 1) === '/'; // In non-strict mode we allow a slash at the end of match. If the path to // match already ends with a slash, we remove it for consistency. The slash // is valid at the end of a path match, not in the middle. This is important // in non-ending mode, where "/test/" shouldn't match "/test//route". if (!strict) { route = (endsWithSlash ? route.slice(0, -2) : route) + '(?:\\/(?=$))?'; } if (end) { route += '$'; } else { // In non-ending mode, we need the capturing groups to match as much as // possible by using a positive lookahead to the end or next path segment. route += strict && endsWithSlash ? '' : '(?=\\/|$)'; } return attachKeys(new RegExp('^' + route, flags(options)), keys); }