index.js 11KB

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