Paul Lewis | 40e2867 | 2020-11-27 09:51:37 +0000 | [diff] [blame^] | 1 | var isarray = require('isarray') |
| 2 | |
| 3 | /** |
| 4 | * Expose `pathToRegexp`. |
| 5 | */ |
| 6 | module.exports = pathToRegexp |
| 7 | module.exports.parse = parse |
| 8 | module.exports.compile = compile |
| 9 | module.exports.tokensToFunction = tokensToFunction |
| 10 | module.exports.tokensToRegExp = tokensToRegExp |
| 11 | |
| 12 | /** |
| 13 | * The main path matching regexp utility. |
| 14 | * |
| 15 | * @type {RegExp} |
| 16 | */ |
| 17 | var PATH_REGEXP = new RegExp([ |
| 18 | // Match escaped characters that would otherwise appear in future matches. |
| 19 | // This allows the user to escape special characters that won't transform. |
| 20 | '(\\\\.)', |
| 21 | // Match Express-style parameters and un-named parameters with a prefix |
| 22 | // and optional suffixes. Matches appear as: |
| 23 | // |
| 24 | // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined] |
| 25 | // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined] |
| 26 | // "/*" => ["/", undefined, undefined, undefined, undefined, "*"] |
| 27 | '([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))' |
| 28 | ].join('|'), 'g') |
| 29 | |
| 30 | /** |
| 31 | * Parse a string for the raw tokens. |
| 32 | * |
| 33 | * @param {string} str |
| 34 | * @param {Object=} options |
| 35 | * @return {!Array} |
| 36 | */ |
| 37 | function parse (str, options) { |
| 38 | var tokens = [] |
| 39 | var key = 0 |
| 40 | var index = 0 |
| 41 | var path = '' |
| 42 | var defaultDelimiter = options && options.delimiter || '/' |
| 43 | var res |
| 44 | |
| 45 | while ((res = PATH_REGEXP.exec(str)) != null) { |
| 46 | var m = res[0] |
| 47 | var escaped = res[1] |
| 48 | var offset = res.index |
| 49 | path += str.slice(index, offset) |
| 50 | index = offset + m.length |
| 51 | |
| 52 | // Ignore already escaped sequences. |
| 53 | if (escaped) { |
| 54 | path += escaped[1] |
| 55 | continue |
| 56 | } |
| 57 | |
| 58 | var next = str[index] |
| 59 | var prefix = res[2] |
| 60 | var name = res[3] |
| 61 | var capture = res[4] |
| 62 | var group = res[5] |
| 63 | var modifier = res[6] |
| 64 | var asterisk = res[7] |
| 65 | |
| 66 | // Push the current path onto the tokens. |
| 67 | if (path) { |
| 68 | tokens.push(path) |
| 69 | path = '' |
| 70 | } |
| 71 | |
| 72 | var partial = prefix != null && next != null && next !== prefix |
| 73 | var repeat = modifier === '+' || modifier === '*' |
| 74 | var optional = modifier === '?' || modifier === '*' |
| 75 | var delimiter = res[2] || defaultDelimiter |
| 76 | var pattern = capture || group |
| 77 | |
| 78 | tokens.push({ |
| 79 | name: name || key++, |
| 80 | prefix: prefix || '', |
| 81 | delimiter: delimiter, |
| 82 | optional: optional, |
| 83 | repeat: repeat, |
| 84 | partial: partial, |
| 85 | asterisk: !!asterisk, |
| 86 | pattern: pattern ? escapeGroup(pattern) : (asterisk ? '.*' : '[^' + escapeString(delimiter) + ']+?') |
| 87 | }) |
| 88 | } |
| 89 | |
| 90 | // Match any characters still remaining. |
| 91 | if (index < str.length) { |
| 92 | path += str.substr(index) |
| 93 | } |
| 94 | |
| 95 | // If the path exists, push it onto the end. |
| 96 | if (path) { |
| 97 | tokens.push(path) |
| 98 | } |
| 99 | |
| 100 | return tokens |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * Compile a string to a template function for the path. |
| 105 | * |
| 106 | * @param {string} str |
| 107 | * @param {Object=} options |
| 108 | * @return {!function(Object=, Object=)} |
| 109 | */ |
| 110 | function compile (str, options) { |
| 111 | return tokensToFunction(parse(str, options), options) |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * Prettier encoding of URI path segments. |
| 116 | * |
| 117 | * @param {string} |
| 118 | * @return {string} |
| 119 | */ |
| 120 | function encodeURIComponentPretty (str) { |
| 121 | return encodeURI(str).replace(/[\/?#]/g, function (c) { |
| 122 | return '%' + c.charCodeAt(0).toString(16).toUpperCase() |
| 123 | }) |
| 124 | } |
| 125 | |
| 126 | /** |
| 127 | * Encode the asterisk parameter. Similar to `pretty`, but allows slashes. |
| 128 | * |
| 129 | * @param {string} |
| 130 | * @return {string} |
| 131 | */ |
| 132 | function encodeAsterisk (str) { |
| 133 | return encodeURI(str).replace(/[?#]/g, function (c) { |
| 134 | return '%' + c.charCodeAt(0).toString(16).toUpperCase() |
| 135 | }) |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * Expose a method for transforming tokens into the path function. |
| 140 | */ |
| 141 | function tokensToFunction (tokens, options) { |
| 142 | // Compile all the tokens into regexps. |
| 143 | var matches = new Array(tokens.length) |
| 144 | |
| 145 | // Compile all the patterns before compilation. |
| 146 | for (var i = 0; i < tokens.length; i++) { |
| 147 | if (typeof tokens[i] === 'object') { |
| 148 | matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$', flags(options)) |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | return function (obj, opts) { |
| 153 | var path = '' |
| 154 | var data = obj || {} |
| 155 | var options = opts || {} |
| 156 | var encode = options.pretty ? encodeURIComponentPretty : encodeURIComponent |
| 157 | |
| 158 | for (var i = 0; i < tokens.length; i++) { |
| 159 | var token = tokens[i] |
| 160 | |
| 161 | if (typeof token === 'string') { |
| 162 | path += token |
| 163 | |
| 164 | continue |
| 165 | } |
| 166 | |
| 167 | var value = data[token.name] |
| 168 | var segment |
| 169 | |
| 170 | if (value == null) { |
| 171 | if (token.optional) { |
| 172 | // Prepend partial segment prefixes. |
| 173 | if (token.partial) { |
| 174 | path += token.prefix |
| 175 | } |
| 176 | |
| 177 | continue |
| 178 | } else { |
| 179 | throw new TypeError('Expected "' + token.name + '" to be defined') |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | if (isarray(value)) { |
| 184 | if (!token.repeat) { |
| 185 | throw new TypeError('Expected "' + token.name + '" to not repeat, but received `' + JSON.stringify(value) + '`') |
| 186 | } |
| 187 | |
| 188 | if (value.length === 0) { |
| 189 | if (token.optional) { |
| 190 | continue |
| 191 | } else { |
| 192 | throw new TypeError('Expected "' + token.name + '" to not be empty') |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | for (var j = 0; j < value.length; j++) { |
| 197 | segment = encode(value[j]) |
| 198 | |
| 199 | if (!matches[i].test(segment)) { |
| 200 | throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '", but received `' + JSON.stringify(segment) + '`') |
| 201 | } |
| 202 | |
| 203 | path += (j === 0 ? token.prefix : token.delimiter) + segment |
| 204 | } |
| 205 | |
| 206 | continue |
| 207 | } |
| 208 | |
| 209 | segment = token.asterisk ? encodeAsterisk(value) : encode(value) |
| 210 | |
| 211 | if (!matches[i].test(segment)) { |
| 212 | throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"') |
| 213 | } |
| 214 | |
| 215 | path += token.prefix + segment |
| 216 | } |
| 217 | |
| 218 | return path |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | /** |
| 223 | * Escape a regular expression string. |
| 224 | * |
| 225 | * @param {string} str |
| 226 | * @return {string} |
| 227 | */ |
| 228 | function escapeString (str) { |
| 229 | return str.replace(/([.+*?=^!:${}()[\]|\/\\])/g, '\\$1') |
| 230 | } |
| 231 | |
| 232 | /** |
| 233 | * Escape the capturing group by escaping special characters and meaning. |
| 234 | * |
| 235 | * @param {string} group |
| 236 | * @return {string} |
| 237 | */ |
| 238 | function escapeGroup (group) { |
| 239 | return group.replace(/([=!:$\/()])/g, '\\$1') |
| 240 | } |
| 241 | |
| 242 | /** |
| 243 | * Attach the keys as a property of the regexp. |
| 244 | * |
| 245 | * @param {!RegExp} re |
| 246 | * @param {Array} keys |
| 247 | * @return {!RegExp} |
| 248 | */ |
| 249 | function attachKeys (re, keys) { |
| 250 | re.keys = keys |
| 251 | return re |
| 252 | } |
| 253 | |
| 254 | /** |
| 255 | * Get the flags for a regexp from the options. |
| 256 | * |
| 257 | * @param {Object} options |
| 258 | * @return {string} |
| 259 | */ |
| 260 | function flags (options) { |
| 261 | return options && options.sensitive ? '' : 'i' |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * Pull out keys from a regexp. |
| 266 | * |
| 267 | * @param {!RegExp} path |
| 268 | * @param {!Array} keys |
| 269 | * @return {!RegExp} |
| 270 | */ |
| 271 | function regexpToRegexp (path, keys) { |
| 272 | // Use a negative lookahead to match only capturing groups. |
| 273 | var groups = path.source.match(/\((?!\?)/g) |
| 274 | |
| 275 | if (groups) { |
| 276 | for (var i = 0; i < groups.length; i++) { |
| 277 | keys.push({ |
| 278 | name: i, |
| 279 | prefix: null, |
| 280 | delimiter: null, |
| 281 | optional: false, |
| 282 | repeat: false, |
| 283 | partial: false, |
| 284 | asterisk: false, |
| 285 | pattern: null |
| 286 | }) |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | return attachKeys(path, keys) |
| 291 | } |
| 292 | |
| 293 | /** |
| 294 | * Transform an array into a regexp. |
| 295 | * |
| 296 | * @param {!Array} path |
| 297 | * @param {Array} keys |
| 298 | * @param {!Object} options |
| 299 | * @return {!RegExp} |
| 300 | */ |
| 301 | function arrayToRegexp (path, keys, options) { |
| 302 | var parts = [] |
| 303 | |
| 304 | for (var i = 0; i < path.length; i++) { |
| 305 | parts.push(pathToRegexp(path[i], keys, options).source) |
| 306 | } |
| 307 | |
| 308 | var regexp = new RegExp('(?:' + parts.join('|') + ')', flags(options)) |
| 309 | |
| 310 | return attachKeys(regexp, keys) |
| 311 | } |
| 312 | |
| 313 | /** |
| 314 | * Create a path regexp from string input. |
| 315 | * |
| 316 | * @param {string} path |
| 317 | * @param {!Array} keys |
| 318 | * @param {!Object} options |
| 319 | * @return {!RegExp} |
| 320 | */ |
| 321 | function stringToRegexp (path, keys, options) { |
| 322 | return tokensToRegExp(parse(path, options), keys, options) |
| 323 | } |
| 324 | |
| 325 | /** |
| 326 | * Expose a function for taking tokens and returning a RegExp. |
| 327 | * |
| 328 | * @param {!Array} tokens |
| 329 | * @param {(Array|Object)=} keys |
| 330 | * @param {Object=} options |
| 331 | * @return {!RegExp} |
| 332 | */ |
| 333 | function tokensToRegExp (tokens, keys, options) { |
| 334 | if (!isarray(keys)) { |
| 335 | options = /** @type {!Object} */ (keys || options) |
| 336 | keys = [] |
| 337 | } |
| 338 | |
| 339 | options = options || {} |
| 340 | |
| 341 | var strict = options.strict |
| 342 | var end = options.end !== false |
| 343 | var route = '' |
| 344 | |
| 345 | // Iterate over the tokens and create our regexp string. |
| 346 | for (var i = 0; i < tokens.length; i++) { |
| 347 | var token = tokens[i] |
| 348 | |
| 349 | if (typeof token === 'string') { |
| 350 | route += escapeString(token) |
| 351 | } else { |
| 352 | var prefix = escapeString(token.prefix) |
| 353 | var capture = '(?:' + token.pattern + ')' |
| 354 | |
| 355 | keys.push(token) |
| 356 | |
| 357 | if (token.repeat) { |
| 358 | capture += '(?:' + prefix + capture + ')*' |
| 359 | } |
| 360 | |
| 361 | if (token.optional) { |
| 362 | if (!token.partial) { |
| 363 | capture = '(?:' + prefix + '(' + capture + '))?' |
| 364 | } else { |
| 365 | capture = prefix + '(' + capture + ')?' |
| 366 | } |
| 367 | } else { |
| 368 | capture = prefix + '(' + capture + ')' |
| 369 | } |
| 370 | |
| 371 | route += capture |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | var delimiter = escapeString(options.delimiter || '/') |
| 376 | var endsWithDelimiter = route.slice(-delimiter.length) === delimiter |
| 377 | |
| 378 | // In non-strict mode we allow a slash at the end of match. If the path to |
| 379 | // match already ends with a slash, we remove it for consistency. The slash |
| 380 | // is valid at the end of a path match, not in the middle. This is important |
| 381 | // in non-ending mode, where "/test/" shouldn't match "/test//route". |
| 382 | if (!strict) { |
| 383 | route = (endsWithDelimiter ? route.slice(0, -delimiter.length) : route) + '(?:' + delimiter + '(?=$))?' |
| 384 | } |
| 385 | |
| 386 | if (end) { |
| 387 | route += '$' |
| 388 | } else { |
| 389 | // In non-ending mode, we need the capturing groups to match as much as |
| 390 | // possible by using a positive lookahead to the end or next path segment. |
| 391 | route += strict && endsWithDelimiter ? '' : '(?=' + delimiter + '|$)' |
| 392 | } |
| 393 | |
| 394 | return attachKeys(new RegExp('^' + route, flags(options)), keys) |
| 395 | } |
| 396 | |
| 397 | /** |
| 398 | * Normalize the given path string, returning a regular expression. |
| 399 | * |
| 400 | * An empty array can be passed in for the keys, which will hold the |
| 401 | * placeholder key descriptions. For example, using `/user/:id`, `keys` will |
| 402 | * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. |
| 403 | * |
| 404 | * @param {(string|RegExp|Array)} path |
| 405 | * @param {(Array|Object)=} keys |
| 406 | * @param {Object=} options |
| 407 | * @return {!RegExp} |
| 408 | */ |
| 409 | function pathToRegexp (path, keys, options) { |
| 410 | if (!isarray(keys)) { |
| 411 | options = /** @type {!Object} */ (keys || options) |
| 412 | keys = [] |
| 413 | } |
| 414 | |
| 415 | options = options || {} |
| 416 | |
| 417 | if (path instanceof RegExp) { |
| 418 | return regexpToRegexp(path, /** @type {!Array} */ (keys)) |
| 419 | } |
| 420 | |
| 421 | if (isarray(path)) { |
| 422 | return arrayToRegexp(/** @type {!Array} */ (path), /** @type {!Array} */ (keys), options) |
| 423 | } |
| 424 | |
| 425 | return stringToRegexp(/** @type {string} */ (path), /** @type {!Array} */ (keys), options) |
| 426 | } |