index.js 10 KB

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